From ec53e524033c4c1362d9476ea2650c89fa1ed703 Mon Sep 17 00:00:00 2001 From: ShadelessFox Date: Wed, 24 Apr 2024 00:49:00 +0200 Subject: [PATCH] Core Editor: Show notification banner in case of error or outdated file --- .../shade/decima/model/archive/Archive.java | 20 ++- .../decima/model/archive/ArchiveFile.java | 6 +- .../decima/model/archive/ArchiveManager.java | 20 ++- .../shade/decima/model/packfile/Packfile.java | 18 +- .../decima/model/packfile/PackfileFile.java | 21 +-- .../model/packfile/PackfileManager.java | 6 +- .../decima/ui/editor/NodeEditorInputLazy.java | 8 +- .../decima/ui/editor/core/CoreEditor.java | 43 ++++- .../impl/DefaultEditorOnboardingProvider.java | 2 +- ...dWithErrorsEditorNotificationProvider.java | 31 ++++ ...ededByPatchEditorNotificationProvider.java | 49 +++++ .../decima/ui/navigator/NavigatorPath.java | 6 +- .../navigator/impl/NavigatorPackfileNode.java | 2 +- ....ui.editors.spi.EditorNotificationProvider | 2 + .../ui/editors/EditorNotification.java | 34 ++++ .../editors/{spi => }/EditorOnboarding.java | 2 +- .../spi/EditorNotificationProvider.java | 12 ++ .../editors/spi/EditorOnboardingProvider.java | 1 + .../ui/editors/stack/EditorStackManager.java | 169 ++++++++++++++---- .../resources/themes/FlatDarkLaf.properties | 2 + .../main/resources/themes/FlatLaf.properties | 7 +- 21 files changed, 360 insertions(+), 101 deletions(-) create mode 100644 modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/LoadedWithErrorsEditorNotificationProvider.java create mode 100644 modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/SupersededByPatchEditorNotificationProvider.java create mode 100644 modules/decima-ui/src/main/resources/META-INF/services/com.shade.platform.ui.editors.spi.EditorNotificationProvider create mode 100644 modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorNotification.java rename modules/platform-ui/src/main/java/com/shade/platform/ui/editors/{spi => }/EditorOnboarding.java (95%) create mode 100644 modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorNotificationProvider.java diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/archive/Archive.java b/modules/decima-model/src/main/java/com/shade/decima/model/archive/Archive.java index 9e0bf2a0f..d6a8cf162 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/archive/Archive.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/archive/Archive.java @@ -22,6 +22,24 @@ public interface Archive extends Closeable { @Nullable ArchiveFile findFile(@NotNull String identifier); + @Nullable + ArchiveFile findFile(long identifier); + + @NotNull + default ArchiveFile getFile(@NotNull String identifier) { + final ArchiveFile file = findFile(identifier); + if (file == null) { + throw new IllegalArgumentException("Can't find file '%s' in archive %s".formatted(identifier, getName())); + } + return file; + } + @NotNull - ArchiveFile getFile(@NotNull String identifier); + default ArchiveFile getFile(long identifier) { + final ArchiveFile file = findFile(identifier); + if (file == null) { + throw new IllegalArgumentException("Can't find file '%#018x' in archive %s".formatted(identifier, getName())); + } + return file; + } } diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveFile.java b/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveFile.java index f321621c7..a5fe3f628 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveFile.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveFile.java @@ -6,11 +6,7 @@ import java.io.InputStream; public interface ArchiveFile { - @NotNull - String getName(); - - @NotNull - String getPath(); + long getIdentifier(); @NotNull Archive getArchive(); diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveManager.java b/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveManager.java index 0b15716ba..cc856ba04 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveManager.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/archive/ArchiveManager.java @@ -10,8 +10,26 @@ public interface ArchiveManager extends Closeable { @Nullable ArchiveFile findFile(@NotNull String identifier); + @Nullable + ArchiveFile findFile(long identifier); + + @NotNull + default ArchiveFile getFile(@NotNull String identifier) { + final ArchiveFile file = findFile(identifier); + if (file == null) { + throw new IllegalArgumentException("Can't find file '%s'".formatted(identifier)); + } + return file; + } + @NotNull - ArchiveFile getFile(@NotNull String identifier); + default ArchiveFile getFile(long identifier) { + final ArchiveFile file = findFile(identifier); + if (file == null) { + throw new IllegalArgumentException("Can't find file '%#018x'".formatted(identifier)); + } + return file; + } @NotNull Collection getArchives(); diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/Packfile.java b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/Packfile.java index 7e9c5aa0b..4f1ecb8db 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/Packfile.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/Packfile.java @@ -2,7 +2,6 @@ import com.shade.decima.model.archive.Archive; import com.shade.decima.model.archive.ArchiveFile; -import com.shade.decima.model.archive.ArchiveManager; import com.shade.decima.model.packfile.edit.Change; import com.shade.decima.model.packfile.resource.Resource; import com.shade.decima.model.util.FilePath; @@ -59,7 +58,7 @@ public class Packfile implements Archive, Comparable { @NotNull @Override - public ArchiveManager getManager() { + public PackfileManager getManager() { return manager; } @@ -283,21 +282,12 @@ public ArchiveFile findFile(@NotNull String identifier) { return new PackfileFile(this, entry); } - @NotNull + @Nullable @Override - public ArchiveFile getFile(@NotNull String identifier) { + public ArchiveFile findFile(long identifier) { final FileEntry entry = getFileEntry(identifier); if (entry == null) { - throw new IllegalArgumentException("Can't find file '%s' in archive %s".formatted(identifier, getName())); - } - return new PackfileFile(this, entry); - } - - @NotNull - public ArchiveFile getFile(long hash) { - final FileEntry entry = getFileEntry(hash); - if (entry == null) { - throw new IllegalArgumentException("Can't find file '%#018x' in archive %s".formatted(hash, getName())); + return null; } return new PackfileFile(this, entry); } diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileFile.java b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileFile.java index 1c8d32230..80b3e37f6 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileFile.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileFile.java @@ -7,25 +7,10 @@ import java.io.IOException; import java.io.InputStream; -public class PackfileFile implements ArchiveFile { - private final Packfile packfile; - private final Packfile.FileEntry entry; - - public PackfileFile(@NotNull Packfile packfile, @NotNull Packfile.FileEntry entry) { - this.packfile = packfile; - this.entry = entry; - } - - @NotNull - @Override - public String getName() { - return "%#018x".formatted(entry.hash()); - } - - @NotNull +public record PackfileFile(@NotNull Packfile packfile, @NotNull Packfile.FileEntry entry) implements ArchiveFile { @Override - public String getPath() { - return getName(); + public long getIdentifier() { + return entry.hash(); } @NotNull diff --git a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileManager.java b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileManager.java index ad3e3b0b2..a0e95a5f5 100644 --- a/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileManager.java +++ b/modules/decima-model/src/main/java/com/shade/decima/model/packfile/PackfileManager.java @@ -60,12 +60,12 @@ public ArchiveFile findFile(@NotNull String identifier) { return archive.getFile(identifier); } - @NotNull + @Nullable @Override - public ArchiveFile getFile(@NotNull String identifier) { + public ArchiveFile findFile(long identifier) { final Packfile archive = findFirst(identifier); if (archive == null) { - throw new IllegalArgumentException("Can't find file '%s'".formatted(identifier)); + return null; } return archive.getFile(identifier); } diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/NodeEditorInputLazy.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/NodeEditorInputLazy.java index 170e6720e..dc2f60620 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/NodeEditorInputLazy.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/NodeEditorInputLazy.java @@ -1,7 +1,7 @@ package com.shade.decima.ui.editor; import com.shade.decima.model.app.ProjectContainer; -import com.shade.decima.model.packfile.Packfile; +import com.shade.decima.model.archive.Archive; import com.shade.decima.model.util.FilePath; import com.shade.decima.ui.Application; import com.shade.decima.ui.navigator.NavigatorPath; @@ -27,8 +27,8 @@ public NodeEditorInputLazy(@NotNull String container, @NotNull String packfile, this(UUID.fromString(container), packfile, FilePath.of(path, false, false)); } - public NodeEditorInputLazy(@NotNull ProjectContainer container, @NotNull Packfile packfile, @NotNull String path) { - this(container.getId(), packfile.getPath().getFileName().toString(), FilePath.of(path, false, false)); + public NodeEditorInputLazy(@NotNull ProjectContainer container, @NotNull Archive archive, @NotNull String path) { + this(container.getId(), archive.getId(), FilePath.of(path, false, false)); } @NotNull @@ -86,7 +86,7 @@ public String getDescription() { public boolean representsSameResource(@NotNull EditorInput other) { if (other instanceof NodeEditorInputSimple o) { return container().equals(o.getNode().getProjectContainer().getId()) - && packfile().equals(o.getNode().getPackfile().getPath().getFileName().toString()) + && packfile().equals(o.getNode().getPackfile().getId()) && path().equals(o.getNode().getPath()); } if (other instanceof NodeEditorInputLazy o) { diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/CoreEditor.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/CoreEditor.java index 276641198..4a7ab5e87 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/CoreEditor.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/CoreEditor.java @@ -54,6 +54,7 @@ public class CoreEditor extends JSplitPane implements SaveableEditor, StatefulEd private final ProjectEditorInput input; private final RTTICoreFile file; private final MessageBusConnection connection; + private final int errors; // Initialized in CoreEditor#createComponent private CoreTree tree; @@ -67,16 +68,17 @@ public class CoreEditor extends JSplitPane implements SaveableEditor, StatefulEd private boolean sortingEnabled; public CoreEditor(@NotNull FileEditorInput input) { - this(input, loadCoreFile(input)); + this(input, loadFile(input)); } public CoreEditor(@NotNull NodeEditorInput input) { - this(input, loadCoreFile(input)); + this(input, loadFile(input)); } - private CoreEditor(@NotNull ProjectEditorInput input, @NotNull RTTICoreFile file) { + private CoreEditor(@NotNull ProjectEditorInput input, @NotNull FileLoadResult result) { this.input = input; - this.file = file; + this.file = result.file; + this.errors = result.errors; this.connection = MessageBus.getInstance().connect(); connection.subscribe(CoreEditorSettings.SETTINGS, () -> { @@ -350,6 +352,10 @@ public RTTICoreFile getCoreFile() { return file; } + public int getErrorCount() { + return errors; + } + private void fireDirtyStateChange() { firePropertyChange("dirty", null, isDirty()); } @@ -421,23 +427,30 @@ private void fitValueViewer(@NotNull JComponent component) { } @NotNull - private static RTTICoreFile loadCoreFile(@NotNull NodeEditorInput input) { - try { - return input.getProject().getCoreFileReader().read(input.getNode().getFile(), LoggingErrorHandlingStrategy.getInstance()); + private static FileLoadResult loadFile(@NotNull NodeEditorInput input) { + try (InputStream is = input.getNode().getFile().newInputStream()) { + return loadFile(input.getProject(), is); } catch (IOException e) { throw new UncheckedIOException(e); } } @NotNull - private static RTTICoreFile loadCoreFile(@NotNull FileEditorInput input) { + private static FileLoadResult loadFile(@NotNull FileEditorInput input) { try (InputStream is = Files.newInputStream(input.getPath())) { - return input.getProject().getCoreFileReader().read(is, LoggingErrorHandlingStrategy.getInstance()); + return loadFile(input.getProject(), is); } catch (IOException e) { throw new UncheckedIOException(e); } } + @NotNull + private static FileLoadResult loadFile(@NotNull Project project, @NotNull InputStream is) throws IOException { + final MetricLoggingErrorHandlingStrategy strategy = new MetricLoggingErrorHandlingStrategy(); + final RTTICoreFile file = project.getCoreFileReader().read(is, strategy); + return new FileLoadResult(file, strategy.errors); + } + @NotNull private static String serializePath(@NotNull RTTIPath path) { final StringBuilder selection = new StringBuilder(); @@ -512,4 +525,16 @@ private static RTTIPath deserializePath(@NotNull Object object) { return new RTTIPath(elements.toArray(RTTIPathElement[]::new)); } + + private record FileLoadResult(@NotNull RTTICoreFile file, int errors) {} + + private static class MetricLoggingErrorHandlingStrategy extends LoggingErrorHandlingStrategy { + private int errors = 0; + + @Override + public void handle(@NotNull Exception e) { + super.handle(e); + errors++; + } + } } diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/DefaultEditorOnboardingProvider.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/DefaultEditorOnboardingProvider.java index cecd8ce58..e2fc899cb 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/DefaultEditorOnboardingProvider.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/DefaultEditorOnboardingProvider.java @@ -3,7 +3,7 @@ import com.shade.decima.ui.menu.menus.EditMenu; import com.shade.decima.ui.menu.menus.FileMenu; import com.shade.decima.ui.menu.menus.ViewMenu; -import com.shade.platform.ui.editors.spi.EditorOnboarding; +import com.shade.platform.ui.editors.EditorOnboarding; import com.shade.platform.ui.editors.spi.EditorOnboardingProvider; import com.shade.util.NotNull; diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/LoadedWithErrorsEditorNotificationProvider.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/LoadedWithErrorsEditorNotificationProvider.java new file mode 100644 index 000000000..e29ccb268 --- /dev/null +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/LoadedWithErrorsEditorNotificationProvider.java @@ -0,0 +1,31 @@ +package com.shade.decima.ui.editor.impl.notifications; + +import com.shade.decima.ui.editor.core.CoreEditor; +import com.shade.platform.ui.editors.Editor; +import com.shade.platform.ui.editors.EditorNotification; +import com.shade.platform.ui.editors.spi.EditorNotificationProvider; +import com.shade.util.NotNull; + +import java.text.MessageFormat; +import java.util.Collection; +import java.util.List; + +public class LoadedWithErrorsEditorNotificationProvider implements EditorNotificationProvider { + @NotNull + @Override + public Collection getNotifications(@NotNull Editor editor) { + if (!(editor instanceof CoreEditor coreEditor) || coreEditor.getErrorCount() == 0) { + return List.of(); + } + + return List.of(new EditorNotification( + EditorNotification.Status.ERROR, + MessageFormat.format( + "This file was loaded with {0} {0,choice,1#error|1", + coreEditor.getErrorCount() + ), + List.of() + )); + } +} diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/SupersededByPatchEditorNotificationProvider.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/SupersededByPatchEditorNotificationProvider.java new file mode 100644 index 000000000..2d53393a4 --- /dev/null +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/impl/notifications/SupersededByPatchEditorNotificationProvider.java @@ -0,0 +1,49 @@ +package com.shade.decima.ui.editor.impl.notifications; + +import com.shade.decima.model.archive.ArchiveFile; +import com.shade.decima.ui.Application; +import com.shade.decima.ui.editor.NodeEditorInput; +import com.shade.decima.ui.editor.NodeEditorInputSimple; +import com.shade.decima.ui.navigator.NavigatorPath; +import com.shade.decima.ui.navigator.impl.NavigatorFileNode; +import com.shade.platform.model.runtime.VoidProgressMonitor; +import com.shade.platform.ui.editors.Editor; +import com.shade.platform.ui.editors.EditorManager; +import com.shade.platform.ui.editors.EditorNotification; +import com.shade.platform.ui.editors.spi.EditorNotificationProvider; +import com.shade.util.NotNull; + +import java.util.Collection; +import java.util.List; + +public class SupersededByPatchEditorNotificationProvider implements EditorNotificationProvider { + @NotNull + @Override + public Collection getNotifications(@NotNull Editor editor) { + if (!(editor.getInput() instanceof NodeEditorInput input)) { + return List.of(); + } + + final NavigatorFileNode node = input.getNode(); + final ArchiveFile currentFile = node.getFile(); + final ArchiveFile actualFile = node.getArchive().getManager().getFile(currentFile.getIdentifier()); + + if (currentFile.equals(actualFile)) { + return List.of(); + } + + return List.of(new EditorNotification( + EditorNotification.Status.WARNING, + "This file has been superseded by a patch file. The displayed content may be outdated", + List.of( + new EditorNotification.Action("Open File", () -> Application.getNavigator().getModel() + .findFileNode(new VoidProgressMonitor(), NavigatorPath.of( + node.getProject().getContainer(), + actualFile.getArchive(), + node.getPath() + )) + .thenApply(n -> EditorManager.getInstance().openEditor(new NodeEditorInputSimple(n), true))) + ) + )); + } +} diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/NavigatorPath.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/NavigatorPath.java index 1e88cdc49..d1c6d3968 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/NavigatorPath.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/NavigatorPath.java @@ -1,13 +1,13 @@ package com.shade.decima.ui.navigator; import com.shade.decima.model.app.ProjectContainer; -import com.shade.decima.model.packfile.Packfile; +import com.shade.decima.model.archive.Archive; import com.shade.decima.model.util.FilePath; import com.shade.util.NotNull; public record NavigatorPath(@NotNull String projectId, @NotNull String packfileId, @NotNull FilePath filePath) { @NotNull - public static NavigatorPath of(@NotNull ProjectContainer container, @NotNull Packfile packfile, @NotNull FilePath filePath) { - return new NavigatorPath(container.getId().toString(), packfile.getPath().getFileName().toString(), filePath); + public static NavigatorPath of(@NotNull ProjectContainer container, @NotNull Archive archive, @NotNull FilePath filePath) { + return new NavigatorPath(container.getId().toString(), archive.getId(), filePath); } } diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/impl/NavigatorPackfileNode.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/impl/NavigatorPackfileNode.java index f078f41c0..b1ecf9393 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/impl/NavigatorPackfileNode.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/impl/NavigatorPackfileNode.java @@ -74,7 +74,7 @@ public Packfile getPackfile() { @Override public boolean contains(@NotNull NavigatorPath path) { - return packfile.getPath().getFileName().toString().equals(path.packfileId()); + return packfile.getId().equals(path.packfileId()); } @NotNull diff --git a/modules/decima-ui/src/main/resources/META-INF/services/com.shade.platform.ui.editors.spi.EditorNotificationProvider b/modules/decima-ui/src/main/resources/META-INF/services/com.shade.platform.ui.editors.spi.EditorNotificationProvider new file mode 100644 index 000000000..54ed6b2c2 --- /dev/null +++ b/modules/decima-ui/src/main/resources/META-INF/services/com.shade.platform.ui.editors.spi.EditorNotificationProvider @@ -0,0 +1,2 @@ +com.shade.decima.ui.editor.impl.notifications.LoadedWithErrorsEditorNotificationProvider +com.shade.decima.ui.editor.impl.notifications.SupersededByPatchEditorNotificationProvider diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorNotification.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorNotification.java new file mode 100644 index 000000000..6b23b1b44 --- /dev/null +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorNotification.java @@ -0,0 +1,34 @@ +package com.shade.platform.ui.editors; + +import com.shade.platform.ui.UIColor; +import com.shade.util.NotNull; + +import java.awt.*; +import java.util.Collection; + +public record EditorNotification(@NotNull Status status, @NotNull String message, @NotNull Collection actions) { + public enum Status { + ERROR(UIColor.named("Component.error.background"), UIColor.named("Component.error.borderColor")), + WARNING(UIColor.named("Component.warning.background"), UIColor.named("Component.warning.borderColor")); + + private final Color background; + private final Color border; + + Status(@NotNull Color background, @NotNull Color border) { + this.background = background; + this.border = border; + } + + @NotNull + public Color getBackground() { + return background; + } + + @NotNull + public Color getBorder() { + return border; + } + } + + public record Action(@NotNull String name, @NotNull Runnable callback) {} +} diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboarding.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorOnboarding.java similarity index 95% rename from modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboarding.java rename to modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorOnboarding.java index 17a705b02..ffd6a8819 100644 --- a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboarding.java +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/EditorOnboarding.java @@ -1,4 +1,4 @@ -package com.shade.platform.ui.editors.spi; +package com.shade.platform.ui.editors; import com.shade.platform.ui.menus.MenuItemRegistration; import com.shade.util.NotNull; diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorNotificationProvider.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorNotificationProvider.java new file mode 100644 index 000000000..6eb07e59e --- /dev/null +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorNotificationProvider.java @@ -0,0 +1,12 @@ +package com.shade.platform.ui.editors.spi; + +import com.shade.platform.ui.editors.Editor; +import com.shade.platform.ui.editors.EditorNotification; +import com.shade.util.NotNull; + +import java.util.Collection; + +public interface EditorNotificationProvider { + @NotNull + Collection getNotifications(@NotNull Editor editor); +} diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboardingProvider.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboardingProvider.java index 91cc6fdc0..038a2e11d 100644 --- a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboardingProvider.java +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/spi/EditorOnboardingProvider.java @@ -1,5 +1,6 @@ package com.shade.platform.ui.editors.spi; +import com.shade.platform.ui.editors.EditorOnboarding; import com.shade.util.NotNull; public interface EditorOnboardingProvider { diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/stack/EditorStackManager.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/stack/EditorStackManager.java index 9a2d3b5d1..3f5fc7cda 100644 --- a/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/stack/EditorStackManager.java +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/editors/stack/EditorStackManager.java @@ -13,7 +13,7 @@ import com.shade.platform.model.util.IOUtils; import com.shade.platform.ui.PlatformDataKeys; import com.shade.platform.ui.editors.*; -import com.shade.platform.ui.editors.spi.EditorOnboarding; +import com.shade.platform.ui.editors.spi.EditorNotificationProvider; import com.shade.platform.ui.editors.spi.EditorOnboardingProvider; import com.shade.platform.ui.menus.MenuItemRegistration; import com.shade.platform.ui.menus.MenuManager; @@ -25,8 +25,7 @@ import javax.swing.*; import java.awt.*; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; +import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.List; @@ -83,25 +82,26 @@ public void editorStackCreated(@NotNull EditorStack stack) { }); stack.addChangeListener(e -> { - final int index = stack.getSelectedIndex(); + final EditorComponent component = (EditorComponent) stack.getSelectedComponent(); - if (index >= 0 && stack.getComponentAt(index) instanceof PlaceholderComponent placeholder) { - final Editor editor = EDITOR_KEY.get(placeholder); - final JComponent component = editor.createComponent(); - - component.putClientProperty(EDITOR_KEY, editor); - stack.setComponentAt(index, component); + if (component != null && !component.hasComponent()) { + component.setComponent(EDITOR_KEY.get(component).createComponent()); } }); } + @Override + public void editorOpened(@NotNull Editor editor) { + updateNotifications(editor); + } + @Override public void editorChanged(@Nullable Editor editor) { if (editor == null) { return; } - final JComponent component = findEditorComponent(editor::equals); + final EditorComponent component = findEditorComponent(editor::equals); if (component != null) { final EditorInput input = (EditorInput) component.getClientProperty(NEW_INPUT_KEY); @@ -122,7 +122,7 @@ public void editorChanged(@Nullable Editor editor) { @Nullable @Override public Editor findEditor(@NotNull EditorInput input) { - final JComponent component = findEditorComponent(e -> input.representsSameResource(e.getInput())); + final EditorComponent component = findEditorComponent(e -> input.representsSameResource(e.getInput())); if (component != null) { return EDITOR_KEY.get(component); @@ -146,7 +146,7 @@ public Editor openEditor(@NotNull EditorInput input, @Nullable EditorProvider pr @NotNull @Override public Editor openEditor(@NotNull EditorInput input, @Nullable EditorProvider provider, @Nullable EditorStack stack, boolean select, boolean focus, int index) { - JComponent component = findEditorComponent(e -> input.representsSameResource(e.getInput())); + EditorComponent component = findEditorComponent(e -> input.representsSameResource(e.getInput())); if (component == null) { final Editor editor; @@ -163,12 +163,14 @@ public Editor openEditor(@NotNull EditorInput input, @Nullable EditorProvider pr se.addPropertyChangeListener(this); } - component = select ? editor.createComponent() : new PlaceholderComponent(); + component = new EditorComponent(select ? editor.createComponent() : null); component.putClientProperty(EDITOR_KEY, editor); component.putClientProperty(LAST_USAGE_KEY, System.currentTimeMillis()); stack = Objects.requireNonNullElseGet(stack, this::getActiveStack); stack.insertTab(input.getName(), provider.getIcon(), component, input.getDescription(), index < 0 ? stack.getSelectedIndex() + 1 : index); + + MessageBus.getInstance().publisher(EditorManager.EDITORS).editorOpened(editor); } else { stack = ((EditorStack) component.getParent()); } @@ -180,8 +182,6 @@ public Editor openEditor(@NotNull EditorInput input, @Nullable EditorProvider pr stack.setFocusable(false); stack.setSelectedComponent(component); stack.setFocusable(true); - - MessageBus.getInstance().publisher(EditorManager.EDITORS).editorOpened(editor); } if (focus) { @@ -194,7 +194,7 @@ public Editor openEditor(@NotNull EditorInput input, @Nullable EditorProvider pr @Nullable @Override public Editor reuseEditor(@NotNull Editor oldEditor, @NotNull EditorInput newInput) { - final JComponent oldComponent = findEditorComponent(e -> e.equals(oldEditor)); + final EditorComponent oldComponent = findEditorComponent(e -> e.equals(oldEditor)); if (oldComponent != null) { final EditorStack stack = (EditorStack) oldComponent.getParent(); @@ -208,17 +208,18 @@ public Editor reuseEditor(@NotNull Editor oldEditor, @NotNull EditorInput newInp se.removePropertyChangeListener(this); } - if (!(oldComponent instanceof PlaceholderComponent)) { + if (oldComponent.hasComponent()) { oldEditor.dispose(); } final EditorResult result = createEditorForInput(newInput); + final Editor editor = result.editor; - if (result.editor() instanceof SaveableEditor se) { + if (editor instanceof SaveableEditor se) { se.addPropertyChangeListener(EditorStackManager.this); } - if (oldEditor instanceof StatefulEditor o && result.editor() instanceof StatefulEditor n) { + if (oldEditor instanceof StatefulEditor o && editor instanceof StatefulEditor n) { final Map state = new HashMap<>(); o.saveState(state); @@ -228,8 +229,8 @@ public Editor reuseEditor(@NotNull Editor oldEditor, @NotNull EditorInput newInp } } - final JComponent newComponent = selected ? result.editor().createComponent() : new PlaceholderComponent(); - newComponent.putClientProperty(EDITOR_KEY, result.editor()); + final EditorComponent newComponent = new EditorComponent(selected ? editor.createComponent() : null); + newComponent.putClientProperty(EDITOR_KEY, editor); stack.setComponentAt(index, newComponent); stack.setTitleAt(index, newInput.getName()); @@ -238,10 +239,11 @@ public Editor reuseEditor(@NotNull Editor oldEditor, @NotNull EditorInput newInp if (selected && oldEditor.isFocused()) { newComponent.validate(); - result.editor().setFocus(); + editor.setFocus(); } - return result.editor(); + MessageBus.getInstance().publisher(EditorManager.EDITORS).editorOpened(editor); + return editor; } } } @@ -324,7 +326,7 @@ public int getEditorsCount(@NotNull EditorStack stack) { @Override public void closeEditor(@NotNull Editor editor) { - final JComponent component = findEditorComponent(e -> e.equals(editor)); + final EditorComponent component = findEditorComponent(e -> e.equals(editor)); if (component != null) { final EditorStack stack = (EditorStack) component.getParent(); @@ -351,7 +353,7 @@ public void closeEditor(@NotNull Editor editor) { se.removePropertyChangeListener(this); } - if (!(component instanceof PlaceholderComponent)) { + if (component.hasComponent()) { editor.dispose(); } @@ -407,7 +409,7 @@ public void propertyChange(PropertyChangeEvent event) { if (SaveableEditor.PROP_DIRTY.equals(event.getPropertyName())) { final SaveableEditor editor = (SaveableEditor) event.getSource(); final EditorInput input = editor.getInput(); - final JComponent component = findEditorComponent(e -> e.equals(editor)); + final EditorComponent component = findEditorComponent(e -> e.equals(editor)); if (component != null) { final EditorStack stack = (EditorStack) component.getParent(); @@ -457,6 +459,80 @@ public void noStateLoaded() { } } + private void updateNotifications(@NotNull Editor editor) { + final List notifications = ServiceLoader.load(EditorNotificationProvider.class).stream() + .flatMap(provider -> provider.get().getNotifications(editor).stream()) + .sorted(Comparator.comparing(EditorNotification::status)) + .toList(); + + if (notifications.isEmpty()) { + setTopComponent(editor, null); + return; + } + + final JPanel container = new JPanel(); + container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); + + for (EditorNotification notification : notifications) { + final JPanel panel = new JPanel(); + panel.setBackground(notification.status().getBackground()); + panel.setLayout(new BorderLayout()); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, notification.status().getBorder()), + BorderFactory.createEmptyBorder(0, 10, 0, 10) + )); + + final JToolBar toolBar = new JToolBar(); + toolBar.setBackground(notification.status().getBackground()); + + for (EditorNotification.Action action : notification.actions()) { + final JLabel label = UIUtils.createBoldLabel(); + label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + label.setText("" + action.name() + ""); + label.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + action.callback().run(); + } + }); + toolBar.add(label); + } + + toolBar.add(Box.createHorizontalStrut(4)); + toolBar.add(new AbstractAction("Close", UIManager.getIcon("TabbedPane.closeIcon")) { + @Override + public void actionPerformed(ActionEvent e) { + container.remove(panel); + container.revalidate(); + } + }); + + panel.add(new JLabel(notification.message()), BorderLayout.WEST); + panel.add(toolBar, BorderLayout.EAST); + + container.add(panel); + } + + setTopComponent(editor, container); + } + + private void setTopComponent(@NotNull Editor editor, @Nullable JComponent topComponent) { + final EditorComponent editorComponent = findEditorComponent(e -> e.equals(editor)); + + if (editorComponent != null) { + final BorderLayout layout = (BorderLayout) editorComponent.getLayout(); + final Component currentComponent = layout.getLayoutComponent(BorderLayout.NORTH); + + if (currentComponent != null) { + editorComponent.remove(currentComponent); + } + + if (topComponent != null) { + editorComponent.add(topComponent, BorderLayout.NORTH); + } + } + } + @Nullable private static Container saveState(@NotNull EditorStackContainer container) { if (container.isSplit()) { @@ -668,8 +744,8 @@ private EditorResult createEditorForInput(@NotNull EditorInput input) { } @Nullable - private JComponent findEditorComponent(@NotNull Predicate predicate) { - for (JComponent component : getTabs()) { + private EditorComponent findEditorComponent(@NotNull Predicate predicate) { + for (EditorComponent component : getTabs()) { final Editor editor = EDITOR_KEY.get(component); if (predicate.test(editor)) { @@ -699,14 +775,14 @@ private EditorStack getActiveStack() { } @NotNull - private JComponent[] getTabs() { - final List components = new ArrayList<>(); + private EditorComponent[] getTabs() { + final List components = new ArrayList<>(); forEachStack(stack -> { for (int i = 0; i < stack.getTabCount(); i++) { - components.add((JComponent) stack.getComponentAt(i)); + components.add((EditorComponent) stack.getComponentAt(i)); } }); - return components.toArray(JComponent[]::new); + return components.toArray(EditorComponent[]::new); } @NotNull @@ -746,7 +822,7 @@ private void forEachStack(@NotNull Component component, @NotNull Consumer input, @Nullable Map state) {} + public record File(boolean selected, @NotNull String factory, @NotNull Map input, @Nullable Map state) {} - private record Split(@NotNull Orientation orientation, double proportion, @NotNull Container first, @NotNull Container second) {} + public record Split(@NotNull Orientation orientation, double proportion, @NotNull Container first, @NotNull Container second) {} - private enum Orientation { + public enum Orientation { VERTICAL, HORIZONTAL; @@ -774,7 +850,24 @@ public static Orientation valueOf(int value) { } } - private static class PlaceholderComponent extends JComponent {} + private static class EditorComponent extends JComponent { + EditorComponent(@Nullable JComponent inner) { + setLayout(new BorderLayout()); + + if (inner != null) { + add(inner); + } + } + + public void setComponent(@NotNull JComponent component) { + removeAll(); + add(component); + } + + boolean hasComponent() { + return getComponentCount() > 0; + } + } private record EditorResult(@NotNull Editor editor, @NotNull EditorProvider provider) {} diff --git a/modules/platform-ui/src/main/resources/themes/FlatDarkLaf.properties b/modules/platform-ui/src/main/resources/themes/FlatDarkLaf.properties index 21803085b..45f02c559 100644 --- a/modules/platform-ui/src/main/resources/themes/FlatDarkLaf.properties +++ b/modules/platform-ui/src/main/resources/themes/FlatDarkLaf.properties @@ -7,7 +7,9 @@ Icon.accentColor = saturate(lighten(@accentBaseColor, 5%), 10%) Icon.accentColor2 = fade($Icon.accentColor, 20%) # Validation +Component.error.borderColor = #875f61 Component.error.background = darken($Component.error.borderColor, 10%) +Component.warning.borderColor = #8d8663 Component.warning.background = darken($Component.warning.borderColor, 10%) # Dialog diff --git a/modules/platform-ui/src/main/resources/themes/FlatLaf.properties b/modules/platform-ui/src/main/resources/themes/FlatLaf.properties index a93225619..1cd655af7 100644 --- a/modules/platform-ui/src/main/resources/themes/FlatLaf.properties +++ b/modules/platform-ui/src/main/resources/themes/FlatLaf.properties @@ -34,8 +34,11 @@ Button.toolbar.margin = 2,2,2,2 # Validation ToolTip.border = 4,8,4,8 -Component.error.background = lighten($Component.error.borderColor, 10%) -Component.warning.background = lighten($Component.warning.borderColor, 10%) + +Component.error.borderColor = #fad4d8 +Component.error.background = lighten($Component.error.borderColor, 4%) +Component.warning.borderColor = #fedd77 +Component.warning.background = lighten($Component.warning.borderColor, 15%) # Dialog Dialog.buttonBackground = darken(@background,3%)