containersWithChild = new ArrayList<>(containers.size() + 1);
27 | containersWithChild.addAll(containers);
28 | containersWithChild.add(child);
29 | return new DockContainerPath(containersWithChild);
30 | }
31 |
32 | /**
33 | * @param child
34 | * Child to append to new path.
35 | *
36 | * @return New path with the given child at the end.
37 | */
38 | @Nonnull
39 | public DockablePath withChild(@Nonnull Dockable child) {
40 | return new DockablePath(containers, child);
41 | }
42 |
43 | @Nonnull
44 | @Override
45 | public DockContainer rootContainer() {
46 | // There must always be at least one container since we must have a result for the path.
47 | return containers.getFirst();
48 | }
49 |
50 | /**
51 | * @return Tail container in the path / intended target of the path.
52 | */
53 | @Nonnull
54 | public DockContainer tailContainer() {
55 | // There must always be at least one container since we must have a result for the path.
56 | return containers.getLast();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/layout/container/DockContainerRootBranch.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.layout.container;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.scene.Scene;
6 | import javafx.scene.layout.Region;
7 | import software.coley.bentofx.Bento;
8 | import software.coley.bentofx.control.DragDropStage;
9 | import software.coley.bentofx.path.DockContainerPath;
10 |
11 | import java.util.Collections;
12 |
13 | import static software.coley.bentofx.util.BentoStates.PSEUDO_ROOT;
14 |
15 | /**
16 | * Root branch container.
17 | *
18 | * @author Matt Coley
19 | */
20 | public class DockContainerRootBranch extends DockContainerBranch {
21 | private final DockContainerPath path = new DockContainerPath(Collections.singletonList(this));
22 |
23 | /**
24 | * @param bento
25 | * Parent bento instance.
26 | * @param identifier
27 | * This container's identifier.
28 | */
29 | public DockContainerRootBranch(@Nonnull Bento bento, @Nonnull String identifier) {
30 | super(bento, identifier);
31 |
32 | pseudoClassStateChanged(PSEUDO_ROOT, true);
33 |
34 | sceneProperty().addListener((on, old, cur) -> {
35 | if (cur != null) {
36 | bento.registerRoot(this);
37 | } else {
38 | bento.unregisterRoot(this);
39 | }
40 | });
41 | }
42 |
43 | @Override
44 | public boolean removeFromParent() {
45 | Region thisAsRegion = asRegion();
46 | Scene scene = thisAsRegion.getScene();
47 | if (scene != null
48 | && scene.getRoot() == thisAsRegion
49 | && scene.getWindow() instanceof DragDropStage ddStage
50 | && ddStage.isAutoCloseWhenEmpty()) {
51 | ddStage.close();
52 | return true;
53 | }
54 | return false;
55 | }
56 |
57 | @Nullable
58 | @Override
59 | public DockContainerBranch getParentContainer() {
60 | return null;
61 | }
62 |
63 | @Override
64 | public void setParentContainer(@Nonnull DockContainerBranch parent) {
65 | throw new IllegalStateException("Root should not have a parent container assigned");
66 | }
67 |
68 |
69 | @Nonnull
70 | @Override
71 | public DockContainerPath getPath() {
72 | return path;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/dockable/DragDropBehavior.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.dockable;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.geometry.Side;
6 | import software.coley.bentofx.layout.container.DockContainerLeaf;
7 |
8 | /**
9 | * Drag drop operations.
10 | *
11 | * @author Matt Coley
12 | */
13 | public class DragDropBehavior {
14 | /**
15 | * Determines if a given dockable can be placed into a container.
16 | * Generally this is used to control how the {@link Dockable#getDragGroupMask()} behaves.
17 | * You can override this method to support alternative grouping models.
18 | *
19 | * For example, the default implementation is a simple equality check.
20 | * Any dockable can be put into a container that has other dockables of the same mask.
21 | * {@code
22 | * return targetContainer.getDockables().stream()
23 | * .anyMatch(d -> d.getDragMask() == dockable.getDragMask());
24 | * }
25 | *
26 | * As an alternative, you can make the mask... more like a mask!
27 | * In this example, drag groups are specified as bit-masks, allowing more fine-control over
28 | * what can go where.
29 | * {@code
30 | * return targetContainer.getDockables().stream()
31 | * .anyMatch(d -> (d.getDragMask() & dockable.getDragMask()) != 0);
32 | * }
33 | *
34 | * @param targetContainer
35 | * Target container the dockable is dragged over.
36 | * @param targetSide
37 | * The side the dockable will be dropped to as part of a DnD operation into the target container.
38 | * @param dockable
39 | * Some dockable being dragged.
40 | *
41 | * @return {@code true} when this container can receive the dockable.
42 | */
43 | public boolean canReceiveDockable(@Nonnull DockContainerLeaf targetContainer,
44 | @Nullable Side targetSide,
45 | @Nonnull Dockable dockable) {
46 | // The incoming dockable must have a compatible group.
47 | return targetContainer.getDockables().stream()
48 | .anyMatch(d -> d.getDragGroupMask() == dockable.getDragGroupMask());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/canvas/ArgbImageSource.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control.canvas;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.image.Image;
5 | import javafx.scene.image.PixelFormat;
6 |
7 | import java.nio.IntBuffer;
8 | import java.util.Arrays;
9 |
10 | /**
11 | * ARGB source wrapping an {@link Image}.
12 | *
13 | * @author Matt Coley
14 | */
15 | public class ArgbImageSource implements ArgbSource {
16 | private final Image image;
17 | private int[] fullArgbCache;
18 | private int hash;
19 |
20 | /**
21 | * @param image
22 | * Wrapped image.
23 | */
24 | public ArgbImageSource(@Nonnull Image image) {
25 | this.image = image;
26 | }
27 |
28 | @Override
29 | public int getWidth() {
30 | return (int) image.getWidth();
31 | }
32 |
33 | @Override
34 | public int getHeight() {
35 | return (int) image.getHeight();
36 | }
37 |
38 | @Override
39 | public int getArgb(int x, int y) {
40 | try {
41 | return image.getPixelReader().getArgb(x, y);
42 | } catch (Throwable t) {
43 | // Thrown when coordinates are out of bounds.
44 | // Default to transparent black.
45 | return 0;
46 | }
47 | }
48 |
49 | @Override
50 | public int[] getArgb(int x, int y, int width, int height) {
51 | try {
52 | IntBuffer buffer = IntBuffer.allocate(width * height);
53 | image.getPixelReader().getPixels(x, y, width, height, PixelFormat.getIntArgbInstance(), buffer, width);
54 | return buffer.array();
55 | } catch (Throwable t) {
56 | // Thrown when coordinates are out of bounds.
57 | return null;
58 | }
59 | }
60 |
61 | @Nonnull
62 | @Override
63 | public int[] getArgb() {
64 | // We will likely be using this a bit, so it makes sense to cache the result.
65 | if (fullArgbCache == null)
66 | fullArgbCache = ArgbSource.super.getArgb();
67 | return fullArgbCache;
68 | }
69 |
70 | @Override
71 | public boolean equals(Object o) {
72 | if (this == o) return true;
73 | if (!(o instanceof ArgbImageSource that)) return false;
74 |
75 | if (!image.equals(that.image)) return false;
76 | return Arrays.equals(fullArgbCache, that.fullArgbCache);
77 | }
78 |
79 | @Override
80 | public int hashCode() {
81 | if (hash == 0)
82 | hash = Arrays.hashCode(getArgb());
83 | return hash;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/canvas/PixelPainterByteBgraPre.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control.canvas;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.image.PixelFormat;
5 | import javafx.scene.image.PixelWriter;
6 |
7 | import java.nio.ByteBuffer;
8 |
9 | /**
10 | * Pixel painter instance backed by {@link PixelFormat#getByteBgraPreInstance()}.
11 | *
12 | * @author Matt Coley
13 | */
14 | public class PixelPainterByteBgraPre extends PixelPainterByteBgra {
15 | @Override
16 | public void fillRect(int x, int y, int width, int height, int color) {
17 | int alpha = (color >>> 24);
18 | int red, green, blue;
19 | if (alpha > 0x00) {
20 | red = (color >> 16) & 0xFF;
21 | green = (color >> 8) & 0xFF;
22 | blue = (color) & 0xFF;
23 | if (alpha < 0xFF) {
24 | red = (red * alpha + 127) / 0xFF;
25 | green = (green * alpha + 127) / 0xFF;
26 | blue = (blue * alpha + 127) / 0xFF;
27 | }
28 | } else {
29 | red = green = blue = 0;
30 | }
31 | int yBound = Math.min(y + height, imageHeight);
32 | int xBound = Math.min(x + width, imageWidth);
33 | ByteBuffer drawBuffer = this.drawBuffer;
34 | int capacity = drawBufferCapacity();
35 | for (int ly = y; ly < yBound; ly++) {
36 | int yOffset = ly * imageWidth;
37 | for (int lx = x; lx < xBound; lx++) {
38 | int index = (yOffset + lx) * DATA_SIZE;
39 | if (index < capacity) {
40 | drawBuffer.put(index, (byte) blue);
41 | drawBuffer.put(index + 1, (byte) green);
42 | drawBuffer.put(index + 2, (byte) red);
43 | drawBuffer.put(index + 3, (byte) alpha);
44 | }
45 | }
46 | }
47 | }
48 |
49 | @Override
50 | public void setColor(int x, int y, int color) {
51 | int i = ((y * imageWidth) + x) * DATA_SIZE;
52 | if (i >= 0 && i < drawBufferCapacity()) {
53 | int alpha = (color >>> 24);
54 | int red, green, blue;
55 | if (alpha > 0x00) {
56 | red = (color >> 16) & 0xFF;
57 | green = (color >> 8) & 0xFF;
58 | blue = (color) & 0xFF;
59 | if (alpha < 0xFF) {
60 | red = (red * alpha + 127) / 0xFF;
61 | green = (green * alpha + 127) / 0xFF;
62 | blue = (blue * alpha + 127) / 0xFF;
63 | }
64 | } else {
65 | red = green = blue = 0;
66 | }
67 | drawBuffer.put(i, (byte) blue);
68 | drawBuffer.put(i + 1, (byte) green);
69 | drawBuffer.put(i + 2, (byte) red);
70 | drawBuffer.put(i + 3, (byte) alpha);
71 | }
72 | }
73 |
74 | @Nonnull
75 | @Override
76 | public PixelFormat getPixelFormat() {
77 | return PixelFormat.getByteBgraPreInstance();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/building/PlaceholderBuilding.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.building;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.Node;
5 | import javafx.scene.layout.Pane;
6 | import software.coley.bentofx.dockable.Dockable;
7 | import software.coley.bentofx.dockable.DockablePlaceholderFactory;
8 | import software.coley.bentofx.layout.container.DockContainerLeaf;
9 | import software.coley.bentofx.layout.container.DockContainerLeafPlaceholderFactory;
10 |
11 | /**
12 | * Builders for placeholder content when:
13 | *
14 | * - A {@link Dockable} is selected in a {@link DockContainerLeaf} but has no content to show
15 | * - A {@link DockContainerLeaf} has no selected dockable
16 | *
17 | *
18 | * @author Matt Coley
19 | */
20 | public class PlaceholderBuilding implements DockablePlaceholderFactory, DockContainerLeafPlaceholderFactory {
21 | private DockablePlaceholderFactory dockablePlaceholderFactory = dockable -> new Pane();
22 | private DockContainerLeafPlaceholderFactory containerPlaceholderFactory = container -> new Pane();
23 |
24 | /**
25 | * @return Current placeholder factory for dockables with no content to show.
26 | */
27 | @Nonnull
28 | public DockablePlaceholderFactory getDockablePlaceholderFactory() {
29 | return dockablePlaceholderFactory;
30 | }
31 |
32 | /**
33 | * @param dockablePlaceholderFactory
34 | * Placeholder factory for dockables with no content to show.
35 | */
36 | public void setDockablePlaceholderFactory(@Nonnull DockablePlaceholderFactory dockablePlaceholderFactory) {
37 | this.dockablePlaceholderFactory = dockablePlaceholderFactory;
38 | }
39 |
40 | /**
41 | * @return Current placeholder factory for containers with no content to show.
42 | */
43 | @Nonnull
44 | public DockContainerLeafPlaceholderFactory getContainerPlaceholderFactory() {
45 | return containerPlaceholderFactory;
46 | }
47 |
48 | /**
49 | * @param containerPlaceholderFactory
50 | * Placeholder factory for containers with no content to show.
51 | */
52 | public void setContainerPlaceholderFactory(@Nonnull DockContainerLeafPlaceholderFactory containerPlaceholderFactory) {
53 | this.containerPlaceholderFactory = containerPlaceholderFactory;
54 | }
55 |
56 | @Nonnull
57 | @Override
58 | public Node build(@Nonnull Dockable dockable) {
59 | return getDockablePlaceholderFactory().build(dockable);
60 | }
61 |
62 | @Nonnull
63 | @Override
64 | public Node build(@Nonnull DockContainerLeaf container) {
65 | return getContainerPlaceholderFactory().build(container);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/building/DockBuilding.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.building;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import software.coley.bentofx.Bento;
5 | import software.coley.bentofx.dockable.Dockable;
6 | import software.coley.bentofx.layout.DockContainer;
7 | import software.coley.bentofx.layout.container.DockContainerBranch;
8 | import software.coley.bentofx.layout.container.DockContainerLeaf;
9 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
10 |
11 | import java.util.Random;
12 |
13 | /**
14 | * Builders for {@link DockContainer} and {@link Dockable} instances.
15 | *
16 | * @author Matt Coley
17 | */
18 | public class DockBuilding {
19 | private static final Random RANDOM = new Random();
20 | private final Bento bento;
21 |
22 | /**
23 | * @param bento
24 | * Parent bento instance.
25 | */
26 | public DockBuilding(@Nonnull Bento bento) {
27 | this.bento = bento;
28 | }
29 |
30 | /**
31 | * @return New dockable.
32 | */
33 | @Nonnull
34 | public Dockable dockable() {
35 | return dockable(uid("dockable"));
36 | }
37 |
38 | /**
39 | * @param identifier
40 | * Identifier to assign to the created dockable.
41 | *
42 | * @return New dockable.
43 | */
44 | @Nonnull
45 | public Dockable dockable(@Nonnull String identifier) {
46 | return new Dockable(bento, identifier);
47 | }
48 |
49 | /**
50 | * @return New root container.
51 | *
52 | * @see Bento#registerRoot(DockContainerRootBranch)
53 | * @see Bento#unregisterRoot(DockContainerRootBranch)
54 | */
55 | @Nonnull
56 | public DockContainerRootBranch root() {
57 | return root(uid("croot"));
58 | }
59 |
60 |
61 | /**
62 | * @param identifier
63 | * Identifier to assign to the created container.
64 | *
65 | * @return New root container.
66 | *
67 | * @see Bento#registerRoot(DockContainerRootBranch)
68 | * @see Bento#unregisterRoot(DockContainerRootBranch)
69 | */
70 | @Nonnull
71 | public DockContainerRootBranch root(@Nonnull String identifier) {
72 | return new DockContainerRootBranch(bento, identifier);
73 | }
74 |
75 | /**
76 | * @return New branch container.
77 | */
78 | @Nonnull
79 | public DockContainerBranch branch() {
80 | return branch(uid("cbranch"));
81 | }
82 |
83 | /**
84 | * @param identifier
85 | * Identifier to assign to the created container.
86 | *
87 | * @return New branch container.
88 | */
89 | @Nonnull
90 | public DockContainerBranch branch(@Nonnull String identifier) {
91 | return new DockContainerBranch(bento, identifier);
92 | }
93 |
94 | /**
95 | * @return New leaf container.
96 | */
97 | @Nonnull
98 | public DockContainerLeaf leaf() {
99 | return leaf(uid("cleaf"));
100 | }
101 |
102 | /**
103 | * @param identifier
104 | * Identifier to assign to the created container.
105 | *
106 | * @return New branch container.
107 | */
108 | @Nonnull
109 | public DockContainerLeaf leaf(@Nonnull String identifier) {
110 | return new DockContainerLeaf(bento, identifier);
111 | }
112 |
113 | @Nonnull
114 | private static String uid(@Nonnull String prefix) {
115 | StringBuilder suffix = new StringBuilder(8);
116 | for (int i = 0; i < 8; i++)
117 | suffix.append((char) RANDOM.nextInt('A', 'Z'));
118 | return prefix + ":" + suffix;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/DragDropStage.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control;
2 |
3 | import javafx.scene.Parent;
4 | import javafx.scene.Scene;
5 | import javafx.scene.layout.Region;
6 | import javafx.stage.Stage;
7 | import javafx.stage.WindowEvent;
8 | import software.coley.bentofx.building.StageBuilding;
9 | import software.coley.bentofx.dockable.Dockable;
10 | import software.coley.bentofx.layout.DockContainer;
11 | import software.coley.bentofx.layout.container.DockContainerBranch;
12 | import software.coley.bentofx.layout.container.DockContainerLeaf;
13 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
14 |
15 | import java.lang.ref.WeakReference;
16 | import java.util.List;
17 |
18 | /**
19 | * Stage subtype created by {@link StageBuilding#newStageForDockable(Scene, DockContainerRootBranch, DockContainerLeaf, Dockable, double, double)}
20 | *
21 | * @author Matt Coley
22 | */
23 | public class DragDropStage extends Stage {
24 | private final boolean autoCloseWhenEmpty;
25 | private WeakReference content;
26 |
27 | /**
28 | * @param autoCloseWhenEmpty
29 | * Flag to determine if this stage should auto-close if its sole content is removed.
30 | * See {@link #isAutoCloseWhenEmpty()} for more details.
31 | */
32 | public DragDropStage(boolean autoCloseWhenEmpty) {
33 | this.autoCloseWhenEmpty = autoCloseWhenEmpty;
34 |
35 | // Cancel closure if headers that are not closable exist.
36 | addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, e -> {
37 | Parent root = getScene().getRoot();
38 | if (root instanceof DockContainerBranch rootBranch) {
39 | boolean canClose = true;
40 |
41 | // Try to close all dockables and track if any remain.
42 | List dockables = rootBranch.getDockables();
43 | for (Dockable dockable : dockables) {
44 | DockContainer container = dockable.getContainer();
45 | if (container != null && !container.closeDockable(dockable))
46 | canClose = false;
47 | }
48 |
49 | // If some headers remain, abort the close.
50 | if (!canClose)
51 | e.consume();
52 | }
53 | });
54 |
55 | // Add event filters to clear/restore the scene contents when hiding/showing.
56 | // Each respective action will trigger the root contents listeners that handle registering/unregistering.
57 | addEventFilter(WindowEvent.WINDOW_HIDDEN, e -> {
58 | Parent parent = getScene().getRoot();
59 | if (parent != null)
60 | content = new WeakReference<>(parent);
61 | getScene().setRoot(new Region());
62 | });
63 | addEventFilter(WindowEvent.WINDOW_SHOWN, e -> {
64 | if (content != null) {
65 | getScene().setRoot(content.get());
66 | content.clear();
67 | content = null;
68 | }
69 | });
70 | }
71 |
72 | /**
73 | * Context:These stages are created when a user drags a {@link Header} into empty space.
74 | *
75 | * Most of the time, if a user drags the {@link Header} from this stage into some other place in another stage,
76 | * leaving this stage with nothing in {@link DockContainer} it would be ideal to automatically
77 | * close this window.
78 | *
79 | * When this is {@code true} we do just that.
80 | *
81 | * @return {@code true} when this stage should auto-close if its {@link DockContainer} is cleared/removed.
82 | */
83 | public boolean isAutoCloseWhenEmpty() {
84 | return autoCloseWhenEmpty;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/assets/containers.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/canvas/PixelPainterIntArgb.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control.canvas;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.image.PixelFormat;
5 | import javafx.scene.image.PixelWriter;
6 |
7 | import java.nio.IntBuffer;
8 | import java.util.Arrays;
9 |
10 | /**
11 | * Pixel painter instance backed by {@link PixelFormat#getIntArgbInstance()}.
12 | *
13 | * @author Matt Coley
14 | */
15 | public class PixelPainterIntArgb implements PixelPainter {
16 | /** ARGB pixel buffer to draw with. */
17 | protected IntBuffer drawBuffer = PixelPainterUtils.EMPTY_BUFFER_I;
18 | /** Current width of an image. */
19 | protected int imageWidth;
20 | /** Current height of an image. */
21 | protected int imageHeight;
22 |
23 | @Override
24 | public boolean initialize(int width, int height) {
25 | if (imageWidth != width || imageHeight != height) {
26 | imageWidth = width;
27 | imageHeight = height;
28 | int drawBufferCapacity = drawBufferCapacity();
29 | if (drawBufferCapacity > drawBuffer.limit()) {
30 | drawBuffer = IntBuffer.wrap(new int[drawBufferCapacity]);
31 | return true;
32 | }
33 | }
34 | clear();
35 | return false;
36 | }
37 |
38 | @Override
39 | public void release() {
40 | imageWidth = 0;
41 | imageHeight = 0;
42 | drawBuffer = PixelPainterUtils.EMPTY_BUFFER_I;
43 | }
44 |
45 | @Override
46 | public void commit(@Nonnull PixelWriter pixelWriter) {
47 | pixelWriter.setPixels(
48 | 0,
49 | 0,
50 | imageWidth,
51 | imageHeight,
52 | getPixelFormat(),
53 | drawBuffer,
54 | imageWidth
55 | );
56 | }
57 |
58 | @Override
59 | public void fillRect(int x, int y, int width, int height, int color) {
60 | int yBound = Math.min(y + height, imageHeight);
61 | int xBound = Math.min(x + width, imageWidth);
62 | IntBuffer drawBuffer = this.drawBuffer;
63 | int capacity = drawBufferCapacity();
64 | for (int ly = y; ly < yBound; ly++) {
65 | int yOffset = ly * imageWidth;
66 | for (int lx = x; lx < xBound; lx++) {
67 | int index = yOffset + lx;
68 | if (index < capacity)
69 | drawBuffer.put(index, color);
70 | }
71 | }
72 | }
73 |
74 | @Override
75 | public void drawImage(int x, int y, @Nonnull ArgbSource source) {
76 | int sourceWidth = source.getWidth();
77 | int sourceHeight = source.getHeight();
78 | int[] argb = source.getArgb();
79 | int yBound = Math.min(y + sourceHeight, imageHeight);
80 | int xBound = Math.min(x + sourceWidth, imageWidth);
81 | for (int ly = y; ly < yBound; ly++) {
82 | int yOffsetSource = (ly - y) * sourceWidth;
83 | for (int lx = x; lx < xBound; lx++) {
84 | int sourceIndex = yOffsetSource + (lx - x);
85 | if (sourceIndex < argb.length)
86 | setColor(lx, ly, argb[sourceIndex]);
87 | }
88 | }
89 | }
90 |
91 | @Override
92 | public void drawImage(int x, int y, int sx, int sy, int sw, int sh, @Nonnull ArgbSource source) {
93 | int[] argb = source.getArgb(sx, sy, sw, sh);
94 | if (argb == null)
95 | return;
96 | int yBound = Math.min(y + sh, imageHeight);
97 | int xBound = Math.min(x + sw, imageWidth);
98 | for (int ly = y; ly < yBound; ly++) {
99 | int yOffsetSource = (ly - y) * sw;
100 | for (int lx = x; lx < xBound; lx++) {
101 | int sourceIndex = yOffsetSource + (lx - x);
102 | if (sourceIndex < argb.length)
103 | setColor(lx, ly, argb[sourceIndex]);
104 | }
105 | }
106 | }
107 |
108 | @Override
109 | public void setColor(int x, int y, int color) {
110 | int i = adapt(x, y);
111 | if (i >= 0 && i < drawBufferCapacity())
112 | drawBuffer.put(i, color);
113 | }
114 |
115 | @Override
116 | public void clear() {
117 | Arrays.fill(drawBuffer.array(), 0, drawBufferCapacity(), 0);
118 | }
119 |
120 | @Nonnull
121 | @Override
122 | public IntBuffer getBuffer() {
123 | return drawBuffer;
124 | }
125 |
126 | @Nonnull
127 | @Override
128 | public PixelFormat getPixelFormat() {
129 | return PixelFormat.getIntArgbInstance();
130 | }
131 |
132 | protected int adapt(int x, int y) {
133 | return (y * imageWidth) + x;
134 | }
135 |
136 | protected int drawBufferCapacity() {
137 | return imageWidth * imageHeight;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/layout/DockContainer.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.layout;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.scene.layout.Region;
6 | import software.coley.bentofx.BentoBacked;
7 | import software.coley.bentofx.Identifiable;
8 | import software.coley.bentofx.dockable.Dockable;
9 | import software.coley.bentofx.layout.container.DockContainerBranch;
10 | import software.coley.bentofx.layout.container.DockContainerLeaf;
11 | import software.coley.bentofx.path.DockContainerPath;
12 | import software.coley.bentofx.search.SearchVisitor;
13 |
14 | import java.util.Collections;
15 | import java.util.List;
16 |
17 | /**
18 | * Outlines the types of containers housing dockable content.
19 | *
20 | * @author Matt Coley
21 | * @see DockContainerBranch
22 | * @see DockContainerLeaf
23 | */
24 | public sealed interface DockContainer extends BentoBacked, Identifiable permits DockContainerBranch, DockContainerLeaf {
25 | /**
26 | * @return Path to this container from the root container that holds this container.
27 | */
28 | @Nonnull
29 | default DockContainerPath getPath() {
30 | DockContainer parent = getParentContainer();
31 | if (parent != null)
32 | return parent.getPath().withChild(this);
33 | return new DockContainerPath(Collections.singletonList(this));
34 | }
35 |
36 | /**
37 | * @return Parent container that holds this container. {@code null} when this container is a root.
38 | */
39 | @Nullable
40 | DockContainerBranch getParentContainer();
41 |
42 | /**
43 | * Record the given container as this container's parent.
44 | * Does not actually mutate the hierarchy and is just for state tracking.
45 | *
46 | * @param container
47 | * Container to assign as this container's parent.
48 | */
49 | void setParentContainer(@Nonnull DockContainerBranch container);
50 |
51 | /**
52 | * Remove the given container as this container's parent.
53 | * Does not actually mutate the hierarchy and is just for state tracking.
54 | *
55 | * @param parent
56 | * Container to remove as this container's parent.
57 | */
58 | void removeAsParentContainer(@Nonnull DockContainerBranch parent);
59 |
60 | /**
61 | * @param visitor
62 | * Visitor to control continued traversal.
63 | *
64 | * @return {@code true} when the visit shall continue.
65 | */
66 | boolean visit(@Nonnull SearchVisitor visitor);
67 |
68 | /**
69 | * @return Unmodifiable list of dockables within this container.
70 | */
71 | @Nonnull
72 | List getDockables();
73 |
74 | /**
75 | * @param dockables
76 | * Dockables to add.
77 | *
78 | * @return {@code true} if one or more of the dockables were added.
79 | */
80 | default boolean addDockables(@Nonnull Dockable... dockables) {
81 | boolean changed = false;
82 | for (Dockable dockable : dockables)
83 | changed |= addDockable(dockable);
84 | return changed;
85 | }
86 |
87 | /**
88 | * @param dockable
89 | * Dockable to add.
90 | *
91 | * @return {@code true} when added.
92 | */
93 | boolean addDockable(@Nonnull Dockable dockable);
94 |
95 | /**
96 | * @param dockable
97 | * Dockable to add.
98 | * @param index
99 | * Index to add the dockable at.
100 | *
101 | * @return {@code true} when added.
102 | */
103 | boolean addDockable(int index, @Nonnull Dockable dockable);
104 |
105 | /**
106 | * @param dockable
107 | * Dockable to remove.
108 | *
109 | * @return {@code true} when removed.
110 | */
111 | boolean removeDockable(@Nonnull Dockable dockable);
112 |
113 | /**
114 | * @param dockable
115 | * Dockable to close and then remove.
116 | *
117 | * @return {@code true} when removed.
118 | */
119 | boolean closeDockable(@Nonnull Dockable dockable);
120 |
121 | /**
122 | * Remove this container within the {@link #getParentContainer() parent container}.
123 | *
124 | * @return {@code true} when removed.
125 | */
126 | default boolean removeFromParent() {
127 | DockContainerBranch parent = getParentContainer();
128 | if (parent != null)
129 | return parent.removeContainer(this);
130 | return false;
131 | }
132 |
133 | /**
134 | * @return {@code true} to {@link #removeFromParent() prune} when this container has no remaining dockables.
135 | */
136 | boolean doPruneWhenEmpty();
137 |
138 | /**
139 | * @param pruneWhenEmpty
140 | * {@code true} to {@link #removeFromParent() prune} when this container has no remaining dockables.
141 | */
142 | void setPruneWhenEmpty(boolean pruneWhenEmpty);
143 |
144 | /**
145 | * @return Self, cast to region.
146 | */
147 | @Nonnull
148 | default Region asRegion() {
149 | return (Region) this;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/assets/controls.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/canvas/PixelPainterByteBgra.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control.canvas;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.image.PixelFormat;
5 | import javafx.scene.image.PixelWriter;
6 |
7 | import java.nio.ByteBuffer;
8 | import java.util.Arrays;
9 |
10 | /**
11 | * Pixel painter instance backed by {@link PixelFormat#getByteBgraInstance()}.
12 | *
13 | * @author Matt Coley
14 | */
15 | public class PixelPainterByteBgra implements PixelPainter {
16 | protected static final int DATA_SIZE = 4;
17 | /** ARGB pixel buffer to draw with. */
18 | protected ByteBuffer drawBuffer = PixelPainterUtils.EMPTY_BUFFER_B;
19 | /** Current width of an image. */
20 | protected int imageWidth;
21 | /** Current height of an image. */
22 | protected int imageHeight;
23 |
24 | @Override
25 | public boolean initialize(int width, int height) {
26 | if (imageWidth != width || imageHeight != height) {
27 | imageWidth = width;
28 | imageHeight = height;
29 | int drawBufferCapacity = drawBufferCapacity();
30 | if (drawBufferCapacity > drawBuffer.limit()) {
31 | drawBuffer = ByteBuffer.wrap(new byte[drawBufferCapacity]);
32 | return true;
33 | }
34 | }
35 | clear();
36 | return false;
37 | }
38 |
39 | @Override
40 | public void release() {
41 | imageWidth = 0;
42 | imageHeight = 0;
43 | drawBuffer = PixelPainterUtils.EMPTY_BUFFER_B;
44 | }
45 |
46 | @Override
47 | public void commit(@Nonnull PixelWriter pixelWriter) {
48 | pixelWriter.setPixels(
49 | 0,
50 | 0,
51 | imageWidth,
52 | imageHeight,
53 | getPixelFormat(),
54 | drawBuffer,
55 | imageWidth * DATA_SIZE
56 | );
57 | }
58 |
59 | @Override
60 | public void fillRect(int x, int y, int width, int height, int color) {
61 | byte alpha = (byte) ((color >> 24) & 0xFF);
62 | byte red = (byte) ((color >> 16) & 0xFF);
63 | byte green = (byte) ((color >> 8) & 0xFF);
64 | byte blue = (byte) (color & 0xFF);
65 | int yBound = Math.min(y + height, imageHeight);
66 | int xBound = Math.min(x + width, imageWidth);
67 | ByteBuffer drawBuffer = this.drawBuffer;
68 | int capacity = drawBufferCapacity();
69 | for (int ly = y; ly < yBound; ly++) {
70 | int yOffset = ly * imageWidth;
71 | for (int lx = x; lx < xBound; lx++) {
72 | int index = (yOffset + lx) * DATA_SIZE;
73 | if (index < capacity) {
74 | drawBuffer.put(index, blue);
75 | drawBuffer.put(index + 1, green);
76 | drawBuffer.put(index + 2, red);
77 | drawBuffer.put(index + 3, alpha);
78 | }
79 | }
80 | }
81 | }
82 |
83 | @Override
84 | public void drawImage(int x, int y, @Nonnull ArgbSource source) {
85 | int sourceWidth = source.getWidth();
86 | int sourceHeight = source.getHeight();
87 | int[] argb = source.getArgb(0, 0, sourceWidth, sourceHeight);
88 | if (argb == null)
89 | return;
90 | int yBound = Math.min(y + sourceHeight, imageHeight);
91 | int xBound = Math.min(x + sourceWidth, imageWidth);
92 | for (int ly = y; ly < yBound; ly++) {
93 | int yOffsetSource = (ly - y) * sourceWidth;
94 | for (int lx = x; lx < xBound; lx++) {
95 | int sourceIndex = yOffsetSource + (lx - x);
96 | if (sourceIndex < argb.length)
97 | setColor(lx, ly, argb[sourceIndex]);
98 | }
99 | }
100 | }
101 |
102 | @Override
103 | public void drawImage(int x, int y, int sx, int sy, int sw, int sh, @Nonnull ArgbSource source) {
104 | int[] argb = source.getArgb(sx, sy, sw, sh);
105 | if (argb == null)
106 | return;
107 | int yBound = Math.min(y + sh, imageHeight);
108 | int xBound = Math.min(x + sw, imageWidth);
109 | for (int ly = y; ly < yBound; ly++) {
110 | int yOffsetSource = (ly - y) * sw;
111 | for (int lx = x; lx < xBound; lx++) {
112 | int sourceIndex = yOffsetSource + (lx - x);
113 | if (sourceIndex < argb.length)
114 | setColor(lx, ly, argb[sourceIndex]);
115 | }
116 | }
117 | }
118 |
119 | @Override
120 | public void setColor(int x, int y, int color) {
121 | int i = ((y * imageWidth) + x) * DATA_SIZE;
122 | if (i >= 0 && i < drawBufferCapacity()) {
123 | byte alpha = (byte) ((color >> 24) & 0xFF);
124 | byte red = (byte) ((color >> 16) & 0xFF);
125 | byte green = (byte) ((color >> 8) & 0xFF);
126 | byte blue = (byte) (color & 0xFF);
127 | drawBuffer.put(i, blue);
128 | drawBuffer.put(i + 1, green);
129 | drawBuffer.put(i + 2, red);
130 | drawBuffer.put(i + 3, alpha);
131 | }
132 | }
133 |
134 | @Override
135 | public void clear() {
136 | Arrays.fill(drawBuffer.array(), 0, drawBufferCapacity(), (byte) 0);
137 | }
138 |
139 | @Nonnull
140 | @Override
141 | public ByteBuffer getBuffer() {
142 | return drawBuffer;
143 | }
144 |
145 | @Nonnull
146 | @Override
147 | public PixelFormat getPixelFormat() {
148 | return PixelFormat.getByteBgraInstance();
149 | }
150 |
151 | protected int drawBufferCapacity() {
152 | return (imageWidth * imageHeight) * DATA_SIZE;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/Bento.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.collections.FXCollections;
5 | import javafx.collections.ObservableList;
6 | import software.coley.bentofx.building.ControlsBuilding;
7 | import software.coley.bentofx.building.DockBuilding;
8 | import software.coley.bentofx.building.PlaceholderBuilding;
9 | import software.coley.bentofx.building.StageBuilding;
10 | import software.coley.bentofx.control.DragDropStage;
11 | import software.coley.bentofx.dockable.Dockable;
12 | import software.coley.bentofx.dockable.DragDropBehavior;
13 | import software.coley.bentofx.event.DockEvent;
14 | import software.coley.bentofx.event.EventBus;
15 | import software.coley.bentofx.layout.DockContainer;
16 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
17 | import software.coley.bentofx.search.SearchHandler;
18 |
19 | /**
20 | * Top level controller for docking operations.
21 | *
22 | * @author Matt Coley
23 | */
24 | public class Bento {
25 | private final ObservableList rootContainers = FXCollections.observableArrayList();
26 | private final ObservableList rootContainersView = FXCollections.unmodifiableObservableList(rootContainers);
27 | private final EventBus eventBus = newEventBus();
28 | private final SearchHandler searchHandler = newSearchHandler();
29 | private final StageBuilding stageBuilding = newStageBuilding();
30 | private final ControlsBuilding controlsBuilding = newControlsBuilding();
31 | private final DockBuilding dockBuilding = newDockBuilding();
32 | private final PlaceholderBuilding placeholderBuilding = newPlaceholderBuilding();
33 | private final DragDropBehavior dragDropBehavior = newDragDropBehavior();
34 |
35 | @Nonnull
36 | protected EventBus newEventBus() {
37 | return new EventBus();
38 | }
39 |
40 | @Nonnull
41 | protected SearchHandler newSearchHandler() {
42 | return new SearchHandler(this);
43 | }
44 |
45 | @Nonnull
46 | protected StageBuilding newStageBuilding() {
47 | return new StageBuilding(this);
48 | }
49 |
50 | @Nonnull
51 | protected ControlsBuilding newControlsBuilding() {
52 | return new ControlsBuilding();
53 | }
54 |
55 | @Nonnull
56 | protected DockBuilding newDockBuilding() {
57 | return new DockBuilding(this);
58 | }
59 |
60 | @Nonnull
61 | protected PlaceholderBuilding newPlaceholderBuilding() {
62 | return new PlaceholderBuilding();
63 | }
64 |
65 | @Nonnull
66 | protected DragDropBehavior newDragDropBehavior() {
67 | return null;
68 | }
69 |
70 | /**
71 | * @return Bus for handling event firing and event listeners.
72 | */
73 | @Nonnull
74 | public EventBus events() {
75 | return eventBus;
76 | }
77 |
78 | /**
79 | * @return Search operations.
80 | */
81 | @Nonnull
82 | public SearchHandler search() {
83 | return searchHandler;
84 | }
85 |
86 | /**
87 | * @return Builders for {@link DragDropStage}.
88 | */
89 | @Nonnull
90 | public StageBuilding stageBuilding() {
91 | return stageBuilding;
92 | }
93 |
94 | /**
95 | * @return Builders for various bento UI controls.
96 | */
97 | @Nonnull
98 | public ControlsBuilding controlsBuilding() {
99 | return controlsBuilding;
100 | }
101 |
102 | /**
103 | * @return Builders for {@link DockContainer} and {@link Dockable}.
104 | */
105 | @Nonnull
106 | public DockBuilding dockBuilding() {
107 | return dockBuilding;
108 | }
109 |
110 | /**
111 | * @return Builders for placeholder content.
112 | */
113 | @Nonnull
114 | public PlaceholderBuilding placeholderBuilding() {
115 | return placeholderBuilding;
116 | }
117 |
118 | /**
119 | * @return Behavior implementation for drag-drop.
120 | */
121 | @Nonnull
122 | public DragDropBehavior getDragDropBehavior() {
123 | return dragDropBehavior;
124 | }
125 |
126 | /**
127 | * @return List of tracked root contents.
128 | *
129 | * @see #registerRoot(DockContainerRootBranch)
130 | * @see #unregisterRoot(DockContainerRootBranch)
131 | */
132 | @Nonnull
133 | public ObservableList getRootContainers() {
134 | return rootContainersView;
135 | }
136 |
137 | /**
138 | * @param container
139 | * Root container to register.
140 | *
141 | * @return {@code true} when registered.
142 | */
143 | public boolean registerRoot(@Nonnull DockContainerRootBranch container) {
144 | if (!rootContainers.contains(container)) {
145 | rootContainers.add(container);
146 | eventBus.fire(new DockEvent.RootContainerAdded(container));
147 | return true;
148 | }
149 | return false;
150 | }
151 |
152 | /**
153 | * @param container
154 | * Root container to unregister.
155 | *
156 | * @return {@code true} when unregistered.
157 | */
158 | public boolean unregisterRoot(@Nonnull DockContainerRootBranch container) {
159 | if (rootContainers.remove(container)) {
160 | eventBus.fire(new DockEvent.RootContainerRemoved(container));
161 | return true;
162 | }
163 | return false;
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/search/SearchHandler.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.search;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.scene.input.DragEvent;
6 | import software.coley.bentofx.Bento;
7 | import software.coley.bentofx.dockable.Dockable;
8 | import software.coley.bentofx.layout.DockContainer;
9 | import software.coley.bentofx.layout.container.DockContainerBranch;
10 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
11 | import software.coley.bentofx.path.DockContainerPath;
12 | import software.coley.bentofx.path.DockablePath;
13 | import software.coley.bentofx.util.DragUtils;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 | import java.util.Objects;
18 | import java.util.function.Predicate;
19 | import java.util.function.Supplier;
20 |
21 | /**
22 | * Search operations within a bento instance.
23 | *
24 | * @author Matt Coley
25 | */
26 | public class SearchHandler {
27 | private final Bento bento;
28 |
29 | /**
30 | * @param bento
31 | * Parent bento instance.
32 | */
33 | public SearchHandler(@Nonnull Bento bento) {
34 | this.bento = bento;
35 | }
36 |
37 | /**
38 | * @param identifier
39 | * Some {@link DockContainer#getIdentifier()}.
40 | * @param replacement
41 | * Content to replace the matched container with.
42 | *
43 | * @return {@code true} when replacement was completed.
44 | */
45 | public boolean replaceContainer(@Nonnull String identifier, @Nonnull DockContainer replacement) {
46 | return replaceContainer(identifier, () -> replacement);
47 | }
48 |
49 | /**
50 | * @param identifier
51 | * Some {@link DockContainer#getIdentifier()}.
52 | * @param replacement
53 | * Supplier of content to replace the matched container with.
54 | *
55 | * @return {@code true} when replacement was completed.
56 | */
57 | public boolean replaceContainer(@Nonnull String identifier, @Nonnull Supplier replacement) {
58 | DockContainerPath container = container(identifier);
59 | if (container == null)
60 | return false;
61 |
62 | DockContainer original = container.tailContainer();
63 | DockContainerBranch parent = original.getParentContainer();
64 | if (parent == null)
65 | return false;
66 |
67 | return parent.replaceContainer(original, replacement.get());
68 | }
69 |
70 | /**
71 | * @param identifier
72 | * Some {@link DockContainer#getIdentifier()}.
73 | *
74 | * @return Path to the matched container, if found.
75 | */
76 | @Nullable
77 | public DockContainerPath container(@Nonnull String identifier) {
78 | return container(c -> c.getIdentifier().equals(identifier));
79 | }
80 |
81 | /**
82 | * @param predicate
83 | * Predicate to match against some container.
84 | *
85 | * @return Path to the first matched container, if found.
86 | */
87 | @Nullable
88 | public DockContainerPath container(@Nonnull Predicate predicate) {
89 | DockContainerVisitor visitor = new DockContainerVisitor(predicate);
90 | for (DockContainer container : bento.getRootContainers()) {
91 | if (!container.visit(visitor))
92 | break;
93 | }
94 | DockContainer result = visitor.getMatchedContainer();
95 | return result == null ? null : result.getPath();
96 | }
97 |
98 | /**
99 | * @param event
100 | * A drag event that may have a {@link Dockable} associated with it.
101 | *
102 | * @return Path to the associated {@link Dockable} if found.
103 | */
104 | @Nullable
105 | public DockablePath dockable(@Nonnull DragEvent event) {
106 | String identifier = DragUtils.extractIdentifier(event.getDragboard());
107 | return identifier == null ? null : dockable(identifier);
108 | }
109 |
110 | /**
111 | * @param identifier
112 | * Some {@link Dockable#getIdentifier()}.
113 | *
114 | * @return Path to the matched container, if found.
115 | */
116 | @Nullable
117 | public DockablePath dockable(@Nonnull String identifier) {
118 | return dockable(d -> d.getIdentifier().equals(identifier));
119 | }
120 |
121 | /**
122 | * @param predicate
123 | * Predicate to match against some dockable.
124 | *
125 | * @return Path to the first matched dockable, if found.
126 | */
127 | @Nullable
128 | public DockablePath dockable(@Nonnull Predicate predicate) {
129 | DockableVisitor visitor = new DockableVisitor(predicate);
130 | for (DockContainer container : bento.getRootContainers()) {
131 | if (!container.visit(visitor))
132 | break;
133 | }
134 | Dockable result = visitor.getMatchedDockable();
135 | return result == null ? null : result.getPath();
136 | }
137 |
138 | /**
139 | * @return All found dockable paths in the current bento instance.
140 | */
141 | @Nonnull
142 | public List allDockables() {
143 | List paths = new ArrayList<>();
144 | for (DockContainerRootBranch root : bento.getRootContainers()) {
145 | paths.addAll(root.getDockables().stream()
146 | .map(Dockable::getPath)
147 | .filter(Objects::nonNull) // Sanity check
148 | .toList());
149 | }
150 | return paths;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/building/ControlsBuilding.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.building;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.geometry.Orientation;
6 | import javafx.geometry.Side;
7 | import software.coley.bentofx.control.ContentWrapper;
8 | import software.coley.bentofx.control.Header;
9 | import software.coley.bentofx.control.HeaderPane;
10 | import software.coley.bentofx.control.Headers;
11 | import software.coley.bentofx.control.canvas.PixelCanvas;
12 | import software.coley.bentofx.dockable.Dockable;
13 | import software.coley.bentofx.layout.container.DockContainerLeaf;
14 |
15 | /**
16 | * Builders for various bento UI controls.
17 | *
18 | * @author Matt Coley
19 | */
20 | public class ControlsBuilding implements HeaderPaneFactory, HeadersFactory, HeaderFactory, ContentWrapperFactory, CanvasFactory {
21 | private static final HeaderPaneFactory DEFAULT_HEADER_PANE_FACTORY = HeaderPane::new;
22 | private static final HeadersFactory DEFAULT_HEADERS_FACTORY = Headers::new;
23 | private static final HeaderFactory DEFAULT_HEADER_FACTORY = (dockable, parentPane) -> new Header(dockable, parentPane).withDragDrop();
24 | private static final ContentWrapperFactory DEFAULT_CONTENT_WRAPPER_FACTORY = ContentWrapper::new;
25 | private static final CanvasFactory DEFAULT_CANVAS_FACTORY = (parentPane) -> new PixelCanvas();
26 | private HeaderPaneFactory headerPaneFactory = DEFAULT_HEADER_PANE_FACTORY;
27 | private HeadersFactory headersFactory = DEFAULT_HEADERS_FACTORY;
28 | private HeaderFactory headerFactory = DEFAULT_HEADER_FACTORY;
29 | private ContentWrapperFactory contentWrapperFactory = DEFAULT_CONTENT_WRAPPER_FACTORY;
30 | private CanvasFactory canvasFactory = DEFAULT_CANVAS_FACTORY;
31 |
32 | /**
33 | * @return Factory for creating {@link HeaderPane}.
34 | */
35 | @Nonnull
36 | public HeaderPaneFactory getHeaderPaneFactory() {
37 | return headerPaneFactory;
38 | }
39 |
40 | /**
41 | * @param headerPaneFactory
42 | * Factory for creating {@link HeaderPane}.
43 | * {@code null} to use the default factory.
44 | */
45 | public void setHeaderPaneFactory(@Nullable HeaderPaneFactory headerPaneFactory) {
46 | if (headerPaneFactory == null)
47 | headerPaneFactory = DEFAULT_HEADER_PANE_FACTORY;
48 | this.headerPaneFactory = headerPaneFactory;
49 | }
50 |
51 | /**
52 | * @return Factory for creating {@link Headers}.
53 | */
54 | @Nonnull
55 | public HeadersFactory getHeadersFactory() {
56 | return headersFactory;
57 | }
58 |
59 | /**
60 | * @param headersFactory
61 | * Factory for creating {@link Headers}.
62 | * {@code null} to use the default factory.
63 | */
64 | public void setHeadersFactory(@Nullable HeadersFactory headersFactory) {
65 | if (headersFactory == null)
66 | headersFactory = DEFAULT_HEADERS_FACTORY;
67 | this.headersFactory = headersFactory;
68 | }
69 |
70 | /**
71 | * @return Factory for creating {@link Header}.
72 | */
73 | @Nonnull
74 | public HeaderFactory getHeaderFactory() {
75 | return headerFactory;
76 | }
77 |
78 | /**
79 | * @param headerFactory
80 | * Factory for creating {@link Header}.
81 | * {@code null} to use the default factory.
82 | */
83 | public void setHeaderFactory(@Nullable HeaderFactory headerFactory) {
84 | if (headerFactory == null)
85 | headerFactory = DEFAULT_HEADER_FACTORY;
86 | this.headerFactory = headerFactory;
87 | }
88 |
89 | /**
90 | * @return Factory for creating {@link ContentWrapper}.
91 | */
92 | @Nonnull
93 | public ContentWrapperFactory getContentWrapperFactory() {
94 | return contentWrapperFactory;
95 | }
96 |
97 | /**
98 | * @param contentWrapperFactory
99 | * Factory for creating {@link ContentWrapper}.
100 | * {@code null} to use the default factory.
101 | */
102 | public void setContentWrapperFactory(@Nullable ContentWrapperFactory contentWrapperFactory) {
103 | if (contentWrapperFactory == null)
104 | contentWrapperFactory = DEFAULT_CONTENT_WRAPPER_FACTORY;
105 | this.contentWrapperFactory = contentWrapperFactory;
106 | }
107 |
108 | /**
109 | * @return Factory for creating {@link PixelCanvas}.
110 | */
111 | @Nonnull
112 | public CanvasFactory getCanvasFactory() {
113 | return canvasFactory;
114 | }
115 |
116 | /**
117 | * @param canvasFactory
118 | * Factory for creating {@link PixelCanvas}.
119 | * {@code null} to use the default factory.
120 | */
121 | public void setCanvasFactory(@Nullable CanvasFactory canvasFactory) {
122 | if (canvasFactory == null)
123 | canvasFactory = DEFAULT_CANVAS_FACTORY;
124 | this.canvasFactory = canvasFactory;
125 | }
126 |
127 | @Nonnull
128 | @Override
129 | public HeaderPane newHeaderPane(@Nonnull DockContainerLeaf container) {
130 | return headerPaneFactory.newHeaderPane(container);
131 | }
132 |
133 | @Nonnull
134 | @Override
135 | public Headers newHeaders(@Nonnull DockContainerLeaf container, @Nonnull Orientation orientation, @Nonnull Side side) {
136 | return headersFactory.newHeaders(container, orientation, side);
137 | }
138 |
139 | @Nonnull
140 | @Override
141 | public Header newHeader(@Nonnull Dockable dockable, @Nonnull HeaderPane parentPane) {
142 | return headerFactory.newHeader(dockable, parentPane);
143 | }
144 |
145 | @Nonnull
146 | @Override
147 | public ContentWrapper newContentWrapper(@Nonnull DockContainerLeaf container) {
148 | return contentWrapperFactory.newContentWrapper(container);
149 | }
150 |
151 | @Nonnull
152 | @Override
153 | public PixelCanvas newCanvas(@Nonnull DockContainerLeaf container) {
154 | return canvasFactory.newCanvas(container);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | workflow_dispatch:
11 | inputs:
12 | is-a-release:
13 | description: Publish release? (Only works on master, and for untagged versions)
14 | type: boolean
15 |
16 | permissions:
17 | contents: write
18 |
19 | jobs:
20 | test:
21 | name: Run test suite
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | os: [ ubuntu-latest ]
26 | java-version: [ 21 ]
27 | runs-on: ubuntu-latest
28 | timeout-minutes: 10
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v3
32 | - name: Setup JDK
33 | uses: actions/setup-java@v3
34 | with:
35 | distribution: temurin
36 | java-version: 21
37 | check-latest: true
38 | - name: Setup Gradle
39 | uses: gradle/actions/setup-gradle@v4
40 | - name: Run tests
41 | run: ./gradlew test
42 | - name: Upload test results
43 | if: always()
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: Test artifacts
47 | retention-days: 21
48 | path: |
49 | **/TEST-*
50 | **/hs_err_pid*
51 |
52 | # Publishes the test results of 'test'
53 | publish-test-results:
54 | name: Publish tests results
55 | needs: test
56 | if: always()
57 | runs-on: ubuntu-latest
58 | permissions:
59 | checks: write
60 | pull-requests: write
61 | steps:
62 | - name: Download artifacts
63 | uses: actions/download-artifact@v4
64 | with:
65 | path: artifacts
66 | - name: Publish test results
67 | uses: EnricoMi/publish-unit-test-result-action@v2
68 | with:
69 | check_name: Unit Test results
70 | files: |
71 | **/TEST-*
72 |
73 | # Builds the projects and attempts to publish a release if the current project version
74 | # does not match any existing tags in the repository.
75 | build-and-release:
76 | name: Publish release
77 | needs: test
78 | if: inputs.is-a-release && github.repository == 'Col-E/BentoFX' && github.ref == 'refs/heads/master'
79 | strategy:
80 | fail-fast: false
81 | runs-on: ubuntu-latest
82 | timeout-minutes: 30
83 | steps:
84 | - name: Checkout
85 | uses: actions/checkout@v3
86 | with:
87 | fetch-depth: 0 # Required depth for JReleaser
88 | - name: Setup Java JDK
89 | uses: actions/setup-java@v3
90 | with:
91 | distribution: temurin
92 | java-version: 21
93 | - name: Setup Gradle
94 | uses: gradle/actions/setup-gradle@v4
95 | # Set environment variable for the project version: "var_to_set=$(command_to_run)" >> sink
96 | # - For maven: echo "PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
97 | # - For gradle: echo "PROJECT_VERSION=$(./gradlew properties | grep -Po '(?<=version: ).*')" >> $GITHUB_ENV
98 | - name: Extract project version to environment variable
99 | run: echo "PROJECT_VERSION=$(./gradlew properties | grep -Po '(?<=version\W ).*')" >> $GITHUB_ENV
100 | # Check if a tag exists that matches the current project version.
101 | # Write the existence state to the step output 'tagExists'.
102 | - name: Check the package version has corresponding Git tag
103 | id: tagged
104 | shell: bash
105 | run: |
106 | git show-ref --tags --verify --quiet -- "refs/tags/${{ env.PROJECT_VERSION }}" && echo "tagExists=1" >> $GITHUB_OUTPUT || echo "tagExists=0" >> $GITHUB_OUTPUT
107 | git show-ref --tags --verify --quiet -- "refs/tags/${{ env.PROJECT_VERSION }}" && echo "Tag for current version exists" || echo "Tag for current version does not exist"
108 | # If the tag could not be fetched, show a message and abort the job.
109 | # The wonky if logic is a workaround for: https://github.com/actions/runner/issues/1173
110 | - name: Abort if tag exists, or existence check fails
111 | if: ${{ false && steps.tagged.outputs.tagExists }}
112 | run: |
113 | echo "Output of 'tagged' step: ${{ steps.tagged.outputs.tagExists }}"
114 | echo "Failed to check if tag exists."
115 | echo "PROJECT_VERSION: ${{ env.PROJECT_VERSION }}"
116 | echo "Tags $(git tag | wc -l):"
117 | git tag
118 | git show-ref --tags --verify -- "refs/tags/${{ env.PROJECT_VERSION }}"
119 | exit 1
120 | # Make release with JReleaser, only running when the project version does not exist as a tag on the repository.
121 | - name: Publish release
122 | run: ./gradlew publish jreleaserFullRelease
123 | env:
124 | JRELEASER_PROJECT_VERSION: ${{ env.PROJECT_VERSION }}
125 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
127 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
128 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
129 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }}
130 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_TOKEN }}
131 | # Upload JRelease debug log
132 | - name: JReleaser output
133 | uses: actions/upload-artifact@v4
134 | if: always()
135 | with:
136 | name: jreleaser-release
137 | path: |
138 | build/jreleaser/trace.log
139 | build/jreleaser/output.properties
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/ContentWrapper.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.geometry.Orientation;
5 | import javafx.geometry.Side;
6 | import javafx.scene.input.Dragboard;
7 | import javafx.scene.input.TransferMode;
8 | import javafx.scene.layout.BorderPane;
9 | import software.coley.bentofx.Bento;
10 | import software.coley.bentofx.dockable.Dockable;
11 | import software.coley.bentofx.layout.container.DockContainerBranch;
12 | import software.coley.bentofx.layout.container.DockContainerLeaf;
13 | import software.coley.bentofx.path.DockablePath;
14 | import software.coley.bentofx.util.BentoUtils;
15 | import software.coley.bentofx.util.DragDropTarget;
16 | import software.coley.bentofx.util.DragUtils;
17 |
18 | import java.util.Objects;
19 |
20 | /**
21 | * Border pane with handling for drag-drop in the context of a {@link HeaderPane}'s parent {@link DockContainerLeaf}.
22 | *
23 | * @author Matt Coley
24 | */
25 | public class ContentWrapper extends BorderPane {
26 | /**
27 | * @param container
28 | * Parent container.
29 | */
30 | public ContentWrapper(@Nonnull DockContainerLeaf container) {
31 | getStyleClass().add("node-wrapper");
32 |
33 | // Handle drag-drop
34 | setupDragDrop(container);
35 | }
36 |
37 | protected void setupDragDrop(@Nonnull DockContainerLeaf container) {
38 | Bento bento = container.getBento();
39 | setOnDragOver(e -> {
40 | Dragboard dragboard = e.getDragboard();
41 | String dockableIdentifier = DragUtils.extractIdentifier(dragboard);
42 | if (dockableIdentifier != null) {
43 | DockablePath dragSourcePath = bento.search().dockable(dockableIdentifier);
44 | if (dragSourcePath != null) {
45 | Dockable dragSourceDockable = dragSourcePath.dockable();
46 | Side side = container.isCanSplit() ? BentoUtils.computeClosestSide(this, e.getX(), e.getY()) : null;
47 | if (container.canReceiveDockable(dragSourceDockable, side)) {
48 | container.drawCanvasHint(this, side);
49 | } else {
50 | container.clearCanvas();
51 | }
52 | }
53 |
54 | // We always need to accept content if there is a dockable identifier.
55 | // In the case where it is not actually receivable, we'll handle that in the completion logic.
56 | e.acceptTransferModes(TransferMode.MOVE);
57 | }
58 |
59 | // Do not propagate upwards.
60 | e.consume();
61 | });
62 | setOnDragExited(e -> container.clearCanvas());
63 | setOnDragDropped(e -> {
64 | // Skip if dragboard doesn't contain a dockable identifier.
65 | Dragboard dragboard = e.getDragboard();
66 | String dockableIdentifier = DragUtils.extractIdentifier(dragboard);
67 | if (dockableIdentifier == null)
68 | return;
69 |
70 | // Skip if the dockable cannot be found in our bento instance.
71 | DockablePath dragSourcePath = bento.search().dockable(dockableIdentifier);
72 | if (dragSourcePath == null)
73 | return;
74 |
75 | // Skip if this source/target containers are the same, and there is only one dockable.
76 | // This means there would be no change after the "move" and thus its wasted effort to do anything.
77 | DockContainerLeaf sourceContainer = dragSourcePath.leafContainer();
78 | Dockable sourceDockable = dragSourcePath.dockable();
79 | if (container == sourceContainer && container.getDockables().size() == 1)
80 | return;
81 |
82 | // If our container can receive the header, move it over.
83 | Side side = container.isCanSplit() ? BentoUtils.computeClosestSide(this, e.getX(), e.getY()) : null;
84 | if (container.canReceiveDockable(sourceDockable, side)) {
85 | // Disable empty pruning while we handle splitting.
86 | boolean pruneState = sourceContainer.doPruneWhenEmpty();
87 | sourceContainer.setPruneWhenEmpty(false);
88 |
89 | // Remove the dockable from its current parent.
90 | sourceContainer.removeDockable(sourceDockable);
91 |
92 | // Handle splitting by side if provided.
93 | if (side != null) {
94 | // Keep track of the current container's parent for later.
95 | DockContainerBranch ourParent = Objects.requireNonNull(container.getParentContainer());
96 |
97 | // Create container for dropped header.
98 | DockContainerLeaf containerForDropped = bento.dockBuilding().leaf();
99 | containerForDropped.setSide(container.getSide()); // Copy our container's side-ness.
100 | containerForDropped.addDockable(sourceDockable);
101 |
102 | // Create container to hold both our own container and the dropped header.
103 | // This will combine them in a split view according to the side the user dropped
104 | // the incoming dockable on.
105 | DockContainerBranch splitContainer = bento.dockBuilding().branch();
106 | if (side == Side.TOP || side == Side.BOTTOM)
107 | splitContainer.setOrientation(Orientation.VERTICAL);
108 | if (side == Side.TOP || side == Side.LEFT) {
109 | // User dropped on top/left, so the dropped item is first in the split.
110 | splitContainer.addContainer(containerForDropped);
111 | splitContainer.addContainer(container);
112 | } else {
113 | // User dropped on bottom/right, so the dropped item is last in the split.
114 | splitContainer.addContainer(container);
115 | splitContainer.addContainer(containerForDropped);
116 | }
117 |
118 | // Now we get the parent container (a branch) that holds our container (a leaf) and have it replace
119 | // the leaf it currently has (our current container) with the new branch container we just made.
120 | ourParent.replaceContainer(container, splitContainer);
121 | } else {
122 | // Just move the dockable from its prior container to our container.
123 | container.addDockable(sourceDockable);
124 | container.selectDockable(sourceDockable);
125 | }
126 |
127 | // Restore original prune state.
128 | sourceContainer.setPruneWhenEmpty(pruneState);
129 | if (sourceContainer.doPruneWhenEmpty() && sourceContainer.getDockables().isEmpty())
130 | sourceContainer.removeFromParent();
131 |
132 | DragUtils.completeDnd(e, sourceDockable, DragDropTarget.REGION);
133 | }
134 | });
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/event/DockEvent.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.event;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import software.coley.bentofx.Bento;
6 | import software.coley.bentofx.dockable.Dockable;
7 | import software.coley.bentofx.layout.DockContainer;
8 | import software.coley.bentofx.layout.container.DockContainerBranch;
9 | import software.coley.bentofx.layout.container.DockContainerLeaf;
10 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
11 |
12 | /**
13 | * Outline of all docking events.
14 | *
15 | * @author Matt Coley
16 | */
17 | public sealed interface DockEvent {
18 | /**
19 | * Event for when a {@link DockContainer} is {@link Bento#registerRoot(DockContainerRootBranch) registered}.
20 | *
21 | * @param container
22 | * Root container added.
23 | */
24 | record RootContainerAdded(@Nonnull DockContainer container) implements DockEvent {}
25 |
26 | /**
27 | * Event for when a {@link DockContainer} is {@link Bento#unregisterRoot(DockContainerRootBranch) uunregistered}.
28 | *
29 | * @param container
30 | * Root container removed.
31 | */
32 | record RootContainerRemoved(@Nonnull DockContainer container) implements DockEvent {}
33 |
34 | /**
35 | * Event for when a {@link DockContainer}'s parent is changed.
36 | *
37 | * @param container
38 | * Container being updated.
39 | * @param priorParent
40 | * The container's prior parent.
41 | * @param newParent
42 | * The container's new parent.
43 | */
44 | record ContainerParentChanged(@Nonnull DockContainer container, @Nullable DockContainerBranch priorParent,
45 | @Nullable DockContainerBranch newParent) implements DockEvent {}
46 |
47 | /**
48 | * Event for when a {@link DockContainerBranch} adds a child {@link DockContainer}.
49 | *
50 | * @param container
51 | * Container being updated.
52 | * @param child
53 | * Child added to the container.
54 | */
55 | record ContainerChildAdded(@Nonnull DockContainerBranch container,
56 | @Nonnull DockContainer child) implements DockEvent {}
57 |
58 | /**
59 | * Event for when a {@link DockContainerBranch} removes a child {@link DockContainer}.
60 | *
61 | * @param container
62 | * Container being updated.
63 | * @param child
64 | * Child removed from the container.
65 | */
66 | record ContainerChildRemoved(@Nonnull DockContainerBranch container,
67 | @Nonnull DockContainer child) implements DockEvent {}
68 |
69 | /**
70 | * Event for when a {@link DockContainerLeaf} adds a {@link Dockable} item.
71 | *
72 | * @param container
73 | * Container the dockable was added to.
74 | * @param dockable
75 | * Dockable added.
76 | */
77 | record DockableAdded(@Nonnull DockContainerLeaf container, @Nonnull Dockable dockable) implements DockEvent {}
78 |
79 | /**
80 | * Event for when a {@link DockContainerLeaf} closes a {@link Dockable} item.
81 | * Can be cancelled to prevent closure.
82 | */
83 | final class DockableClosing implements DockEvent {
84 | private final @Nonnull Dockable dockable;
85 | private final @Nonnull DockContainerLeaf container;
86 | private boolean cancelled;
87 |
88 | /**
89 | * @param dockable
90 | * Dockable being closed.
91 | * @param container
92 | * Container the dockable belongs to.
93 | */
94 | public DockableClosing(@Nonnull Dockable dockable, @Nonnull DockContainerLeaf container) {
95 | this.dockable = dockable;
96 | this.container = container;
97 | }
98 |
99 | /**
100 | * @return Dockable being closed.
101 | */
102 | @Nonnull
103 | public Dockable dockable() {
104 | return dockable;
105 | }
106 |
107 | /**
108 | * @return Container the dockable belongs to.
109 | */
110 | @Nonnull
111 | public DockContainerLeaf container() {
112 | return container;
113 | }
114 |
115 | /**
116 | * Cancel closing this dockable.
117 | */
118 | public void cancel() {
119 | cancelled = true;
120 | }
121 |
122 | /**
123 | * @return {@code true} when this closure has been cancelled.
124 | */
125 | public boolean isCancelled() {
126 | return cancelled;
127 | }
128 |
129 | @Override
130 | public boolean equals(Object o) {
131 | if (!(o instanceof DockableClosing that)) return false;
132 | return cancelled == that.cancelled
133 | && dockable.equals(that.dockable)
134 | && container.equals(that.container);
135 | }
136 |
137 | @Override
138 | public int hashCode() {
139 | int result = dockable.hashCode();
140 | result = 31 * result + container.hashCode();
141 | return result;
142 | }
143 |
144 | @Override
145 | public String toString() {
146 | return "DockableClosing[" +
147 | "dockable=" + dockable +
148 | ", container=" + container +
149 | ", shouldClose=" + cancelled +
150 | "]";
151 | }
152 | }
153 |
154 | /**
155 | * Event for when a {@link DockContainerLeaf} removes a {@link Dockable} item.
156 | *
157 | * @param dockable
158 | * Dockable being removed.
159 | * @param container
160 | * Container the dockable belonged to.
161 | */
162 | record DockableRemoved(@Nonnull Dockable dockable, @Nonnull DockContainerLeaf container) implements DockEvent {}
163 |
164 | /**
165 | * Event for when a {@link DockContainerLeaf} updates its selected {@link Dockable} item.
166 | *
167 | * @param dockable
168 | * Dockable being selected.
169 | * @param container
170 | * Container the dockable belongs to.
171 | */
172 | record DockableSelected(@Nonnull Dockable dockable, @Nonnull DockContainerLeaf container) implements DockEvent {}
173 |
174 | /**
175 | * Event for when a {@link Dockable}'s parent is changed.
176 | *
177 | * @param dockable
178 | * Dockable being updated.
179 | * @param priorParent
180 | * Dockable's prior parent.
181 | * @param newParent
182 | * Dockable's new parent.
183 | */
184 | record DockableParentChanged(@Nonnull Dockable dockable, @Nullable DockContainerLeaf priorParent,
185 | @Nullable DockContainerLeaf newParent) implements DockEvent {}
186 | }
187 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/Headers.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.collections.ListChangeListener;
5 | import javafx.geometry.Orientation;
6 | import javafx.geometry.Side;
7 | import javafx.scene.Node;
8 | import javafx.scene.input.Dragboard;
9 | import javafx.scene.input.TransferMode;
10 | import javafx.scene.layout.Region;
11 | import javafx.scene.shape.Rectangle;
12 | import software.coley.bentofx.dockable.Dockable;
13 | import software.coley.bentofx.layout.container.DockContainerLeaf;
14 | import software.coley.bentofx.path.DockablePath;
15 | import software.coley.bentofx.util.DragDropTarget;
16 | import software.coley.bentofx.util.DragUtils;
17 |
18 | import static software.coley.bentofx.util.BentoStates.*;
19 |
20 | /**
21 | * Linear item pane to hold {@link Header} displays of {@link DockContainerLeaf#getDockables()}.
22 | *
23 | * @author Matt Coley
24 | */
25 | public class Headers extends LinearItemPane {
26 | /**
27 | * @param container
28 | * Parent container.
29 | * @param orientation
30 | * Which axis to layout children on.
31 | * @param side
32 | * Side in the parent container where tabs are displayed.
33 | */
34 | public Headers(@Nonnull DockContainerLeaf container, @Nonnull Orientation orientation, @Nonnull Side side) {
35 | super(orientation);
36 |
37 | // Create side-specific header region class.
38 | getStyleClass().add("header-region");
39 | switch (side) {
40 | case TOP -> pseudoClassStateChanged(PSEUDO_SIDE_TOP, true);
41 | case BOTTOM -> pseudoClassStateChanged(PSEUDO_SIDE_BOTTOM, true);
42 | case LEFT -> pseudoClassStateChanged(PSEUDO_SIDE_LEFT, true);
43 | case RIGHT -> pseudoClassStateChanged(PSEUDO_SIDE_RIGHT, true);
44 | }
45 |
46 | // Make this pane fill the full width/height (matching orientation) of the parent container.
47 | if (orientation == Orientation.HORIZONTAL) {
48 | prefWidthProperty().bind(container.widthProperty());
49 | } else {
50 | prefHeightProperty().bind(container.heightProperty());
51 | }
52 |
53 | // Keep the minimum size with the last added header item.
54 | // This will ensure this pane doesn't resize to 0 width/height when the last child is removed,
55 | // allowing the user to later drag another header back into this space.
56 | setupMinSizeTracking();
57 |
58 | // Make children fill the full width/height of this pane on the perpendicular (to orientation) axis.
59 | fitChildrenToPerpendicularProperty().set(true);
60 |
61 | // Keep the selected dockable in view.
62 | keepInViewProperty().bind(container.selectedDockableProperty().map(container::getHeader));
63 |
64 | // Support drag-drop.
65 | setupDragDrop(container);
66 | }
67 |
68 | protected void setupMinSizeTracking() {
69 | getChildren().addListener((ListChangeListener) c -> {
70 | Orientation orientation = getOrientation();
71 |
72 | double min = MIN_PERPENDICULAR;
73 | while (c.next()) {
74 | for (Node child : c.getAddedSubList()) {
75 | if (child instanceof Region r) {
76 | if (orientation == Orientation.HORIZONTAL) {
77 | min = Math.max(min, r.getHeight());
78 | } else {
79 | min = Math.max(min, r.getWidth());
80 | }
81 | }
82 | }
83 | for (Node child : c.getRemoved()) {
84 | if (child instanceof Region r) {
85 | if (orientation == Orientation.HORIZONTAL) {
86 | min = Math.max(min, r.getHeight());
87 | } else {
88 | min = Math.max(min, r.getWidth());
89 | }
90 | }
91 | }
92 | }
93 |
94 | if (orientation == Orientation.HORIZONTAL) {
95 | minHeightProperty().setValue(min);
96 | } else {
97 | minWidthProperty().setValue(min);
98 | }
99 | });
100 | }
101 |
102 | protected void setupClip() {
103 | // Use a clip to prevent headers from rendering beyond expected bounds.
104 | // With the example CSS in use this is not needed, but some users who make their own may need this.
105 | Rectangle clip = new Rectangle();
106 | clip.widthProperty().bind(widthProperty());
107 | clip.heightProperty().bind(heightProperty());
108 | setClip(clip);
109 | }
110 |
111 | protected void setupDragDrop(@Nonnull DockContainerLeaf container) {
112 | setOnDragOver(e -> {
113 | Dragboard dragboard = e.getDragboard();
114 | String dockableIdentifier = DragUtils.extractIdentifier(dragboard);
115 | if (dockableIdentifier != null) {
116 | DockablePath dragSourcePath = container.getBento().search().dockable(dockableIdentifier);
117 | if (dragSourcePath != null) {
118 | Dockable dragSourceDockable = dragSourcePath.dockable();
119 | if (container.canReceiveDockable(dragSourceDockable, null)) {
120 | container.drawCanvasHint(this);
121 | } else {
122 | container.clearCanvas();
123 | }
124 | }
125 |
126 | // We always need to accept content if there is a dockable identifier.
127 | // In the case where it is not actually receivable, we'll handle that in the completion logic.
128 | e.acceptTransferModes(TransferMode.MOVE);
129 | }
130 |
131 | // Do not propagate upwards.
132 | e.consume();
133 | });
134 | setOnDragDropped(e -> {
135 | // Skip if dragboard doesn't contain a dockable identifier.
136 | Dragboard dragboard = e.getDragboard();
137 | String dockableIdentifier = DragUtils.extractIdentifier(dragboard);
138 | if (dockableIdentifier == null)
139 | return;
140 |
141 | // Skip if the dockable cannot be found in our bento instance.
142 | DockablePath dragSourcePath = container.getBento().search().dockable(dockableIdentifier);
143 | if (dragSourcePath == null)
144 | return;
145 |
146 | // If our container can receive the header, move it over.
147 | DockContainerLeaf sourceContainer = dragSourcePath.leafContainer();
148 | Dockable sourceDockable = dragSourcePath.dockable();
149 | if (container.canReceiveDockable(sourceDockable, null)) {
150 | sourceContainer.removeDockable(sourceDockable);
151 | container.addDockable(sourceDockable);
152 | container.selectDockable(sourceDockable);
153 | DragUtils.completeDnd(e, sourceDockable, DragDropTarget.REGION);
154 | }
155 | });
156 | setOnDragExited(e -> container.clearCanvas());
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/util/DragUtils.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.util;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.scene.input.ClipboardContent;
6 | import javafx.scene.input.DataFormat;
7 | import javafx.scene.input.DragEvent;
8 | import javafx.scene.input.Dragboard;
9 | import javafx.stage.Stage;
10 | import software.coley.bentofx.Bento;
11 | import software.coley.bentofx.control.Header;
12 | import software.coley.bentofx.dockable.Dockable;
13 | import software.coley.bentofx.path.DockablePath;
14 |
15 | import java.util.Map;
16 |
17 | /**
18 | * Drag-n-drop based utilities.
19 | *
20 | * @author Matt Coley
21 | */
22 | public class DragUtils {
23 | public static final String PREFIX = "dnd-bento;";
24 |
25 | /**
26 | * Creates a map containing details about the given {@link Dockable} that can be retrieved later.
27 | *
28 | * @param dockable
29 | * Dockable content being dragged.
30 | *
31 | * @return Content to put into {@link Dragboard#setContent(Map)}.
32 | *
33 | * @see #extractIdentifier(Dragboard)
34 | * @see #extractDragGroup(Dragboard)
35 | */
36 | @Nonnull
37 | public static Map content(@Nonnull Dockable dockable) {
38 | return content(dockable, null);
39 | }
40 |
41 | /**
42 | * Creates a map containing details about the given {@link Dockable} that can be retrieved later.
43 | *
44 | * @param dockable
45 | * Dockable content being dragged.
46 | * @param target
47 | * The completed drag-drop type for a completed operation. Otherwise {@code null} for incomplete operations.
48 | *
49 | * @return Content to put into {@link Dragboard#setContent(Map)}.
50 | *
51 | * @see #extractIdentifier(Dragboard)
52 | * @see #extractDragGroup(Dragboard)
53 | * @see #extractDropTargetType(Dragboard)
54 | */
55 | @Nonnull
56 | public static Map content(@Nonnull Dockable dockable, @Nullable DragDropTarget target) {
57 | ClipboardContent content = new ClipboardContent();
58 | String format = PREFIX + dockable.getDragGroupMask() + ";" + dockable.getIdentifier();
59 | if (target != null)
60 | format += ";" + target.name();
61 | content.putString(format);
62 | return content;
63 | }
64 |
65 | /**
66 | * Updates the event to model the completed drag-n-drop of a {@link Header}.
67 | *
68 | * @param event
69 | * Event to update {@link Dragboard} content of.
70 | * @param dockable
71 | * Dockable content being dragged.
72 | * @param target
73 | * The completed drag-drop type for a completed operation
74 | */
75 | public static void completeDnd(@Nonnull DragEvent event, @Nonnull Dockable dockable, @Nonnull DragDropTarget target) {
76 | event.getDragboard().setContent(content(dockable, target));
77 | event.consume();
78 | }
79 |
80 | /**
81 | * @param dragboard
82 | * Some dragboard that may contain a dragged {@link Header}.
83 | *
84 | * @return The {@link Dockable#getIdentifier()} of the dragged {@link Header}
85 | * if the board's respective {@link DragEvent} originates from a dragged {@link Header}.
86 | *
87 | * @see #content(Dockable)
88 | */
89 | @Nullable
90 | public static String extractIdentifier(@Nonnull Dragboard dragboard) {
91 | if (!dragboard.hasString())
92 | return null;
93 | String[] parts = dragboard.getString().split(";");
94 | if (parts.length < 3)
95 | return null;
96 | return parts[2];
97 | }
98 |
99 | /**
100 | * @param dragboard
101 | * Some dragboard that may contain a dragged {@link Header}.
102 | *
103 | * @return The {@link Dockable#getDragGroupMask()} of the dragged {@link Header}
104 | * if the board's respective {@link DragEvent} originates from a dragged {@link Header}.
105 | *
106 | * @see #content(Dockable)
107 | */
108 | @Nullable
109 | public static Integer extractDragGroup(@Nonnull Dragboard dragboard) {
110 | if (!dragboard.hasString())
111 | return null;
112 | String[] parts = dragboard.getString().split(";");
113 | if (parts.length < 2)
114 | return null;
115 | try {
116 | return Integer.parseInt(parts[1]);
117 | } catch (Throwable t) {
118 | return null;
119 | }
120 | }
121 |
122 | /**
123 | * @param dragboard
124 | * Some dragboard that may contain a dragged {@link Header}.
125 | *
126 | * @return The {@link DragDropTarget} of the dragged {@link Header}
127 | * if the board's respective {@link DragEvent} originates from a dragged {@link Header} that has been completed.
128 | *
129 | * @see #content(Dockable, DragDropTarget)
130 | */
131 | @Nullable
132 | public static DragDropTarget extractDropTargetType(@Nonnull Dragboard dragboard) {
133 | if (!dragboard.hasString())
134 | return null;
135 | String[] parts = dragboard.getString().split(";");
136 | if (parts.length < 4)
137 | return null;
138 | try {
139 | return DragDropTarget.valueOf(parts[3]);
140 | } catch (Exception ex) {
141 | // Not a recognized target type.
142 | return null;
143 | }
144 | }
145 |
146 | /**
147 | * This goofy method exists because {@link DragEvent#getGestureSource()} is {@code null} when anything
148 | * is dragged between two separate {@link Stage}s. When that occurs we need some way to recover the {@link Header}.
149 | *
150 | * @param bento
151 | * Bento instance to search in.
152 | * @param event
153 | * Drag event to extract the {@link Header}'s associated {@link Dockable#getIdentifier()}.
154 | *
155 | * @return The {@link Header} that initiated this drag gesture.
156 | */
157 | @Nullable
158 | public static Header getHeader(@Nonnull Bento bento, @Nonnull DragEvent event) {
159 | // Ideally the header is just known to the event.
160 | Object source = event.getGestureSource();
161 | if (source instanceof Header headerSource)
162 | return headerSource;
163 |
164 | // If the source is NOT null and NOT a header, we're in an unexpected state.
165 | if (source != null)
166 | return null;
167 |
168 | // The source being 'null' happens when drag-n-drop happens across stages.
169 | // In this case, we search for the header based on the event contents.
170 | DockablePath path = bento.search().dockable(event);
171 | if (path == null)
172 | return null;
173 | return path.leafContainer().getHeader(path.dockable());
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/building/StageBuilding.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.building;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.scene.Scene;
6 | import javafx.scene.layout.Region;
7 | import javafx.stage.Stage;
8 | import javafx.stage.Window;
9 | import software.coley.bentofx.Bento;
10 | import software.coley.bentofx.control.DragDropStage;
11 | import software.coley.bentofx.dockable.Dockable;
12 | import software.coley.bentofx.layout.DockContainer;
13 | import software.coley.bentofx.layout.container.DockContainerLeaf;
14 | import software.coley.bentofx.layout.container.DockContainerRootBranch;
15 |
16 | /**
17 | * Builders for {@link DragDropStage}.
18 | */
19 | public class StageBuilding {
20 | private static final StageFactory DEFAULT_STAGE_FACTORY = (sourceStage) -> new DragDropStage(true);
21 | private static final SceneFactory DEFAULT_SCENE_FACTORY = (sourceScene, content, width, height) -> new Scene(content, width, height);
22 | private final Bento bento;
23 | private StageFactory stageFactory = DEFAULT_STAGE_FACTORY;
24 | private SceneFactory sceneFactory = DEFAULT_SCENE_FACTORY;
25 |
26 | public StageBuilding(@Nonnull Bento bento) {
27 | this.bento = bento;
28 | }
29 |
30 | /**
31 | * Create a new stage for the given dockable.,
32 | *
33 | * @param sourceScene
34 | * Original scene to copy state from.
35 | * @param source
36 | * Container holding the dockable.
37 | * @param dockable
38 | * Dockable to place into the newly created stage.
39 | *
40 | * @return Newly created stage.
41 | */
42 | @Nonnull
43 | public DragDropStage newStageForDockable(@Nonnull Scene sourceScene, @Nonnull DockContainer source, @Nonnull Dockable dockable) {
44 | Region sourceRegion = source.asRegion();
45 | double width = sourceRegion.getWidth();
46 | double height = sourceRegion.getHeight();
47 | return newStageForDockable(sourceScene, dockable, width, height);
48 | }
49 |
50 | /**
51 | * Create a new stage for the given dockable.,
52 | *
53 | * @param sourceScene
54 | * Original scene to copy state from.
55 | * @param dockable
56 | * Dockable to place into the newly created stage.
57 | * @param width
58 | * Preferred stage width.
59 | * @param height
60 | * Preferred stage height.
61 | *
62 | * @return Newly created stage.
63 | */
64 | @Nonnull
65 | public DragDropStage newStageForDockable(@Nullable Scene sourceScene, @Nonnull Dockable dockable, double width, double height) {
66 | DockBuilding builder = bento.dockBuilding();
67 | DockContainerRootBranch root = builder.root();
68 | DockContainerLeaf leaf = builder.leaf();
69 | return newStageForDockable(sourceScene, root, leaf, dockable, width, height);
70 | }
71 |
72 | /**
73 | * Create a new stage for the given dockable.,
74 | *
75 | * @param sourceScene
76 | * Original scene to copy state from.
77 | * @param root
78 | * Newly created root branch to place into the resulting stage.
79 | * @param leaf
80 | * Newly created leaf container to place the dockable into.
81 | * @param dockable
82 | * Dockable to place into the newly created stage.
83 | * @param width
84 | * Preferred stage width.
85 | * @param height
86 | * Preferred stage height.
87 | *
88 | * @return Newly created stage.
89 | */
90 | @Nonnull
91 | public DragDropStage newStageForDockable(@Nullable Scene sourceScene,
92 | @Nonnull DockContainerRootBranch root,
93 | @Nonnull DockContainerLeaf leaf,
94 | @Nonnull Dockable dockable,
95 | double width, double height) {
96 | // Sanity check, leaf shouldn't have an existing parent.
97 | if (leaf.getParentContainer() != root && leaf.getParentContainer() != null)
98 | leaf.removeFromParent();
99 |
100 | // Add the leaf to the given root, and the dockable to the leaf.
101 | root.addContainer(leaf);
102 | leaf.addDockable(dockable);
103 |
104 | // Create new stage/scene for the dockable to spawn in.
105 | Region region = root.asRegion();
106 | Stage sourceStage = sourceScene == null ? null : (Stage) sourceScene.getWindow();
107 | DragDropStage stage = stageFactory.newStage(sourceStage);
108 | Scene scene = sceneFactory.newScene(sourceScene, region, width, height);
109 | stage.setScene(scene);
110 |
111 | // Copy properties from the source scene/stage.
112 | if (sourceScene != null)
113 | initializeFromSource(sourceScene, scene, sourceStage, stage, true);
114 |
115 | return stage;
116 | }
117 |
118 | /**
119 | * Copy attributes from the source scene/stage housing a dockable
120 | * to the new scene/stage the dockable will be moved to.
121 | *
122 | * @param sourceScene
123 | * Source scene the dockable belonged to.
124 | * @param newScene
125 | * New scene the dockable is being moved to.
126 | * @param sourceStage
127 | * Source stage a dockable belonged to.
128 | * @param newStage
129 | * New stage the dockable is being moved to.
130 | * @param sourceIsOwner
131 | * {@code true} to invoke {@link Stage#initOwner(Window)}, where the owner is the source stage.
132 | */
133 | protected void initializeFromSource(@Nonnull Scene sourceScene,
134 | @Nonnull Scene newScene,
135 | @Nullable Stage sourceStage,
136 | @Nonnull DragDropStage newStage,
137 | boolean sourceIsOwner) {
138 | // Copy stylesheets.
139 | newScene.setUserAgentStylesheet(sourceScene.getUserAgentStylesheet());
140 | newScene.getStylesheets().addAll(sourceScene.getStylesheets());
141 |
142 | // Copy icon.
143 | if (sourceStage != null)
144 | newStage.getIcons().addAll(sourceStage.getIcons());
145 |
146 | // Just to prevent 1x1 tiny spawns.
147 | newStage.setMinWidth(150);
148 | newStage.setMinHeight(100);
149 |
150 | // Make the source stage the owner of the new stage.
151 | // - Will prevent minimizing.
152 | if (sourceIsOwner)
153 | newStage.initOwner(sourceStage);
154 | }
155 |
156 | /**
157 | * @param factory
158 | * New factory for creating stages.
159 | */
160 | public void setStageFactory(@Nullable StageFactory factory) {
161 | if (factory == null)
162 | factory = DEFAULT_STAGE_FACTORY;
163 | stageFactory = factory;
164 | }
165 |
166 | /**
167 | * @param factory
168 | * New factory for creating scenes.
169 | */
170 | public void setSceneFactory(@Nullable SceneFactory factory) {
171 | if (factory == null)
172 | factory = DEFAULT_SCENE_FACTORY;
173 | sceneFactory = factory;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/resources/bento.css:
--------------------------------------------------------------------------------
1 | /*
2 | This serves as an example of how you would create a stylesheet for Bento.
3 | It outlines all of the classes and pseudo-states used by controls.
4 | */
5 | .root {
6 | -color-base-9: white;
7 | -color-base-8: #dddddd;
8 | -color-base-7: #d0d0d0;
9 | -color-base-6: #c4c4c4;
10 | -color-base-5: rgb(170, 170, 170);
11 | -color-base-4: #6a6a6a;
12 | -color-base-3: #5e5e5e;
13 | -color-base-2: #444444;
14 | -color-base-1: #373737;
15 | -color-base-0: #373737;
16 | -color-accent-9: #bddeff;
17 | -color-accent-8: #a3d1ff;
18 | -color-accent-7: #7abcff;
19 | -color-accent-6: #57aaff;
20 | -color-accent-5: #3d9eff;
21 | -color-accent-4: rgb(10, 132, 255);
22 | -color-accent-3: #006bd6;
23 | -color-accent-2: #005bb7;
24 | -color-accent-1: #00448a;
25 | -color-accent-0: #002b57;
26 | -color-fg-default: black;
27 | -color-fg-muted: -color-base-1;
28 | -color-fg-subtle: -color-base-3;
29 | -color-bg-default: -color-base-9;
30 | -color-bg-overlay: -color-base-8;
31 | -color-bg-subtle: -color-base-6;
32 | -color-bg-inset: -color-base-4;
33 | -color-border-default: -color-base-3;
34 | -color-border-muted: -color-base-4;
35 | -color-border-subtle: -color-base-5;
36 | -color-accent-fg: -color-accent-8;
37 | -color-accent-emphasis: -color-accent-5;
38 | -color-accent-muted: -color-accent-2;
39 | -color-accent-subtle: -color-accent-0;
40 | -fx-background-color: -color-bg-default;
41 | }
42 |
43 | .container-leaf {}
44 | .container-branch {}
45 | .container-branch:root {}
46 | .container-branch {
47 | -fx-background-insets: 0;
48 | -fx-padding: 0;
49 | }
50 | .container-branch > .split-pane-divider {
51 | -fx-background-color: -color-bg-inset !important;
52 | -fx-padding: 0 2px 0 2px;
53 | }
54 | .container-branch > .split-pane-divider > .horizontal-grabber {
55 | -fx-background-color: -color-bg-subtle;
56 | -fx-padding: 5px 1px 5px 1px;
57 | }
58 | .container-branch > .split-pane-divider > .vertical-grabber {
59 | -fx-background-color: -color-bg-subtle;
60 | -fx-padding: 1px 5px 1px 5px;
61 | }
62 | .container-branch > .split-pane-divider:hover {}
63 | .container-branch > .split-pane-divider:pressed {}
64 | .container-branch > .split-pane-divider:disabled {}
65 | .container-branch > .split-pane-divider:hover> .horizontal-grabber,
66 | .container-branch > .split-pane-divider:hover> .vertical-grabber {
67 | -fx-background-color: -color-accent-emphasis;
68 | }
69 | .container-branch > .split-pane-divider:pressed > .horizontal-grabber,
70 | .container-branch > .split-pane-divider:pressed > .vertical-grabber {
71 | -fx-background-color: -color-accent-fg;
72 | }
73 |
74 | /* HeaderPane */
75 | .header-pane {
76 | -fx-background-insets: 0, 1px;
77 | -fx-background-color: -color-border-default, -color-bg-default;
78 | }
79 | /* The "x" button on closable headers */
80 | .header-pane .close-button {
81 | -fx-effect: none;
82 | -fx-opacity: 0.5;
83 | -fx-background-insets: 0;
84 | -fx-background-radius: 50px;
85 | -fx-background-color: transparent;
86 | -fx-border-width: 0px;
87 | -fx-padding: 0 2px 0 2px;
88 | }
89 | .header-pane .header:selected .close-button:hover {
90 | -fx-background-color: -color-bg-subtle;
91 | }
92 | .header-pane .header .close-button:hover {
93 | -fx-background-color: -color-bg-default;
94 | }
95 | /* The "▼" and "≡" buttons in HeaderView */
96 | .header-pane .corner-button {
97 | -fx-effect: none;
98 | -fx-background-insets: 0, 1px;
99 | -fx-background-radius: 0px;
100 | -fx-background-color: -color-border-default, -color-bg-overlay;
101 | -fx-border-width: 0px;
102 | }
103 | .header-pane .corner-button:hover {
104 | -fx-background-color: -color-border-default, -color-bg-default;
105 | }
106 | .header-pane:top .corner-button {
107 | -fx-background-insets: 0, 0 0 1px 1px;
108 | }
109 | .header-pane:bottom .corner-button {
110 | -fx-background-insets: 0, 1px 0 0 1px;
111 | }
112 | .header-pane:left .corner-button {
113 | -fx-background-insets: 0, 1px 1px 0 0;
114 | }
115 | .header-pane:right .corner-button {
116 | -fx-background-insets: 0, 1px 0 0 1px;
117 | }
118 | .button-bar {
119 | -fx-spacing: -1;
120 | -fx-padding: 0;
121 | -fx-background-color: -color-border-default, -color-bg-default;
122 | }
123 | .button-bar:vertical {
124 | -fx-fill-height: true;
125 | }
126 | .button-bar:horizontal {
127 | -fx-fill-width: true;
128 | }
129 |
130 | /* HeaderRegion (Wrapper) */
131 | .header-region-wrapper {}
132 | .header-pane:top .header-region-wrapper {}
133 | .header-pane:bottom .header-region-wrapper {}
134 | .header-pane:left .header-region-wrapper {}
135 | .header-pane:right .header-region-wrapper {}
136 |
137 | /* HeaderRegion */
138 | .header-region {
139 | -fx-background-color: -color-border-default, -color-bg-overlay;
140 | }
141 | .header-region:top {
142 | -fx-background-insets: 0, 0 0 1px 0;
143 | }
144 | .header-region:bottom {
145 | -fx-background-insets: 0, 1px 0 0 0;
146 | }
147 | .header-region:left {
148 | -fx-background-insets: 0, 0 1px 0 0;
149 | }
150 | .header-region:right {
151 | -fx-background-insets: 0, 0 0 0 1px;
152 | }
153 | .header-region .node-wrapper {}
154 |
155 | /* Header */
156 | .header {
157 | -fx-background-color: -color-border-default, -color-bg-overlay;
158 | }
159 | .header:top {
160 | -fx-background-insets: 0, 0 0 1px 0;
161 | }
162 | .header:bottom {
163 | -fx-background-insets: 0, 1px 0 0 0;
164 | }
165 | .header:left {
166 | -fx-background-insets: 0, 0 1px 0 0;
167 | }
168 | .header:right {
169 | -fx-background-insets: 0, 0 0 0 1px;
170 | }
171 | .header:hover {
172 | -fx-background-color: -color-border-subtle, -color-bg-subtle;
173 | }
174 | .header-pane .header:selected {
175 | -fx-background-color: -color-bg-inset, -color-bg-default;
176 | }
177 | .header-pane:active .header:selected {
178 | -fx-background-color: -color-accent-4, -color-bg-default;
179 | }
180 | .header:selected:top {
181 | -fx-background-insets: 0, 0 0 2px 0;
182 | }
183 | .header:selected:bottom {
184 | -fx-background-insets: 0, 2px 0 0 0;
185 | }
186 | .header:selected:left {
187 | -fx-background-insets: 0, 0 2px 0 0;
188 | }
189 | .header:selected:right {
190 | -fx-background-insets: 0, 0 0 0 2px;
191 | }
192 |
193 | /* Collapsed */
194 | .container:collapsed .header { -fx-background-color: -color-bg-subtle; }
195 | .container:collapsed .header-pane { -fx-background-color: -color-bg-subtle; }
196 | .container:collapsed .header-region { -fx-background-color: -color-bg-subtle; }
197 | .container:collapsed .corner-button { -fx-background-color: -color-bg-subtle; }
198 |
199 | /* Misc */
200 | .dock-ghost-zone {
201 | -fx-opacity: 0.3;
202 | }
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/event/EventBus.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.event;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import software.coley.bentofx.dockable.Dockable;
5 | import software.coley.bentofx.dockable.DockableCloseListener;
6 | import software.coley.bentofx.dockable.DockableMoveListener;
7 | import software.coley.bentofx.dockable.DockableOpenListener;
8 | import software.coley.bentofx.dockable.DockableSelectListener;
9 | import software.coley.bentofx.layout.container.DockContainerLeaf;
10 | import software.coley.bentofx.path.DockablePath;
11 |
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.Objects;
15 | import java.util.concurrent.CopyOnWriteArrayList;
16 |
17 | /**
18 | * Bus for handling event firing and event listeners.
19 | *
20 | * @author Matt Coley
21 | */
22 | public class EventBus {
23 | private final List eventListeners = new CopyOnWriteArrayList<>();
24 | private final List openListeners = new CopyOnWriteArrayList<>();
25 | private final List moveListeners = new CopyOnWriteArrayList<>();
26 | private final List closeListeners = new CopyOnWriteArrayList<>();
27 | private final List selectListeners = new CopyOnWriteArrayList<>();
28 |
29 | /**
30 | * @param event
31 | * Event to fire.
32 | */
33 | public void fire(@Nonnull DockEvent event) {
34 | // Fire generic event listeners
35 | for (DockEventListener listener : eventListeners)
36 | listener.onDockEvent(event);
37 |
38 | // Fire specific listeners
39 | switch (event) {
40 | case DockEvent.ContainerChildAdded containerChildAdded -> {}
41 | case DockEvent.ContainerChildRemoved containerChildRemoved -> {}
42 | case DockEvent.ContainerParentChanged containerParentChanged -> {}
43 | case DockEvent.DockableAdded dockableAdded -> {
44 | Dockable dockable = dockableAdded.dockable();
45 | DockablePath path = Objects.requireNonNull(dockable.getPath());
46 | for (DockableOpenListener listener : openListeners) {
47 | listener.onOpen(path, dockable);
48 | }
49 | }
50 | case DockEvent.DockableClosing dockableClosing -> {
51 | Dockable dockable = dockableClosing.dockable();
52 | DockablePath path = Objects.requireNonNull(dockable.getPath());
53 | for (DockableCloseListener listener : closeListeners) {
54 | listener.onClose(path, dockable);
55 | }
56 | }
57 | case DockEvent.DockableParentChanged dockableParentChanged -> {
58 | DockContainerLeaf priorParent = dockableParentChanged.priorParent();
59 | DockContainerLeaf newParent = dockableParentChanged.newParent();
60 | if (priorParent == null || newParent == null)
61 | return;
62 | Dockable dockable = dockableParentChanged.dockable();
63 | DockablePath oldPath = priorParent.getPath().withChild(dockable);
64 | DockablePath newPath = newParent.getPath().withChild(dockable);
65 | for (DockableMoveListener listener : moveListeners) {
66 | listener.onMove(oldPath, newPath, dockable);
67 | }
68 | }
69 | case DockEvent.DockableRemoved dockableRemoved -> {}
70 | case DockEvent.DockableSelected dockableSelected -> {
71 | Dockable dockable = dockableSelected.dockable();
72 | DockablePath path = Objects.requireNonNull(dockable.getPath());
73 | for (DockableSelectListener listener : selectListeners) {
74 | listener.onSelect(path, dockable);
75 | }
76 | }
77 | case DockEvent.RootContainerAdded rootContainerAdded -> {}
78 | case DockEvent.RootContainerRemoved rootContainerRemoved -> {}
79 | }
80 | }
81 |
82 | /**
83 | * @return Generic event listeners.
84 | */
85 | @Nonnull
86 | public List getEventListeners() {
87 | return Collections.unmodifiableList(eventListeners);
88 | }
89 |
90 | /**
91 | * @param listener
92 | * Generic event listener to add.
93 | */
94 | public void addEventListener(@Nonnull DockEventListener listener) {
95 | eventListeners.add(listener);
96 | }
97 |
98 | /**
99 | * @param listener
100 | * Generic event listener to remove.
101 | */
102 | public boolean removeEventListener(@Nonnull DockEventListener listener) {
103 | return eventListeners.remove(listener);
104 | }
105 |
106 | /**
107 | * @return Dockable opening listeners.
108 | */
109 | @Nonnull
110 | public List getDockableOpenListener() {
111 | return Collections.unmodifiableList(openListeners);
112 | }
113 |
114 | /**
115 | * @param listener
116 | * Dockable opening listener to add.
117 | */
118 | public void addDockableOpenListener(@Nonnull DockableOpenListener listener) {
119 | openListeners.add(listener);
120 | }
121 |
122 | /**
123 | * @param listener
124 | * Dockable opening listener to remove.
125 | */
126 | public boolean removeDockableOpenListener(@Nonnull DockableOpenListener listener) {
127 | return openListeners.remove(listener);
128 | }
129 |
130 | /**
131 | * @return Dockable moving listeners.
132 | */
133 | @Nonnull
134 | public List getDockableMoveListener() {
135 | return Collections.unmodifiableList(moveListeners);
136 | }
137 |
138 | /**
139 | * @param listener
140 | * Dockable moving listener to add.
141 | */
142 | public void addDockableMoveListener(@Nonnull DockableMoveListener listener) {
143 | moveListeners.add(listener);
144 | }
145 |
146 | /**
147 | * @param listener
148 | * Dockable moving listener to remove.
149 | */
150 | public boolean removeDockableMoveListener(@Nonnull DockableMoveListener listener) {
151 | return moveListeners.remove(listener);
152 | }
153 |
154 | /**
155 | * @return Dockable closing listeners.
156 | */
157 | @Nonnull
158 | public List getDockableCloseListener() {
159 | return Collections.unmodifiableList(closeListeners);
160 | }
161 |
162 | /**
163 | * @param listener
164 | * Dockable closing listener to add.
165 | */
166 | public void addDockableCloseListener(@Nonnull DockableCloseListener listener) {
167 | closeListeners.add(listener);
168 | }
169 |
170 | /**
171 | * @param listener
172 | * Dockable closing listener to remove.
173 | */
174 | public boolean removeDockableCloseListener(@Nonnull DockableCloseListener listener) {
175 | return closeListeners.remove(listener);
176 | }
177 |
178 | /**
179 | * @return Dockable selecting listeners.
180 | */
181 | @Nonnull
182 | public List getDockableSelectListener() {
183 | return Collections.unmodifiableList(selectListeners);
184 | }
185 |
186 | /**
187 | * @param listener
188 | * Dockable selecting listener to add.
189 | */
190 | public void addDockableSelectListener(@Nonnull DockableSelectListener listener) {
191 | selectListeners.add(listener);
192 | }
193 |
194 | /**
195 | * @param listener
196 | * Dockable selecting listener to remove.
197 | */
198 | public boolean removeDockableSelectListener(@Nonnull DockableSelectListener listener) {
199 | return selectListeners.remove(listener);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/LinearItemPane.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.beans.property.BooleanProperty;
5 | import javafx.beans.property.ObjectProperty;
6 | import javafx.beans.property.SimpleBooleanProperty;
7 | import javafx.beans.property.SimpleObjectProperty;
8 | import javafx.geometry.Bounds;
9 | import javafx.geometry.HPos;
10 | import javafx.geometry.Insets;
11 | import javafx.geometry.Orientation;
12 | import javafx.geometry.VPos;
13 | import javafx.scene.Node;
14 | import javafx.scene.Parent;
15 | import javafx.scene.layout.HBox;
16 | import javafx.scene.layout.Pane;
17 | import javafx.scene.layout.VBox;
18 |
19 | /**
20 | * A basic pane that lays out children in a single line.
21 | * Children that go beyond the bounds of this pane are made invisible/unmanaged.
22 | *
23 | * @author Matt Coley
24 | */
25 | @SuppressWarnings("DuplicatedCode")
26 | public class LinearItemPane extends Pane {
27 | protected static final int MIN_PERPENDICULAR = 16;
28 | private final Orientation orientation;
29 | private final BooleanProperty overflowing = new SimpleBooleanProperty();
30 | private final BooleanProperty fitChildrenToPerpendicular = new SimpleBooleanProperty();
31 | private final ObjectProperty keepInView = new SimpleObjectProperty<>();
32 |
33 | /**
34 | * @param orientation
35 | * Which axis to layout children on.
36 | */
37 | public LinearItemPane(@Nonnull Orientation orientation) {
38 | this.orientation = orientation;
39 |
40 | // When the child to keep in view changes, update the layout.
41 | keepInView.addListener((ob, old, cur) -> requestLayout());
42 |
43 | // Same for perpendicular fitting.
44 | fitChildrenToPerpendicular.addListener((ob, old, cur) -> requestLayout());
45 | }
46 |
47 | /**
48 | * @return Orientation of this linear pane.
49 | */
50 | @Nonnull
51 | public Orientation getOrientation() {
52 | return orientation;
53 | }
54 |
55 | /**
56 | * @return {@code true} when children overflow beyond the visible bounds of this pane.
57 | * {@code false} when all children are visible in-bounds.
58 | */
59 | @Nonnull
60 | public BooleanProperty overflowingProperty() {
61 | return overflowing;
62 | }
63 |
64 | /**
65 | * @return A child to keep in view.
66 | */
67 | @Nonnull
68 | public ObjectProperty keepInViewProperty() {
69 | return keepInView;
70 | }
71 |
72 | /**
73 | * Similar to {@link HBox#setFillHeight(boolean)} and {@link VBox#setFillWidth(boolean)}.
74 | * Requires any child node added to this pane to {@link Node#isResizable() support resizing}.
75 | *
76 | * @return {@code true} to fit child widths/height to the dimensions of this pane on the perpendicular axis.
77 | */
78 | @Nonnull
79 | public BooleanProperty fitChildrenToPerpendicularProperty() {
80 | return fitChildrenToPerpendicular;
81 | }
82 |
83 | /**
84 | * Convenience call for {@code getChildren().add(node)}
85 | *
86 | * @param node
87 | * Child to add.
88 | */
89 | public void add(@Nonnull Node node) {
90 | getChildren().add(node);
91 | }
92 |
93 | @Override
94 | protected void layoutChildren() {
95 | if (orientation == Orientation.HORIZONTAL) {
96 | layoutHorizontal();
97 | } else {
98 | layoutVertical();
99 | }
100 | }
101 |
102 | protected void layoutHorizontal() {
103 | final int maxX = (int) getWidth();
104 | final int y = 0;
105 | int x = 0;
106 |
107 | // Offset initial X value to keep the target child in the view.
108 | Node viewTarget = keepInView.get();
109 | if (viewTarget != null) {
110 | double offset = 0;
111 | for (Node child : getChildren()) {
112 | Bounds childBounds = child.getBoundsInParent();
113 | double childWidth = childBounds.getWidth();
114 | offset += childWidth;
115 | if (child == viewTarget) {
116 | if (offset > maxX)
117 | x = (int) (maxX - offset);
118 | break;
119 | }
120 | }
121 | }
122 |
123 | // Layout all children.
124 | boolean overflow = false;
125 | for (Node child : getChildren()) {
126 | // Do layout on child to ensure the bounds lookup we do next is up-to-date.
127 | if (child instanceof Parent childParent)
128 | childParent.layout();
129 | Bounds childBounds = child.getBoundsInParent();
130 | double childWidth = childBounds.getWidth();
131 | double childHeight = computeChildPerpendicularSize(childBounds, Orientation.HORIZONTAL);
132 | boolean visible = x + childWidth >= 0 && x < maxX;
133 |
134 | if (!child.visibleProperty().isBound()) {
135 | // We can optimize a bit by making children that can't be shown not visible and handle layout requests.
136 | child.setManaged(visible);
137 | child.setVisible(visible);
138 | }
139 | if (visible) {
140 | // The only bounds we need to modify is the width.
141 | // By adding +1 this will bump the size until the child is able to show all of its content.
142 | // At that point, adding +1 will not result in any further changes.
143 | layoutInArea(child, x, y, childWidth, childHeight,
144 | 0, Insets.EMPTY, false, true,
145 | HPos.LEFT, VPos.TOP);
146 | } else {
147 | overflow = true;
148 | }
149 |
150 | x += (int) childWidth;
151 | }
152 | overflowing.set(overflow);
153 | }
154 |
155 | protected void layoutVertical() {
156 | final int maxY = (int) getHeight();
157 | final int x = 0;
158 | int y = 0;
159 |
160 | // Offset initial Y value to keep the target child in the view.
161 | Node viewTarget = keepInView.get();
162 | if (viewTarget != null) {
163 | double offset = 0;
164 | for (Node child : getChildren()) {
165 | Bounds childBounds = child.getBoundsInParent();
166 | double childHeight = childBounds.getHeight();
167 | offset += childHeight;
168 | if (child == viewTarget) {
169 | if (offset > maxY)
170 | y = (int) (maxY - offset);
171 | break;
172 | }
173 | }
174 | }
175 |
176 | // Layout all children.
177 | boolean overflow = false;
178 | for (Node child : getChildren()) {
179 | // Do layout on child to ensure the bounds lookup we do next is up-to-date.
180 | if (child instanceof Parent childParent)
181 | childParent.layout();
182 | Bounds childBounds = child.getBoundsInParent();
183 | double childWidth = computeChildPerpendicularSize(childBounds, Orientation.VERTICAL);
184 | double childHeight = childBounds.getHeight();
185 | boolean visible = y + childHeight >= 0 && y < maxY;
186 |
187 | // We can optimize a bit by making children that can't be shown not visible and handle layout requests.
188 | if (!child.visibleProperty().isBound()) {
189 | child.setManaged(visible);
190 | child.setVisible(visible);
191 | }
192 | if (visible) {
193 | // The only bounds we need to modify is the height.
194 | // By adding +1 this will bump the size until the child is able to show all of its content.
195 | // At that point, adding +1 will not result in any further changes.
196 | layoutInArea(child, x, y, childWidth, childHeight,
197 | 0, Insets.EMPTY, true, false,
198 | HPos.LEFT, VPos.TOP);
199 | } else {
200 | overflow = true;
201 | }
202 |
203 | y += (int) childHeight;
204 | }
205 | overflowing.set(overflow);
206 | }
207 |
208 | protected double computeChildPerpendicularSize(@Nonnull Bounds childBounds, @Nonnull Orientation orientation) {
209 | if (orientation == Orientation.HORIZONTAL) {
210 | return Math.max(fitChildrenToPerpendicular.get() ? getHeight() : childBounds.getHeight(), MIN_PERPENDICULAR);
211 | } else {
212 | return Math.max(fitChildrenToPerpendicular.get() ? getWidth() : childBounds.getWidth(), MIN_PERPENDICULAR);
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/util/BentoUtils.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.util;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.application.Platform;
6 | import javafx.beans.value.ChangeListener;
7 | import javafx.beans.value.ObservableValue;
8 | import javafx.css.Selector;
9 | import javafx.geometry.Orientation;
10 | import javafx.geometry.Point2D;
11 | import javafx.geometry.Side;
12 | import javafx.scene.Node;
13 | import javafx.scene.Parent;
14 | import javafx.scene.Scene;
15 | import javafx.scene.layout.BorderPane;
16 | import javafx.scene.layout.Region;
17 | import software.coley.bentofx.control.HeaderPane;
18 |
19 | import java.util.ArrayList;
20 | import java.util.List;
21 | import java.util.function.Consumer;
22 |
23 | /**
24 | * Various utilities for bento internals.
25 | *
26 | * @author Matt Coley
27 | */
28 | public class BentoUtils {
29 | /**
30 | * @param side
31 | * Some side.
32 | *
33 | * @return Respective orientation if it were to be used for a {@link HeaderPane}.
34 | */
35 | @Nonnull
36 | public static Orientation sideToOrientation(@Nullable Side side) {
37 | return switch (side) {
38 | case TOP, BOTTOM -> Orientation.HORIZONTAL;
39 | case LEFT, RIGHT -> Orientation.VERTICAL;
40 | case null -> Orientation.HORIZONTAL;
41 | };
42 | }
43 |
44 | /**
45 | * @param target
46 | * Some target to base calculations in.
47 | * @param x
48 | * Target x.
49 | * @param y
50 | * Target y.
51 | *
52 | * @return The closest side for the given target position in the given region.
53 | */
54 | @Nullable
55 | public static Side computeClosestSide(@Nonnull Region target, double x, double y) {
56 | double w = target.getWidth();
57 | double h = target.getHeight();
58 | double mw = w / 2;
59 | double mh = h / 2;
60 |
61 | Point2D top = new Point2D(mw, 0);
62 | Point2D bottom = new Point2D(mw, h);
63 | Point2D left = new Point2D(0, mh);
64 | Point2D right = new Point2D(w, mh);
65 | Point2D center = new Point2D(mw, mh);
66 | Point2D[] candidates = new Point2D[]{center, top, bottom, left, right};
67 | Side[] sides = new Side[]{null, Side.TOP, Side.BOTTOM, Side.LEFT, Side.RIGHT};
68 | int closest = 0;
69 | double closestDistance = Double.MAX_VALUE;
70 | for (int i = 0; i < candidates.length; i++) {
71 | Point2D candidate = candidates[i];
72 | double distance = candidate.distance(x, y);
73 | if (distance < closestDistance) {
74 | closest = i;
75 | closestDistance = distance;
76 | }
77 | }
78 |
79 | return sides[closest];
80 | }
81 |
82 | /**
83 | * Find all children with the given type in the given parent.
84 | *
85 | * The search does not continue for children that match the type. For instance if you had five
86 | * {@link BorderPane} embedded in a row all, only the top-most {@link BorderPane} would be yielded here.
87 | *
88 | * @param parent
89 | * Parent to search in.
90 | * @param nodeType
91 | * Type of children to find.
92 | *
93 | * @return All matching children of any level with the given type.
94 | */
95 | @SuppressWarnings("unchecked")
96 | public static List getCastChildren(@Nonnull Parent parent, @Nonnull Class nodeType) {
97 | return (List) getChildren(parent, nodeType);
98 | }
99 |
100 | /**
101 | * Find all children with the given type in the given parent.
102 | *
103 | * The search does not continue for children that match the type. For instance if you had five
104 | * {@link BorderPane} embedded in a row all, only the top-most {@link BorderPane} would be yielded here.
105 | *
106 | * @param parent
107 | * Parent to search in.
108 | * @param nodeType
109 | * Type of children to find.
110 | *
111 | * @return All matching children of any level with the given type.
112 | */
113 | @Nonnull
114 | public static List getChildren(@Nonnull Parent parent, @Nonnull Class> nodeType) {
115 | List list = new ArrayList<>();
116 | visitAndMatchChildren(parent, nodeType, list);
117 | return list;
118 | }
119 |
120 | /**
121 | * Find all children with the given CSS selector in the given parent.
122 | *
123 | * The search does not continue for children that match the selector. For instance if you had five
124 | * panes embedded in a row all with the same selector, only the top-most pane would be yielded here.
125 | *
126 | * @param parent
127 | * Parent to search in.
128 | * @param cssSelector
129 | * CSS selector of children to find.
130 | *
131 | * @return All matching children of any level with the given CSS selector.
132 | */
133 | @Nonnull
134 | public static List getChildren(@Nonnull Parent parent, @Nonnull String cssSelector) {
135 | Selector selector = Selector.createSelector(cssSelector);
136 | List list = new ArrayList<>();
137 | visitAndMatchChildren(parent, selector, list);
138 | return list;
139 | }
140 |
141 | private static void visitAndMatchChildren(@Nonnull Parent parent,
142 | @Nonnull Selector selector,
143 | @Nonnull List list) {
144 | for (Node node : parent.getChildrenUnmodifiable()) {
145 | if (selector.applies(node)) {
146 | list.add(node);
147 | } else if (node instanceof Parent childParent) {
148 | visitAndMatchChildren(childParent, selector, list);
149 | }
150 | }
151 | }
152 |
153 | private static void visitAndMatchChildren(@Nonnull Parent parent,
154 | @Nonnull Class> nodeType,
155 | @Nonnull List list) {
156 | for (Node node : parent.getChildrenUnmodifiable()) {
157 | if (nodeType.isAssignableFrom(node.getClass())) {
158 | list.add(node);
159 | } else if (node instanceof Parent childParent) {
160 | visitAndMatchChildren(childParent, nodeType, list);
161 | }
162 | }
163 | }
164 |
165 | /**
166 | * Schedules some action to be run later when a {@link Node} is attached to a {@link Scene}.
167 | *
168 | * @param node
169 | * Node to operate on.
170 | * @param action
171 | * Action to run on the node when it is attached to a scene.
172 | * @param
173 | * Node type.
174 | */
175 | public static void scheduleWhenShown(@Nonnull T node, @Nonnull Consumer action) {
176 | // Already showing, do the action immediately.
177 | Scene scene = node.getScene();
178 | if (scene != null) {
179 | action.accept(node);
180 | return;
181 | }
182 |
183 | // Schedule again when the node is attached to a scene.
184 | node.sceneProperty().addListener(new ChangeListener<>() {
185 | @Override
186 | public void changed(ObservableValue extends Scene> observable, Scene oldScene, Scene newScene) {
187 | if (newScene != null) {
188 | node.sceneProperty().removeListener(this);
189 |
190 | // We schedule the action rather than handling things immediately.
191 | // The layout pass still needs to run, and if we operated now some properties like dimensions
192 | // would not be up-to-date with the expectations of when the target node is "showing".
193 | Platform.runLater(() -> {
194 | action.accept(node);
195 |
196 | //In the case that these actions were queued immediately after the final layout pass,
197 | //requestLayout to execute the queued action so that they aren't
198 | //idling until another layout pass occurs.
199 | //Another layout pass being the user moving a divider, collapsing a header, etc.
200 | if(node instanceof Parent parent){
201 | Runnable postListener = new Runnable() {
202 | @Override
203 | public void run() {
204 | newScene.removePostLayoutPulseListener(this);
205 | parent.requestLayout();
206 | }
207 | };
208 | newScene.addPostLayoutPulseListener(postListener);
209 | }
210 |
211 | });
212 | }
213 | }
214 | });
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/test/java/software/coley/boxfx/demo/BoxApp.java:
--------------------------------------------------------------------------------
1 | package software.coley.boxfx.demo;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.application.Application;
5 | import javafx.geometry.Orientation;
6 | import javafx.geometry.Side;
7 | import javafx.scene.Scene;
8 | import javafx.scene.control.Alert;
9 | import javafx.scene.control.ButtonType;
10 | import javafx.scene.control.ContextMenu;
11 | import javafx.scene.control.Label;
12 | import javafx.scene.control.MenuItem;
13 | import javafx.scene.control.SeparatorMenuItem;
14 | import javafx.scene.effect.BlurType;
15 | import javafx.scene.effect.InnerShadow;
16 | import javafx.scene.paint.Color;
17 | import javafx.scene.shape.Circle;
18 | import javafx.scene.shape.Polygon;
19 | import javafx.scene.shape.Rectangle;
20 | import javafx.scene.shape.Shape;
21 | import javafx.stage.Stage;
22 | import software.coley.bentofx.Bento;
23 | import software.coley.bentofx.building.DockBuilding;
24 | import software.coley.bentofx.dockable.Dockable;
25 | import software.coley.bentofx.event.DockEvent;
26 | import software.coley.bentofx.layout.container.DockContainerBranch;
27 | import software.coley.bentofx.layout.container.DockContainerLeaf;
28 |
29 | public class BoxApp extends Application {
30 | @Override
31 | public void start(Stage stage) {
32 | stage.setWidth(1000);
33 | stage.setHeight(700);
34 |
35 | Bento bento = new Bento();
36 | bento.placeholderBuilding().setDockablePlaceholderFactory(dockable -> new Label("Empty Dockable"));
37 | bento.placeholderBuilding().setContainerPlaceholderFactory(container -> new Label("Empty Container"));
38 | bento.events().addEventListener((DockEvent event) -> {
39 | if (event instanceof DockEvent.DockableClosing closingEvent)
40 | handleDockableClosing(closingEvent);
41 | });
42 |
43 | DockBuilding builder = bento.dockBuilding();
44 | DockContainerBranch branchRoot = builder.root("root");
45 | DockContainerBranch branchWorkspace = builder.branch("workspace");
46 | DockContainerLeaf leafWorkspaceTools = builder.leaf("workspace-tools");
47 | DockContainerLeaf leafWorkspaceHeaders = builder.leaf("workspace-headers");
48 | DockContainerLeaf leafTools = builder.leaf("misc-tools");
49 |
50 | branchWorkspace.setPruneWhenEmpty(false);
51 | leafWorkspaceTools.setPruneWhenEmpty(false);
52 | leafTools.setPruneWhenEmpty(false);
53 | leafTools.setPruneWhenEmpty(false);
54 |
55 | // Add dummy menus to each.
56 | leafTools.setMenuFactory(d -> addSideOptions(new ContextMenu(), leafTools));
57 | leafWorkspaceHeaders.setMenuFactory(d -> addSideOptions(new ContextMenu(), leafWorkspaceHeaders));
58 | leafWorkspaceTools.setMenuFactory(d -> addSideOptions(new ContextMenu(), leafWorkspaceTools));
59 |
60 | // These leaves shouldn't auto-expand. They are intended to be a set size.
61 | DockContainerBranch.setResizableWithParent(leafTools, false);
62 | DockContainerBranch.setResizableWithParent(leafWorkspaceTools, false);
63 |
64 | // Root: Workspace on top, tools on bottom
65 | // Workspace: Explorer on left, primary editor tabs on right
66 | branchRoot.setOrientation(Orientation.VERTICAL);
67 | branchWorkspace.setOrientation(Orientation.HORIZONTAL);
68 | branchRoot.addContainers(branchWorkspace, leafTools);
69 | branchWorkspace.addContainers(leafWorkspaceTools, leafWorkspaceHeaders);
70 |
71 | // Changing tool header sides to be aligned with application's far edges (to facilitate better collapsing UX)
72 | leafWorkspaceTools.setSide(Side.LEFT);
73 | leafTools.setSide(Side.BOTTOM);
74 |
75 | // Tools shouldn't allow splitting (mirroring intellij behavior)
76 | leafWorkspaceTools.setCanSplit(false);
77 | leafTools.setCanSplit(false);
78 |
79 | // Primary editor space should not prune when empty
80 | leafWorkspaceHeaders.setPruneWhenEmpty(false);
81 |
82 | // Set intended sizes for tools (leaf does not need to be a direct child, just some level down in the chain)
83 | branchRoot.setContainerSizePx(leafTools, 200);
84 | branchRoot.setContainerSizePx(leafWorkspaceTools, 300);
85 |
86 | // Make the bottom collapsed by default
87 | branchRoot.setContainerCollapsed(leafTools, true);
88 |
89 | // Adding dockables to the leafs
90 | leafWorkspaceTools.addDockables(
91 | buildDockable(builder, 1, 0, "Workspace"),
92 | buildDockable(builder, 1, 1, "Bookmarks"),
93 | buildDockable(builder, 1, 2, "Modifications")
94 | );
95 | leafTools.addDockables(
96 | buildDockable(builder, 2, 0, "Logging"),
97 | buildDockable(builder, 2, 1, "Terminal"),
98 | buildDockable(builder, 2, 2, "Problems")
99 | );
100 | leafWorkspaceHeaders.addDockables(
101 | buildDockable(builder, 0, 0, "Class 1"),
102 | buildDockable(builder, 0, 1, "Class 2"),
103 | buildDockable(builder, 0, 2, "Class 3"),
104 | buildDockable(builder, 0, 3, "Class 4"),
105 | buildDockable(builder, 0, 4, "Class 5")
106 | );
107 |
108 | Scene scene = new Scene(branchRoot);
109 | scene.getStylesheets().add("/bento.css");
110 | stage.setScene(scene);
111 | stage.setOnHidden(e -> System.exit(0));
112 | stage.show();
113 | }
114 |
115 | @Nonnull
116 | private Dockable buildDockable(@Nonnull DockBuilding builder, int s, int i, @Nonnull String title) {
117 | Dockable dockable = builder.dockable();
118 | dockable.setTitle(title);
119 | dockable.setIconFactory(d -> makeIcon(s, i));
120 | dockable.setNode(new Label("<" + title + ":" + i + ">"));
121 | dockable.setContextMenuFactory(d -> {
122 | return new ContextMenu(
123 | new MenuItem("Menu for : " + dockable.getTitle()),
124 | new SeparatorMenuItem(),
125 | new MenuItem("Stuff")
126 | );
127 | });
128 | if (s > 0) {
129 | dockable.setDragGroupMask(1);
130 | dockable.setClosable(false);
131 | }
132 | return dockable;
133 | }
134 |
135 | private void handleDockableClosing(@Nonnull DockEvent.DockableClosing closingEvent) {
136 | final Dockable dockable = closingEvent.dockable();
137 | if (!dockable.getTitle().startsWith("Class "))
138 | return;
139 |
140 | final Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
141 | alert.setTitle("Confirmation");
142 | alert.setHeaderText(null);
143 | alert.setContentText("Save changes to [" + dockable.getTitle() + "] before closing?");
144 | alert.getButtonTypes().setAll(
145 | ButtonType.YES,
146 | ButtonType.NO,
147 | ButtonType.CANCEL
148 | );
149 |
150 | final ButtonType result = alert.showAndWait()
151 | .orElse(ButtonType.CANCEL);
152 |
153 | if (result.equals(ButtonType.YES)) {
154 | // simulate saving
155 | System.out.println("Saving " + dockable.getTitle() + "...");
156 | } else if (result.equals(ButtonType.NO)) {
157 | // nothing to do - just close
158 | } else if (result.equals(ButtonType.CANCEL)) {
159 | // prevent closing
160 | closingEvent.cancel();
161 | }
162 | }
163 |
164 | @Nonnull
165 | private static Shape makeIcon(int shapeMode, int i) {
166 | final int radius = 6;
167 | Shape icon = switch (shapeMode) {
168 | case 1 -> new Polygon(radius, 0, 0, radius * 2, radius * 2, radius * 2);
169 | case 2 -> new Rectangle(radius * 2, radius * 2);
170 | default -> new Circle(radius);
171 | };
172 | switch (i) {
173 | case 0 -> icon.setFill(Color.RED);
174 | case 1 -> icon.setFill(Color.ORANGE);
175 | case 2 -> icon.setFill(Color.LIME);
176 | case 3 -> icon.setFill(Color.CYAN);
177 | case 4 -> icon.setFill(Color.BLUE);
178 | case 5 -> icon.setFill(Color.PURPLE);
179 | default -> icon.setFill(Color.GREY);
180 | }
181 | icon.setEffect(new InnerShadow(BlurType.ONE_PASS_BOX, Color.BLACK, 2F, 10F, 0, 0));
182 | return icon;
183 | }
184 |
185 | @Nonnull
186 | private static ContextMenu addSideOptions(@Nonnull ContextMenu menu, @Nonnull DockContainerLeaf space) {
187 | for (Side side : Side.values()) {
188 | MenuItem item = new MenuItem(side.name());
189 | item.setGraphic(new Label(side == space.getSide() ? "✓" : " "));
190 | item.setOnAction(e -> space.setSide(side));
191 | menu.getItems().add(item);
192 | }
193 | return menu;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/canvas/PixelPainter.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control.canvas;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import javafx.scene.image.PixelFormat;
5 | import javafx.scene.image.PixelWriter;
6 |
7 | import java.nio.Buffer;
8 |
9 | /**
10 | * Outline of a helper for drawing pixels into a temporary buffer.
11 | *
12 | * Implementations may change how the buffer is constructed and interpreted.
13 | *
14 | * @param
15 | * Backing buffer type.
16 | *
17 | * @author Matt Coley
18 | * @author xxDark
19 | * @see PixelCanvas
20 | */
21 | public interface PixelPainter {
22 | /**
23 | * Initializes the painter.
24 | *
25 | * @param width
26 | * Assigned width.
27 | * @param height
28 | * Assigned height.
29 | *
30 | * @return {@code true} when the backing buffer was modified as a result of this operation.
31 | * {@code false} when the backing buffer has already been initialized to the given dimensions.
32 | */
33 | boolean initialize(int width, int height);
34 |
35 | /**
36 | * Releases any resources held by the painter.
37 | * Call {@link #initialize(int, int)} to initialize the painter again.
38 | */
39 | void release();
40 |
41 | /**
42 | * Commits any pending state to the display.
43 | *
44 | * @param pixelWriter
45 | * Pixel writer.
46 | */
47 | void commit(@Nonnull PixelWriter pixelWriter);
48 |
49 | /**
50 | * Fills the given rectangle with the given color.
51 | *
52 | * @param x
53 | * Rect x coordinate.
54 | * @param y
55 | * Rect y coordinate.
56 | * @param width
57 | * Rect width.
58 | * @param height
59 | * Rect height.
60 | * @param borderSize
61 | * Border size (inset into the rect).
62 | * @param color
63 | * ARGB Color to fill.
64 | * @param borderColor
65 | * ARGB Color to draw as a border.
66 | */
67 | default void fillBorderedRect(double x, double y, double width, double height, int borderSize, int color, int borderColor) {
68 | fillRect(x + borderSize, y + borderSize, width - borderSize * 2, height - borderSize * 2, color);
69 | drawRect(x, y, width, height, borderSize, borderColor);
70 | }
71 |
72 | /**
73 | * Fills the given rectangle with the given color.
74 | *
75 | * @param x
76 | * Rect x coordinate.
77 | * @param y
78 | * Rect y coordinate.
79 | * @param width
80 | * Rect width.
81 | * @param height
82 | * Rect height.
83 | * @param color
84 | * ARGB Color to fill.
85 | */
86 | default void fillRect(double x, double y, double width, double height, int color) {
87 | fillRect((int) x, (int) y, (int) width, (int) height, color);
88 | }
89 |
90 | /**
91 | * Fills the given rectangle with the given color.
92 | *
93 | * @param x
94 | * Rect x coordinate.
95 | * @param y
96 | * Rect y coordinate.
97 | * @param width
98 | * Rect width.
99 | * @param height
100 | * Rect height.
101 | * @param color
102 | * ARGB Color to fill.
103 | */
104 | void fillRect(int x, int y, int width, int height, int color);
105 |
106 | /**
107 | * Draws the edges of a given rectangle with the given color.
108 | *
109 | * @param x
110 | * Rect x coordinate.
111 | * @param y
112 | * Rect y coordinate.
113 | * @param width
114 | * Rect width.
115 | * @param height
116 | * Rect height.
117 | * @param borderSize
118 | * Border size (inset into the rect).
119 | * @param color
120 | * ARGB Color to draw.
121 | */
122 | default void drawRect(double x, double y, double width, double height, double borderSize, int color) {
123 | drawRect((int) x, (int) y, (int) width, (int) height, (int) borderSize, color);
124 | }
125 |
126 | /**
127 | * Draws the edges of a given rectangle with the given color.
128 | *
129 | * @param x
130 | * Rect x coordinate.
131 | * @param y
132 | * Rect y coordinate.
133 | * @param width
134 | * Rect width.
135 | * @param height
136 | * Rect height.
137 | * @param borderSize
138 | * Border size (inset into the rect).
139 | * @param color
140 | * ARGB Color to draw.
141 | */
142 | default void drawRect(int x, int y, int width, int height, int borderSize, int color) {
143 | fillRect(x, y, width, borderSize, color);
144 | fillRect(x, y + height - borderSize, width, borderSize, color);
145 | fillRect(x, y + borderSize, borderSize, height - borderSize, color);
146 | fillRect(x + width - borderSize, y + borderSize, borderSize, height - borderSize, color);
147 | }
148 |
149 | /**
150 | * Draws a horizontal line from the given point/width with the given color.
151 | *
152 | * @param x
153 | * Line x coordinate.
154 | * @param y
155 | * Line y coordinate.
156 | * @param lineWidth
157 | * Width of the line (Centered around y).
158 | * @param lineLength
159 | * Line width.
160 | * @param color
161 | * ARGB Color to draw.
162 | */
163 | default void drawHorizontalLine(double x, double y, double lineLength, double lineWidth, int color) {
164 | drawHorizontalLine((int) x, (int) y, (int) lineLength, (int) lineWidth, color);
165 | }
166 |
167 | /**
168 | * Draws a horizontal line from the given point/width with the given color.
169 | *
170 | * @param x
171 | * Line x coordinate.
172 | * @param y
173 | * Line y coordinate.
174 | * @param lineWidth
175 | * Width of the line (Centered around y).
176 | * @param lineLength
177 | * Line length.
178 | * @param color
179 | * ARGB Color to draw.
180 | */
181 | default void drawHorizontalLine(int x, int y, int lineLength, int lineWidth, int color) {
182 | fillRect(x, y - Math.max(1, lineWidth / 2), lineLength, lineWidth, color);
183 | }
184 |
185 | /**
186 | * Draws a vertical line from the given point/height with the given color.
187 | *
188 | * @param x
189 | * Line x coordinate.
190 | * @param y
191 | * Line y coordinate.
192 | * @param lineWidth
193 | * Width of the line (Centered around x).
194 | * @param lineLength
195 | * Line height.
196 | * @param color
197 | * ARGB Color to draw.
198 | */
199 | default void drawVerticalLine(double x, double y, double lineLength, double lineWidth, int color) {
200 | drawVerticalLine((int) x, (int) y, (int) lineLength, (int) lineWidth, color);
201 | }
202 |
203 | /**
204 | * Draws a vertical line from the given point/height with the given color.
205 | *
206 | * @param x
207 | * Line x coordinate.
208 | * @param y
209 | * Line y coordinate.
210 | * @param lineWidth
211 | * Width of the line (Centered around x).
212 | * @param lineLength
213 | * Line height.
214 | * @param color
215 | * ARGB Color to draw.
216 | */
217 | default void drawVerticalLine(int x, int y, int lineLength, int lineWidth, int color) {
218 | fillRect(x - Math.max(1, lineWidth / 2), y, lineWidth, lineLength, color);
219 | }
220 |
221 | /**
222 | * Draws an image at the given coordinates.
223 | *
224 | * @param x
225 | * X coordinate to draw image at.
226 | * @param y
227 | * Y coordinate to draw image at.
228 | * @param image
229 | * Image to draw.
230 | */
231 | void drawImage(int x, int y, @Nonnull ArgbSource image);
232 |
233 | /**
234 | * Draws an image at the given coordinates.
235 | *
236 | * @param x
237 | * X coordinate to draw image at.
238 | * @param y
239 | * Y coordinate to draw image at.
240 | * @param sx
241 | * X coordinate offset into the image.
242 | * @param sy
243 | * Y coordinate offset into the image.
244 | * @param sw
245 | * Width of the image to draw.
246 | * @param sh
247 | * Height of the image to draw.
248 | * @param image
249 | * Image to draw.
250 | */
251 | void drawImage(int x, int y, int sx, int sy, int sw, int sh, @Nonnull ArgbSource image);
252 |
253 | /**
254 | * Set a given pixel to the given color.
255 | *
256 | * @param x
257 | * X coordinate.
258 | * @param y
259 | * Y coordinate.
260 | * @param color
261 | * ARGB Color to set.
262 | */
263 | void setColor(int x, int y, int color);
264 |
265 | /**
266 | * Clears the buffer.
267 | */
268 | void clear();
269 |
270 | /**
271 | * @return Backing buffer.
272 | */
273 | @Nonnull
274 | B getBuffer();
275 |
276 | /**
277 | * @return Pixel format for contents in this painter's buffer.
278 | */
279 | @Nonnull
280 | PixelFormat getPixelFormat();
281 | }
282 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BentoFX
2 |
3 | A docking system for JavaFX.
4 |
5 | ## Usage
6 |
7 | Requirements:
8 |
9 | - JavaFX 19+
10 | - Java 17+
11 |
12 | Gradle syntax:
13 |
14 | ```groovy
15 | implementation "software.coley:bento-fx:${version}"
16 | ```
17 |
18 | Maven syntax:
19 |
20 | ```xml
21 |
22 | software.coley
23 | bento-fx
24 | ${version}
25 |
26 | ```
27 |
28 | ## Overview
29 |
30 | 
31 |
32 | In terms of hierarchy, the `Node` structure of Bento goes like:
33 |
34 | - `DockContainerRootBranch`
35 | - `DockContainerBranch` _(Nesting levels depends on which kind of implementation used)_
36 | - `DockContainerLeaf`
37 | - `Dockable` _(Zero or more)_
38 |
39 | Each level of `*DockContainer` in the given hierarchy and `Dockable` instances can be constructed via a `Bento`
40 | instance's builder offered by `bento.dockBuilding()`.
41 |
42 | ### Containers
43 |
44 | 
45 |
46 | Bento has a very simple model of branches and leaves. Branches hold additional child containers. Leaves
47 | display `Dockable` items and handle drag-n-drop operations.
48 |
49 | | Container type | Description |
50 | |-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
51 | | `DockContainerBranch` | Used to show multiple child `DockContainer` instances in a `SplitPane` display. Orientation and child node scaling are thus specified the same way as with `SplitPane`. |
52 | | `DockContainerLeaf` | Used to show any number of `Dockable` instance rendered by a `HeaderPane`. |
53 |
54 | ### Controls
55 |
56 | 
57 |
58 | Bento comes with a few custom controls that you will want to create a custom stylesheet for to best fit the intended
59 | look and feel of your application.
60 |
61 | An example reference sheet _(which is included in the dependency)_ can be found
62 | in [`bento.css`](src/main/resources/bento.css).
63 |
64 | | Control | Description |
65 | |-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
66 | | `Header` | Visual model of a `Dockable`. |
67 | | `HeaderPane` | Control that holds multiple `Header` children, and displays the currently selected `Header`'s associated `Dockable` content. |
68 | | `Headers` | Child of `HeaderPane` that acts as a `HBox`/`VBox` holding multiple `Headers`. |
69 | | `ButtonHBar` / `ButtonVBar` | Child of `HeaderPane` used to show buttons for the `DockContainerLeaf` for things like context menus and selection of overflowing `Header` items. |
70 |
71 | ### Dockable
72 |
73 | The `Dockable` can be thought of as the model behind each of a `HeaderPane`'s `Header` _(Much like a `Tab` of
74 | a `TabPane`)_.
75 | It outlines capabilities like whether the `Header` can be draggable, where it can be dropped, what text/graphic to
76 | display,
77 | and the associated JavaFX `Node` to display when placed into a `DockContainerLeaf`.
78 |
79 | ## Example
80 |
81 | 
82 |
83 | In this example we create a layout structure that loosely models how an IDE is laid out.
84 | There are tool-tabs on the left and bottom sides. The primary content like Java sources files
85 | reside in the middle and occupy the most space. The tool tabs are intended to be smaller and not
86 | automatically scale when we resize the window since we want the primary content to take up all
87 | of the available space when possible.
88 |
89 | We'll first create a vertically split container and put tools like logging/terminal at the bottom.
90 | The bottom section will be set to not resize with the parent for the reason mentioned previously.
91 |
92 | The top of the vertical split will hold our primary docking leaf container and the remaining tools.
93 | The tools will go on the left, and the main container on the right via a horizontally split container.
94 | The first item in this horizontal split will show up on the left, so that's where we'll put the tools.
95 | Then the second item will be our primary docking container.
96 |
97 | Our primary docking container is a glorified tab-pane, and we'll fill it up with some dummy items as if we
98 | were in the midst of working on some project. These tabs won't have any special properties,
99 | but we'll want to make sure the tools have some additional values set.
100 |
101 | All tool tabs will be constructed such that they are not closable and all belong to a shared
102 | drag group called `TOOLS`. Since these tabs all have a shared group they can be dragged
103 | amongst one another. However, the primary docking container tabs with our _"project files"_ cannot be
104 | dragged into the areas housing our tools. If you try this out in IntelliJ you'll find it
105 | follows the same behavior.
106 |
107 | ```java
108 | Bento bento = new Bento();
109 | bento.placeholderBuilding().setDockablePlaceholderFactory(dockable -> new Label("Empty Dockable"));
110 | bento.placeholderBuilding().setContainerPlaceholderFactory(container -> new Label("Empty Container"));
111 | bento.events().addEventListener(System.out::println);
112 | DockBuilding builder = bento.dockBuilding();
113 | DockContainerBranch branchRoot = builder.root("root");
114 | DockContainerBranch branchWorkspace = builder.branch("workspace");
115 | DockContainerLeaf leafWorkspaceTools = builder.leaf("workspace-tools");
116 | DockContainerLeaf leafWorkspaceHeaders = builder.leaf("workspace-headers");
117 | DockContainerLeaf leafTools = builder.leaf("misc-tools");
118 |
119 | // These leaves shouldn't auto-expand. They are intended to be a set size.
120 | DockContainerBranch.setResizableWithParent(leafTools, false);
121 | DockContainerBranch.setResizableWithParent(leafWorkspaceTools, false);
122 |
123 | // Root: Workspace on top, tools on bottom
124 | // Workspace: Explorer on left, primary editor tabs on right
125 | branchRoot.setOrientation(Orientation.VERTICAL);
126 | branchWorkspace.setOrientation(Orientation.HORIZONTAL);
127 | branchRoot.addContainers(branchWorkspace, leafTools);
128 | branchWorkspace.addContainers(leafWorkspaceTools, leafWorkspaceHeaders);
129 |
130 | // Changing tool header sides to be aligned with application's far edges (to facilitate better collaps
131 | leafWorkspaceTools.setSide(Side.LEFT);
132 | leafTools.setSide(Side.BOTTOM);
133 |
134 | // Tools shouldn't allow splitting (mirroring intellij behavior)
135 | leafWorkspaceTools.setCanSplit(false);
136 | leafTools.setCanSplit(false);
137 |
138 | // Primary editor space should not prune when empty
139 | leafWorkspaceHeaders.setPruneWhenEmpty(false);
140 |
141 | // Set intended sizes for tools (leaf does not need to be a direct child, just some level down in the
142 | branchRoot.setContainerSizePx(leafTools, 200);
143 | branchRoot.setContainerSizePx(leafWorkspaceTools, 300);
144 |
145 | // Make the bottom collapsed by default
146 | branchRoot.setContainerCollapsed(leafTools, true);
147 |
148 | // Adding dockables to the leafs
149 | leafWorkspaceTools.addDockables(
150 | buildDockable(builder, 1, 0, "Workspace"),
151 | buildDockable(builder, 1, 1, "Bookmarks"),
152 | buildDockable(builder, 1, 2, "Modifications")
153 | );
154 | leafTools.addDockables(
155 | buildDockable(builder, 2, 0, "Logging"),
156 | buildDockable(builder, 2, 1, "Terminal"),
157 | buildDockable(builder, 2, 2, "Problems")
158 | );
159 | leafWorkspaceHeaders.addDockables(
160 | buildDockable(builder, 0, 0, "Class 1"),
161 | buildDockable(builder, 0, 1, "Class 2"),
162 | buildDockable(builder, 0, 2, "Class 3"),
163 | buildDockable(builder, 0, 3, "Class 4"),
164 | buildDockable(builder, 0, 4, "Class 5")
165 | );
166 |
167 | // Show it
168 | Scene scene = new Scene(branchRoot);
169 | scene.getStylesheets().add("/bento.css");
170 | stage.setScene(scene);
171 | stage.setOnHidden(e -> System.exit(0));
172 | stage.show();
173 | ```
174 |
175 | For a more real-world example you can check out [Recaf](https://github.com/Col-E/Recaf/)
176 |
177 | 
178 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/src/main/java/software/coley/bentofx/control/HeaderPane.java:
--------------------------------------------------------------------------------
1 | package software.coley.bentofx.control;
2 |
3 | import jakarta.annotation.Nonnull;
4 | import jakarta.annotation.Nullable;
5 | import javafx.beans.binding.BooleanBinding;
6 | import javafx.beans.property.ObjectProperty;
7 | import javafx.collections.ListChangeListener;
8 | import javafx.collections.ObservableList;
9 | import javafx.geometry.Orientation;
10 | import javafx.geometry.Side;
11 | import javafx.scene.AccessibleRole;
12 | import javafx.scene.Node;
13 | import javafx.scene.control.Button;
14 | import javafx.scene.control.ContextMenu;
15 | import javafx.scene.control.MenuItem;
16 | import javafx.scene.control.TabPane;
17 | import javafx.scene.layout.BorderPane;
18 | import software.coley.bentofx.Bento;
19 | import software.coley.bentofx.dockable.Dockable;
20 | import software.coley.bentofx.layout.container.DockContainerLeaf;
21 | import software.coley.bentofx.util.BentoUtils;
22 |
23 | import static software.coley.bentofx.util.BentoStates.*;
24 |
25 | /**
26 | * Basically just a re-implementation of a {@link TabPane} except for {@link Dockable}.
27 | *
28 | * @author Matt Coley
29 | */
30 | public class HeaderPane extends BorderPane {
31 | private final DockContainerLeaf container;
32 | private final ContentWrapper contentWrapper;
33 | private Headers headers;
34 |
35 | /**
36 | * @param container
37 | * Parent container.
38 | */
39 | public HeaderPane(@Nonnull DockContainerLeaf container) {
40 | this.container = container;
41 | this.contentWrapper = container.getBento().controlsBuilding().newContentWrapper(container);
42 |
43 | getStyleClass().add("header-pane");
44 | setAccessibleRole(AccessibleRole.TAB_PANE);
45 |
46 | // Track that this view has focus somewhere in the hierarchy.
47 | // This will allow us to style the active view's subclasses specially.
48 | container.focusWithinProperty().addListener((ob, old, cur) -> pseudoClassStateChanged(PSEUDO_ACTIVE, cur));
49 |
50 | // Setup layout + observers to handle layout updates
51 | recomputeLayout(container.getSide());
52 | container.sideProperty().addListener((ob, old, cur) -> recomputeLayout(cur));
53 | container.selectedDockableProperty().addListener((ob, old, cur) -> {
54 | Header oldSelectedHeader = getHeader(old);
55 | Header newSelectedHeader = getHeader(cur);
56 |
57 | if (oldSelectedHeader != null) oldSelectedHeader.setSelected(false);
58 | if (newSelectedHeader != null) newSelectedHeader.setSelected(true);
59 |
60 | if (cur != null) {
61 | // We need to ensure that the dockable's prior containing display unbinds it as a child.
62 | // - https://bugs.openjdk.org/browse/JDK-8137251
63 | // - This control will unbind its prior value when we tell it to bind the new value
64 | ObjectProperty dockableNodeProperty = cur.nodeProperty();
65 | if (dockableNodeProperty.get() != null && dockableNodeProperty.get().getParent() instanceof BorderPane oldContentWrapper)
66 | oldContentWrapper.centerProperty().unbind();
67 |
68 | // Rebind to display newly selected dockable's content.
69 | contentWrapper.centerProperty().unbind();
70 | contentWrapper.centerProperty().bind(dockableNodeProperty
71 | .map(display -> display != null ? display : getBento().placeholderBuilding().build(cur)));
72 | } else {
73 | // No current content, fill in with a placeholder (unless collapsed).
74 | contentWrapper.centerProperty().unbind();
75 | contentWrapper.setCenter(container.isCollapsed() ? null : getBento().placeholderBuilding().build(container));
76 | }
77 | });
78 | container.getDockables().addListener((ListChangeListener) c -> {
79 | ObservableList headerList = headers.getChildren();
80 | while (c.next()) {
81 | if (c.wasPermutated()) {
82 | headerList.subList(c.getFrom(), c.getTo()).clear();
83 | headerList.addAll(c.getFrom(), c.getList().subList(c.getFrom(), c.getTo()).stream()
84 | .map(this::createHeader)
85 | .toList());
86 | } else if (c.wasRemoved()) {
87 | headerList.subList(c.getFrom(), c.getFrom() + c.getRemovedSize()).clear();
88 | } else if (c.wasAdded()) {
89 | headerList.addAll(c.getFrom(), c.getAddedSubList().stream()
90 | .map(this::createHeader)
91 | .toList());
92 | }
93 | }
94 | });
95 |
96 | BooleanBinding notCollapsed = container.collapsedProperty().not();
97 | contentWrapper.visibleProperty().bind(notCollapsed);
98 | contentWrapper.managedProperty().bind(notCollapsed);
99 | setCenter(contentWrapper);
100 | }
101 |
102 | private void recomputeLayout(@Nullable Side side) {
103 | // Clear CSS state
104 | pseudoClassStateChanged(PSEUDO_SIDE_TOP, false);
105 | pseudoClassStateChanged(PSEUDO_SIDE_BOTTOM, false);
106 | pseudoClassStateChanged(PSEUDO_SIDE_LEFT, false);
107 | pseudoClassStateChanged(PSEUDO_SIDE_RIGHT, false);
108 |
109 | // Clear edge nodes
110 | setTop(null);
111 | setBottom(null);
112 | setLeft(null);
113 | setRight(null);
114 |
115 | // Skip populating headers if there is no side specified.
116 | // - Yes, this also means no container-config button
117 | if (side == null)
118 | return;
119 |
120 | // Update CSS state and edge node to display our headers + controls aligned to the given side.
121 | headers = getBento().controlsBuilding().newHeaders(container, BentoUtils.sideToOrientation(side), side);
122 | Button dockableListButton = createDockableListButton();
123 | Button containerConfigButton = createContainerConfigButton();
124 | BorderPane headersWrapper = new BorderPane(headers);
125 | headersWrapper.getStyleClass().add("header-region-wrapper");
126 | if (BentoUtils.sideToOrientation(side) == Orientation.HORIZONTAL) {
127 | headersWrapper.setRight(new ButtonHBar(headers, dockableListButton, containerConfigButton));
128 | } else {
129 | headersWrapper.setBottom(new ButtonVBar(headers, dockableListButton, containerConfigButton));
130 | }
131 | switch (side) {
132 | case TOP -> {
133 | setTop(headersWrapper);
134 | pseudoClassStateChanged(PSEUDO_SIDE_TOP, true);
135 | }
136 | case BOTTOM -> {
137 | setBottom(headersWrapper);
138 | pseudoClassStateChanged(PSEUDO_SIDE_BOTTOM, true);
139 | }
140 | case LEFT -> {
141 | setLeft(headersWrapper);
142 | pseudoClassStateChanged(PSEUDO_SIDE_LEFT, true);
143 | }
144 | case RIGHT -> {
145 | setRight(headersWrapper);
146 | pseudoClassStateChanged(PSEUDO_SIDE_RIGHT, true);
147 | }
148 | }
149 |
150 | // Add all dockables to the headers display
151 | container.getDockables().stream()
152 | .map(d -> {
153 | Header header = createHeader(d);
154 | if (container.getSelectedDockable() == d)
155 | header.setSelected(true);
156 | return header;
157 | })
158 | .forEach(headers::add);
159 | }
160 |
161 | @Nonnull
162 | private Button createDockableListButton() {
163 | Button button = new Button("▼");
164 | button.setEllipsisString("▼");
165 | button.getStyleClass().addAll("corner-button", "list-button");
166 | button.setOnMousePressed(e -> {
167 | // TODO: A name filter that appears when you begin to type would be nice
168 | ContextMenu menu = new ContextMenu();
169 | menu.getItems().addAll(container.getDockables().stream().map(d -> {
170 | MenuItem item = new MenuItem();
171 | item.textProperty().bind(d.titleProperty());
172 | item.graphicProperty().bind(d.iconFactoryProperty().map(ic -> ic.build(d)));
173 | item.setOnAction(ignored -> container.selectDockable(d));
174 | return item;
175 | }).toList());
176 | button.setContextMenu(menu);
177 | });
178 | button.setOnMouseClicked(e -> button.getContextMenu().show(button, e.getScreenX(), e.getScreenY()));
179 | button.visibleProperty().bind(headers.overflowingProperty());
180 | button.managedProperty().bind(button.visibleProperty());
181 | return button;
182 | }
183 |
184 | @Nonnull
185 | private Button createContainerConfigButton() {
186 | Button button = new Button("≡");
187 | button.setEllipsisString("≡");
188 | button.getStyleClass().addAll("corner-button", "context-button");
189 | button.setOnMousePressed(e -> button.setContextMenu(container.buildContextMenu()));
190 | button.setOnMouseClicked(e -> button.getContextMenu().show(button, e.getScreenX(), e.getScreenY()));
191 | button.visibleProperty().bind(container.menuFactoryProperty().isNotNull());
192 | button.managedProperty().bind(button.visibleProperty());
193 | return button;
194 | }
195 |
196 | @Nonnull
197 | private Header createHeader(@Nonnull Dockable dockable) {
198 | return getBento().controlsBuilding().newHeader(dockable, this);
199 | }
200 |
201 | /**
202 | * @param dockable
203 | * Some dockable.
204 | *
205 | * @return Associated header within this pane that represents the given dockable.
206 | */
207 | @Nullable
208 | public Header getHeader(@Nullable Dockable dockable) {
209 | if (dockable == null)
210 | return null;
211 | for (Node child : headers.getChildren())
212 | if (child instanceof Header header && header.getDockable() == dockable)
213 | return header;
214 | return null;
215 | }
216 |
217 | /**
218 | * @return Parent container.
219 | */
220 | @Nonnull
221 | public DockContainerLeaf getContainer() {
222 | return container;
223 | }
224 |
225 | /**
226 | * @return The border-pane that holds the currently selected {@link Dockable#getNode()}.
227 | */
228 | @Nonnull
229 | public ContentWrapper getContentWrapper() {
230 | return contentWrapper;
231 | }
232 |
233 | /**
234 | * @return The linear-item-pane holding {@link Header} children.
235 | */
236 | @Nullable
237 | public Headers getHeaders() {
238 | return headers;
239 | }
240 |
241 | /**
242 | * @return Convenience call.
243 | */
244 | @Nonnull
245 | private Bento getBento() {
246 | return container.getBento();
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/assets/overview.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------