├── settings.gradle ├── assets ├── logo.xcf ├── controls.png ├── example.png ├── logo-256.png ├── overview.png ├── containers.png ├── logo-1080.png ├── example-recaf.png ├── containers.drawio ├── controls.drawio └── overview.drawio ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ └── java │ │ └── software │ │ └── coley │ │ └── boxfx │ │ └── demo │ │ ├── Runner.java │ │ └── BoxApp.java └── main │ ├── java │ ├── software │ │ └── coley │ │ │ └── bentofx │ │ │ ├── BentoBacked.java │ │ │ ├── event │ │ │ ├── DockEventListener.java │ │ │ ├── DockEvent.java │ │ │ └── EventBus.java │ │ │ ├── path │ │ │ ├── BentoPath.java │ │ │ ├── DockablePath.java │ │ │ └── DockContainerPath.java │ │ │ ├── dockable │ │ │ ├── DockableSelectListener.java │ │ │ ├── DockableMenuFactory.java │ │ │ ├── DockableOpenListener.java │ │ │ ├── DockableIconFactory.java │ │ │ ├── DockablePlaceholderFactory.java │ │ │ ├── DockableCloseListener.java │ │ │ ├── DockableMoveListener.java │ │ │ └── DragDropBehavior.java │ │ │ ├── control │ │ │ ├── canvas │ │ │ │ ├── PixelPainterUtils.java │ │ │ │ ├── PixelPainterIntArgbPre.java │ │ │ │ ├── ArgbSource.java │ │ │ │ ├── ArgbBufferedImageSource.java │ │ │ │ ├── ArgbImageSource.java │ │ │ │ ├── PixelPainterByteBgraPre.java │ │ │ │ ├── PixelPainterIntArgb.java │ │ │ │ ├── PixelPainterByteBgra.java │ │ │ │ └── PixelPainter.java │ │ │ ├── ButtonVBar.java │ │ │ ├── ButtonHBar.java │ │ │ ├── DragDropStage.java │ │ │ ├── ContentWrapper.java │ │ │ ├── Headers.java │ │ │ ├── LinearItemPane.java │ │ │ └── HeaderPane.java │ │ │ ├── building │ │ │ ├── StageFactory.java │ │ │ ├── CanvasFactory.java │ │ │ ├── HeaderPaneFactory.java │ │ │ ├── HeaderFactory.java │ │ │ ├── ContentWrapperFactory.java │ │ │ ├── SceneFactory.java │ │ │ ├── HeadersFactory.java │ │ │ ├── PlaceholderBuilding.java │ │ │ ├── DockBuilding.java │ │ │ ├── ControlsBuilding.java │ │ │ └── StageBuilding.java │ │ │ ├── Identifiable.java │ │ │ ├── util │ │ │ ├── DragDropTarget.java │ │ │ ├── BentoStates.java │ │ │ ├── DragUtils.java │ │ │ └── BentoUtils.java │ │ │ ├── layout │ │ │ ├── container │ │ │ │ ├── DockContainerLeafMenuFactory.java │ │ │ │ ├── DockContainerLeafPlaceholderFactory.java │ │ │ │ └── DockContainerRootBranch.java │ │ │ └── DockContainer.java │ │ │ ├── search │ │ │ ├── DockableVisitor.java │ │ │ ├── SearchVisitor.java │ │ │ ├── DockContainerVisitor.java │ │ │ └── SearchHandler.java │ │ │ └── Bento.java │ └── module-info.java │ └── resources │ └── bento.css ├── .gitignore ├── LICENSE ├── .gitattributes ├── gradlew.bat ├── .github └── workflows │ └── build.yml ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'bento-fx' 2 | 3 | -------------------------------------------------------------------------------- /assets/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/logo.xcf -------------------------------------------------------------------------------- /assets/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/controls.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/example.png -------------------------------------------------------------------------------- /assets/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/logo-256.png -------------------------------------------------------------------------------- /assets/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/overview.png -------------------------------------------------------------------------------- /assets/containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/containers.png -------------------------------------------------------------------------------- /assets/logo-1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/logo-1080.png -------------------------------------------------------------------------------- /assets/example-recaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/assets/example-recaf.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Col-E/BentoFX/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/java/software/coley/boxfx/demo/Runner.java: -------------------------------------------------------------------------------- 1 | package software.coley.boxfx.demo; 2 | 3 | public class Runner { 4 | public static void main(String[] args) { 5 | BoxApp.launch(BoxApp.class, args); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 18 02:53:32 EST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/BentoBacked.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | /** 6 | * Outline of an object with access to its originating {@link Bento} instance. 7 | * 8 | * @author Matt Coley 9 | */ 10 | public interface BentoBacked { 11 | /** 12 | * @return Bento instance responsible for this object. 13 | */ 14 | @Nonnull 15 | Bento getBento(); 16 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/event/DockEventListener.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.event; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | /** 6 | * Listener invoked by the firing of any {@link DockEvent}. 7 | * 8 | * @author Matt Coley 9 | */ 10 | public interface DockEventListener { 11 | /** 12 | * @param event 13 | * Event fired. 14 | */ 15 | void onDockEvent(@Nonnull DockEvent event); 16 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/path/BentoPath.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.path; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.layout.DockContainer; 5 | 6 | /** 7 | * Outline of a path to some bento content. 8 | * 9 | * @author Matt Coley 10 | */ 11 | public sealed interface BentoPath permits DockContainerPath, DockablePath { 12 | /** 13 | * @return Root container of the path. 14 | */ 15 | @Nonnull 16 | DockContainer rootContainer(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableSelectListener.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.path.DockablePath; 5 | 6 | /** 7 | * Listener that is invoked when a {@link Dockable} is selected. 8 | * 9 | * @author Matt Coley 10 | */ 11 | public interface DockableSelectListener { 12 | /** 13 | * @param path 14 | * Path to selected dockable. 15 | * @param dockable 16 | * Selected dockable. 17 | */ 18 | void onSelect(@Nonnull DockablePath path, @Nonnull Dockable dockable); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea/* 10 | *.iws 11 | *.iml 12 | *.ipr 13 | 14 | ### Eclipse ### 15 | .apt_generated 16 | .classpath 17 | .factorypath 18 | .project 19 | .settings 20 | .springBeans 21 | .sts4-cache 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | 36 | ### Mac OS ### 37 | .DS_Store -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/canvas/PixelPainterUtils.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.control.canvas; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.IntBuffer; 5 | 6 | /** 7 | * Common utils for {@link PixelPainter} implementations. 8 | * 9 | * @author Matt Coley 10 | */ 11 | public class PixelPainterUtils { 12 | public static final int[] EMPTY_ARRAY_I = new int[0]; 13 | public static final byte[] EMPTY_ARRAY_B = new byte[0]; 14 | public static final IntBuffer EMPTY_BUFFER_I = IntBuffer.wrap(EMPTY_ARRAY_I); 15 | public static final ByteBuffer EMPTY_BUFFER_B = ByteBuffer.wrap(EMPTY_ARRAY_B); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableMenuFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import javafx.scene.control.ContextMenu; 6 | 7 | /** 8 | * Factory to create a {@link ContextMenu} for some given {@link Dockable}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockableMenuFactory { 13 | /** 14 | * @param dockable 15 | * Dockable to create a context menu for. 16 | * 17 | * @return Context menu for the dockable. 18 | */ 19 | @Nullable 20 | ContextMenu build(@Nonnull Dockable dockable); 21 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/StageFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import javafx.stage.Stage; 6 | import software.coley.bentofx.control.DragDropStage; 7 | 8 | /** 9 | * Factory for building new {@link DragDropStage}. 10 | * 11 | * @author Matt Coley 12 | */ 13 | public interface StageFactory { 14 | /** 15 | * @param sourceStage 16 | * Original stage to copy state from. 17 | * 18 | * @return Newly created stage. 19 | */ 20 | @Nonnull 21 | DragDropStage newStage(@Nullable Stage sourceStage); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/CanvasFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.control.canvas.PixelCanvas; 5 | import software.coley.bentofx.layout.container.DockContainerLeaf; 6 | 7 | /** 8 | * Factory for building a {@link PixelCanvas} for a {@link DockContainerLeaf}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface CanvasFactory { 13 | /** 14 | * @param container 15 | * Parent container. 16 | * 17 | * @return New canvas. 18 | */ 19 | @Nonnull 20 | PixelCanvas newCanvas(@Nonnull DockContainerLeaf container); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/HeaderPaneFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.control.HeaderPane; 5 | import software.coley.bentofx.layout.container.DockContainerLeaf; 6 | 7 | /** 8 | * Factory for building a {@link HeaderPane} for a {@link DockContainerLeaf}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface HeaderPaneFactory { 13 | /** 14 | * @param container 15 | * Parent container. 16 | * 17 | * @return New header pane. 18 | */ 19 | @Nonnull 20 | HeaderPane newHeaderPane(@Nonnull DockContainerLeaf container); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/Identifiable.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | 6 | /** 7 | * Outline of an (ideally uniquely) identifiable object. 8 | * 9 | * @author Matt Coley 10 | */ 11 | public interface Identifiable { 12 | /** 13 | * @return This objects identifier. 14 | */ 15 | @Nonnull 16 | String getIdentifier(); 17 | 18 | /** 19 | * @param other 20 | * Another identifiable object. 21 | * 22 | * @return {@code true} when the other object has the same identifier. 23 | */ 24 | boolean matchesIdentity(@Nonnull Identifiable other); 25 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableOpenListener.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.layout.DockContainer; 5 | import software.coley.bentofx.path.DockablePath; 6 | 7 | /** 8 | * Listener that is invoked when a {@link Dockable} is added to a {@link DockContainer}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockableOpenListener { 13 | /** 14 | * @param path 15 | * Path to opened dockable. 16 | * @param dockable 17 | * Closed dockable. 18 | */ 19 | void onOpen(@Nonnull DockablePath path, @Nonnull Dockable dockable); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/util/DragDropTarget.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.util; 2 | 3 | import software.coley.bentofx.control.Header; 4 | import software.coley.bentofx.layout.container.DockContainerLeaf; 5 | 6 | /** 7 | * Type of drag-n-drop target for the completion of a {@link Header}'s drag operation. 8 | * 9 | * @author Matt Coley 10 | */ 11 | public enum DragDropTarget { 12 | /** 13 | * Drag-n-drop completed on a {@link Header}. 14 | */ 15 | HEADER, 16 | /** 17 | * Drag-n-drop completed on a {@link DockContainerLeaf}. 18 | */ 19 | REGION, 20 | /** 21 | * Drag-n-drop completed on {@code null} (nothing). 22 | */ 23 | EXTERNAL 24 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableIconFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import javafx.scene.Node; 6 | 7 | /** 8 | * Factory to create a {@link Node} graphic for some given {@link Dockable}. 9 | * Implementations should create NEW instances for EACH call. 10 | * 11 | * @author Matt Coley 12 | */ 13 | public interface DockableIconFactory { 14 | /** 15 | * @param dockable 16 | * Dockable to create a graphic for. 17 | * 18 | * @return Graphic for the dockable. 19 | */ 20 | @Nullable 21 | Node build(@Nonnull Dockable dockable); 22 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockablePlaceholderFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import javafx.scene.Node; 5 | 6 | /** 7 | * Factory to create a {@link Node} placeholder display for some given {@link Dockable}. 8 | * Implementations should create NEW instances for EACH call. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockablePlaceholderFactory { 13 | /** 14 | * @param dockable 15 | * Dockable to create a placeholder display for. 16 | * 17 | * @return Placeholder for the dockable. 18 | */ 19 | @Nonnull 20 | Node build(@Nonnull Dockable dockable); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/layout/container/DockContainerLeafMenuFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.layout.container; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import javafx.scene.control.ContextMenu; 6 | 7 | /** 8 | * Factory to create a {@link ContextMenu} for some given {@link DockContainerLeaf}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockContainerLeafMenuFactory { 13 | /** 14 | * @param container 15 | * Container to create a context menu for. 16 | * 17 | * @return Context menu for the container. 18 | */ 19 | @Nullable 20 | ContextMenu build(@Nonnull DockContainerLeaf container); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableCloseListener.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.layout.DockContainer; 5 | import software.coley.bentofx.path.DockablePath; 6 | 7 | /** 8 | * Listener invoked when a {@link Dockable} is removed from a {@link DockContainer} with the intent to close it. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockableCloseListener { 13 | /** 14 | * @param path 15 | * Path to dockable prior to closure. 16 | * @param dockable 17 | * Closed dockable. 18 | */ 19 | void onClose(@Nonnull DockablePath path, @Nonnull Dockable dockable); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/HeaderFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.control.Header; 5 | import software.coley.bentofx.control.HeaderPane; 6 | import software.coley.bentofx.dockable.Dockable; 7 | 8 | /** 9 | * Factory for building {@link Header} in a parent {@link HeaderPane}. 10 | * 11 | * @author Matt Coley 12 | */ 13 | public interface HeaderFactory { 14 | /** 15 | * @param dockable 16 | * Dockable to wrap. 17 | * @param parentPane 18 | * Parent header pane. 19 | * 20 | * @return New header. 21 | */ 22 | @Nonnull 23 | Header newHeader(@Nonnull Dockable dockable, @Nonnull HeaderPane parentPane); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/layout/container/DockContainerLeafPlaceholderFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.layout.container; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import javafx.scene.Node; 5 | 6 | /** 7 | * Factory to create a {@link Node} placeholder display for some given {@link DockContainerLeaf}. 8 | * Implementations should create NEW instances for EACH call. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockContainerLeafPlaceholderFactory { 13 | /** 14 | * @param container 15 | * Container to create a placeholder display for. 16 | * 17 | * @return Placeholder for the container. 18 | */ 19 | @Nonnull 20 | Node build(@Nonnull DockContainerLeaf container); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/ContentWrapperFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.control.ContentWrapper; 5 | import software.coley.bentofx.control.HeaderPane; 6 | import software.coley.bentofx.layout.container.DockContainerLeaf; 7 | 8 | /** 9 | * Factory for building {@link ContentWrapper} in a parent {@link HeaderPane}. 10 | * 11 | * @author Matt Coley 12 | */ 13 | public interface ContentWrapperFactory { 14 | /** 15 | * @param container 16 | * Parent container. 17 | * 18 | * @return Newly created content wrapper. 19 | */ 20 | @Nonnull 21 | ContentWrapper newContentWrapper(@Nonnull DockContainerLeaf container); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/dockable/DockableMoveListener.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.dockable; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.layout.DockContainer; 5 | import software.coley.bentofx.path.DockablePath; 6 | 7 | /** 8 | * Listener that is invoked when a {@link Dockable} is moved to a new {@link DockContainer}. 9 | * 10 | * @author Matt Coley 11 | */ 12 | public interface DockableMoveListener { 13 | /** 14 | * @param oldPath 15 | * Path to old dockable location. 16 | * @param newPath 17 | * Path to new dockable location. 18 | * @param dockable 19 | * Moved dockable. 20 | */ 21 | void onMove(@Nonnull DockablePath oldPath, @Nonnull DockablePath newPath, @Nonnull Dockable dockable); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/SceneFactory.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 software.coley.bentofx.control.DragDropStage; 8 | 9 | /** 10 | * Factory for building scenes when a new {@link DragDropStage} is being prepared. 11 | * 12 | * @author Matt Coley 13 | */ 14 | public interface SceneFactory { 15 | /** 16 | * @param sourceScene 17 | * Original scene to copy state from. 18 | * @param content 19 | * Content to place in the scene. 20 | * @param width 21 | * Content width. 22 | * @param height 23 | * Content height. 24 | * 25 | * @return Newly created scene. 26 | */ 27 | @Nonnull 28 | Scene newScene(@Nullable Scene sourceScene, @Nonnull Region content, double width, double height); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/building/HeadersFactory.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.building; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import javafx.geometry.Orientation; 5 | import javafx.geometry.Side; 6 | import software.coley.bentofx.control.HeaderPane; 7 | import software.coley.bentofx.control.Headers; 8 | import software.coley.bentofx.layout.container.DockContainerLeaf; 9 | 10 | /** 11 | * Factory for building {@link Headers} in a parent {@link HeaderPane}. 12 | * 13 | * @author Matt Coley 14 | */ 15 | public interface HeadersFactory { 16 | /** 17 | * @param container 18 | * Associated container. 19 | * @param orientation 20 | * Orientation of the headers. 21 | * @param side 22 | * Side this headers bar will be located at. 23 | * 24 | * @return Newly created headers. 25 | */ 26 | @Nonnull 27 | Headers newHeaders(@Nonnull DockContainerLeaf container, @Nonnull Orientation orientation, @Nonnull Side side); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Matthew Coley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Java sources 5 | *.java text diff=java eol=lf 6 | *.gradle text diff=java eol=lf 7 | 8 | # These files are text and should be normalized (Convert crlf => lf) 9 | *.css text diff=css eol=lf 10 | *.html text diff=html eol=lf 11 | *.md text diff=markdown eol=lf 12 | *.js text eol=lf 13 | *.csv text eol=lf 14 | *.json text eol=lf 15 | *.properties text eol=lf 16 | *.svg text eol=lf 17 | *.xml text eol=lf 18 | *.yaml text eol=lf 19 | *.yml text eol=lf 20 | *.toml text eol=lf 21 | *.lang text eol=lf 22 | 23 | # These files are binary and should be left untouched 24 | *.png binary 25 | *.gif binary 26 | *.jpg binary 27 | *.jpeg binary 28 | 29 | # Common build-tool wrapper scripts 30 | mvnw text eol=lf 31 | gradlew text eol=lf 32 | *.sh text eol=lf 33 | *.bat text eol=crlf 34 | *.cmd text eol=crlf -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/ButtonVBar.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.control; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import javafx.scene.Node; 5 | import javafx.scene.layout.Region; 6 | import javafx.scene.layout.VBox; 7 | import software.coley.bentofx.layout.container.DockContainerLeaf; 8 | 9 | import static software.coley.bentofx.util.BentoStates.PSEUDO_ORIENTATION_V; 10 | 11 | /** 12 | * {@link VBox} for {@link DockContainerLeaf} level controls in a {@link HeaderPane}. 13 | * 14 | * @author Matt Coley 15 | */ 16 | public class ButtonVBar extends VBox { 17 | /** 18 | * @param parent 19 | * Parent region to bind child width to. 20 | * @param children 21 | * Children to add to this box. 22 | */ 23 | public ButtonVBar(@Nonnull Region parent, Node... children) { 24 | getStyleClass().add("button-bar"); 25 | pseudoClassStateChanged(PSEUDO_ORIENTATION_V, true); 26 | 27 | for (Node child : children) { 28 | if (child instanceof Region childRegion) 29 | childRegion.prefWidthProperty().bind(parent.widthProperty()); 30 | getChildren().add(child); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/ButtonHBar.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.control; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import javafx.scene.Node; 5 | import javafx.scene.layout.HBox; 6 | import javafx.scene.layout.Region; 7 | import software.coley.bentofx.layout.container.DockContainerLeaf; 8 | 9 | import static software.coley.bentofx.util.BentoStates.PSEUDO_ORIENTATION_H; 10 | 11 | /** 12 | * {@link HBox} for {@link DockContainerLeaf} level controls in a {@link HeaderPane}. 13 | * 14 | * @author Matt Coley 15 | */ 16 | public class ButtonHBar extends HBox { 17 | /** 18 | * @param parent 19 | * Parent region to bind child height to. 20 | * @param children 21 | * Children to add to this box. 22 | */ 23 | public ButtonHBar(@Nonnull Region parent, Node... children) { 24 | getStyleClass().add("button-bar"); 25 | pseudoClassStateChanged(PSEUDO_ORIENTATION_H, true); 26 | 27 | for (Node child : children) { 28 | if (child instanceof Region childRegion) 29 | childRegion.prefHeightProperty().bind(parent.heightProperty()); 30 | getChildren().add(child); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/search/DockableVisitor.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.search; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import software.coley.bentofx.dockable.Dockable; 6 | 7 | import java.util.function.Predicate; 8 | 9 | /** 10 | * Search visitor that yields the first matching {@link Dockable} of some predicate. 11 | * 12 | * @author Matt Coley 13 | */ 14 | public class DockableVisitor implements SearchVisitor { 15 | private final Predicate matcher; 16 | private Dockable result; 17 | 18 | /** 19 | * @param matcher 20 | * Dockable predicate. 21 | */ 22 | public DockableVisitor(@Nonnull Predicate matcher) { 23 | this.matcher = matcher; 24 | } 25 | 26 | @Override 27 | public boolean visitDockable(@Nonnull Dockable dockable) { 28 | if (matcher.test(dockable)) { 29 | // Match found, stop visiting. 30 | result = dockable; 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | /** 37 | * @return Matched dockable if found. 38 | */ 39 | @Nullable 40 | public Dockable getMatchedDockable() { 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module bento.fx { 2 | requires static jakarta.annotation; 3 | 4 | requires javafx.base; 5 | requires javafx.graphics; 6 | requires javafx.controls; 7 | requires java.desktop; 8 | 9 | // Just open/export everything. Do whatever you want. 10 | exports software.coley.bentofx; 11 | exports software.coley.bentofx.building; 12 | exports software.coley.bentofx.control; 13 | exports software.coley.bentofx.control.canvas; 14 | exports software.coley.bentofx.dockable; 15 | exports software.coley.bentofx.event; 16 | exports software.coley.bentofx.layout; 17 | exports software.coley.bentofx.layout.container; 18 | exports software.coley.bentofx.path; 19 | exports software.coley.bentofx.search; 20 | exports software.coley.bentofx.util; 21 | opens software.coley.bentofx; 22 | opens software.coley.bentofx.building; 23 | opens software.coley.bentofx.control; 24 | opens software.coley.bentofx.control.canvas; 25 | opens software.coley.bentofx.dockable; 26 | opens software.coley.bentofx.event; 27 | opens software.coley.bentofx.layout; 28 | opens software.coley.bentofx.layout.container; 29 | opens software.coley.bentofx.path; 30 | opens software.coley.bentofx.search; 31 | opens software.coley.bentofx.util; 32 | } -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/search/SearchVisitor.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.search; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.dockable.Dockable; 5 | import software.coley.bentofx.layout.DockContainer; 6 | import software.coley.bentofx.layout.container.DockContainerBranch; 7 | import software.coley.bentofx.layout.container.DockContainerLeaf; 8 | 9 | /** 10 | * Visitor model to traverse {@link DockContainer} hierarchies. 11 | * 12 | * @author Matt Coley 13 | */ 14 | public interface SearchVisitor { 15 | /** 16 | * @param container 17 | * Container to visit. 18 | * 19 | * @return {@code true} to continue visitation. 20 | */ 21 | default boolean visitBranch(@Nonnull DockContainerBranch container) { 22 | return true; 23 | } 24 | 25 | /** 26 | * @param container 27 | * Container to visit. 28 | * 29 | * @return {@code true} to continue visitation. 30 | */ 31 | default boolean visitLeaf(@Nonnull DockContainerLeaf container) { 32 | return true; 33 | } 34 | 35 | /** 36 | * @param dockable 37 | * Dockable to visit. 38 | * 39 | * @return {@code true} to continue visitation. 40 | */ 41 | default boolean visitDockable(@Nonnull Dockable dockable) { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/path/DockablePath.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.path; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.dockable.Dockable; 5 | import software.coley.bentofx.layout.DockContainer; 6 | import software.coley.bentofx.layout.container.DockContainerLeaf; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Path to a given dockable, starting from the root container (Top {@link DockContainer#getParentContainer()} value) 12 | * all the way down to the container that holds the target dockable. 13 | * 14 | * @param containers 15 | * Containers up to and including some dockable's parent container. 16 | * @param dockable 17 | * Target dockable. 18 | */ 19 | public record DockablePath(@Nonnull List containers, @Nonnull Dockable dockable) implements BentoPath { 20 | @Nonnull 21 | @Override 22 | public DockContainer rootContainer() { 23 | // There must always be at least one container in a path since a dockable needs a parent to be placed into. 24 | return containers.getFirst(); 25 | } 26 | 27 | @Nonnull 28 | public DockContainerLeaf leafContainer() { 29 | // A dockable can only be put in a leaf, so this should be a safe cast. 30 | return (DockContainerLeaf) containers.getLast(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/util/BentoStates.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.util; 2 | 3 | import javafx.css.PseudoClass; 4 | 5 | /** 6 | * All pseudo-states used by bento controls. 7 | * 8 | * @author Matt Coley 9 | */ 10 | public class BentoStates { 11 | public static final PseudoClass PSEUDO_ROOT = PseudoClass.getPseudoClass("root"); 12 | public static final PseudoClass PSEUDO_ACTIVE = PseudoClass.getPseudoClass("active"); 13 | public static final PseudoClass PSEUDO_COLLAPSED = PseudoClass.getPseudoClass("collapsed"); 14 | public static final PseudoClass PSEUDO_ORIENTATION_H = PseudoClass.getPseudoClass("horizontal"); 15 | public static final PseudoClass PSEUDO_ORIENTATION_V = PseudoClass.getPseudoClass("vertical"); 16 | public static final PseudoClass PSEUDO_SELECTED = PseudoClass.getPseudoClass("selected"); 17 | public static final PseudoClass PSEUDO_HOVER = PseudoClass.getPseudoClass("hover"); 18 | public static final PseudoClass PSEUDO_SIDE_TOP = PseudoClass.getPseudoClass("top"); 19 | public static final PseudoClass PSEUDO_SIDE_BOTTOM = PseudoClass.getPseudoClass("bottom"); 20 | public static final PseudoClass PSEUDO_SIDE_LEFT = PseudoClass.getPseudoClass("left"); 21 | public static final PseudoClass PSEUDO_SIDE_RIGHT = PseudoClass.getPseudoClass("right"); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/search/DockContainerVisitor.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.search; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import software.coley.bentofx.layout.DockContainer; 6 | import software.coley.bentofx.layout.container.DockContainerBranch; 7 | import software.coley.bentofx.layout.container.DockContainerLeaf; 8 | 9 | import java.util.function.Predicate; 10 | 11 | /** 12 | * Search visitor that yields the first matching {@link DockContainer} of some predicate. 13 | * 14 | * @author Matt Coley 15 | */ 16 | public class DockContainerVisitor implements SearchVisitor { 17 | private final Predicate matcher; 18 | private DockContainer result; 19 | 20 | public DockContainerVisitor(@Nonnull Predicate matcher) { 21 | this.matcher = matcher; 22 | } 23 | 24 | @Override 25 | public boolean visitBranch(@Nonnull DockContainerBranch container) { 26 | return visitContainer(container); 27 | } 28 | 29 | @Override 30 | public boolean visitLeaf(@Nonnull DockContainerLeaf container) { 31 | return visitContainer(container); 32 | } 33 | 34 | private boolean visitContainer(@Nonnull DockContainer container) { 35 | if (matcher.test(container)) { 36 | // Match found, stop visiting. 37 | result = container; 38 | return false; 39 | } 40 | return true; 41 | } 42 | 43 | 44 | /** 45 | * @return Matched container if found. 46 | */ 47 | @Nullable 48 | public DockContainer getMatchedContainer() { 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/canvas/PixelPainterIntArgbPre.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 | 9 | /** 10 | * Pixel painter instance backed by {@link PixelFormat#getIntArgbPreInstance()}. 11 | * 12 | * @author Matt Coley 13 | */ 14 | public class PixelPainterIntArgbPre extends PixelPainterIntArgb { 15 | @Override 16 | public void fillRect(int x, int y, int width, int height, int color) { 17 | super.fillRect(x, y, width, height, argbToArgbPre(color)); 18 | } 19 | 20 | @Override 21 | public void setColor(int x, int y, int color) { 22 | super.setColor(x, y, argbToArgbPre(color)); 23 | } 24 | 25 | @Nonnull 26 | @Override 27 | public PixelFormat getPixelFormat() { 28 | return PixelFormat.getIntArgbPreInstance(); 29 | } 30 | 31 | protected static int argbToArgbPre(int color) { 32 | int alpha = (color >>> 24); 33 | int red, green, blue; 34 | if (alpha > 0x00) { 35 | red = (color >> 16) & 0xFF; 36 | green = (color >> 8) & 0xFF; 37 | blue = (color) & 0xFF; 38 | if (alpha < 0xFF) { 39 | red = (red * alpha + 127) / 0xFF; 40 | green = (green * alpha + 127) / 0xFF; 41 | blue = (blue * alpha + 127) / 0xFF; 42 | } 43 | } else { 44 | red = green = blue = 0; 45 | } 46 | return ((alpha & 0xFF) << 24) | 47 | ((red & 0xFF) << 16) | 48 | ((green & 0xFF) << 8) | 49 | ((blue & 0xFF)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/canvas/ArgbSource.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.control.canvas; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * ARGB source to wrap arbitrary inputs representing image data. 10 | * 11 | * @author Matt Coley. 12 | */ 13 | public interface ArgbSource { 14 | /** 15 | * @return Image width. 16 | */ 17 | int getWidth(); 18 | 19 | /** 20 | * @return Image height. 21 | */ 22 | int getHeight(); 23 | 24 | /** 25 | * @param x 26 | * Image X coordinate. 27 | * @param y 28 | * Image X coordinate. 29 | * 30 | * @return ARGB {@code int} at coordinate. 31 | * Defaults to {@code 0} for any coordinate out of the image bounds. 32 | */ 33 | int getArgb(int x, int y); 34 | 35 | /** 36 | * @param x 37 | * Image X coordinate. 38 | * @param y 39 | * Image X coordinate. 40 | * @param width 41 | * Width of image section to grab. 42 | * @param height 43 | * Height of image section to grab. 44 | * 45 | * @return ARGB {@code int[]} at coordinates for the given width/height. 46 | * {@code null} when coordinates are out of the image bounds. 47 | */ 48 | @Nullable 49 | int[] getArgb(int x, int y, int width, int height); 50 | 51 | /** 52 | * @return ARGB {@code int[]} for the full image. 53 | */ 54 | @Nonnull 55 | default int[] getArgb() { 56 | return Objects.requireNonNull(getArgb(0, 0, getWidth(), getHeight()), 57 | "Failed computing ARGB for full image dimensions"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/control/canvas/ArgbBufferedImageSource.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.control.canvas; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.util.Arrays; 7 | 8 | /** 9 | * ARGB source wrapping a {@link BufferedImage}. 10 | * 11 | * @author Matt Coley 12 | */ 13 | public class ArgbBufferedImageSource implements ArgbSource { 14 | private final BufferedImage image; 15 | private int[] fullArgbCache; 16 | private int hash; 17 | 18 | /** 19 | * @param image 20 | * Wrapped image. 21 | */ 22 | public ArgbBufferedImageSource(@Nonnull BufferedImage image) { 23 | this.image = image; 24 | } 25 | 26 | @Override 27 | public int getWidth() { 28 | return image.getWidth(); 29 | } 30 | 31 | @Override 32 | public int getHeight() { 33 | return image.getHeight(); 34 | } 35 | 36 | @Override 37 | public int getArgb(int x, int y) { 38 | try { 39 | return image.getRGB(x, y); 40 | } catch (Throwable t) { 41 | // Thrown when coordinates are out of bounds. 42 | // Default to transparent black. 43 | return 0; 44 | } 45 | } 46 | 47 | @Override 48 | public int[] getArgb(int x, int y, int width, int height) { 49 | try { 50 | return image.getRGB(x, y, width, height, null, 0, width); 51 | } catch (Throwable t) { 52 | // Thrown when coordinates are out of bounds. 53 | return null; 54 | } 55 | } 56 | 57 | @Nonnull 58 | @Override 59 | public int[] getArgb() { 60 | // We will likely be using this a bit, so it makes sense to cache the result. 61 | if (fullArgbCache == null) 62 | fullArgbCache = ArgbSource.super.getArgb(); 63 | return fullArgbCache; 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | if (hash == 0) 69 | hash = Arrays.hashCode(getArgb()); 70 | return hash; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/software/coley/bentofx/path/DockContainerPath.java: -------------------------------------------------------------------------------- 1 | package software.coley.bentofx.path; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import software.coley.bentofx.dockable.Dockable; 5 | import software.coley.bentofx.layout.DockContainer; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Path to a given container, starting from the root container (Top {@link DockContainer#getParentContainer()} value) 12 | * all the way down to the target container (Assuming this is a result of a lookup for a specific container). 13 | * 14 | * @param containers 15 | * Containers up to and including some target container. 16 | */ 17 | public record DockContainerPath(@Nonnull List containers) implements BentoPath { 18 | /** 19 | * @param child 20 | * Child to append to new path. 21 | * 22 | * @return New path with the given child at the end. 23 | */ 24 | @Nonnull 25 | public DockContainerPath withChild(@Nonnull DockContainer child) { 26 | List 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 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 | ![overview](assets/overview.png) 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 | ![containers](assets/containers.png) 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 | ![controls](assets/controls.png) 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 | ![containers](assets/example.png) 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 | ![containers](assets/example-recaf.png) 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 | --------------------------------------------------------------------------------