diff --git a/.github/workflows/sonar-main.yml b/.github/workflows/sonar-main.yml index d991c057..fec0a006 100644 --- a/.github/workflows/sonar-main.yml +++ b/.github/workflows/sonar-main.yml @@ -27,4 +27,4 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew build sonar -x :demo-single-app:build -x :demo-multi-app:build \ No newline at end of file + run: ./gradlew build sonar -x :demo-common:build -x :demo-single-app:build -x :demo-multi-app:build \ No newline at end of file diff --git a/.github/workflows/sonar-pr.yml b/.github/workflows/sonar-pr.yml index 30be227a..22ba8617 100644 --- a/.github/workflows/sonar-pr.yml +++ b/.github/workflows/sonar-pr.yml @@ -32,6 +32,7 @@ jobs: SONAR_PR_BASE: ${{ github.event.pull_request.base.ref }} run: | ./gradlew build sonar \ + -x :demo-common:build \ -x :demo-single-app:build \ -x :demo-multi-app:build \ -Dsonar.pullrequest.key=$SONAR_PR_KEY \ diff --git a/Writerside/c.list b/Writerside/c.list deleted file mode 100644 index c4c77a29..00000000 --- a/Writerside/c.list +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Writerside/cfg/buildprofiles.xml b/Writerside/cfg/buildprofiles.xml deleted file mode 100644 index ada02b10..00000000 --- a/Writerside/cfg/buildprofiles.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - true - - - - diff --git a/Writerside/images/adjusting_split.gif b/Writerside/images/adjusting_split.gif deleted file mode 100644 index aebcee8b..00000000 Binary files a/Writerside/images/adjusting_split.gif and /dev/null differ diff --git a/Writerside/images/close_panel.gif b/Writerside/images/close_panel.gif deleted file mode 100644 index 5aee99aa..00000000 Binary files a/Writerside/images/close_panel.gif and /dev/null differ diff --git a/Writerside/images/completion_procedure.png b/Writerside/images/completion_procedure.png deleted file mode 100644 index 3535a3f3..00000000 Binary files a/Writerside/images/completion_procedure.png and /dev/null differ diff --git a/Writerside/images/completion_procedure_dark.png b/Writerside/images/completion_procedure_dark.png deleted file mode 100644 index a65beb0e..00000000 Binary files a/Writerside/images/completion_procedure_dark.png and /dev/null differ diff --git a/Writerside/images/convert_table_to_xml.png b/Writerside/images/convert_table_to_xml.png deleted file mode 100644 index 2518a64c..00000000 Binary files a/Writerside/images/convert_table_to_xml.png and /dev/null differ diff --git a/Writerside/images/convert_table_to_xml_dark.png b/Writerside/images/convert_table_to_xml_dark.png deleted file mode 100644 index 47161226..00000000 Binary files a/Writerside/images/convert_table_to_xml_dark.png and /dev/null differ diff --git a/Writerside/images/creating_tab_group.gif b/Writerside/images/creating_tab_group.gif deleted file mode 100644 index b4927b4f..00000000 Binary files a/Writerside/images/creating_tab_group.gif and /dev/null differ diff --git a/Writerside/images/docking_handles.gif b/Writerside/images/docking_handles.gif deleted file mode 100644 index cf57df91..00000000 Binary files a/Writerside/images/docking_handles.gif and /dev/null differ diff --git a/Writerside/images/docking_regions.gif b/Writerside/images/docking_regions.gif deleted file mode 100644 index c179e09f..00000000 Binary files a/Writerside/images/docking_regions.gif and /dev/null differ diff --git a/Writerside/images/floating_panel.gif b/Writerside/images/floating_panel.gif deleted file mode 100644 index b9b731fc..00000000 Binary files a/Writerside/images/floating_panel.gif and /dev/null differ diff --git a/Writerside/images/has_more_options.gif b/Writerside/images/has_more_options.gif deleted file mode 100644 index c7142085..00000000 Binary files a/Writerside/images/has_more_options.gif and /dev/null differ diff --git a/Writerside/images/new_topic_options.png b/Writerside/images/new_topic_options.png deleted file mode 100644 index ea9744dc..00000000 Binary files a/Writerside/images/new_topic_options.png and /dev/null differ diff --git a/Writerside/images/new_topic_options_dark.png b/Writerside/images/new_topic_options_dark.png deleted file mode 100644 index c9196387..00000000 Binary files a/Writerside/images/new_topic_options_dark.png and /dev/null differ diff --git a/Writerside/images/new_window.gif b/Writerside/images/new_window.gif deleted file mode 100644 index c5cc05a8..00000000 Binary files a/Writerside/images/new_window.gif and /dev/null differ diff --git a/Writerside/images/pinning_panel.gif b/Writerside/images/pinning_panel.gif deleted file mode 100644 index 293dfbd1..00000000 Binary files a/Writerside/images/pinning_panel.gif and /dev/null differ diff --git a/Writerside/images/root_docking_handles.gif b/Writerside/images/root_docking_handles.gif deleted file mode 100644 index 73bea585..00000000 Binary files a/Writerside/images/root_docking_handles.gif and /dev/null differ diff --git a/Writerside/m.tree b/Writerside/m.tree deleted file mode 100644 index b9d6396c..00000000 --- a/Writerside/m.tree +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Writerside/project.ihp b/Writerside/project.ihp deleted file mode 100644 index b3afb718..00000000 --- a/Writerside/project.ihp +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Writerside/redirection-rules.xml b/Writerside/redirection-rules.xml deleted file mode 100644 index 7d8d8494..00000000 --- a/Writerside/redirection-rules.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Created after removal of "Dockable Customization" from ModernDocking - Dockable-Customization.html - - - Created after removal of "Using Docking Handles" from ModernDocking - Using-Docking-Handles.html - - \ No newline at end of file diff --git a/Writerside/topics/Customization-Points.md b/Writerside/topics/Customization-Points.md deleted file mode 100644 index cf68aebb..00000000 --- a/Writerside/topics/Customization-Points.md +++ /dev/null @@ -1,19 +0,0 @@ -# Customization Points - -## Tab Placement - -By default, Modern Docking places all tabs in `JTabbedPane`s at the bottom. This can be changed using `Settings.alwaysDisplayTabsMode` which will switch the tab placement to the top. - - -## Tab Layout Policy - -The tab layout policy of `JTabbedPane` can be customized with `Settings.setTabLayoutPolicy`. -The default tab layout policy is `JTabbedPane.SCROLL_TAB_LAYOUT` and it can be set to either `JTabbedPane.SCROLL_TAB_LAYOUT` or `JTabbedPane.WRAP_TAB_LAYOUT`. - -## Active Highlighter - -By default, Modern Docking will highlight the dockable that the house is currently over. This can be disabled before initializing the docking framework by using `Settings.setActiveHighlighterEnabled`. - -## Custom Dockable Header - -Create your own implementation of the header UI and return it in Dockable. \ No newline at end of file diff --git a/Writerside/topics/Dockable-Interface.md b/Writerside/topics/Dockable-Interface.md deleted file mode 100644 index f1ff0300..00000000 --- a/Writerside/topics/Dockable-Interface.md +++ /dev/null @@ -1,142 +0,0 @@ -# Dockable Interface - -The `Dockable` interface is required to be implemented by any component that will be dockable. - -Typically, this interface is implemented by a class that extends from `JPanel`. - -The `Dockable` interface has a minimum subset of methods that must be implemented by the application and a set of methods that are optional. -These optional methods provide extra customization points for the application per dockable. - - -The return values for the Dockable interface methods should be constant after the creation of the dockable component. -Certain methods can be dynamic and the framework provides methods to update their values within the framework. Any method that doesn't specify such -a method can lead to unexpected behavior if changed after the dockable component is created. - -Generally, if you wish to modify the return values after creating the dockable component, do so while the dockable is not docked. This can be checked with Docking.isDocked. - - -## Mandatory Methods - -These two methods do not provide default implementations in the interface and must be implemented by the application. - - -String getPersistentID() -

getPersistentID provides a unique ID that the framework can use to refer to the dockable. This is the main piece of information that the application and the framework share in order to refer to specific dockables.

-
- - -String getTabText() -

getTabText provides the text that should be displayed on a tab when this dockable is in a JTabbedPane.

-If the text displayed on the tab ever changes, the application must call Docking.updateTabText with the dockables persistentID to force the framework to update the text. This should be done anytime the text is changed, just in case the dockable is displaying in a JTabbedPane. Undocking and docking the dockable again will also update the tab text. -
- -## Optional Methods - -The following methods are provided to the application with a default. This means the application only needs to implement the methods -if it wishes to change the default. - - -int getType() -

getType provides an int to Modern Docking. This represents a unique type category for the dockable. Modern Docking will use this value when docking to determine which dockables to dock to.

-

Default value is 0

-
- - -String getTitleText() -

getTitleText provides the text that Modern Docking should display on the header. This string can be different than getTabText

-

Default value is getTabText

-
- - -String getTabTooltip() -

Used by the framework to get the text to display as a tooltip on JTabbedPane tabs.

-

Default value is null

-
- - -Icon getIcon() -

Used by the framework to get the icon for the dockable to use in a JTabbedPane tab.

-

Default value is null

-
- - -boolean isFloatingAllowed() -

Tells Modern Docking if the dockable is allowed to be opened in its own window

-

Default value is true

-
- - -boolean isLimitedToWindow() -

Allows the application to limit the dockable to the window it was initially docked in.

-

Default value is false

-
- - -DockableStyle getStyle() -

The docking style of the dockable which can be DockableStyle.VERTICAL, DockableStyle.HORIZONTAL, DockableStyle.BOTH or DockableStyle.CENTER_ONLY. Modern Docking will use this to determine which docking regions to allow when docking this dockable. Docking handles that do not match this style will be hidden.

-

Default value is DockableStyle.BOTH

-
- - -DockableStyle getAutoHideStyle() -

Determines which toolbars this dockable can be displayed on. Uses the same values as getStyle. DockableStyle.VERTICAL will allow the dockable on the east and west auto hide toolbars. DockableStyle.HORIZONTAL will allow the dockable on the south auto hide toolbar. DockableStyle.CENTER_ONLY is invalid for this method.

-

Default value is DockableStyle.BOTH

-
- - -boolean isClosable() -

Indicates to the docking framework whether the Dockable component can be closed and undocked.

-

Default value is true

-
- - -boolean requestClose() -

Called by Modern Docking when the dockable is in the process of closing due to undock. This allows the application to stop the dockable from closing. For example, maybe the user has unsaved changes and the application wishes to confirm closing of the dockable.

-

Default value is true

-
- - -boolean isAutoHideAllowed() -

Determines if the dockable can be set to the auto hide toolbars.

-

Default value is false

-
- - -boolean isMinMaxAllowed() -

Determines if the dockable can be maximized so that it takes up all the space in the window.

-

Default value is false

-
- - -boolean isWrappableInScrollpane() -

Allows the application to specify whether the docking framework should automatically wrap the Dockable component in a JScrollPane.

-

Default value is false

-
- - -boolean getHasMoreOptions() -

Flag that tells Modern Docking that this dockable has more menu items it wishes to add to the context menu. If this method returns true then Modern Docking will call addMoreOptions

-

Default value is false

-
- - -DockableTabPreference getTabPreference() -

Gives the dockables preferred tab location when in a JTabbedPane

-

Default value is DockableTabPreference.NONE

-
- - -void addMoreOptions(JPopupMenu menu) -

Adds this dockables menu items to the context menu

-
- - -DockingHeaderUI createHeaderUI(HeaderController headerController, HeaderModel headerModel) -

Creates the header UI for this dockable. The default implementation will create the default Modern Docking header.

-

Default value is DockingInternal.createDefaultHeaderUI(headerController, headerModel);

-
- - -void updateProperties() -

Modern Docking will call this method after setting the values of any fields annotated with DockingProperty. If there are no fields with that annotation then this method is not called

-
\ No newline at end of file diff --git a/Writerside/topics/Dockable-Properties.md b/Writerside/topics/Dockable-Properties.md deleted file mode 100644 index e90a6b6f..00000000 --- a/Writerside/topics/Dockable-Properties.md +++ /dev/null @@ -1,31 +0,0 @@ -# Dockable Properties - -The `DockingProperty` attribute is used to dynamically add properties to dockable components and was inspired by the excellent Java command line parsing library [Picocli](https://picocli.info/). - -For example, say we have a simple `JCheckBox` on our Dockable component and wish to persist it together with the Docking layout. We could create a `boolean` member variable in our Dockable component class and tie it to a property. - - - -```java -public class Example implements Dockable { - @DockingProperty(name = "enabled", defaultValue = "false") - private boolean enabled; - - @Override - public void updateProperties() { - // enabled value is now usable - } -} -``` - -These dockable properties can be done for the following Java types: - -- byte -- short -- int -- long -- float -- double -- char -- boolean -- String \ No newline at end of file diff --git a/Writerside/topics/Docking-Programatically.md b/Writerside/topics/Docking-Programatically.md deleted file mode 100644 index e9d90d50..00000000 --- a/Writerside/topics/Docking-Programatically.md +++ /dev/null @@ -1,80 +0,0 @@ -# Docking Programmatically - - -

-The dock methods of the Docking class are used to dock dockables. There are several variations of this method that allow docking directly to a window or to specific regions of other dockables. There are variations that allow specifying the divider proportions to use for JSplitPanes and each form of dock allows using the persistentID directly or an instance of Dockable -

- - - - - - - - - - - - - -
dock(String persistentID, Window window)Allows docking the dockable with persistentID to a given Window. This will only work if the root docking panel of the window is empty.
dock(Dockable dockable, Window window)Allows docking the dockable to a given Window. This will only work if the root docking panel of the window is empty.
dock(String persistentID, Window window, DockingRegion region)Docks the dockable with persistentID to the specified root region of the window. The divider proportion is set to .25.
dock(Dockable dockable, Window window, DockingRegion region)Docks the dockable with persistentID to the specified root region of the window. The divider proportion is set to .25.
dock(String persistentID, Window window, DockingRegion, double dividerProportion)
dock(Dockable dockable, Window window, DockingRegion, double dividerProportion)
dock(String sourcePersistentID, String targetPersistentID, DockingRegion region)
dock(String sourcePersistentID, Dockable target, DockingRegion region)
dock(Dockable source, String targetPersistentID, DockingRegion region)
dock(Dockable source, Dockable target, DockingRegion region)
dock(String sourcePersistentID, String targetPersistentID, DockingRegion region, double dividerProportion)
dock(Dockable source, Dockable target, DockingRegion region, double dividerProportion)
-
- - - - - -
undock(String persistentID)Undocks the dockable with the persistentID. Nothing is done if the dockable is not docked
undock(Dockable dockableUndocks dockable. Nothing is done if the dockable is not docked
-
- - - - - - -
newWindow(Dockable dockable)Opens the dockable in a new `FloatingFrame` instance
newWindow(String persistentID, Point location, Dimension size)Opens the dockable with persistentID in a new FloatingFrame at the given location with the given size
newWindow(Dockable dockable, Point location, Dimension size)Opens the dockable in a new FloatingFrame at the given location with the given size
-
- - -Brings the dockable to the front if it is not showing. If the dockable is in a tab group it will be made the active tab. If the dockable is hidden due to the Auto Hide feature, then it will be shown. Finally, the frame containing the dockable will be brought to the front with the JFrame::toFront function. - -A NotDockedException exception is thrown if the dockable is not docked. - - - - - - - - - -
- -bringToFront(Dockable dockable) - - -Call with an existing Dockable instance -
- -bringToFront(String persistentID) - - -Call with the persistent ID of the dockable. Modern Docking will lookup the dockable. If not found, a DockableNotFoundException is thrown. -
-
- - -

This method is a combination of dock and bringToFront. If the dockable is not docked it will be docked and then brought to the front

- - - -
display(Dockable dockable)
display(String persistentID)
-
- - -

Checks if a dockable is already docked

-
- - -

Checks if a dockable is currently maximized

-
\ No newline at end of file diff --git a/Writerside/topics/Events.md b/Writerside/topics/Events.md deleted file mode 100644 index 69eb32b5..00000000 --- a/Writerside/topics/Events.md +++ /dev/null @@ -1,8 +0,0 @@ -# Events - - -

This event is fired when dockables are docked, undocked, shown, hidden, pinned or unpinned. Shown and hidden are used when a dockable is in a JTabbedPane and the active tab changes. Pinned and Unpinned are used when the dockable is added to a toolbar or removed from a toolbar. Shown and hidden will also be fired when a pinned dockable is shown and hidden.

-
- -

Fired when layouts are added to or removed from DockingLayouts and when layouts are restored or persisted to a file

-
\ No newline at end of file diff --git a/Writerside/topics/Exceptions.md b/Writerside/topics/Exceptions.md deleted file mode 100644 index df119154..00000000 --- a/Writerside/topics/Exceptions.md +++ /dev/null @@ -1,26 +0,0 @@ -# Exceptions - - -

Thrown when a dockable is not found when restoring a DockingLayout

-Thrown by DockingState.restoreState -
- -

Thrown when a dockable with the persistentID has already been registered

-Thrown by Docking.registerDockable -
- -

This exception is thrown when there is an issue saving or loading a layout file. The exception provides the file that failed and the failure type

-Thrown by AppState.restore, LayoutPersistence.saveLayoutToFile and LayoutPersistence.loadApplicationLayoutFromFile -
- -

This exception is thrown when Modern Docking attempts to use a dockable that should be docked but isn't. Thrown when the target dockable when docking is not docked or when attempting to bring a dockable to front that isn't already docked

-Thrown by Docking.dock and Docking.bringToFront -
- -

Thrown when the root for a window is not found

-Thrown by Docking.configurePinning, Docking.dock and DockingComponentUtils.rootForWindow -
- -

Thrown when Modern Docking fails to register a RootDockingPanel because one is already registered for the window

-Thrown by Docing.registerDockingPanel -
\ No newline at end of file diff --git a/Writerside/topics/Hello-World.md b/Writerside/topics/Hello-World.md deleted file mode 100644 index 62216f6f..00000000 --- a/Writerside/topics/Hello-World.md +++ /dev/null @@ -1,151 +0,0 @@ -# Hello World - -This page will demonstrate a hello world application with Modern Docking. - -At the end of this page you'll know how to: -* create and register dockables -* create and register root panels -* dock programmatically (covered more in depth [here](Docking-Programatically.md)) - -First we will create `HelloWorld` and `MainFrame` classes. `HelloWorld` will contain our `main` method and `MainFrame` will extend `JFrame` to provide the main frame for our application. - -```java -public class HelloWorld { - public static class MainFrame extends JFrame { - } - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true)); - } -} -``` - -That will give us a very basic application to start working from. - -Now we need to create a constructor that does the following: -* sets a size -* initializes Modern Docking -* adds a `RootDockingPanel` -* creates, registers and docks some dockables - -First we set a size so that our `JFrame` shows up on screen in a reasonable way - -```java -setSize(400, 300); -``` - -Next step is to initialize the docking framework. - -```java -Docking.initialize(this); -``` - -This will initialize the docking framework with our `MainFrame` class as the Main Frame of the application. This also initializes internal components in the framework that are needed later. - -Next, we create and add a `RootDockingPanel`. This can be further customized if you want to use a different layout or have other custom components surrounding the dockable area. For now, we're going to simple use a `BorderLayout` (the default `JFrame` layout manager) and let the root consume all the space. - -```java -RootDockingPanel root = new RootDockingPanel(this); -add(root, BorderLayout.CENTER); -``` - -This creates a new root and adds it to the `JFrame`. `this` is passed to tell the `RootDockingPanel` what `Window` it belongs to. This is used to register the panel with the docking framework and to create any toolbars required for pinning. - -Next, we will create a dockable. This will involve creating a new `JPanel` that extends the `Dockable` interface. Most of the methods in the `Dockable` interface have default implementations and are not required to be implemented. - -The following methods are required to be implemented: -| Method | Description | -| ------ | ----------- | -| `getPersistentID` | A unique identifier for the dockable within the docking framework | -| `getTabText` | The text to display on the titlebar and on a `JTabbedPane` tab, if docked into a tab group | - -We will make a simple panel that takes text as the lone constructor argument and uses it as the persistentID and tab text. - -```java -static class DockingPanel extends JPanel implements Dockable { - private final String text; - - public DockingPanel(String text) { - this.text = text; - - Docking.registerDockable(this); - } - - @Override - public String getPersistentID() { - return text; - } - - @Override - public String getTabText() { - return text; - } -} -``` - -Now that we've created a panel that implements `Dockable` we can start creating dockables in our constructor. - -```java -DockingPanel helloWorld = new DockingPanel("Hello World"); -``` - -This will register the dockable with the docking framework. The persistentID will be used to uniquely identify the dockable throughout the framework. In more complicated applications we would call `Docking.registerDockable` from within the constructor of our panel, as shown below. - -We can now dock the dockable. First, we can do this with the panel reference directly. - -```java -Docking.dock(helloWorld, this); -``` - -or, we can use the persistentID value: - -```java -Docking.dock("Hello World", this); -``` - -In both cases `this` again refers to our `MainFrame`, requesting that the framework dock our panel to this frame. - -Now we have a complete sample that will create a `JFrame` with a dockable with the display text of "Hello World". Full sample is shown below: - -```java -public class HelloWorld { - static class DockingPanel extends JPanel implements Dockable { - private final String text; - - public DockingPanel(String text) { - this.text = text; - - Docking.registerDockable(this); - } - - @Override - public String getPersistentID() { - return text; - } - - @Override - public String getTabText() { - return text; - } - } - - public static class MainFrame extends JFrame { - public MainFrame() { - setSize(400, 300); - - Docking.initialize(this); - - RootDockingPanel root = new RootDockingPanel(this); - add(root, BorderLayout.CENTER); - - DockingPanel helloWorld = new DockingPanel("Hello World"); - - Docking.dock(helloWorld, this); - } - } - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true)); - } -} -``` \ No newline at end of file diff --git a/Writerside/topics/Internal-Workings.md b/Writerside/topics/Internal-Workings.md deleted file mode 100644 index a6da6721..00000000 --- a/Writerside/topics/Internal-Workings.md +++ /dev/null @@ -1,15 +0,0 @@ -# Internal Workings - -These topics cover internal functionality of the framework for anyone wanting to become more familiar with how Modern Docking works. This knowledge is not required for using Modern Docking. - - -

The ActiveDockableHighlighter is responsible for drawing a border around the dockable that the mouse is currently over. Using an AWT event listener on the AWT Toolkit lets us listen for all mouse events in the entire application.

-
- - -

Used to call AppState.persist whenever a Window instance resizes, moves or changes state.

-
- - -

-
\ No newline at end of file diff --git a/Writerside/topics/Listeners.md b/Writerside/topics/Listeners.md deleted file mode 100644 index e312f2c4..00000000 --- a/Writerside/topics/Listeners.md +++ /dev/null @@ -1,14 +0,0 @@ -# Listeners - - -

Listen for when layouts are added to or removed from DockingLayouts and when layouts are restored or saved to a file

-
- -

Listen for any changes to active dockables. This includes docking, undocking, shown, hidden, auto hide

-
- -

Listen for maximize events triggered by Docking.maximize

-
- -

Listen for the creation of new floating frames. This allows the application to set icons, titles, menu bars, etc. on the new frame.

-
\ No newline at end of file diff --git a/Writerside/topics/Look-and-Feel.md b/Writerside/topics/Look-and-Feel.md deleted file mode 100644 index 639a62ed..00000000 --- a/Writerside/topics/Look-and-Feel.md +++ /dev/null @@ -1,53 +0,0 @@ -# Look and Feel - -## Colors - -Modern Docking handles all colors by using properties in the UIManager. For all the colors listed here, Modern Docking will first attempt to use the Modern Docking property name, then the theme color or a custom configured property from the user. If none of these are found, Modern Docking will default to a predefined color. - - -

This setting controls the color used for the background of Docking Handles.

-

UIManager property used for the background color on Docking Handles. This property can be modified by calling DockingSettings.setHandleBackgroundProperty

-

Modern Docking property: ModernDocking.handleBackground

-

Default UIManager property: TableHeader.background

-
- - -

This setting controls the color used for the foreground of Docking Handles. The foreground color is used both for the borders and the mouse over color.

-

UIManager property used for the foreground color on Docking Handles. This property can be modified by calling DockingSettings.setHandleForegroundProperty

-

Modern Docking property: ModernDocking.handleForeground

-

Default UIManager property: TableHeader.foreground

-
- - -

This setting controls the color used for the background of the default dockable header provided by Modern Docking.

-

This property can be modified by calling DockingSettings.setHeaderBackgroundProperty

-

Modern Docking property: ModernDocking.headerBackground

-

Default UIManager property: TableHeader.background

-
- - -

This setting controls the color used for the foreground of the default dockable header provided by Modern Docking.

-

This property can be modified by calling DockingSettings.setHeaderForegroundProperty

-

Modern Docking property: ModernDocking.headerForeground

-

Default UIManager property: TableHeader.foreground

-
- - -

This setting controls the color used for the background of the docking overlay. An alpha value less than 100% is typically used for this color.

-

This property can be modified by calling DockingSettings.setOverlayBackgroundProperty

-

Modern Docking property: ModernDocking.overlayBackground

-
- - -

This setting controls the color used for the border color when the mouse is over a dockable and the active dockable highlighter is enabled.

-

This property can be modified by calling DockingSettings.setHighlighterSelectedBorderProperty

-

Modern Docking property: ModernDocking.highlighterSelectedBorder

-

Default UIManager property: Component.focusColor

-
- - -

This setting controls the color used to reset to the default border when a dockable is no longer under the mouse. Modern Docking will use this color when the active dockable highlighter is active. The color should be set to the theme's default border color.

-

This property can be modified by calling DockingSettings.setHighlighterNotSelectedBorderProperty

-

Modern Docking property: ModernDocking.highlighterNotSelectedBorder

-

Default UIManager property: Component.borderColor

-
diff --git a/Writerside/topics/Menu-Items.md b/Writerside/topics/Menu-Items.md deleted file mode 100644 index 6ea27295..00000000 --- a/Writerside/topics/Menu-Items.md +++ /dev/null @@ -1,13 +0,0 @@ -# Menu Items - -Modern Docking provides several specialized menu classes explained below - - -

Displays a list of all layouts that the layout manager knows about. These are automatically added and removed. When clicked the menu item will load that layout.

-
- -

Menu item specific to one layout. Simply displays the name and when clicked loads the layout with Docking.restore()

-
- -

Displays a single dockable. Shows a checkmark if the dockable is docked. If the dockable is not docked, docks it, if it is docked, it displays it.

-
diff --git a/Writerside/topics/Modern-Docking-API.md b/Writerside/topics/Modern-Docking-API.md deleted file mode 100644 index c9e8e4e9..00000000 --- a/Writerside/topics/Modern-Docking-API.md +++ /dev/null @@ -1,3 +0,0 @@ -# Modern Docking API - -This section of topics explains the Modern Docking API that can be used by an application. This API is used to create and maintain the dockables that users interact with. \ No newline at end of file diff --git a/Writerside/topics/Modern-Docking-UI.md b/Writerside/topics/Modern-Docking-UI.md deleted file mode 100644 index 569d66e2..00000000 --- a/Writerside/topics/Modern-Docking-UI.md +++ /dev/null @@ -1,29 +0,0 @@ -# Modern Docking UI - -Modern Docking UI is an extension to Modern Docking that changes png's to svg's for the Settings and Close icons using the FlatLaf library. - -## Initializing Modern Docking UI - -Using our [Hello World](Hello-World.md) example, we can update it to initialize the Modern Docking UI like below, which is all that's required to use Modern Docking UI. - -```java -public static class MainFrame extends JFrame { - public MainFrame() { - setSize(400, 300); - - Docking.initialize(this); - DockingUI.initialize(); - - RootDockingPanel root = new RootDockingPanel(this); - add(root, BorderLayout.CENTER); - - DockingPanel helloWorld = new DockingPanel("Hello World"); - - Docking.dock(helloWorld, this); - } -} -``` - -## FlatLaf - - diff --git a/Writerside/topics/Packaging.md b/Writerside/topics/Packaging.md deleted file mode 100644 index f86b63f1..00000000 --- a/Writerside/topics/Packaging.md +++ /dev/null @@ -1,7 +0,0 @@ -# Packaging - -Modern Docking is provided on Maven Central as a collection of packages. - -Most applications will use `modern-docking-api` and `modern-docking-single-app`. There is an additional UI package ([Modern Docking UI](Modern-Docking-UI.md)) that adds special support for the [FlatLaf](https://www.formdev.com/flatlaf) look and feel library. - -In addition to the above packages, there is a package for applications that launch multiple instances in a single JVM, which is how IntelliJ IDEA works. This package is available as `modern-docking-multi-app` and replaces `modern-docking-single-app` for the multi-app in JVM use case. \ No newline at end of file diff --git a/Writerside/topics/Persistence.md b/Writerside/topics/Persistence.md deleted file mode 100644 index 6271e850..00000000 --- a/Writerside/topics/Persistence.md +++ /dev/null @@ -1,5 +0,0 @@ -# Persistence - -Modern Docking will persist the current layout of the application to a file specified through the API. When auto persistence is enabled, this file is saved after a number of different UI actions listed below. A delay mechanism is employed to avoid unnecessarily saving the file, such as when the user is dragging splitters. - -The persistence feature defaults to off and can be enabled by calling the `setPersist`. The file that Modern Docking should use to persist the layout can be configured with `setPersistFile`. Finally, a default layout can be configured with `setDefaultApplicationLayout` for when persistence is disabled or Modern Docking fails to load the current auto persist file. diff --git a/Writerside/topics/User-Facing-Features.md b/Writerside/topics/User-Facing-Features.md deleted file mode 100644 index 0d7b51a2..00000000 --- a/Writerside/topics/User-Facing-Features.md +++ /dev/null @@ -1,72 +0,0 @@ -# User Facing Features - -Users of your application will interact with several UI components provided by Modern Docking. -The framework is built around using Java Swing's `JTabbedPane` and `JSplitPane` to provide the docking layouts. -The positioning of the applications dockable components within these `JTabbedPane`s and `JSplitPane`s is done by dragging the dockables. -When the dockable is being dragged it will float above the window of the application and show Docking Handles and Docking Regions. -Docking Handles allow the user to place the dockable precisely where they want it and Docking Regions provide extra visual feedback of this action. -Docking Regions are also a quick way to "snap" dockables into place without needing to precisely drag to a Docking Handle. - -## Using Docking Handles to Dock -When dragging a dockable hovering over another dockable will display docking handles in the center of the dockable. -These handles provide easy access to dock to the North, South, East and West regions. - - - -![Docking Handles](docking_handles.gif) - -The dockable can also be docked to any of these regions by hovering over the region itself and dropping the dockable. - - -![Docking Regions](docking_regions.gif) - -The root also has North, South, East and West handles to dock the dockable directly to the root of the panel. - - -![Root Docking Handles](root_docking_handles.gif) - -## Adjusting Split -Splits can be adjusted and perform continuous layout. Double-clicking the split will return it to 50-50 split for the 2 sides. - - -![Adjusting Splits](adjusting_split.gif) - -## Creating Tab Group -Panels can be grouped into tabbed panes by dragging a dockable to the center region of another dockable. - - -![Creating Tab Group](creating_tab_group.gif) - -## Closing Panels -Panels can be closed using the X button on their headers. This option can be disabled in the source code. - - -![Closing Panels](close_panel.gif) - -## Floating a Panel -Panels can be floated as their own window by dragging them by their header and dropping them outside the frame. -This creates a new JFrame with the dockable in it. More dockables can then be docked to this dockable. - - -![Floating Panels](floating_panel.gif) - -## Auto Hide a Panel -Panels can be set to Auto Hide with View Mode > Auto Hide from the settings button on the panel header. -This option will display the panel on a side toolbar as a button which can be pressed to display the panel. -Clicking the button again, or clicking off the panel, will return it to the toolbar. To return the panel to -normal, select the View Mode > Auto Hide option again to deselect it. - -![Auto Hide Panel](pinning_panel.gif) - - -## Option Panel in New Window -Panels can be opened in their own window using View Mode > Window. - -![New Window](new_window.gif) - - -## Additional Settings Options -Display custom settings options on the settings menu. - - -![Additional Settings Options](has_more_options.gif) \ No newline at end of file diff --git a/Writerside/topics/Working-With-Layouts.md b/Writerside/topics/Working-With-Layouts.md deleted file mode 100644 index 30d72c5f..00000000 --- a/Writerside/topics/Working-With-Layouts.md +++ /dev/null @@ -1,13 +0,0 @@ -# Working With Layouts - -Layouts can be created in memory from scratch with a builder, pulled from the current window or application layout and saved to files for long term storage. - -In memory layouts are created using the `WindowLayoutBuilder` class and are built using persistentIDs only, by calling similar `dock` functions to the ones provided in the `Docking` class. These persistentIDs do not need to be currently registered in the docking framework to be added to `WindowLayoutBuilder`. An error will be thrown if the persistentID already exists in the layout. When done building the layout, call `build()` to create a `WindowLayout` or `buildApplicationLayout()` to directly build an `ApplicationLayout`. If you're building a layout for multiple windows, you can manually create an `ApplicationLayout` and use `WindowLayoutBuilder` to create layouts and add them to `ApplicationLayout` with `addFrame`. - -The `WindowLayout` and `ApplicationLayout` can then be saved to XML, as well as loaded, with the `WindowLayoutXML` and `ApplicationLayoutXML` classes, respectively. - -The docking framework can store these layouts for you and provides special `JMenuItem`s that can restore named layouts on the application. - -Layouts can be restored by using the `restoreApplicationLayout` and `restoreWindowLayout` methods of the `DockingState` class. This undocks all dockables from the window (or entire application for an ApplicationLayout) and docks the dockables specified by the layout. - -Default layout management and restore is discussed in [Persistence](Persistence.md) \ No newline at end of file diff --git a/Writerside/topics/starter-topic.md b/Writerside/topics/starter-topic.md deleted file mode 100644 index 3c1a59fb..00000000 --- a/Writerside/topics/starter-topic.md +++ /dev/null @@ -1,5 +0,0 @@ -# Welcome to Modern Docking - -Welcome! This documentation explains the user facing features of Modern Docking, its API, and some of the internals. - -Modern Docking provides a rich set of features to enhance any Java Swing User Interface with a desire to provide user customizable layouts of UI components. Java 11 and newer are supported. diff --git a/Writerside/v.list b/Writerside/v.list deleted file mode 100644 index 2d12cb39..00000000 --- a/Writerside/v.list +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build.gradle b/build.gradle index 99993561..aaa5d4cd 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ sonar { property "sonar.host.url", "https://sonarcloud.io" property "sonar.sources", "src" property "sonar.inclusions", "**/*.java" - property "sonar.exclusions", "**/demo-single-app/**,**/demo-multi-app/**" + property "sonar.exclusions", "**/demo-common/**,**/demo-single-app/**,**/demo-multi-app/**" property "sonar.coverage.exclusions", "**/*" } } \ No newline at end of file diff --git a/demo-common/build.gradle b/demo-common/build.gradle new file mode 100644 index 00000000..5ad0eab9 --- /dev/null +++ b/demo-common/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'idea' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +sourceSets.main.java.srcDirs = ['src'] +sourceSets.main.resources.srcDirs = ['resources'] + +dependencies { + if (project.hasProperty("useMavenVersion")) { + implementation 'io.github.andrewauclair:modern-docking-ui:' + version + } + else { + implementation project(':docking-ui') + } + + implementation 'com.formdev:flatlaf:' + flatLafVersion + implementation 'com.formdev:flatlaf-extras:' + flatLafVersion + implementation 'com.formdev:flatlaf-intellij-themes:' + flatLafVersion + + implementation 'info.picocli:picocli:4.7.7' +} + +tasks.withType(Javadoc).all { enabled = false } diff --git a/demo-multi-app/resources/docking/FlatDarkLaf.properties b/demo-common/resources/docking/FlatDarkLaf.properties similarity index 100% rename from demo-multi-app/resources/docking/FlatDarkLaf.properties rename to demo-common/resources/docking/FlatDarkLaf.properties diff --git a/demo-multi-app/resources/docking/FlatLightLaf.properties b/demo-common/resources/docking/FlatLightLaf.properties similarity index 100% rename from demo-multi-app/resources/docking/FlatLightLaf.properties rename to demo-common/resources/docking/FlatLightLaf.properties diff --git a/demo-multi-app/resources/icons/dark/close.png b/demo-common/resources/icons/dark/close.png similarity index 100% rename from demo-multi-app/resources/icons/dark/close.png rename to demo-common/resources/icons/dark/close.png diff --git a/demo-multi-app/resources/icons/dark/maximize.png b/demo-common/resources/icons/dark/maximize.png similarity index 100% rename from demo-multi-app/resources/icons/dark/maximize.png rename to demo-common/resources/icons/dark/maximize.png diff --git a/demo-multi-app/resources/icons/dark/minimize.png b/demo-common/resources/icons/dark/minimize.png similarity index 100% rename from demo-multi-app/resources/icons/dark/minimize.png rename to demo-common/resources/icons/dark/minimize.png diff --git a/demo-multi-app/resources/icons/dark/pin.png b/demo-common/resources/icons/dark/pin.png similarity index 100% rename from demo-multi-app/resources/icons/dark/pin.png rename to demo-common/resources/icons/dark/pin.png diff --git a/demo-multi-app/resources/icons/light/close.png b/demo-common/resources/icons/light/close.png similarity index 100% rename from demo-multi-app/resources/icons/light/close.png rename to demo-common/resources/icons/light/close.png diff --git a/demo-multi-app/resources/icons/light/icons8-vga-100.png b/demo-common/resources/icons/light/icons8-vga-100.png similarity index 100% rename from demo-multi-app/resources/icons/light/icons8-vga-100.png rename to demo-common/resources/icons/light/icons8-vga-100.png diff --git a/demo-multi-app/resources/icons/light/icons8-vga-16.png b/demo-common/resources/icons/light/icons8-vga-16.png similarity index 100% rename from demo-multi-app/resources/icons/light/icons8-vga-16.png rename to demo-common/resources/icons/light/icons8-vga-16.png diff --git a/demo-multi-app/resources/icons/light/icons8-vga-50.png b/demo-common/resources/icons/light/icons8-vga-50.png similarity index 100% rename from demo-multi-app/resources/icons/light/icons8-vga-50.png rename to demo-common/resources/icons/light/icons8-vga-50.png diff --git a/demo-multi-app/resources/icons/light/maximize.png b/demo-common/resources/icons/light/maximize.png similarity index 100% rename from demo-multi-app/resources/icons/light/maximize.png rename to demo-common/resources/icons/light/maximize.png diff --git a/demo-multi-app/resources/icons/light/minimize.png b/demo-common/resources/icons/light/minimize.png similarity index 100% rename from demo-multi-app/resources/icons/light/minimize.png rename to demo-common/resources/icons/light/minimize.png diff --git a/demo-multi-app/resources/icons/light/pin.png b/demo-common/resources/icons/light/pin.png similarity index 100% rename from demo-multi-app/resources/icons/light/pin.png rename to demo-common/resources/icons/light/pin.png diff --git a/demo-common/resources/modern-docking-demo.svg b/demo-common/resources/modern-docking-demo.svg new file mode 100644 index 00000000..f6e1d33a --- /dev/null +++ b/demo-common/resources/modern-docking-demo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/demo-common/src/demo/CommonDemoFrame.java b/demo-common/src/demo/CommonDemoFrame.java new file mode 100644 index 00000000..d79c9991 --- /dev/null +++ b/demo-common/src/demo/CommonDemoFrame.java @@ -0,0 +1,530 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.extras.FlatSVGUtils; +import demo.MiscPanels.FixedPanel; +import demo.MiscPanels.MoreOptionsPanel; +import demo.MiscPanels.NoTabGroupPanel; +import demo.MiscPanels.ScrollablePanel; +import demo.MiscPanels.VetoClosePanel; +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockableTabPreference; +import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.Property; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.api.RootDockingPanelAPI; +import io.github.andrewauclair.moderndocking.api.WindowLayoutBuilderAPI; +import io.github.andrewauclair.moderndocking.event.NewFloatingFrameListener; +import io.github.andrewauclair.moderndocking.exception.DockingLayoutException; +import io.github.andrewauclair.moderndocking.ext.ui.DockingUI; +import io.github.andrewauclair.moderndocking.layouts.ApplicationLayout; +import io.github.andrewauclair.moderndocking.layouts.DockingLayouts; +import io.github.andrewauclair.moderndocking.settings.Settings; +import io.github.andrewauclair.moderndocking.ui.DockingSettings; +import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; +import java.awt.BorderLayout; +import java.awt.Frame; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButtonMenuItem; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +/** + * Base frame for both single-app and multi-app comprehensive demos. + * Subclasses supply the three framework-specific factory methods. + */ +public abstract class CommonDemoFrame extends JFrame { + + private final DockingAPI docking; + + private final DocumentsAnchor documentsAnchor; + private final ProjectPanel projectTree; + private final PropertiesPanel propertiesPanel; + private final OutputPanel outputPanel; + private final EventLogPanel eventLogPanel; + private final ScrollablePanel scrollablePanel; + private final FixedPanel fixedPanel; + private final VetoClosePanel vetoPanel; + private final MoreOptionsPanel moreOptionsPanel; + private final NoTabGroupPanel noTabGroupPanel; + private final DockableInspectorPanel inspectorPanel; + private final TestHarnessPanel testHarnessPanel; + + // ========================================================================= + // Abstract factory methods + // ========================================================================= + + /** Initialize and return the DockingAPI instance for this frame. */ + protected abstract DockingAPI createDocking(); + + /** Create the concrete RootDockingPanel for this frame. */ + protected abstract RootDockingPanelAPI createRoot(DockingAPI docking); + + /** Create the concrete WindowLayoutBuilder for this frame. */ + protected abstract WindowLayoutBuilderAPI createLayoutBuilder(DockingAPI docking, String firstId); + + /** Create a DockableMenuItem for the given dockable, using the appropriate single- or multi-app implementation. */ + protected abstract JMenuItem createViewMenuItem(Dockable dockable); + + protected DockingAPI getDocking() { + return docking; + } + + // ========================================================================= + // Constructor + // ========================================================================= + + protected CommonDemoFrame(String title, File persistFile) { + super(title); + + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + System.err.println("Uncaught exception on thread: " + t.getName()); + e.printStackTrace(); + }); + SwingUtilities.invokeLater(() -> + Thread.currentThread().setUncaughtExceptionHandler((t, e) -> { + System.err.println("Uncaught exception on EDT"); + e.printStackTrace(); + }) + ); + + setSize(1200, 800); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + setIconImages(FlatSVGUtils.createWindowIconImages("/modern-docking-demo.svg")); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + docking.getAppState().persist(); + dispose(); + boolean anyVisible = false; + for (Frame f : Frame.getFrames()) { + if (f.isVisible() && f != CommonDemoFrame.this) { + anyVisible = true; + break; + } + } + if (!anyVisible) { + System.exit(0); + } + } + }); + + docking = createDocking(); + DockingUI.initialize(); + + // Create all panels now that the framework is ready + documentsAnchor = new DocumentsAnchor(docking); + projectTree = new ProjectPanel(docking, "project-tree", new File("docking-api"), DocumentsAnchor.PERSISTENT_ID); + propertiesPanel = new PropertiesPanel(docking, "properties-panel"); + outputPanel = new OutputPanel(docking, "output-panel"); + eventLogPanel = new EventLogPanel(docking, "event-log"); + scrollablePanel = new ScrollablePanel(docking, "scrollable-panel"); + fixedPanel = new FixedPanel(docking, "fixed-panel"); + vetoPanel = new VetoClosePanel(docking, "veto-close-panel"); + moreOptionsPanel = new MoreOptionsPanel(docking, "more-options-panel"); + noTabGroupPanel = new NoTabGroupPanel(docking, "no-tab-group-panel"); + inspectorPanel = new DockableInspectorPanel(docking, "dockable-inspector"); + + List allDockables = new ArrayList<>(); + allDockables.add(projectTree); + allDockables.add(propertiesPanel); + allDockables.add(outputPanel); + allDockables.add(eventLogPanel); + allDockables.add(scrollablePanel); + allDockables.add(fixedPanel); + allDockables.add(vetoPanel); + allDockables.add(moreOptionsPanel); + allDockables.add(noTabGroupPanel); + allDockables.add(inspectorPanel); + + testHarnessPanel = new TestHarnessPanel(docking, "test-harness", allDockables); + + docking.addDockingListener(eventLogPanel); + docking.addNewFloatingFrameListener(new NewFloatingFrameListener() { + @Override + public void newFrameCreated(JFrame frame, RootDockingPanelAPI root) { + } + + @Override + public void newFrameCreated(JFrame frame, RootDockingPanelAPI root, Dockable dockable) { + frame.setTitle("Floating — " + dockable.getTitleText()); + } + }); + + // Restore dynamic editor panels (created when files are opened from the project tree) + docking.setUserDynamicDockableCreationListener((persistentID, className, titleText, tabText, properties) -> { + if (className.equals(EditorPanel.class.getName())) { + Property filePathProp = properties.get("file-path"); + String filePath = (filePathProp instanceof Property.StringProperty) + ? ((Property.StringProperty) filePathProp).getValue() + : ""; + return new EditorPanel(docking, persistentID, tabText, filePath); + } + return null; + }); + + // Root panel + setLayout(new BorderLayout()); + RootDockingPanelAPI root = createRoot(docking); + JPanel rootWrapper = new JPanel(new BorderLayout()); + rootWrapper.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + rootWrapper.add(root, BorderLayout.CENTER); + add(rootWrapper, BorderLayout.CENTER); + + setJMenuBar(buildMenuBar()); + + // Default layout + ApplicationLayout defaultLayout = buildDefaultLayout(); + DockingLayouts.addLayout("default", defaultLayout); + + docking.getAppState().setAutoPersist(true); + docking.getAppState().setPersistFile(persistFile); + docking.getAppState().setDefaultApplicationLayout(defaultLayout); + + boolean restored = false; + try { + restored = docking.getAppState().restore(); + } + catch (DockingLayoutException ex) { + // fallback to default + } + + if (!restored) { + setLocationRelativeTo(null); + } + } + + // ========================================================================= + // Default layout + // ========================================================================= + + private ApplicationLayout buildDefaultLayout() { + WindowLayoutBuilderAPI builder = createLayoutBuilder(docking, documentsAnchor.getPersistentID()) + .dockToRoot(projectTree.getPersistentID(), DockingRegion.WEST, 0.20) + .dockToRoot(outputPanel.getPersistentID(), DockingRegion.SOUTH, 0.25) + .dockToRoot(propertiesPanel.getPersistentID(), DockingRegion.EAST, 0.22) + .dock(eventLogPanel.getPersistentID(), propertiesPanel.getPersistentID(), DockingRegion.CENTER) + .dock(inspectorPanel.getPersistentID(), propertiesPanel.getPersistentID(), DockingRegion.CENTER); + + return builder.buildApplicationLayout(); + } + + // ========================================================================= + // Menu bar + // ========================================================================= + + private JMenuBar buildMenuBar() { + JMenuBar bar = new JMenuBar(); + bar.add(buildFileMenu()); + bar.add(buildViewMenu()); + bar.add(buildActionsMenu()); + bar.add(buildSettingsMenu()); + bar.add(buildWindowMenu()); + return bar; + } + + private JMenu buildFileMenu() { + JMenu menu = new JMenu("File"); + + JMenuItem save = new JMenuItem("Save Layout to File..."); + save.addActionListener(e -> { + JFileChooser fc = new JFileChooser(); + if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + try { + docking.getLayoutPersistence().saveLayoutToFile( + fc.getSelectedFile(), + docking.getDockingState().getApplicationLayout()); + } + catch (DockingLayoutException ex) { + JOptionPane.showMessageDialog(this, "Save failed: " + ex.getMessage()); + } + } + }); + menu.add(save); + + JMenuItem load = new JMenuItem("Load Layout from File..."); + load.addActionListener(e -> { + JFileChooser fc = new JFileChooser(); + if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + try { + ApplicationLayout layout = docking.getLayoutPersistence() + .loadApplicationLayoutFromFile(fc.getSelectedFile()); + docking.getDockingState().restoreApplicationLayout(layout); + } + catch (DockingLayoutException ex) { + JOptionPane.showMessageDialog(this, "Load failed: " + ex.getMessage()); + } + } + }); + menu.add(load); + + menu.addSeparator(); + + JMenuItem storeNamed = new JMenuItem("Store Current Layout as Named..."); + storeNamed.addActionListener(e -> { + String name = JOptionPane.showInputDialog(this, "Layout name:"); + if (name != null && !name.isBlank()) { + DockingLayouts.addLayout(name, docking.getDockingState().getApplicationLayout()); + } + }); + menu.add(storeNamed); + + return menu; + } + + private JMenu buildViewMenu() { + JMenu menu = new JMenu("View"); + + addViewItem(menu, projectTree); + addViewItem(menu, propertiesPanel); + addViewItem(menu, outputPanel); + addViewItem(menu, eventLogPanel); + menu.addSeparator(); + addViewItem(menu, scrollablePanel); + addViewItem(menu, fixedPanel); + addViewItem(menu, vetoPanel); + addViewItem(menu, moreOptionsPanel); + addViewItem(menu, noTabGroupPanel); + menu.addSeparator(); + addViewItem(menu, inspectorPanel); + menu.addSeparator(); + addViewItem(menu, testHarnessPanel); + + return menu; + } + + private void addViewItem(JMenu menu, Dockable d) { + menu.add(createViewMenuItem(d)); + } + + private JMenu buildActionsMenu() { + JMenu menu = new JMenu("Actions"); + + // --- Focused mode --- + JMenuItem exitFocused = new JMenuItem("Exit Focused Mode (find active)"); + exitFocused.addActionListener(e -> exitFocusedModeForAny()); + menu.add(exitFocused); + menu.addSeparator(); + + // --- Float in new window --- + JMenu floatMenu = new JMenu("Float in New Window"); + addFloatItem(floatMenu, projectTree); + addFloatItem(floatMenu, propertiesPanel); + addFloatItem(floatMenu, outputPanel); + menu.add(floatMenu); + + // --- Display --- + JMenu displayMenu = new JMenu("Display (if not docked)"); + addDisplayItem(displayMenu, scrollablePanel); + addDisplayItem(displayMenu, fixedPanel); + addDisplayItem(displayMenu, vetoPanel); + addDisplayItem(displayMenu, moreOptionsPanel); + addDisplayItem(displayMenu, noTabGroupPanel); + menu.add(displayMenu); + + menu.addSeparator(); + + // --- Default display region (1.5.0) --- + JMenu regionMenu = new JMenu("Default Display Region"); + ButtonGroup regionGroup = new ButtonGroup(); + for (DockingRegion region : DockingRegion.values()) { + JRadioButtonMenuItem item = new JRadioButtonMenuItem( + region.name(), region == DockingRegion.NORTH); + item.addActionListener(e -> { + // TODO 1.5.0 — Settings.setDefaultDisplayRegion(region) not yet available + }); + regionGroup.add(item); + regionMenu.add(item); + } + menu.add(regionMenu); + + menu.addSeparator(); + + // --- Auto-hide shortcuts --- + JMenu autoHideMenu = new JMenu("Auto-Hide to Toolbar"); + addAutoHideItem(autoHideMenu, outputPanel, ToolbarLocation.SOUTH); + addAutoHideItem(autoHideMenu, propertiesPanel, ToolbarLocation.EAST); + menu.add(autoHideMenu); + + return menu; + } + + private void exitFocusedModeForAny() { + for (Dockable d : docking.getDockables()) { + if (docking.inFocusedMode(d)) { + docking.exitFocusedMode(d); + return; + } + } + JOptionPane.showMessageDialog(this, "No dockable is currently in focused mode."); + } + + private void addFloatItem(JMenu menu, Dockable d) { + JMenuItem item = new JMenuItem(d.getTabText()); + item.addActionListener(e -> { + if (docking.isDocked(d)) { + docking.newWindow(d); + } + else { + JOptionPane.showMessageDialog(this, d.getTabText() + " is not currently docked."); + } + }); + menu.add(item); + } + + private void addDisplayItem(JMenu menu, Dockable d) { + JMenuItem item = new JMenuItem(d.getTabText()); + item.addActionListener(e -> docking.display(d)); + menu.add(item); + } + + private void addAutoHideItem(JMenu menu, Dockable d, ToolbarLocation loc) { + JMenuItem item = new JMenuItem(d.getTabText() + " \u2192 " + loc.name()); + item.addActionListener(e -> { + if (docking.isDocked(d)) { + docking.autoHideDockable(d, loc, this); + } + else { + JOptionPane.showMessageDialog(this, d.getTabText() + " is not currently docked."); + } + }); + menu.add(item); + } + + private JMenu buildSettingsMenu() { + JMenu menu = new JMenu("Settings"); + + JCheckBoxMenuItem alwaysTabs = new JCheckBoxMenuItem("Always Display Tabs Mode"); + alwaysTabs.addActionListener(e -> { + Settings.setDefaultTabPreference(alwaysTabs.isSelected() + ? DockableTabPreference.TOP_ALWAYS + : DockableTabPreference.BOTTOM); + ApplicationLayout layout = docking.getDockingState().getApplicationLayout(); + docking.getDockingState().restoreApplicationLayout(layout); + }); + menu.add(alwaysTabs); + + JCheckBoxMenuItem highlighter = new JCheckBoxMenuItem( + "Active Dockable Highlighter", Settings.isActiveHighlighterEnabled()); + highlighter.addActionListener(e -> + Settings.setActiveHighlighterEnabled(highlighter.isSelected())); + menu.add(highlighter); + + JCheckBoxMenuItem scrollTabs = new JCheckBoxMenuItem("Scroll Tab Layout (unchecked = Wrap)", true); + scrollTabs.addActionListener(e -> + Settings.setTabLayoutPolicy(scrollTabs.isSelected() + ? JTabbedPane.SCROLL_TAB_LAYOUT + : JTabbedPane.WRAP_TAB_LAYOUT)); + menu.add(scrollTabs); + + menu.addSeparator(); + + JCheckBoxMenuItem layeredOverlay = new JCheckBoxMenuItem("Use Layered Pane Overlay (FloatUtilsLayer)"); + layeredOverlay.addActionListener(e -> + DockingSettings.setUseLayeredPaneOverlay(layeredOverlay.isSelected())); + menu.add(layeredOverlay); + + menu.addSeparator(); + + JMenuItem lightTheme = new JMenuItem("FlatLaf Light Theme"); + lightTheme.addActionListener(e -> { + FlatLightLaf.setup(); + DockingUI.initialize(); + for (Frame f : Frame.getFrames()) { + SwingUtilities.updateComponentTreeUI(f); + } + }); + menu.add(lightTheme); + + JMenuItem darkTheme = new JMenuItem("FlatLaf Dark Theme"); + darkTheme.addActionListener(e -> { + FlatDarkLaf.setup(); + DockingUI.initialize(); + for (Frame f : Frame.getFrames()) { + SwingUtilities.updateComponentTreeUI(f); + } + }); + menu.add(darkTheme); + + return menu; + } + + private JMenu buildWindowMenu() { + JMenu menu = new JMenu("Window"); + + String defaultLayoutName = "default"; + JMenuItem restoreDefault = new JMenuItem("Restore Default Layout"); + restoreDefault.addActionListener(e -> { + ApplicationLayout layout = DockingLayouts.getLayout(defaultLayoutName); + if (layout != null) { + docking.getDockingState().restoreApplicationLayout(layout); + } + }); + menu.add(restoreDefault); + + menu.addSeparator(); + + JMenu layoutsMenu = new JMenu("Layouts"); + layoutsMenu.addMenuListener(new MenuListener() { + @Override + public void menuSelected(MenuEvent e) { + layoutsMenu.removeAll(); + for (String name : DockingLayouts.getLayoutNames()) { + JMenuItem item = new JMenuItem(name); + item.addActionListener(ev -> { + ApplicationLayout layout = DockingLayouts.getLayout(name); + if (layout != null) { + docking.getDockingState().restoreApplicationLayout(layout); + } + }); + layoutsMenu.add(item); + } + } + + @Override public void menuDeselected(MenuEvent e) {} + @Override public void menuCanceled(MenuEvent e) {} + }); + menu.add(layoutsMenu); + + return menu; + } +} diff --git a/demo-common/src/demo/DemoOptions.java b/demo-common/src/demo/DemoOptions.java new file mode 100644 index 00000000..4a254dcc --- /dev/null +++ b/demo-common/src/demo/DemoOptions.java @@ -0,0 +1,111 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.intellijthemes.FlatSolarizedDarkIJTheme; +import com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMTGitHubDarkIJTheme; +import exception.FailOnThreadViolationRepaintManager; +import io.github.andrewauclair.moderndocking.DockableTabPreference; +import io.github.andrewauclair.moderndocking.settings.Settings; +import io.github.andrewauclair.moderndocking.ui.DockingSettings; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import picocli.CommandLine; + +public class DemoOptions { + + @CommandLine.Option(names = "--laf", defaultValue = "dark", description = "look and feel to use. one of: system, light, dark, github-dark or solarized-dark") + String lookAndFeel; + + @CommandLine.Option(names = "--enable-edt-violation-detector", defaultValue = "false", description = "enable the Event Dispatch Thread (EDT) violation checker") + boolean edtViolationDetector; + + @CommandLine.Option(names = "--ui-scale", defaultValue = "1", description = "scale to use for the FlatLaf.uiScale value") + int uiScale; + + @CommandLine.Option(names = "--always-use-tabs", defaultValue = "false", description = "always use tabs, even when there is only 1 dockable in the tab group") + boolean alwaysUseTabs; + + @CommandLine.Option(names = "--tab-location", defaultValue = "NONE", description = "Location to display tabs. values: ${COMPLETION-CANDIDATES}") + DockableTabPreference tabLocation; + + @CommandLine.Option(names = "--use-layered-pane-overlay", defaultValue = "false", description = "force the JLayeredPane-based docking overlay instead of the transparent-window overlay") + boolean useLayeredPaneOverlay; + + public void apply() { + DockingSettings.setUseLayeredPaneOverlay(useLayeredPaneOverlay); + + if (alwaysUseTabs) { + if (tabLocation == DockableTabPreference.TOP) { + Settings.setDefaultTabPreference(DockableTabPreference.TOP_ALWAYS); + } else { + Settings.setDefaultTabPreference(DockableTabPreference.BOTTOM_ALWAYS); + } + } else { + Settings.setDefaultTabPreference(tabLocation); + } + + try { + FlatLaf.registerCustomDefaultsSource("docking"); + System.setProperty("flatlaf.uiScale", String.valueOf(uiScale)); + + switch (lookAndFeel) { + case "light": + UIManager.setLookAndFeel(new FlatLightLaf()); + break; + case "dark": + UIManager.setLookAndFeel(new FlatDarkLaf()); + break; + case "github-dark": + UIManager.setLookAndFeel(new FlatMTGitHubDarkIJTheme()); + break; + case "solarized-dark": + UIManager.setLookAndFeel(new FlatSolarizedDarkIJTheme()); + break; + default: + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + UnsupportedLookAndFeelException ex) { + throw new RuntimeException(ex); + } + break; + } + FlatLaf.updateUI(); + } catch (Exception e) { + e.printStackTrace(); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + UnsupportedLookAndFeelException ex) { + throw new RuntimeException(ex); + } + } + + if (edtViolationDetector) { + FailOnThreadViolationRepaintManager.install(); + } + } +} diff --git a/demo-common/src/demo/DockableInspectorPanel.java b/demo-common/src/demo/DockableInspectorPanel.java new file mode 100644 index 00000000..7df9af3e --- /dev/null +++ b/demo-common/src/demo/DockableInspectorPanel.java @@ -0,0 +1,381 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.AWTEvent; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import javax.swing.table.DefaultTableModel; + +public class DockableInspectorPanel extends JPanel implements Dockable { + + private final String id; + private final DockingAPI docking; + + private Dockable lastDockable = null; + private boolean manualMode = false; + private boolean repopulating = false; + + private final JCheckBox manualCheckBox; + private final JComboBox dockableChooser; + private final DefaultTableModel tableModel; + + private static final String[] COLUMNS = {"Property", "Value"}; + + public DockableInspectorPanel(DockingAPI docking, String id) { + this.docking = docking; + this.id = id; + setLayout(new BorderLayout()); + + // ---- toolbar -------------------------------------------------------- + manualCheckBox = new JCheckBox("Manual"); + dockableChooser = new JComboBox<>(); + dockableChooser.setVisible(false); + dockableChooser.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof Dockable) { + setText(((Dockable) value).getTabText()); + } + return this; + } + }); + + dockableChooser.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + repopulateChooser(); + } + @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} + @Override public void popupMenuCanceled(PopupMenuEvent e) {} + }); + + dockableChooser.addActionListener(e -> { + if (repopulating) { + return; + } + Dockable selected = (Dockable) dockableChooser.getSelectedItem(); + if (selected != null) { + lastDockable = selected; + refresh(selected); + } + }); + + manualCheckBox.addActionListener(e -> { + manualMode = manualCheckBox.isSelected(); + dockableChooser.setVisible(manualMode); + if (manualMode) { + repopulateChooser(); + if (lastDockable != null) { + dockableChooser.setSelectedItem(lastDockable); + } + } + }); + + JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + toolbar.add(manualCheckBox); + toolbar.add(dockableChooser); + add(toolbar, BorderLayout.NORTH); + + // ---- table ---------------------------------------------------------- + tableModel = new DefaultTableModel(COLUMNS, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + JTable table = new JTable(tableModel); + table.setFillsViewportHeight(true); + table.getColumnModel().getColumn(0).setPreferredWidth(160); + table.getColumnModel().getColumn(1).setPreferredWidth(120); + + JScrollPane scroll = new JScrollPane(table); + scroll.setBorder(BorderFactory.createEmptyBorder()); + scroll.setViewportBorder(null); + add(scroll, BorderLayout.CENTER); + + // ---- hover listener ------------------------------------------------- + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + @Override + public void eventDispatched(AWTEvent event) { + if (manualMode || event.getID() != MouseEvent.MOUSE_MOVED) { + return; + } + MouseEvent me = (MouseEvent) event; + Component source = me.getComponent(); + if (source == null) { + return; + } + + Component deepest = SwingUtilities.getDeepestComponentAt(source, me.getX(), me.getY()); + if (deepest == null) { + return; + } + + Dockable found = findDockableUnderMouse(source, me.getX(), me.getY(), deepest); + + if (found != null) { + if (found != lastDockable) { + lastDockable = found; + refresh(lastDockable); + } + } else if (lastDockable != null) { + if (!docking.isDockableRegistered(lastDockable.getPersistentID()) + || (!docking.isDocked(lastDockable) && !docking.isHidden(lastDockable))) { + lastDockable = null; + clearTable(); + } + } + } + }, AWTEvent.MOUSE_MOTION_EVENT_MASK); + + // ---- live-refresh timer --------------------------------------------- + Timer timer = new Timer(500, e -> { + if (lastDockable == null) { + return; + } + // In manual mode the user explicitly chose a dockable, so never auto-clear it — + // show it even if it becomes undocked or hidden. Only clear if it has been + // fully deregistered (e.g. an editor panel that was closed). + if (manualMode) { + if (docking.isDockableRegistered(lastDockable.getPersistentID())) { + refresh(lastDockable); + } else { + lastDockable = null; + clearTable(); + } + } else { + if (!docking.isDockableRegistered(lastDockable.getPersistentID())) { + lastDockable = null; + clearTable(); + } else if (docking.isDocked(lastDockable) || docking.isHidden(lastDockable)) { + refresh(lastDockable); + } else { + lastDockable = null; + clearTable(); + } + } + }); + timer.start(); + + docking.registerDockable(this); + } + + private void repopulateChooser() { + Dockable current = (Dockable) dockableChooser.getSelectedItem(); + List all = new ArrayList<>(docking.getDockables()); + all.sort(Comparator.comparing(Dockable::getTabText)); + + repopulating = true; + try { + dockableChooser.removeAllItems(); + for (Dockable d : all) { + dockableChooser.addItem(d); + } + dockableChooser.setSelectedItem(current != null ? current : (!all.isEmpty() ? all.get(0) : null)); + } + finally { + repopulating = false; + } + + Dockable selected = (Dockable) dockableChooser.getSelectedItem(); + if (selected != null) { + lastDockable = selected; + refresh(selected); + } + } + + /** + * Locate the dockable under the mouse using three strategies in order: + * + *
    + *
  1. Walk up the component tree: if any ancestor is itself a {@link Dockable}, return it. + * This covers hovering over a dockable's content area.
  2. + *
  3. Walk up to a {@link JTabbedPane}: tab labels are painted by the L&F with no child + * components, so {@code getDeepestComponentAt} returns the pane itself. + * {@code indexAtLocation} finds the tab under the cursor; if the cursor is over the + * content area (returns -1) fall back to the selected index. Then scan the tab's + * content subtree for the {@link Dockable}.
  4. + *
  5. Walk up to a {@link JPanel} whose direct children include a {@link Dockable}: this + * matches the framework's internal {@code DisplayPanel}, which contains both the header + * UI and the dockable side-by-side. Hovering over the header triggers this path.
  6. + *
+ */ + private Dockable findDockableUnderMouse(Component source, int x, int y, Component deepest) { + Component c = deepest; + while (c != null) { + // Strategy 1: ancestor is directly a Dockable + if (c instanceof Dockable) { + Dockable d = (Dockable) c; + if (docking.isDockableRegistered(d.getPersistentID())) { + return d; + } + } + + // Strategy 2: ancestor is a JTabbedPane — covers tab labels and tab content headers + if (c instanceof JTabbedPane) { + JTabbedPane tabs = (JTabbedPane) c; + Point local = SwingUtilities.convertPoint(source, x, y, tabs); + int tabIdx = tabs.indexAtLocation(local.x, local.y); + if (tabIdx < 0) { + // Mouse is over the content area, not a tab label + tabIdx = tabs.getSelectedIndex(); + } + if (tabIdx >= 0) { + return findDockableInSubtree(tabs.getComponentAt(tabIdx)); + } + return null; + } + + // Strategy 3: ancestor is a JPanel whose direct children include a Dockable — this + // matches the framework's DisplayPanel when hovering over the dockable's header. + if (c instanceof JPanel) { + Dockable d = findDockableDirectChild((JPanel) c); + if (d != null) { + return d; + } + } + + c = c.getParent(); + } + return null; + } + + /** Scan {@code panel}'s immediate children (and one JScrollPane level) for a registered Dockable. */ + private Dockable findDockableDirectChild(JPanel panel) { + for (Component child : panel.getComponents()) { + if (child instanceof Dockable) { + Dockable d = (Dockable) child; + if (docking.isDockableRegistered(d.getPersistentID())) { + return d; + } + } + if (child instanceof JScrollPane) { + Component view = ((JScrollPane) child).getViewport().getView(); + if (view instanceof Dockable) { + Dockable d = (Dockable) view; + if (docking.isDockableRegistered(d.getPersistentID())) { + return d; + } + } + } + } + return null; + } + + /** Depth-first search of {@code root}'s subtree for the first registered Dockable. */ + private Dockable findDockableInSubtree(Component root) { + if (root == null) { + return null; + } + if (root instanceof Dockable) { + Dockable d = (Dockable) root; + if (docking.isDockableRegistered(d.getPersistentID())) { + return d; + } + } + if (root instanceof JScrollPane) { + return findDockableInSubtree(((JScrollPane) root).getViewport().getView()); + } + if (root instanceof Container) { + for (Component child : ((Container) root).getComponents()) { + Dockable found = findDockableInSubtree(child); + if (found != null) { + return found; + } + } + } + return null; + } + + private void refresh(Dockable d) { + if (!docking.isDockableRegistered(d.getPersistentID())) { + lastDockable = null; + clearTable(); + return; + } + tableModel.setRowCount(0); + tableModel.addRow(new Object[]{"getPersistentID()", d.getPersistentID()}); + tableModel.addRow(new Object[]{"getTabText()", d.getTabText()}); + tableModel.addRow(new Object[]{"getTitleText()", d.getTitleText()}); + tableModel.addRow(new Object[]{"isDocked", docking.isDocked(d)}); + tableModel.addRow(new Object[]{"isHidden", docking.isHidden(d)}); + tableModel.addRow(new Object[]{"inFocusedMode", docking.inFocusedMode(d)}); + tableModel.addRow(new Object[]{"isClosable()", d.isClosable()}); + tableModel.addRow(new Object[]{"isFloatingAllowed()", d.isFloatingAllowed()}); + tableModel.addRow(new Object[]{"isAutoHideAllowed()", d.isAutoHideAllowed()}); + tableModel.addRow(new Object[]{"isMinMaxAllowed()", d.isMinMaxAllowed()}); + tableModel.addRow(new Object[]{"isLimitedToWindow()", d.isLimitedToWindow()}); + tableModel.addRow(new Object[]{"getType()", d.getType()}); + } + + private void clearTable() { + tableModel.setRowCount(0); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Inspector"; + } + + @Override + public boolean isAutoHideAllowed() { + return true; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/DocumentsAnchor.java b/demo-common/src/demo/DocumentsAnchor.java new file mode 100644 index 00000000..0de9e683 --- /dev/null +++ b/demo-common/src/demo/DocumentsAnchor.java @@ -0,0 +1,58 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockableStyle; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class DocumentsAnchor extends JPanel implements Dockable { + + public static final String PERSISTENT_ID = "documents-anchor"; + + public DocumentsAnchor(DockingAPI docking) { + docking.registerDockingAnchor(this); + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.CENTER; + add(new JLabel("Documents will open here."), gbc); + } + + @Override + public String getPersistentID() { + return PERSISTENT_ID; + } + + @Override + public String getTabText() { + return ""; + } + + @Override + public DockableStyle getStyle() { + return DockableStyle.CENTER_ONLY; + } +} diff --git a/demo-common/src/demo/EditorPanel.java b/demo-common/src/demo/EditorPanel.java new file mode 100644 index 00000000..040f2fb6 --- /dev/null +++ b/demo-common/src/demo/EditorPanel.java @@ -0,0 +1,127 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockingProperty; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.BorderLayout; +import java.awt.Font; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +public class EditorPanel extends JPanel implements Dockable { + + public static final int TYPE = 1; + + private final String id; + private final String tabText; + private final DockingAPI docking; + + @DockingProperty(name = "file-path", required = true, defaultValue = "") + private String filePath; + + private final JTextArea area = new JTextArea(); + + public EditorPanel(DockingAPI docking, String id, String tabText, String filePath) { + this.docking = docking; + this.id = id; + this.tabText = tabText; + this.filePath = filePath; + setLayout(new BorderLayout()); + area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + area.setEditable(false); + loadContent(); + JScrollPane scroll = new JScrollPane(area); + scroll.setBorder(BorderFactory.createEmptyBorder()); + add(scroll, BorderLayout.CENTER); + docking.registerDockable(this); + } + + private void loadContent() { + area.setText(""); + if (filePath != null && !filePath.isEmpty()) { + File file = new File(filePath); + if (file.exists()) { + try { + area.setText(new String(Files.readAllBytes(file.toPath()))); + area.setCaretPosition(0); + } catch (IOException e) { + area.setText("// Could not read: " + filePath + "\n// " + e.getMessage()); + } + return; + } + } + area.setText("// " + tabText + "\n\npublic class Example {\n\n}"); + } + + @Override + public void updateProperties() { + loadContent(); + } + + @Override + public boolean requestClose() { + docking.deregisterDockable(this); + return false; + } + + @Override + public int getType() { + return TYPE; + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return tabText; + } + + @Override + public String getTitleText() { + return tabText; + } + + @Override + public boolean isFloatingAllowed() { + return false; + } + + @Override + public boolean isMinMaxAllowed() { + return true; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/EventLogPanel.java b/demo-common/src/demo/EventLogPanel.java new file mode 100644 index 00000000..bf27dd82 --- /dev/null +++ b/demo-common/src/demo/EventLogPanel.java @@ -0,0 +1,128 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.event.DockingEvent; +import io.github.andrewauclair.moderndocking.event.DockingListener; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +public class EventLogPanel extends JPanel implements Dockable, DockingListener { + + private final String id; + private final DockingAPI docking; + private final JTextArea log = new JTextArea(); + final Map counters = new LinkedHashMap<>(); + final Map countLabels = new LinkedHashMap<>(); + + public EventLogPanel(DockingAPI docking, String id) { + this.docking = docking; + this.id = id; + setLayout(new BorderLayout()); + + log.setEditable(false); + log.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); + + JPanel counterPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 6, 3)); + for (DockingEvent.ID eventId : DockingEvent.ID.values()) { + counters.put(eventId, new AtomicInteger(0)); + JLabel lbl = new JLabel(eventId.name() + ": 0 "); + lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN, 11f)); + countLabels.put(eventId, lbl); + counterPanel.add(lbl); + } + + JScrollPane counterScroll = new JScrollPane(counterPanel, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + counterScroll.setPreferredSize(new Dimension(0, 60)); + + JButton resetCounters = new JButton("Reset Counters"); + resetCounters.addActionListener(e -> { + counters.forEach((eventId, c) -> c.set(0)); + countLabels.forEach((eventId, lbl) -> lbl.setText(eventId.name() + ": 0 ")); + }); + + JButton clearLog = new JButton("Clear Log"); + clearLog.addActionListener(e -> log.setText("")); + + JPanel buttonRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + buttonRow.add(resetCounters); + buttonRow.add(clearLog); + + add(counterScroll, BorderLayout.NORTH); + JScrollPane logScroll = new JScrollPane(log); + logScroll.setBorder(BorderFactory.createEmptyBorder()); + add(logScroll, BorderLayout.CENTER); + add(buttonRow, BorderLayout.SOUTH); + + docking.registerDockable(this); + } + + @Override + public void dockingChange(DockingEvent e) { + SwingUtilities.invokeLater(() -> { + String ts = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()); + String entry = String.format("[%s] %-30s %s%s%n", + ts, + e.getID().name(), + e.getDockable().getPersistentID(), + e.isTemporary() ? " (temporary)" : ""); + log.append(entry); + log.setCaretPosition(log.getDocument().getLength()); + + int n = counters.get(e.getID()).incrementAndGet(); + countLabels.get(e.getID()).setText(e.getID().name() + ":" + n + " "); + }); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Event Log"; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/MiscPanels.java b/demo-common/src/demo/MiscPanels.java new file mode 100644 index 00000000..899b26f6 --- /dev/null +++ b/demo-common/src/demo/MiscPanels.java @@ -0,0 +1,186 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; + +import java.awt.*; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +public class MiscPanels { + public static class ScrollablePanel extends JPanel implements Dockable { + private final String id; + + public ScrollablePanel(DockingAPI docking, String id) { + this.id = id; + setLayout(new GridLayout(0, 1, 0, 2)); + for (int i = 1; i <= 40; i++) { + add(new JLabel(" Item " + i + " — demonstrating wrappable scroll pane")); + } + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Scrollable"; + } + } + + public static class FixedPanel extends JPanel implements Dockable { + private final String id; + + public FixedPanel(DockingAPI docking, String id) { + this.id = id; + setLayout(new BorderLayout()); + add(centered("
Non-closable Panel

" + + "isClosable() = false
No × button is rendered.
"), + BorderLayout.CENTER); + + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Non-Closable"; + } + + @Override + public boolean isClosable() { + return false; + } + } + + public static class VetoClosePanel extends JPanel implements Dockable { + private final String id; + + public VetoClosePanel(DockingAPI docking, String id) { + this.id = id; + setLayout(new BorderLayout()); + add(centered("
Veto Close Panel

" + + "requestClose() shows a confirm dialog.
" + + "Click No to veto the close.
"), + BorderLayout.CENTER); + + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Veto Close"; + } + + @Override + public boolean requestClose() { + int r = JOptionPane.showConfirmDialog(this, + "Are you sure you want to close this panel?", + "Confirm Close", JOptionPane.YES_NO_OPTION); + return r == JOptionPane.YES_OPTION; + } + } + + public static class MoreOptionsPanel extends JPanel implements Dockable { + private final String id; + + public MoreOptionsPanel(DockingAPI docking, String id) { + this.id = id; + setLayout(new BorderLayout()); + add(centered("
More Options Panel

" + + "Open the header ⚙ menu to see
custom entries added via addMoreOptions().
"), + BorderLayout.CENTER); + + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "More Options"; + } + + @Override + public void addMoreOptions(JPopupMenu menu) { + JMenuItem a = new JMenuItem("Custom Action A"); + a.addActionListener(e -> JOptionPane.showMessageDialog(this, "Custom Action A fired.")); + menu.add(a); + + JMenuItem b = new JMenuItem("Custom Action B"); + b.addActionListener(e -> JOptionPane.showMessageDialog(this, "Custom Action B fired.")); + menu.add(b); + } + } + + public static class NoTabGroupPanel extends JPanel implements Dockable { + private final String id; + + public NoTabGroupPanel(DockingAPI docking, String id) { + this.id = id; + setLayout(new BorderLayout()); + add(centered("
No Tab Group Panel

" + + "isTabGroupAllowed() = false (1.5.0)

" + + "Drag another panel over this one — the
" + + "CENTER handle should not appear.
"), + BorderLayout.CENTER); + + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "No Tab Group"; + } + } + + private static JLabel centered(String html) { + JLabel lbl = new JLabel(html, JLabel.CENTER); + lbl.setVerticalAlignment(JLabel.CENTER); + return lbl; + } +} diff --git a/demo-common/src/demo/OutputPanel.java b/demo-common/src/demo/OutputPanel.java new file mode 100644 index 00000000..961af1d9 --- /dev/null +++ b/demo-common/src/demo/OutputPanel.java @@ -0,0 +1,87 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockableStyle; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Font; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +public class OutputPanel extends JPanel implements Dockable { + + private final String id; + private final DockingAPI docking; + + public OutputPanel(DockingAPI docking, String id) { + this.docking = docking; + this.id = id; + setLayout(new BorderLayout()); + JTextArea out = new JTextArea( + "[INFO] Build started\n" + + "[INFO] Compiling sources...\n" + + "[INFO] BUILD SUCCESS\n" + + "[INFO] Total time: 1.234 s"); + out.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + out.setEditable(false); + JButton clear = new JButton("Clear"); + clear.addActionListener(e -> out.setText("")); + JPanel south = new JPanel(new FlowLayout(FlowLayout.LEFT)); + south.add(clear); + JScrollPane scroll = new JScrollPane(out); + scroll.setBorder(BorderFactory.createEmptyBorder()); + add(scroll, BorderLayout.CENTER); + add(south, BorderLayout.SOUTH); + docking.registerDockable(this); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Output"; + } + + @Override + public DockableStyle getStyle() { + return DockableStyle.HORIZONTAL; + } + + @Override + public boolean isAutoHideAllowed() { + return true; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/ProjectPanel.java b/demo-common/src/demo/ProjectPanel.java new file mode 100644 index 00000000..23fef729 --- /dev/null +++ b/demo-common/src/demo/ProjectPanel.java @@ -0,0 +1,175 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockableStyle; +import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Set; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreePath; + +public class ProjectPanel extends JPanel implements Dockable { + + private static final Set EXCLUDED_NAMES = Set.of( + ".git", ".gradle", ".idea", "build", "out", ".DS_Store" + ); + + private final String id; + private final DockingAPI docking; + private final String anchorId; + + public ProjectPanel(DockingAPI docking, String id, File rootDir, String anchorId) { + this.docking = docking; + this.id = id; + this.anchorId = anchorId; + setLayout(new BorderLayout()); + + DefaultMutableTreeNode root = new DefaultMutableTreeNode(rootDir); + buildTree(rootDir, root); + + JTree tree = new JTree(root); + tree.setRootVisible(true); + tree.setShowsRootHandles(true); + + tree.setCellRenderer(new DefaultTreeCellRenderer() { + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + if (value instanceof DefaultMutableTreeNode) { + Object userObj = ((DefaultMutableTreeNode) value).getUserObject(); + if (userObj instanceof File) { + setText(((File) userObj).getName()); + } + } + return this; + } + }); + + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + Object userObj = node.getUserObject(); + if (userObj instanceof File) { + File file = (File) userObj; + if (file.isFile()) { + openFile(file); + } + } + } + } + } + }); + + JScrollPane scroll = new JScrollPane(tree); + scroll.setBorder(BorderFactory.createEmptyBorder()); + add(scroll, BorderLayout.CENTER); + docking.registerDockable(this); + } + + private void buildTree(File dir, DefaultMutableTreeNode node) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + Arrays.sort(files, Comparator + .comparing(f -> !f.isDirectory()) + .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + + for (File file : files) { + if (EXCLUDED_NAMES.contains(file.getName())) { + continue; + } + if (file.getName().startsWith(".")) { + continue; + } + DefaultMutableTreeNode child = new DefaultMutableTreeNode(file); + node.add(child); + if (file.isDirectory()) { + buildTree(file, child); + } + } + } + + private void openFile(File file) { + String editorId = "editor-" + file.getName(); + if (docking.isDockableRegistered(editorId)) { + if (docking.isDocked(editorId)) { + docking.bringToFront(editorId); + } else { + dockToEditorArea(editorId); + } + } else { + new EditorPanel(docking, editorId, file.getName(), file.getAbsolutePath()); + dockToEditorArea(editorId); + } + } + + private void dockToEditorArea(String editorId) { + if (docking.isDocked(anchorId)) { + docking.dock(editorId, anchorId, DockingRegion.CENTER); + } else { + docking.getDockables().stream() + .filter(d -> d.getType() == EditorPanel.TYPE && docking.isDocked(d)) + .findFirst() + .ifPresent(sibling -> docking.dock(editorId, sibling.getPersistentID(), DockingRegion.CENTER)); + } + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Project"; + } + + @Override + public DockableStyle getStyle() { + return DockableStyle.VERTICAL; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/PropertiesPanel.java b/demo-common/src/demo/PropertiesPanel.java new file mode 100644 index 00000000..1b6132be --- /dev/null +++ b/demo-common/src/demo/PropertiesPanel.java @@ -0,0 +1,101 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockingProperty; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; + +public class PropertiesPanel extends JPanel implements Dockable { + + private final String id; + private final DockingAPI docking; + + @DockingProperty(name = "filter-text", defaultValue = "") + private final String filterText = ""; + + @DockingProperty(name = "show-inherited", defaultValue = "true") + private final boolean showInherited = true; + + public PropertiesPanel(DockingAPI docking, String id) { + this.docking = docking; + this.id = id; + setLayout(new BorderLayout()); + + JPanel controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); + JTextField filter = new JTextField(10); + filter.setToolTipText("Filter properties"); + JCheckBox inherited = new JCheckBox("Inherited", true); + controls.add(new JLabel("Filter:")); + controls.add(filter); + controls.add(inherited); + + Object[][] rows = { + {"background", "Color", "#1a2b3c"}, + {"fontSize", "Integer", "13"}, + {"visible", "Boolean", "true"}, + {"label", "String", "Hello"}, + }; + JTable table = new JTable(rows, new String[]{"Key", "Type", "Value"}); + + add(controls, BorderLayout.NORTH); + JScrollPane scroll = new JScrollPane(table); + scroll.setBorder(BorderFactory.createEmptyBorder()); + scroll.setViewportBorder(null); + add(scroll, BorderLayout.CENTER); + + docking.registerDockable(this); + } + + @Override + public void updateProperties() { + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Properties"; + } + + @Override + public boolean isAutoHideAllowed() { + return true; + } + + @Override + public boolean isWrappableInScrollpane() { + return false; + } +} diff --git a/demo-common/src/demo/TestHarnessPanel.java b/demo-common/src/demo/TestHarnessPanel.java new file mode 100644 index 00000000..2552a7b7 --- /dev/null +++ b/demo-common/src/demo/TestHarnessPanel.java @@ -0,0 +1,318 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.DefaultCellEditor; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; + +public class TestHarnessPanel extends JPanel implements Dockable { + + private final String id; + private final DockingAPI docking; + private final StateTableModel stateModel; + private final Timer refreshTimer; + + public TestHarnessPanel(DockingAPI docking, String id, List dockables) { + this.docking = docking; + this.id = id; + setLayout(new BorderLayout()); + + stateModel = new StateTableModel(docking, dockables); + JTable stateTable = new JTable(stateModel); + stateTable.setRowHeight(22); + stateTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + stateTable.getColumnModel().getColumn(0).setPreferredWidth(220); + stateTable.getColumnModel().getColumn(1).setPreferredWidth(90); + stateTable.getColumnModel().getColumn(2).setPreferredWidth(80); + stateTable.getColumnModel().getColumn(3).setPreferredWidth(45); + + stateTable.getColumnModel().getColumn(3).setCellRenderer( + new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent( + JTable t, Object value, boolean sel, boolean focus, int row, int col) { + JLabel lbl = (JLabel) super.getTableCellRendererComponent( + t, value, sel, focus, row, col); + lbl.setHorizontalAlignment(JLabel.CENTER); + if ("\u2705".equals(value)) { + lbl.setForeground(new Color(0, 150, 0)); + } + else if ("\u274c".equals(value)) { + lbl.setForeground(new Color(200, 0, 0)); + } + else { + lbl.setForeground(UIManager.getColor("Label.foreground")); + } + return lbl; + } + }); + + stateTable.getColumnModel().getColumn(2).setCellEditor( + new DefaultCellEditor(new JCheckBox())); + stateTable.getColumnModel().getColumn(2).setCellRenderer( + new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent( + JTable t, Object value, boolean sel, boolean focus, int row, int col) { + JCheckBox cb = new JCheckBox("", Boolean.TRUE.equals(value)); + cb.setHorizontalAlignment(JLabel.CENTER); + cb.setOpaque(true); + cb.setBackground(sel ? t.getSelectionBackground() : t.getBackground()); + return cb; + } + }); + + JButton refreshBtn = new JButton("Refresh Now"); + refreshBtn.addActionListener(e -> stateModel.refresh()); + + JCheckBox autoRefreshCb = new JCheckBox("Auto (500 ms)", true); + + JButton resetExpected = new JButton("Reset Expected to false"); + resetExpected.addActionListener(e -> stateModel.resetExpected()); + + JPanel stateButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + stateButtons.add(refreshBtn); + stateButtons.add(autoRefreshCb); + stateButtons.add(resetExpected); + + JPanel stateWrapper = new JPanel(new BorderLayout()); + stateWrapper.setBorder(BorderFactory.createTitledBorder("API State Checks")); + stateWrapper.add(new JScrollPane(stateTable), BorderLayout.CENTER); + stateWrapper.add(stateButtons, BorderLayout.SOUTH); + + refreshTimer = new Timer(500, e -> { + if (autoRefreshCb.isSelected()) { + stateModel.refresh(); + } + }); + refreshTimer.start(); + + JPanel checklistPanel = new JPanel(); + checklistPanel.setLayout(new BoxLayout(checklistPanel, BoxLayout.Y_AXIS)); + buildChecklist(checklistPanel); + + JButton uncheckAll = new JButton("Uncheck All"); + uncheckAll.addActionListener(e -> { + for (Component c : checklistPanel.getComponents()) { + if (c instanceof JCheckBox) { + ((JCheckBox) c).setSelected(false); + } + } + }); + + JPanel checklistWrapper = new JPanel(new BorderLayout()); + checklistWrapper.setBorder(BorderFactory.createTitledBorder("Manual Visual Checks")); + checklistWrapper.add(new JScrollPane(checklistPanel), BorderLayout.CENTER); + checklistWrapper.add(uncheckAll, BorderLayout.SOUTH); + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, stateWrapper, checklistWrapper); + split.setResizeWeight(0.55); + + add(split, BorderLayout.CENTER); + + docking.registerDockable(this); + } + + private void buildChecklist(JPanel panel) { + section(panel, "Core Drag & Drop"); + check(panel, "NORTH handle → correct top split"); + check(panel, "SOUTH handle → correct bottom split"); + check(panel, "EAST / WEST handles → correct side splits"); + check(panel, "CENTER handle → tab group formed"); + check(panel, "Root NORTH / SOUTH / EAST / WEST handles work"); + check(panel, "Divider is draggable after docking"); + + section(panel, "Tab Groups"); + check(panel, "Tab text matches getTabText() for each dockable"); + check(panel, "Dragging tab out of group makes it standalone"); + + section(panel, "Float"); + check(panel, "Drag off window → FloatingFrame with correct title"); + check(panel, "Drop floating dockable back → docks correctly"); + check(panel, "Dockables do NOT cross between frame 1 and frame 2"); + + section(panel, "Auto-Hide"); + check(panel, "WEST: tab button appears, slides out/back on click"); + check(panel, "SOUTH: tab button appears, slides out/back on click"); + check(panel, "EAST: tab button appears, slides out/back on click"); + + section(panel, "Focused Mode (1.5.0)"); + check(panel, "Enter: target fills container, all others hidden"); + check(panel, "Exit: all panels restored to correct positions"); + check(panel, "Frame 2 focused mode independent of frame 1"); + + section(panel, "Layout Persistence"); + check(panel, "Frame 1 and frame 2 persist to separate files"); + check(panel, "Save + restore: all panels in correct positions"); + check(panel, "Save + restore: split proportions match"); + + section(panel, "LAF Switch"); + check(panel, "Both frames update when LAF is switched"); + } + + private void section(JPanel panel, String title) { + JLabel lbl = new JLabel(" " + title); + lbl.setFont(lbl.getFont().deriveFont(Font.BOLD, 12f)); + lbl.setForeground(UIManager.getColor("Label.disabledForeground")); + lbl.setBorder(BorderFactory.createEmptyBorder(8, 0, 2, 0)); + lbl.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(lbl); + } + + private void check(JPanel panel, String text) { + JCheckBox cb = new JCheckBox(text); + cb.setAlignmentX(Component.LEFT_ALIGNMENT); + panel.add(cb); + } + + @Override + public String getPersistentID() { + return id; + } + + @Override + public String getTabText() { + return "Test Harness"; + } + + static class StateTableModel extends AbstractTableModel { + + private static final String[] COLS = {"Query", "Live Value", "Expected (true = pass)", "Pass?"}; + + private final List labels = new ArrayList<>(); + private final List> queries = new ArrayList<>(); + private final List expected = new ArrayList<>(); + private final List live = new ArrayList<>(); + private final List passState = new ArrayList<>(); + + StateTableModel(DockingAPI docking, List dockables) { + for (Dockable d : dockables) { + add("isDocked(" + d.getPersistentID() + ")", () -> docking.isDocked(d)); + } + for (Dockable d : dockables) { + add("isHidden(" + d.getPersistentID() + ")", () -> docking.isHidden(d)); + } + for (Dockable d : dockables) { + // 1.5.0 + add("inFocusedMode(" + d.getPersistentID() + ")", () -> docking.inFocusedMode(d)); + } + refresh(); + } + + private void add(String label, Supplier query) { + labels.add(label); + queries.add(query); + expected.add(Boolean.FALSE); + live.add(Boolean.FALSE); + passState.add("—"); + } + + void refresh() { + for (int i = 0; i < queries.size(); i++) { + boolean liveVal = queries.get(i).get(); + live.set(i, liveVal); + passState.set(i, (liveVal == expected.get(i)) ? "\u2705" : "\u274c"); + } + fireTableDataChanged(); + } + + void resetExpected() { + for (int i = 0; i < expected.size(); i++) { + expected.set(i, Boolean.FALSE); + } + refresh(); + } + + @Override + public int getRowCount() { + return labels.size(); + } + + @Override + public int getColumnCount() { + return COLS.length; + } + + @Override + public String getColumnName(int col) { + return COLS[col]; + } + + @Override + public Object getValueAt(int row, int col) { + switch (col) { + case 0: + return labels.get(row); + case 1: + return live.get(row).toString(); + case 2: + return expected.get(row); + case 3: + return passState.get(row); + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int row, int col) { + if (col == 2) { + expected.set(row, Boolean.TRUE.equals(value)); + boolean liveVal = live.get(row); + passState.set(row, (liveVal == expected.get(row)) ? "\u2705" : "\u274c"); + fireTableRowsUpdated(row, row); + } + } + + @Override + public boolean isCellEditable(int row, int col) { + return col == 2; + } + + @Override + public Class getColumnClass(int col) { + return col == 2 ? Boolean.class : String.class; + } + } +} diff --git a/demo-common/src/demo/WrapLayout.java b/demo-common/src/demo/WrapLayout.java new file mode 100644 index 00000000..1db2ca6e --- /dev/null +++ b/demo-common/src/demo/WrapLayout.java @@ -0,0 +1,76 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +public class WrapLayout extends FlowLayout { + + public WrapLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return layoutSize(target, true); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + return layoutSize(target, false); + } + + private Dimension layoutSize(Container target, boolean preferred) { + synchronized (target.getTreeLock()) { + int targetWidth = target.getSize().width; + if (targetWidth == 0) { + targetWidth = Integer.MAX_VALUE; + } + + Insets insets = target.getInsets(); + int maxWidth = targetWidth - insets.left - insets.right - getHgap() * 2; + int height = getVgap(); + int rowW = 0; + int rowH = 0; + + for (Component c : target.getComponents()) { + if (!c.isVisible()) { + continue; + } + Dimension d = preferred ? c.getPreferredSize() : c.getMinimumSize(); + if (rowW > 0 && rowW + d.width > maxWidth) { + height += rowH + getVgap(); + rowW = 0; + rowH = 0; + } + rowW += d.width + getHgap(); + rowH = Math.max(rowH, d.height); + } + height += rowH + getVgap() + insets.top + insets.bottom; + return new Dimension(targetWidth, height); + } + } +} diff --git a/demo-multi-app/src/exception/CheckThreadViolationRepaintManager.java b/demo-common/src/exception/CheckThreadViolationRepaintManager.java similarity index 100% rename from demo-multi-app/src/exception/CheckThreadViolationRepaintManager.java rename to demo-common/src/exception/CheckThreadViolationRepaintManager.java diff --git a/demo-multi-app/src/exception/EdtViolationException.java b/demo-common/src/exception/EdtViolationException.java similarity index 100% rename from demo-multi-app/src/exception/EdtViolationException.java rename to demo-common/src/exception/EdtViolationException.java diff --git a/demo-multi-app/src/exception/FailOnThreadViolationRepaintManager.java b/demo-common/src/exception/FailOnThreadViolationRepaintManager.java similarity index 100% rename from demo-multi-app/src/exception/FailOnThreadViolationRepaintManager.java rename to demo-common/src/exception/FailOnThreadViolationRepaintManager.java diff --git a/demo-multi-app/build.gradle b/demo-multi-app/build.gradle index a42b7dd8..a5e25221 100644 --- a/demo-multi-app/build.gradle +++ b/demo-multi-app/build.gradle @@ -14,12 +14,12 @@ sourceSets.test.java.srcDirs = ['test'] sourceSets.main.resources.srcDirs = ['resources'] dependencies { + implementation project(':demo-common') + if (project.hasProperty("useMavenVersion")) { - implementation 'io.github.andrewauclair:modern-docking-ui:' + version implementation 'io.github.andrewauclair:modern-docking-multi-app:' + version } else { - implementation project(':docking-ui') implementation project(':docking-multi-app') } @@ -43,3 +43,9 @@ jar { } tasks.withType(Javadoc).all { enabled = false } // don't generate javadocs for the demo projects + +task run(type: JavaExec, dependsOn: classes) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'demo.MultiAppDemo' + workingDir = rootProject.projectDir +} diff --git a/demo-multi-app/src/demo/MultiAppDemo.java b/demo-multi-app/src/demo/MultiAppDemo.java new file mode 100644 index 00000000..b8801baf --- /dev/null +++ b/demo-multi-app/src/demo/MultiAppDemo.java @@ -0,0 +1,89 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.api.RootDockingPanelAPI; +import io.github.andrewauclair.moderndocking.api.WindowLayoutBuilderAPI; +import io.github.andrewauclair.moderndocking.app.DockableMenuItem; +import io.github.andrewauclair.moderndocking.app.Docking; +import io.github.andrewauclair.moderndocking.app.RootDockingPanel; +import io.github.andrewauclair.moderndocking.app.WindowLayoutBuilder; +import javax.swing.JMenuItem; +import java.io.File; +import java.util.concurrent.Callable; +import javax.swing.SwingUtilities; +import picocli.CommandLine; + +@CommandLine.Command +public class MultiAppDemo implements Callable { + @CommandLine.Mixin + private DemoOptions options; + + @Override + public Integer call() { + options.apply(); + + CommonDemoFrame frame1 = new AppDemoFrame( + "Modern Docking \u2014 Multi-App Demo (Frame 1)", + new File("multi_app_demo_layout_1.xml")); + + CommonDemoFrame frame2 = new AppDemoFrame( + "Modern Docking \u2014 Multi-App Demo (Frame 2)", + new File("multi_app_demo_layout_2.xml")); + + frame1.setVisible(true); + frame2.setVisible(true); + return 0; + } + + private static class AppDemoFrame extends CommonDemoFrame { + AppDemoFrame(String title, File persistFile) { + super(title, persistFile); + } + + @Override + protected DockingAPI createDocking() { + return new Docking(this); + } + + @Override + protected RootDockingPanelAPI createRoot(DockingAPI docking) { + return new RootDockingPanel(docking, this); + } + + @Override + protected WindowLayoutBuilderAPI createLayoutBuilder(DockingAPI docking, String firstId) { + return new WindowLayoutBuilder(docking, firstId); + } + + @Override + protected JMenuItem createViewMenuItem(Dockable dockable) { + return new DockableMenuItem(getDocking(), dockable); + } + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> new CommandLine(new MultiAppDemo()).execute(args)); + } +} diff --git a/demo-single-app/build.gradle b/demo-single-app/build.gradle index 0c3b45d8..0e5d7161 100644 --- a/demo-single-app/build.gradle +++ b/demo-single-app/build.gradle @@ -14,6 +14,8 @@ sourceSets.test.java.srcDirs = ['test'] sourceSets.main.resources.srcDirs = ['resources'] dependencies { + implementation project(':demo-common') + if (project.hasProperty("useMavenVersion")) { implementation 'io.github.andrewauclair:modern-docking-ui:' + version implementation 'io.github.andrewauclair:modern-docking-single-app:' + version @@ -46,3 +48,9 @@ jar { } tasks.withType(Javadoc).all { enabled = false } // don't generate javadocs for the demo projects + +task run(type: JavaExec, dependsOn: classes) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'demo.SingleAppDemo' + workingDir = rootProject.projectDir +} diff --git a/demo-single-app/resources/docking/FlatDarkLaf.properties b/demo-single-app/resources/docking/FlatDarkLaf.properties deleted file mode 100644 index 391e17fd..00000000 --- a/demo-single-app/resources/docking/FlatDarkLaf.properties +++ /dev/null @@ -1,32 +0,0 @@ -# -# Copyright 2020 FormDev Software GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -#---- Demo.accent ---- - -#Docking.handles.background = #3C3F41 -#Docking.handles.background.border = #616365 -#Docking.handles.outline = #8C8C8C -#Docking.handles.fill = #557394 -#Docking.overlay.color = #42c0ff -#Docking.overlay.border.color = #111111 -#Docking.overlay.alpha = 85 - -#Docking.titlebar.default = #4B6EAF -#Docking.titlebar.background = #46494b -#Docking.titlebar.border.enabled = true -#Docking.titlebar.border.color = #666666 -#Docking.titlebar.border.size = 1 -#Docking.titlebar.button.color = #000000 \ No newline at end of file diff --git a/demo-single-app/resources/docking/FlatLightLaf.properties b/demo-single-app/resources/docking/FlatLightLaf.properties deleted file mode 100644 index eee3b44b..00000000 --- a/demo-single-app/resources/docking/FlatLightLaf.properties +++ /dev/null @@ -1,29 +0,0 @@ -# -# Copyright 2020 FormDev Software GmbH -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#Docking.handles.background = #F2F2F2 -#Docking.handles.background.border = #C2C2C2 -#Docking.handles.outline = #000000 -#Docking.handles.fill = #4E9DE7 -#Docking.overlay.color = #0063d4 -#Docking.overlay.border.color = #C2C2C2 -#Docking.overlay.alpha = 75 - -#Docking.titlebar.default = #2675BF -#Docking.titlebar.background = #ffffff -#Docking.titlebar.border.enabled = true -#Docking.titlebar.border.color = #666666 -#Docking.titlebar.border.size = 1 -#Docking.titlebar.button.color = #000000 \ No newline at end of file diff --git a/demo-single-app/resources/icons/dark/close.png b/demo-single-app/resources/icons/dark/close.png deleted file mode 100644 index da755010..00000000 Binary files a/demo-single-app/resources/icons/dark/close.png and /dev/null differ diff --git a/demo-single-app/resources/icons/dark/maximize.png b/demo-single-app/resources/icons/dark/maximize.png deleted file mode 100644 index 9274e5f1..00000000 Binary files a/demo-single-app/resources/icons/dark/maximize.png and /dev/null differ diff --git a/demo-single-app/resources/icons/dark/minimize.png b/demo-single-app/resources/icons/dark/minimize.png deleted file mode 100644 index b82efb7a..00000000 Binary files a/demo-single-app/resources/icons/dark/minimize.png and /dev/null differ diff --git a/demo-single-app/resources/icons/dark/pin.png b/demo-single-app/resources/icons/dark/pin.png deleted file mode 100644 index d7bbb8e1..00000000 Binary files a/demo-single-app/resources/icons/dark/pin.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/close.png b/demo-single-app/resources/icons/light/close.png deleted file mode 100644 index ae1e071b..00000000 Binary files a/demo-single-app/resources/icons/light/close.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/icons8-vga-100.png b/demo-single-app/resources/icons/light/icons8-vga-100.png deleted file mode 100644 index ccb4ab2f..00000000 Binary files a/demo-single-app/resources/icons/light/icons8-vga-100.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/icons8-vga-16.png b/demo-single-app/resources/icons/light/icons8-vga-16.png deleted file mode 100644 index 62b1e971..00000000 Binary files a/demo-single-app/resources/icons/light/icons8-vga-16.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/icons8-vga-50.png b/demo-single-app/resources/icons/light/icons8-vga-50.png deleted file mode 100644 index 3dc125b3..00000000 Binary files a/demo-single-app/resources/icons/light/icons8-vga-50.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/maximize.png b/demo-single-app/resources/icons/light/maximize.png deleted file mode 100644 index 926dadb4..00000000 Binary files a/demo-single-app/resources/icons/light/maximize.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/minimize.png b/demo-single-app/resources/icons/light/minimize.png deleted file mode 100644 index 958c4919..00000000 Binary files a/demo-single-app/resources/icons/light/minimize.png and /dev/null differ diff --git a/demo-single-app/resources/icons/light/pin.png b/demo-single-app/resources/icons/light/pin.png deleted file mode 100644 index 56ba3d63..00000000 Binary files a/demo-single-app/resources/icons/light/pin.png and /dev/null differ diff --git a/demo-single-app/src/basic/MainFrame.java b/demo-single-app/src/basic/MainFrame.java index 624d38b7..22b4f80a 100644 --- a/demo-single-app/src/basic/MainFrame.java +++ b/demo-single-app/src/basic/MainFrame.java @@ -52,6 +52,7 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.layouts.DockingLayouts; import io.github.andrewauclair.moderndocking.layouts.DynamicDockableCreationListener; import io.github.andrewauclair.moderndocking.settings.Settings; +import io.github.andrewauclair.moderndocking.ui.DockingSettings; import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -95,8 +96,8 @@ public class MainFrame extends JFrame implements Callable { @CommandLine.Option(names = "--tab-location", defaultValue = "NONE", description = "Location to display tabs. values: ${COMPLETION-CANDIDATES}") DockableTabPreference tabLocation; - @CommandLine.Option(names = "--create-docking-instance", defaultValue = "false", description = "create a separate instance of the framework for this MainFrame") - boolean createDockingInstance; + @CommandLine.Option(names = "--use-layered-pane-overlay", defaultValue = "false", description = "force the JLayeredPane-based docking overlay instead of the transparent-window overlay") + boolean useLayeredPaneOverlay; public MainFrame(File layoutFile) { this.layoutFile = layoutFile; @@ -430,6 +431,8 @@ private void configureLookAndFeel() { @Override public Integer call() { + DockingSettings.setUseLayeredPaneOverlay(useLayeredPaneOverlay); + configureLookAndFeel(); // now that the main frame is set up with the defaults, we can restore the layout diff --git a/demo-single-app/src/demo/SingleAppDemo.java b/demo-single-app/src/demo/SingleAppDemo.java new file mode 100644 index 00000000..4ec1fd56 --- /dev/null +++ b/demo-single-app/src/demo/SingleAppDemo.java @@ -0,0 +1,79 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package demo; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.api.RootDockingPanelAPI; +import io.github.andrewauclair.moderndocking.api.WindowLayoutBuilderAPI; +import io.github.andrewauclair.moderndocking.app.DockableMenuItem; +import io.github.andrewauclair.moderndocking.app.Docking; +import io.github.andrewauclair.moderndocking.app.RootDockingPanel; +import io.github.andrewauclair.moderndocking.app.WindowLayoutBuilder; +import javax.swing.JMenuItem; +import java.io.File; +import java.util.concurrent.Callable; +import javax.swing.SwingUtilities; +import picocli.CommandLine; + +@CommandLine.Command +public class SingleAppDemo implements Callable { + @CommandLine.Mixin + private DemoOptions options; + + @Override + public Integer call() { + options.apply(); + + CommonDemoFrame frame = new CommonDemoFrame( + "Modern Docking — Single App Demo", + new File("single_app_demo_layout.xml")) { + @Override + protected DockingAPI createDocking() { + Docking.initialize(this); + return Docking.getSingleInstance(); + } + + @Override + protected RootDockingPanelAPI createRoot(DockingAPI docking) { + return new RootDockingPanel(this); + } + + @Override + protected WindowLayoutBuilderAPI createLayoutBuilder(DockingAPI docking, String firstId) { + return new WindowLayoutBuilder(firstId); + } + + @Override + protected JMenuItem createViewMenuItem(Dockable dockable) { + return new DockableMenuItem(dockable); + } + }; + + frame.setVisible(true); + return 0; + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> new CommandLine(new SingleAppDemo()).execute(args)); + } +} diff --git a/demo-single-app/src/examples/BringToFront.java b/demo-single-app/src/examples/BringToFront.java deleted file mode 100644 index 1179c6a7..00000000 --- a/demo-single-app/src/examples/BringToFront.java +++ /dev/null @@ -1,4 +0,0 @@ -package examples; - -public class BringToFront { -} diff --git a/demo-single-app/src/examples/Display.java b/demo-single-app/src/examples/Display.java deleted file mode 100644 index 2dd5b6ff..00000000 --- a/demo-single-app/src/examples/Display.java +++ /dev/null @@ -1,4 +0,0 @@ -package examples; - -public class Display { -} diff --git a/demo-single-app/src/examples/Docking_dock.java b/demo-single-app/src/examples/Docking_dock.java deleted file mode 100644 index b0f45e70..00000000 --- a/demo-single-app/src/examples/Docking_dock.java +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright (c) 2022 Andrew Auclair - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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 NON-INFRINGEMENT. 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. - */ -package examples; - -import io.github.andrewauclair.moderndocking.Dockable; -import io.github.andrewauclair.moderndocking.DockingRegion; -import io.github.andrewauclair.moderndocking.app.Docking; -import io.github.andrewauclair.moderndocking.app.RootDockingPanel; -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.Point; -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; - -public class Docking_dock extends JFrame { - private static class DockingPanel extends JPanel implements Dockable { - private final String id; - - public DockingPanel(String id) { - this.id = id; - - JButton floatPanel = new JButton("Float This Panel"); - - floatPanel.addActionListener(e -> { - Point location = getLocationOnScreen(); - location.x += 250; - Docking.newWindow(this, location, new Dimension(150, 150)); - }); - - add(floatPanel); - } - - @Override - public String getPersistentID() { - return id; - } - - @Override - public String getTabText() { - return id; - } - - @Override - public boolean isClosable() { - return false; - } - }; - - public Docking_dock() { - setSize(300, 200); - - Docking.initialize(this); - - RootDockingPanel root = new RootDockingPanel(this); - add(root, BorderLayout.CENTER); - - DockingPanel alpha = new DockingPanel("Alpha"); - DockingPanel bravo = new DockingPanel("Bravo"); - DockingPanel charlie = new DockingPanel("Charlie"); - DockingPanel delta = new DockingPanel("Delta"); - - Docking.registerDockable(alpha); - Docking.registerDockable(bravo); - Docking.registerDockable(charlie); - Docking.registerDockable(delta); - - Docking.dock(alpha, this); - Docking.dock(bravo, alpha, DockingRegion.WEST); - } - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> new Docking_dock().setVisible(true)); - } -} diff --git a/demo-single-app/src/examples/Maximize.java b/demo-single-app/src/examples/Maximize.java deleted file mode 100644 index cddda4e8..00000000 --- a/demo-single-app/src/examples/Maximize.java +++ /dev/null @@ -1,4 +0,0 @@ -package examples; - -public class Maximize { -} diff --git a/demo-single-app/src/examples/NewWindow.java b/demo-single-app/src/examples/NewWindow.java deleted file mode 100644 index 86d23d5a..00000000 --- a/demo-single-app/src/examples/NewWindow.java +++ /dev/null @@ -1,40 +0,0 @@ -package examples; - -import io.github.andrewauclair.moderndocking.DockingRegion; -import io.github.andrewauclair.moderndocking.app.Docking; -import io.github.andrewauclair.moderndocking.app.RootDockingPanel; -import io.github.andrewauclair.moderndocking.ext.ui.DockingUI; -import io.github.andrewauclair.moderndocking.ui.DefaultDockingPanel; -import java.awt.BorderLayout; -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.SwingUtilities; - -public class NewWindow extends JFrame { - public NewWindow() { - setSize(300, 200); - - DockingUI.initialize(); - - RootDockingPanel root = new RootDockingPanel(this); - add(root, BorderLayout.CENTER); - - DefaultDockingPanel one = new DefaultDockingPanel("one", "One"); - DefaultDockingPanel two = new DefaultDockingPanel("two", "Two"); - - JButton openOneInNewWindow = new JButton("Open in new window"); - openOneInNewWindow.addActionListener(e -> Docking.newWindow("one")); - one.add(openOneInNewWindow); - - JButton openTwoInNewWindow = new JButton("Open in new window"); - openTwoInNewWindow.addActionListener(e -> Docking.newWindow(two)); - two.add(openTwoInNewWindow); - - Docking.dock(one, this); - Docking.dock(two, one, DockingRegion.WEST); - } - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> new NewWindow().setVisible(true)); - } -} diff --git a/demo-single-app/src/examples/TopTabsMode.java b/demo-single-app/src/examples/TopTabsMode.java deleted file mode 100644 index 739999e3..00000000 --- a/demo-single-app/src/examples/TopTabsMode.java +++ /dev/null @@ -1,4 +0,0 @@ -package examples; - -public class TopTabsMode { -} diff --git a/demo-single-app/src/examples/UpdateTabInfo.java b/demo-single-app/src/examples/UpdateTabInfo.java deleted file mode 100644 index 5ebb1b04..00000000 --- a/demo-single-app/src/examples/UpdateTabInfo.java +++ /dev/null @@ -1,4 +0,0 @@ -package examples; - -public class UpdateTabInfo { -} diff --git a/demo-single-app/src/exception/CheckThreadViolationRepaintManager.java b/demo-single-app/src/exception/CheckThreadViolationRepaintManager.java deleted file mode 100644 index 044ded73..00000000 --- a/demo-single-app/src/exception/CheckThreadViolationRepaintManager.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - *

- * Copyright 2012-2015 the original author or authors. - */ -package exception; - -import java.lang.ref.WeakReference; -import java.util.Objects; -import javax.swing.JComponent; -import javax.swing.RepaintManager; - -import static javax.swing.SwingUtilities.isEventDispatchThread; - -/** - *

- * This class is used to detect Event Dispatch Thread rule violations
- * See How to Use Threads for more info - *

- * - *

- * This is a modification of original idea of Scott Delap.
- *

- * - * @author Scott Delap - * @author Alexander Potochkin - *

- * ... - */ -abstract class CheckThreadViolationRepaintManager extends RepaintManager { - private final boolean completeCheck; - - private WeakReference lastComponent; - - CheckThreadViolationRepaintManager() { - // it is recommended to pass the complete check - this(true); - } - - CheckThreadViolationRepaintManager(boolean completeCheck) { - this.completeCheck = completeCheck; - } - - @Override - public synchronized void addInvalidComponent(JComponent component) { - checkThreadViolations(Objects.requireNonNull(component)); - super.addInvalidComponent(component); - } - - @Override - public void addDirtyRegion(JComponent component, int x, int y, int w, int h) { - checkThreadViolations(Objects.requireNonNull(component)); - super.addDirtyRegion(component, x, y, w, h); - } - - private void checkThreadViolations(JComponent c) { - if (!isEventDispatchThread() && (completeCheck || c.isShowing())) { - boolean imageUpdate = false; - boolean repaint = false; - boolean fromSwing = false; - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - for (StackTraceElement st : stackTrace) { - if (repaint && st.getClassName().startsWith("javax.swing.")) { - fromSwing = true; - } - if (repaint && "imageUpdate".equals(st.getMethodName())) { - imageUpdate = true; - } - if ("repaint".equals(st.getMethodName())) { - repaint = true; - fromSwing = false; - } - } - if (imageUpdate) { - // assuming it is java.awt.image.ImageObserver.imageUpdate(...) - // image was asynchronously updated, that's ok - return; - } - if (repaint && !fromSwing) { - // no problems here, since repaint() is thread safe - return; - } - // ignore the last processed component - if (lastComponent != null && c == lastComponent.get()) { - return; - } - lastComponent = new WeakReference<>(c); - violationFound(c, stackTrace); - } - } - - abstract void violationFound(JComponent c, StackTraceElement[] stackTrace); -} \ No newline at end of file diff --git a/demo-single-app/src/exception/EdtViolationException.java b/demo-single-app/src/exception/EdtViolationException.java deleted file mode 100644 index 68ca4a58..00000000 --- a/demo-single-app/src/exception/EdtViolationException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. - - Copyright 2012-2015 the original author or authors. - */ -package exception; - -/** - * Error thrown when an EDT violation is detected. For more details, please read the Swing's - * Threading Policy. - * - * @author Alex Ruiz - */ -public class EdtViolationException extends RuntimeException { - /** - * Creates a new {@link EdtViolationException}. - * - * @param message the detail message. - */ - public EdtViolationException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/demo-single-app/src/exception/FailOnThreadViolationRepaintManager.java b/demo-single-app/src/exception/FailOnThreadViolationRepaintManager.java deleted file mode 100644 index f430736a..00000000 --- a/demo-single-app/src/exception/FailOnThreadViolationRepaintManager.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. - - Copyright 2012-2015 the original author or authors. - */ -package exception; - -import javax.swing.JComponent; -import javax.swing.RepaintManager; - -/** - *

- * Fails a test when a Event Dispatch Thread rule violation is detected. See How to Use Threads for more information. - *

- * - * @author Alex Ruiz - */ -public class FailOnThreadViolationRepaintManager extends CheckThreadViolationRepaintManager { - /** - * the {@link RepaintManager} that was installed before {@link #install()} has been called. - */ - private static RepaintManager previousRepaintManager; - - /** - *

- * Creates a new {@link FailOnThreadViolationRepaintManager} and sets it as the current repaint manager. - *

- * - *

- * On Sun JVMs, this method will install the new repaint manager the first time only. Once installed, subsequent calls - * to this method will not install new repaint managers. This optimization may not work on non-Sun JVMs, since we use - * reflection to check if a {@code CheckThreadViolationRepaintManager.java} is already installed. - *

- * - * @return the created (and installed) repaint manager. - * @see #uninstall() - * @see RepaintManager#setCurrentManager(RepaintManager) - */ - public static FailOnThreadViolationRepaintManager install() { - Object m = currentRepaintManager(); - if (m instanceof FailOnThreadViolationRepaintManager) { - return (FailOnThreadViolationRepaintManager) m; - } - return installNew(); - } - - /** - *

- * Tries to restore the repaint manager before installing the {@link FailOnThreadViolationRepaintManager} via - * {@link #install()}. - *

- * - * @return the restored (and installed) repaint manager. - * @see #install() - * @see RepaintManager#setCurrentManager(RepaintManager) - */ - public static RepaintManager uninstall() { - RepaintManager restored = previousRepaintManager; - setCurrentManager(restored); - previousRepaintManager = null; - return restored; - } - - private static RepaintManager currentRepaintManager() { - try { - RepaintManager repaintManager = RepaintManager.currentManager(null); - - if (repaintManager != null) { - return repaintManager; - } - } - catch (RuntimeException e) { - return null; - } - return null; - } - - private static FailOnThreadViolationRepaintManager installNew() { - FailOnThreadViolationRepaintManager m = new FailOnThreadViolationRepaintManager(); - previousRepaintManager = currentRepaintManager(); - setCurrentManager(m); - return m; - } - - public FailOnThreadViolationRepaintManager() { - } - - public FailOnThreadViolationRepaintManager(boolean completeCheck) { - super(completeCheck); - } - - /** - * Throws a {@link EdtViolationException} when an EDT access violation is found. - * - * @param c the component involved in the EDT violation. - * @param stackTraceElements stack trace elements to be set to the thrown exception. - * @throws EdtViolationException when an EDT access violation is found. - */ - @Override - void violationFound(JComponent c, StackTraceElement[] stackTraceElements) { - EdtViolationException e = new EdtViolationException("EDT violation detected"); - e.setStackTrace(stackTraceElements); - throw e; - } -} \ No newline at end of file diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/api/AppStateAPI.java b/docking-api/src/io/github/andrewauclair/moderndocking/api/AppStateAPI.java index 17a1f02a..59c840f3 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/api/AppStateAPI.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/api/AppStateAPI.java @@ -45,18 +45,19 @@ of this software and associated documentation files (the "Software"), to deal * This class is used for auto persisting the application layout to a file when there are changes to the layout */ public class AppStateAPI { - private static final Logger logger = Logger.getLogger(AppStateAPI.class.getPackageName()); - private static final int PERSIST_TIMER_DELAY_MS = 500; + private final Logger logger = Logger.getLogger(AppStateAPI.class.getPackageName()); + private final int PERSIST_TIMER_DELAY_MS = 500; - private static boolean autoPersist = false; - private static final Map autoPersistFiles = new HashMap<>(); + private boolean autoPersist = false; + private final Map autoPersistFiles = new HashMap<>(); - private static ApplicationLayout defaultAppLayout = null; - private static ApplicationLayout lastPersistedLayout = null; + private ApplicationLayout defaultAppLayout = null; + private ApplicationLayout lastPersistedLayout = null; - private static boolean paused = false; + private boolean paused = false; + + private Timer persistTimer = null; - private static Timer persistTimer = null; private final DockingAPI docking; /** @@ -75,7 +76,7 @@ protected AppStateAPI(DockingAPI docking) { * @param autoPersist Should the framework auto persist the application layout to a file? */ public void setAutoPersist(boolean autoPersist) { - AppStateAPI.autoPersist = autoPersist; + this.autoPersist = autoPersist; } /** @@ -111,7 +112,7 @@ public File getPersistFile() { * @param paused Whether auto persistence should be enabled */ public void setPaused(boolean paused) { - AppStateAPI.paused = paused; + this.paused = paused; } /** @@ -266,6 +267,19 @@ public Property getProperty(Dockable dockable, String propertyName) { return wrapper.getProperty(propertyName); } + /** + * Set the value of a property on the dockable. If the property does not exist, it will be created + * + * @param dockable The dockable to set a property for + * @param propertyName The name of the property we're setting + * @param value The value of the property + */ + public void setProperty(Dockable dockable, String propertyName, String value) { + DockableWrapper wrapper = DockingInternal.get(docking).getWrapper(dockable); + + wrapper.setProperty(propertyName, new Property.StringProperty(propertyName, value)); + } + /** * Set the value of a property on the dockable. If the property does not exist, it will be created * diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingAPI.java b/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingAPI.java index cdac6453..b2e1eab5 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingAPI.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingAPI.java @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.Dockable; import io.github.andrewauclair.moderndocking.DockableStyle; import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.event.DockingEvent; import io.github.andrewauclair.moderndocking.event.DockingListener; import io.github.andrewauclair.moderndocking.event.MaximizeListener; import io.github.andrewauclair.moderndocking.event.NewFloatingFrameListener; @@ -65,6 +66,7 @@ of this software and associated documentation files (the "Software"), to deal */ public class DockingAPI { private final DockingInternal internals = new DockingInternal(this); + private final DockingListeners listeners = new DockingListeners(); // the applications main frame private Window mainWindow; @@ -96,6 +98,10 @@ public LayoutPersistenceAPI getLayoutPersistence() { return layoutPersistence; } + public DockingListeners getDockingListeners() { + return listeners; + } + /** * Create a new instance of the DockingAPI. Applications will use the single-app or multi-app Docking class * @@ -414,7 +420,7 @@ public void dock(Dockable dockable, Window window, DockingRegion region, double // fire an undock event if the dockable is changing windows if (wrapper.getWindow() != window) { - DockingListeners.fireUndockedEvent(dockable, false); + listeners.fireUndockedEvent(dockable, false); } } @@ -423,8 +429,9 @@ public void dock(Dockable dockable, Window window, DockingRegion region, double internals.getWrapper(dockable).setWindow(window); // fire a docked event when the component is actually added - DockingListeners.fireDockedEvent(dockable); + listeners.fireDockedEvent(dockable); + DockingComponentUtils.updateWindowMinimumSize(this, window); appState.persist(); } @@ -513,7 +520,7 @@ public void dock(Dockable source, Dockable target, DockingRegion region, double // fire an undock event if the dockable is changing windows if (wrapper.getWindow() != internals.getWrapper(source).getWindow()) { - DockingListeners.fireUndockedEvent(source, false); + listeners.fireUndockedEvent(source, false); } } @@ -524,8 +531,9 @@ public void dock(Dockable source, Dockable target, DockingRegion region, double internals.getWrapper(source).setWindow(wrapper.getWindow()); internals.getWrapper(source).setRoot(internals.getRootPanels().get(wrapper.getWindow())); - DockingListeners.fireDockedEvent(source); + listeners.fireDockedEvent(source); + DockingComponentUtils.updateWindowMinimumSize(this, wrapper.getWindow()); appState.persist(); } @@ -546,7 +554,7 @@ public void newWindow(String persistentID) { public void newWindow(Dockable dockable) { DisplayPanel displayPanel = internals.getWrapper(dockable).getDisplayPanel(); - if (isDocked(dockable)) { + if (isDocked(dockable) && displayPanel.isShowing()) { Point location = displayPanel.getLocationOnScreen(); Dimension size = displayPanel.getSize(); @@ -563,7 +571,7 @@ public void newWindow(Dockable dockable) { SwingUtilities.invokeLater(() -> { bringToFront(dockable); - DockingListeners.fireNewFloatingFrameEvent(frame, frame.getRoot(), dockable); + listeners.fireNewFloatingFrameEvent(frame, frame.getRoot(), dockable); }); } } @@ -595,7 +603,7 @@ public void newWindow(Dockable dockable, Point location, Dimension size) { SwingUtilities.invokeLater(() -> { bringToFront(dockable); - DockingListeners.fireNewFloatingFrameEvent(frame, frame.getRoot(), dockable); + listeners.fireNewFloatingFrameEvent(frame, frame.getRoot(), dockable); }); } @@ -703,52 +711,54 @@ public boolean canDisposeWindow(Window window) { } /** - * checks if a dockable is currently maximized + * Checks if a dockable is currently in focused mode * * @param dockable The dockable to check - * @return Whether the dockable is maximized + * @return Whether the dockable is in focused mode */ - public boolean isMaximized(Dockable dockable) { - return internals.getWrapper(dockable).isMaximized(); + public boolean inFocusedMode(Dockable dockable) { + return internals.getWrapper(dockable).isInFocusedMode(); } /** - * maximizes a dockable + * Enter focused mode for a dockable, undocking all others in the same root * - * @param dockable Dockable to maximize + * @param dockable Dockable to enter focused mode */ - public void maximize(Dockable dockable) { + public void enterFocusedMode(Dockable dockable) { Window window = DockingComponentUtils.findWindowForDockable(this, dockable); InternalRootDockingPanel root = DockingComponentUtils.rootForWindow(this, window); - // can only maximize one panel per root + // can only enter focused mode once per root if (!dockingState.maximizeRestoreLayout.containsKey(window) && root != null) { - internals.getWrapper(dockable).setMaximized(true); - DockingListeners.fireMaximizeEvent(dockable, true); + internals.getWrapper(dockable).setInFocusedMode(true); + listeners.fireFocusedModeEnteredEvent(dockable); WindowLayout layout = dockingState.getWindowLayout(window); - layout.setMaximizedDockable(dockable.getPersistentID()); + layout.setFocusedModeDockable(dockable.getPersistentID()); dockingState.maximizeRestoreLayout.put(window, layout); + internals.setInFocusedModeTransition(true); DockingComponentUtils.undockComponents(this, root); + internals.setInFocusedModeTransition(false); dock(dockable, window); } } /** - * minimize a dockable if it is currently maximized + * Exit focused mode for a dockable, restoring the previous layout * - * @param dockable Dockable to minimize + * @param dockable Dockable to exit focused mode */ - public void minimize(Dockable dockable) { + public void exitFocusedMode(Dockable dockable) { Window window = DockingComponentUtils.findWindowForDockable(this, dockable); - // can only minimize if already maximized + // can only exit if in focused mode if (dockingState.maximizeRestoreLayout.containsKey(window)) { - internals.getWrapper(dockable).setMaximized(false); - DockingListeners.fireMaximizeEvent(dockable, false); + internals.getWrapper(dockable).setInFocusedMode(false); + listeners.fireFocusedModeExitedEvent(dockable); dockingState.restoreWindowLayout(window, dockingState.maximizeRestoreLayout.get(window)); @@ -758,6 +768,30 @@ public void minimize(Dockable dockable) { } } + /** + * @deprecated Use {@link #inFocusedMode(Dockable)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public boolean isMaximized(Dockable dockable) { + return inFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #enterFocusedMode(Dockable)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public void maximize(Dockable dockable) { + enterFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #exitFocusedMode(Dockable)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public void minimize(Dockable dockable) { + exitFocusedMode(dockable); + } + public void autoShowDockable(Dockable dockable) { Window window = DockingComponentUtils.findWindowForDockable(this, dockable); InternalRootDockingPanel root = DockingComponentUtils.rootForWindow(this, window); @@ -767,7 +801,7 @@ public void autoShowDockable(Dockable dockable) { internals.getWrapper(dockable).setHidden(false); - DockingListeners.fireAutoShownEvent(dockable); + listeners.fireAutoShownEvent(dockable); } } @@ -877,8 +911,8 @@ public void autoHideDockable(Dockable dockable, ToolbarLocation location, Window internalRoot.setDockableHidden(wrapper, location); - DockingListeners.fireAutoHiddenEvent(dockable); - DockingListeners.fireHiddenEvent(dockable); + listeners.fireAutoHiddenEvent(dockable); + listeners.fireHiddenEvent(dockable); } public void autoHideDockable(String persistentID, ToolbarLocation location, Window window) { @@ -953,21 +987,27 @@ public void updateTabInfo(Dockable dockable) { } /** - * Add a new maximize listener. Will be called when a dockable is maximized + * Add a new maximize listener. Will be called when a dockable enters or exits focused mode. * * @param listener Listener to add + * @deprecated Use {@link #addDockingListener(DockingListener)} and handle + * {@link DockingEvent.ID#FOCUSED_MODE_ENTERED} / {@link DockingEvent.ID#FOCUSED_MODE_EXITED} instead. + * Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public void addMaximizeListener(MaximizeListener listener) { - DockingListeners.addMaximizeListener(listener); + listeners.addMaximizeListener(listener); } /** - * Remove a previously added maximize listener. No-op if the listener isn't in the list + * Remove a previously added maximize listener. No-op if the listener isn't in the list. * * @param listener Listener to remove + * @deprecated Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public void removeMaximizeListener(MaximizeListener listener) { - DockingListeners.removeMaximizeListener(listener); + listeners.removeMaximizeListener(listener); } /** @@ -976,7 +1016,7 @@ public void removeMaximizeListener(MaximizeListener listener) { * @param listener Listener to add */ public void addDockingListener(DockingListener listener) { - DockingListeners.addDockingListener(listener); + listeners.addDockingListener(listener); } /** @@ -985,7 +1025,7 @@ public void addDockingListener(DockingListener listener) { * @param listener Listener to remove */ public void removeDockingListener(DockingListener listener) { - DockingListeners.removeDockingListener(listener); + listeners.removeDockingListener(listener); } /** @@ -994,7 +1034,7 @@ public void removeDockingListener(DockingListener listener) { * @param listener Listener to add */ public void addNewFloatingFrameListener(NewFloatingFrameListener listener) { - DockingListeners.addNewFloatingFrameListener(listener); + listeners.addNewFloatingFrameListener(listener); } /** @@ -1003,7 +1043,7 @@ public void addNewFloatingFrameListener(NewFloatingFrameListener listener) { * @param listener Listener to remove */ public void removeNewFloatingFrameListener(NewFloatingFrameListener listener) { - DockingListeners.removeNewFloatingFrameListener(listener); + listeners.removeNewFloatingFrameListener(listener); } /** diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingStateAPI.java b/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingStateAPI.java index 667fdc26..f13ed3d0 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingStateAPI.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/api/DockingStateAPI.java @@ -54,10 +54,6 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -70,7 +66,6 @@ of this software and associated documentation files (the "Software"), to deal import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFrame; -import javax.swing.JSplitPane; import javax.swing.SwingUtilities; /** @@ -147,6 +142,8 @@ public ApplicationLayout getApplicationLayout() { * @param layout Application layout to restore */ public void restoreApplicationLayout(ApplicationLayout layout) { + docking.getAppState().setPaused(true); + // get rid of all existing windows and undock all dockables Set windows = new HashSet<>(docking.getRootPanels().keySet()); for (Window window : windows) { @@ -160,8 +157,6 @@ public void restoreApplicationLayout(ApplicationLayout layout) { } } - docking.getAppState().setPaused(true); - // setup main frame restoreWindowLayout(docking.getMainWindow(), layout.getMainFrameLayout()); @@ -172,7 +167,7 @@ public void restoreApplicationLayout(ApplicationLayout layout) { restoreWindowLayout(frame, frameLayout); SwingUtilities.invokeLater(() -> { - DockingListeners.fireNewFloatingFrameEvent(frame, frame.getRoot()); + docking.getDockingListeners().fireNewFloatingFrameEvent(frame, frame.getRoot()); }); } @@ -181,6 +176,10 @@ public void restoreApplicationLayout(ApplicationLayout layout) { DockingInternal.fireDockedEventForAll(docking); + for (Window window : docking.getRootPanels().keySet()) { + DockingComponentUtils.updateWindowMinimumSize(docking, window); + } + DockingLayouts.layoutRestored(layout); } @@ -271,20 +270,8 @@ public void restoreWindowLayout(Window window, WindowLayout layout) { root.setSlidePosition(wrapper.getDockable(), (int) (layout.slidePosition(id) * window.getHeight())); } - if (layout.getMaximizedDockable() != null) { - docking.maximize(getDockable(docking, layout.getMaximizedDockable())); - } - } - - private void findSplitPanels(Container container, List panels) { - for (Component component : container.getComponents()) { - if (component instanceof DockedSplitPanel) { - panels.add((DockedSplitPanel) component); - } - - if (component instanceof Container) { - findSplitPanels((Container) component, panels); - } + if (layout.getFocusedModeDockable() != null) { + docking.enterFocusedMode(getDockable(docking, layout.getFocusedModeDockable())); } } @@ -327,13 +314,14 @@ else if (node == null) { } private DockedSplitPanel restoreSplit(DockingAPI docking, DockingSplitPanelNode node, Window window) { - DockedSplitPanel panel = new DockedSplitPanel(docking, window, ""); - - panel.setLeft(restoreLayout(docking, node.getLeft(), window)); - panel.setRight(restoreLayout(docking, node.getRight(), window)); + DockedSplitPanel panel = new DockedSplitPanel(docking, window, node.getAnchor() != null ? node.getAnchor() : ""); panel.setOrientation(node.getOrientation()); - panel.setDividerLocation(node.getDividerProportion()); + for (DockingLayoutNode childNode : node.getChildren()) { + panel.addChildForRestore(restoreLayout(docking, childNode, window)); + } + + panel.setDividerPositions(node.getDividerPositions()); return panel; } @@ -514,96 +502,13 @@ private void undockFailedComponents(DockingAPI docking, Container container) { } private void restoreProperSplitLocations(RootDockingPanelAPI root) { - SwingUtilities.invokeLater(() -> { - // find all the splits and restore their divider locations from the bottom up - List splitPanels = new ArrayList<>(); - - // find all the splits recursively. Pushing new splits onto the front of the deque. this forces the deepest - // splits to be adjusted last, keeping their position proper. - findSplitPanels(root, splitPanels); - - List splits = new ArrayList<>(); - List proportions = new ArrayList<>(); - - // loop through and restore split proportions, bottom up - for (DockedSplitPanel splitPanel : splitPanels) { - splits.add(splitPanel.getSplitPane()); - proportions.add(splitPanel.getLastRequestedDividerProportion()); - } - restoreSplits(splits, proportions); - }); - } - - private void restoreSplits(List splits, List proportions) { - if (splits.size() != proportions.size()) { - return; - } - if (splits.isEmpty()) { - return; - } - - JSplitPane splitPane = splits.get(0); - double proportion = proportions.get(0); - - splits.remove(0); - proportions.remove(0); - - restoreSplit(splitPane, proportion, splits, proportions); - } - - /** - * Restore all splits in the window, starting with the outer most splits and working our way in. Only moving to the next when the previous - * has been completely set. - * - * @param splitPane The current splitpane we're setting - * @param proportion The proportion of the splitpane - * @param splits The list of splitpanes left - * @param proportions The list of proportions for the remaining splitpanes - */ - private void restoreSplit(JSplitPane splitPane, double proportion, List splits, List proportions) { - // calling setDividerLocation on a JSplitPane that isn't visible does nothing, so we need to check if it is showing first - if (splitPane.isShowing()) { - if (splitPane.getWidth() > 0 && splitPane.getHeight() > 0) { - splitPane.setDividerLocation(proportion); - - if (!splits.isEmpty()) { - SwingUtilities.invokeLater(() -> { - JSplitPane nextSplit = splits.get(0); - double nextProportion = proportions.get(0); - - splits.remove(0); - proportions.remove(0); - - restoreSplit(nextSplit, nextProportion, splits, proportions); - }); - } - } else { - // split hasn't been completely calculated yet, wait until componentResize - splitPane.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - // remove this listener, it's a one off - splitPane.removeComponentListener(this); - // call the function again, this time it should actually set the divider location - restoreSplit(splitPane, proportion, splits, proportions); - } - }); - } - } else { - // split hasn't been shown yet, wait until it's showing - splitPane.addHierarchyListener(new HierarchyListener() { - @Override - public void hierarchyChanged(HierarchyEvent e) { - boolean isShowingChangeEvent = (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0; - - if (isShowingChangeEvent && splitPane.isShowing()) { - // remove this listener, it's a one off - splitPane.removeHierarchyListener(this); - // call the function again, this time it might set the size or wait for componentResize - restoreSplit(splitPane, proportion, splits, proportions); - } - } - }); + // Use validate() (synchronous) rather than revalidate() (deferred) so that + // the full layout tree is computed before the EDT task returns. This ensures + // that when Swing processes the accumulated dirty regions it paints the final + // correct state instead of an intermediate blank/unsized state. + if (root instanceof Container) { + ((Container) root).validate(); + ((Container) root).repaint(); } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/api/LayoutPersistenceAPI.java b/docking-api/src/io/github/andrewauclair/moderndocking/api/LayoutPersistenceAPI.java index 8645c9be..afa3d810 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/api/LayoutPersistenceAPI.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/api/LayoutPersistenceAPI.java @@ -290,8 +290,8 @@ private void saveLayoutToFile(XMLStreamWriter writer, WindowLayout layout, boole writer.writeAttribute("size", layout.getSize().width + "," + layout.getSize().height); writer.writeAttribute("state", String.valueOf(layout.getState())); - if (layout.getMaximizedDockable() != null) { - writer.writeAttribute("max-dockable", layout.getMaximizedDockable()); + if (layout.getFocusedModeDockable() != null) { + writer.writeAttribute("focused-dockable", layout.getFocusedModeDockable()); } writer.writeCharacters(NL); @@ -389,22 +389,23 @@ private void writeSimpleNodeToFile(XMLStreamWriter writer, DockingSimplePanelNod } private void writeSplitNodeToFile(XMLStreamWriter writer, DockingSplitPanelNode node) throws XMLStreamException { + double[] positions = node.getDividerPositions(); + List nodeChildren = node.getChildren(); + writer.writeStartElement(TAG_SPLIT); writer.writeAttribute("orientation", String.valueOf(node.getOrientation())); - writer.writeAttribute("divider-proportion", String.valueOf(node.getDividerProportion())); - writer.writeCharacters(NL); - writer.writeStartElement(TAG_LEFT); - writer.writeCharacters(NL); - writeNodeToFile(writer, node.getLeft()); - writer.writeEndElement(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < positions.length; i++) { + if (i > 0) sb.append(','); + sb.append(positions[i]); + } + writer.writeAttribute("divider-positions", sb.toString()); writer.writeCharacters(NL); - writer.writeStartElement(TAG_RIGHT); - writer.writeCharacters(NL); - writeNodeToFile(writer, node.getRight()); - writer.writeEndElement(); - writer.writeCharacters(NL); + for (DockingLayoutNode child : nodeChildren) { + writeNodeToFile(writer, child); + } writer.writeEndElement(); writer.writeCharacters(NL); @@ -525,7 +526,11 @@ private WindowLayout readLayoutFromReader(XMLStreamReader reader) throws XMLStre String locStr = reader.getAttributeValue(null, "location"); String sizeStr = reader.getAttributeValue(null, "size"); int state = Integer.parseInt(reader.getAttributeValue(null, "state")); - String maximizedDockable = reader.getAttributeValue(null, "max-dockable"); + // accept both "focused-dockable" (1.5+) and legacy "max-dockable" (pre-1.5) + String maximizedDockable = reader.getAttributeValue(null, "focused-dockable"); + if (maximizedDockable == null) { + maximizedDockable = reader.getAttributeValue(null, "max-dockable"); + } Point location = new Point(Integer.parseInt(locStr.substring(0, locStr.indexOf(","))), Integer.parseInt(locStr.substring(locStr.indexOf(",") + 1))); Dimension size = new Dimension(Integer.parseInt(sizeStr.substring(0, sizeStr.indexOf(","))), Integer.parseInt(sizeStr.substring(sizeStr.indexOf(",") + 1))); @@ -552,7 +557,7 @@ private WindowLayout readLayoutFromReader(XMLStreamReader reader) throws XMLStre layout.setSlidePosition(dockable.id, dockable.slidePosition); } - layout.setMaximizedDockable(maximizedDockable); + layout.setFocusedModeDockable(maximizedDockable); return layout; } @@ -697,24 +702,66 @@ else if (next == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equals( } private DockingSplitPanelNode readSplitNodeFromFile(XMLStreamReader reader) throws XMLStreamException { - DockingLayoutNode left = null; - DockingLayoutNode right = null; - int orientation = Integer.parseInt(reader.getAttributeValue(null, "orientation")); - double dividerProportion = Double.parseDouble(reader.getAttributeValue(null, "divider-proportion")); String anchor = reader.getAttributeValue(null, TAG_ANCHOR); + String dividerPositionsStr = reader.getAttributeValue(null, "divider-positions"); - // anchor didn't always exist, set it to an empty string if it's null if (anchor == null) { anchor = ""; } - if (dividerProportion < 0.0) { - dividerProportion = 0.0; + if (dividerPositionsStr != null) { + return readSplitNodeNewFormat(reader, orientation, anchor, dividerPositionsStr); } - else if (dividerProportion > 1.0) { - dividerProportion = 1.0; + return readSplitNodeLegacyFormat(reader, orientation, anchor); + } + + private DockingSplitPanelNode readSplitNodeNewFormat(XMLStreamReader reader, int orientation, + String anchor, String dividerPositionsStr) + throws XMLStreamException { + String[] posTokens = dividerPositionsStr.split(","); + double[] positions = new double[posTokens.length]; + + for (int i = 0; i < posTokens.length; i++) { + positions[i] = Math.max(0.0, Math.min(1.0, Double.parseDouble(posTokens[i].trim()))); + } + + List children = new ArrayList<>(); + + while (reader.hasNext()) { + int next = reader.nextTag(); + + if (next == XMLStreamConstants.START_ELEMENT) { + String name = reader.getLocalName(); + + if (name.equals("simple")) { + children.add(readSimpleNodeFromFile(reader)); + } + else if (name.equals(TAG_SPLIT)) { + children.add(readSplitNodeFromFile(reader)); + } + else if (name.equals("tabbed")) { + children.add(readTabNodeFromFile(reader)); + } + else if (name.equals(TAG_ANCHOR)) { + children.add(readAnchorNodeFromFile(reader)); + } + } + else if (next == XMLStreamConstants.END_ELEMENT && reader.getLocalName().equals(TAG_SPLIT)) { + break; + } } + return new DockingSplitPanelNode(docking, children, orientation, positions, anchor); + } + + private DockingSplitPanelNode readSplitNodeLegacyFormat(XMLStreamReader reader, int orientation, + String anchor) throws XMLStreamException { + String divPropStr = reader.getAttributeValue(null, "divider-proportion"); + double dividerProportion = (divPropStr != null) ? Double.parseDouble(divPropStr) : 0.5; + dividerProportion = Math.max(0.0, Math.min(1.0, dividerProportion)); + + DockingLayoutNode left = null; + DockingLayoutNode right = null; while (reader.hasNext()) { int next = reader.nextTag(); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/event/DockingEvent.java b/docking-api/src/io/github/andrewauclair/moderndocking/event/DockingEvent.java index 71f1055a..3d0f1faf 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/event/DockingEvent.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/event/DockingEvent.java @@ -56,7 +56,16 @@ public enum ID { /** * Dockable has been removed from an Auto Hide toolbar */ - AUTO_HIDE_DISABLED + AUTO_HIDE_DISABLED, + + /** + * Dockable has entered focused mode (was maximized) + */ + FOCUSED_MODE_ENTERED, + /** + * Dockable has exited focused mode (was minimized) + */ + FOCUSED_MODE_EXITED } private final ID id; diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/event/MaximizeListener.java b/docking-api/src/io/github/andrewauclair/moderndocking/event/MaximizeListener.java index 2fc13926..eecf2815 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/event/MaximizeListener.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/event/MaximizeListener.java @@ -25,7 +25,11 @@ of this software and associated documentation files (the "Software"), to deal /** * Interface for creating listeners for maximize events + * + * @deprecated Use {@link DockingListener} and handle {@link DockingEvent.ID#FOCUSED_MODE_ENTERED} and + * {@link DockingEvent.ID#FOCUSED_MODE_EXITED} events instead. Will be removed in 2.0. */ +@Deprecated(since = "1.5.0", forRemoval = true) public interface MaximizeListener { /** * Maximized state of dockable has changed diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/ActiveDockableHighlighter.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/ActiveDockableHighlighter.java index 3aede462..b30261e2 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/ActiveDockableHighlighter.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/ActiveDockableHighlighter.java @@ -56,7 +56,14 @@ public class ActiveDockableHighlighter { public ActiveDockableHighlighter(DockingAPI docking) { // use an AWT event listener to set a border around the dockable that the mouse is currently over awtEventListener = e -> { - if (Settings.isActiveHighlighterEnabled() && (e.getID() == MouseEvent.MOUSE_ENTERED || e.getID() == MouseEvent.MOUSE_EXITED)) { + if (!Settings.isActiveHighlighterEnabled()) { + if (activePanel != null) { + setNotSelectedBorder(activePanel); + activePanel = null; + } + return; + } + if (e.getID() == MouseEvent.MOUSE_ENTERED || e.getID() == MouseEvent.MOUSE_EXITED) { DockingPanel dockable = DockingComponentUtils.findDockingPanelAtScreenPos(docking, ((MouseEvent) e).getLocationOnScreen()); if (activePanel != null && dockable == null) { diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DisplayPanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DisplayPanel.java index 3d6567a7..a9c9828e 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DisplayPanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DisplayPanel.java @@ -25,6 +25,7 @@ of this software and associated documentation files (the "Software"), to deal import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -79,7 +80,9 @@ private void buildUI() { gbc.fill = GridBagConstraints.BOTH; if (wrapper.getDockable().isWrappableInScrollpane()) { - add(new JScrollPane((Component) wrapper.getDockable()), gbc); + JScrollPane scrollpane = new JScrollPane((Component) wrapper.getDockable()); + scrollpane.setBorder(BorderFactory.createEmptyBorder()); + add(scrollpane, gbc); } else { add((Component) wrapper.getDockable(), gbc); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableToolbar.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableToolbar.java index d0fbf1f2..8fbd75e1 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableToolbar.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableToolbar.java @@ -188,13 +188,13 @@ private void updateButtons() { boolean isSelected = buttonGroup.getSelection() == entry.button.getModel(); if (entry.panel.isVisible() && !isSelected) { - DockingListeners.fireHiddenEvent(entry.dockable.getDockable()); + docking.getDockingListeners().fireHiddenEvent(entry.dockable.getDockable()); } else if (!entry.panel.isVisible() && isSelected) { - DockingListeners.fireShownEvent(entry.dockable.getDockable()); + docking.getDockingListeners().fireShownEvent(entry.dockable.getDockable()); } else if (isSelected) { - DockingListeners.fireShownEvent(entry.dockable.getDockable()); + docking.getDockingListeners().fireShownEvent(entry.dockable.getDockable()); } // set only a single panel visible diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableWrapper.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableWrapper.java index 699859c0..ad36d1ca 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableWrapper.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockableWrapper.java @@ -55,7 +55,7 @@ public class DockableWrapper { private final FloatListener floatListener; - private boolean maximized = false; + private boolean inFocusedMode = false; private boolean hidden = false; private boolean isAnchor = false; private InternalRootDockingPanel root; @@ -149,21 +149,21 @@ public DockingPanel getParent() { } /** - * Check if the dockable is maximized + * Check if the dockable is in focused mode * - * @return Whether the dockable is maximized + * @return Whether the dockable is in focused mode */ - public boolean isMaximized() { - return maximized; + public boolean isInFocusedMode() { + return inFocusedMode; } /** - * Set the dockable to maximized + * Set the dockable focused mode state * - * @param maximized Maximized flag + * @param inFocusedMode Focused mode flag */ - public void setMaximized(boolean maximized) { - this.maximized = maximized; + public void setInFocusedMode(boolean inFocusedMode) { + this.inFocusedMode = inFocusedMode; } /** diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedAnchorPanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedAnchorPanel.java index 414b3011..df8ea0f9 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedAnchorPanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedAnchorPanel.java @@ -56,6 +56,8 @@ public class DockedAnchorPanel extends DockingPanel { public DockedAnchorPanel(DockingAPI docking, DockableWrapper anchor) { setLayout(new BorderLayout()); + ActiveDockableHighlighter.setNotSelectedBorder(this); + anchor.setParent(this); this.docking = docking; @@ -99,6 +101,7 @@ public void dock(Dockable dockable, DockingRegion region, double dividerProporti @Override public void undock(Dockable dockable) { dockingParent.removeChild(this); + anchor.setParent(null); } @Override diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSimplePanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSimplePanel.java index a31c4cab..ff42205d 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSimplePanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSimplePanel.java @@ -24,13 +24,13 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.Dockable; import io.github.andrewauclair.moderndocking.DockingRegion; import io.github.andrewauclair.moderndocking.api.DockingAPI; -import io.github.andrewauclair.moderndocking.settings.Settings; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Dimension; +import java.awt.Insets; import java.util.Collections; import java.util.List; import javax.swing.BorderFactory; -import javax.swing.JSplitPane; import javax.swing.UIManager; /** @@ -119,6 +119,15 @@ public DockableWrapper getWrapper() { return dockable; } + @Override + public Dimension getMinimumSize() { + Insets in = getInsets(); + Dimension d = dockable.getDisplayPanel().getMinimumSize(); + int w = Math.max(d.width, 50); + int h = Math.max(d.height, 50); + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + @Override public String getAnchor() { return anchor; @@ -152,39 +161,8 @@ else if (region == DockingRegion.CENTER) { dockedParent.replaceChild(this, tabbedPanel); } else { - DockedSplitPanel split = new DockedSplitPanel(docking, this.dockable.getWindow(), anchor); - dockedParent.replaceChild(this, split); - - DockingPanel newPanel; - - if (wrapper.isAnchor()) { - newPanel = new DockedAnchorPanel(docking, wrapper); - } - else if (Settings.alwaysDisplayTabsMode()) { - newPanel = new DockedTabbedPanel(docking, wrapper, anchor); - } - else { - newPanel = new DockedSimplePanel(docking, wrapper, anchor); - } - - if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) { - split.setLeft(this); - split.setRight(newPanel); - dividerProportion = 1.0 - dividerProportion; - } - else { - split.setLeft(newPanel); - split.setRight(this); - } - - if (region == DockingRegion.EAST || region == DockingRegion.WEST) { - split.setOrientation(JSplitPane.HORIZONTAL_SPLIT); - } - else { - split.setOrientation(JSplitPane.VERTICAL_SPLIT); - } - - split.setDividerLocation(dividerProportion); + DockingPanel newPanel = DockedSplitPanel.createLeafPanel(docking, wrapper, anchor); + DockedSplitPanel.dockPanelBeside(this, dockedParent, newPanel, region, dividerProportion, docking, this.dockable.getWindow(), anchor); } revalidate(); @@ -200,7 +178,9 @@ public void undock(Dockable dockable) { anchor = ""; - if (anchorWrapper == null || !DockingComponentUtils.isAnchorEmpty(docking, anchorWrapper.getDockable())) { + if (anchorWrapper == null + || !DockingComponentUtils.isAnchorEmpty(docking, anchorWrapper.getDockable()) + || DockingInternal.get(docking).isInFocusedModeTransition()) { dockedParent.removeChild(this); } else { diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSplitPanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSplitPanel.java index 4ef3f6aa..a4631b06 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSplitPanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedSplitPanel.java @@ -25,389 +25,765 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.DockingRegion; import io.github.andrewauclair.moderndocking.api.DockingAPI; import io.github.andrewauclair.moderndocking.settings.Settings; -import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Point; import java.awt.Window; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.swing.JSplitPane; -import javax.swing.plaf.basic.BasicSplitPaneDivider; -import javax.swing.plaf.basic.BasicSplitPaneUI; +import javax.swing.UIManager; /** - * DockingPanel that has a split pane with 2 dockables, split can be vertical or horizontal + * Flat N-ary split container that replaces nested JSplitPane instances. + * + *

Each instance owns N child panels (N >= 2) and N-1 divider bars laid out + * in a single top-down pass. Every divider position is stored as a fraction of + * this panel's own rect, so dragging one divider changes exactly one + * stored value and leaves all sibling dividers at their absolute pixel positions. + * + *

The old JSplitPane nesting problem is eliminated because there is no + * recursive proportion chain: every proportion is relative to the same rect. */ -public class DockedSplitPanel extends DockingPanel implements MouseListener, PropertyChangeListener { - /** - * Panel in the left/top part of the split - */ - private DockingPanel left = null; - /** - * Panel in the right/bottom part of the split - */ - private DockingPanel right = null; - - /** - * The split pane we're controlling - */ - private final JSplitPane splitPane = new JSplitPane(); - /** - * The parent panel of this split panel - */ - private DockingPanel dockedParent; - /** - * The docking instance this panel belongs to - */ - private final DockingAPI docking; - /** - * The window this panel is in - */ - private final Window window; - /** - * The anchor this panel belongs to, if any - */ - private String anchor = ""; - - /** - * the last divider proportion that setDividerLocation was called with - */ - private double lastRequestedDividerProportion; - - /** - * Create a new DockedSplitPanel - * - * @param docking The docking instance - * @param window The window this panel is in. Used to tell the child DockableWrappers what Window they are a part of - * @param anchor The anchor associated with this docking panel - */ - public DockedSplitPanel(DockingAPI docking, Window window, String anchor) { - this.docking = docking; - this.window = window; - this.anchor = anchor; - - setLayout(new BorderLayout()); - - splitPane.setContinuousLayout(true); - splitPane.setResizeWeight(0.5); - splitPane.setBorder(null); - splitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this); - - setDividerLocation(splitPane.getResizeWeight()); - - lastRequestedDividerProportion = splitPane.getResizeWeight(); - - if (splitPane.getUI() instanceof BasicSplitPaneUI) { - ((BasicSplitPaneUI) splitPane.getUI()).getDivider().addMouseListener(this); - } - - add(splitPane, BorderLayout.CENTER); - } - - /** - * Retrieve the last double that we used to set the divider proportion with. This helps us properly - * restore divider locations when restoring from a layout. - * - * @return Requested divider proportion - */ - public double getLastRequestedDividerProportion() { - return lastRequestedDividerProportion; - } - - /** - * Set the divider location of the splitpane. Tricks are needed to properly set the position. - * The position cannot be set until the JSplitPane is displayed on screen. - * - * @param proportion The new proportion of the splitpane - */ - public void setDividerLocation(final double proportion) { - lastRequestedDividerProportion = proportion; - - // calling setDividerLocation on a JSplitPane that isn't visible does nothing, so we need to check if it is showing first - if (splitPane.isShowing()) { - if (splitPane.getWidth() > 0 && splitPane.getHeight() > 0) { - splitPane.setDividerLocation(proportion); - } - else { - // split hasn't been completely calculated yet, wait until componentResize - splitPane.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - // remove this listener, it's a one off - splitPane.removeComponentListener(this); - // call the function again, this time it should actually set the divider location - setDividerLocation(proportion); - } - }); - } - } - else { - // split hasn't been shown yet, wait until it's showing - splitPane.addHierarchyListener(new HierarchyListener() { - @Override - public void hierarchyChanged(HierarchyEvent e) { - boolean isShowingChangeEvent = (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0; - - if (isShowingChangeEvent && splitPane.isShowing()) { - // remove this listener, it's a one off - splitPane.removeHierarchyListener(this); - // call the function again, this time it might set the size or wait for componentResize - setDividerLocation(proportion); - } - } - }); - } - } - - /** - * Used to set the real divider location, which is set as an int, not a double. - * - * @param location The new proportion of the splitpane - */ - public void setDividerLocation(final int location) { - if (splitPane.isShowing()) { - if ((splitPane.getWidth() > 0) && (splitPane.getHeight() > 0)) { - splitPane.setDividerLocation(location); - - docking.getAppState().persist(); - } - else { - // split hasn't been completely calculated yet, wait until componentResize - splitPane.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - // remove this listener, it's a one off - splitPane.removeComponentListener(this); - // call the function again, this time it should actually set the divider location - setDividerLocation(location); - } - }); - } - } - else { - // split hasn't been shown yet, wait until it's showing - splitPane.addHierarchyListener(new HierarchyListener() { - @Override - public void hierarchyChanged(HierarchyEvent e) { - boolean isShowingChangeEvent = (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0; - - if (isShowingChangeEvent && splitPane.isShowing()) { - // remove this listener, it's a one off - splitPane.removeHierarchyListener(this); - // call the function again, this time it might set the size or wait for componentResize - setDividerLocation(location); - } - } - }); - } - } - - /** - * Access to the underlying JSplitPane. This is required so that we can pull a bunch of values for saving layouts. - * - * @return The JSplitPane used by this DockedSplitPanel - */ - public JSplitPane getSplitPane() { - return splitPane; - } - - /** - * Get the left/top component of the split - * - * @return Left/top component in the split - */ - public DockingPanel getLeft() { - return left; - } - - /** - * Set the panel in the left/top of split - * - * @param panel New left/top panel - */ - public void setLeft(DockingPanel panel) { - left = panel; - left.setParent(this); -// left.setAnchor(anchor); - - // remember where the divider was and put it back - int dividerLocation = splitPane.getDividerLocation(); - - splitPane.setLeftComponent(panel); - - splitPane.setDividerLocation(dividerLocation); - } - - /** - * Get the right/bottom component of the split - * - * @return Right/bottom component in the split - */ - public DockingPanel getRight() { - return right; - } - - /** - * Set the right panel of the split - * - * @param panel New right panel - */ - public void setRight(DockingPanel panel) { - right = panel; - right.setParent(this); -// right.setAnchor(anchor); - - // remember where the divider was and put it back - int dividerLocation = splitPane.getDividerLocation(); - - splitPane.setRightComponent(panel); - - splitPane.setDividerLocation(dividerLocation); - } - - /** - * Set the orientation of the split pane - * - * @param orientation New orientation of the split pane - */ - public void setOrientation(int orientation) { - splitPane.setOrientation(orientation); - - if (splitPane.getUI() instanceof BasicSplitPaneUI) { - // grab the divider from the UI and remove the border from it - BasicSplitPaneDivider divider = ((BasicSplitPaneUI) splitPane.getUI()) - .getDivider(); - - if (divider != null && divider.getBorder() != null) { - divider.setBorder(null); - } - } - } - - @Override - public String getAnchor() { - return anchor; - } - - @Override - public void setAnchor(String anchor) { - this.anchor = anchor; - } - - @Override - public void setParent(DockingPanel parent) { - this.dockedParent = parent; - } - - @Override - public void dock(Dockable dockable, DockingRegion region, double dividerProportion) { - DockableWrapper wrapper = DockingInternal.get(docking).getWrapper(dockable); - - // docking to the center of a split isn't something we allow - // wouldn't be difficult to support, but isn't a complication we want in this framework - if (region == DockingRegion.CENTER) { - region = splitPane.getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? DockingRegion.WEST : DockingRegion.NORTH; - } - - wrapper.setWindow(window); - - DockedSplitPanel split = new DockedSplitPanel(docking, window, anchor); - dockedParent.replaceChild(this, split); - - DockingPanel newPanel; - - if (Settings.alwaysDisplayTabsMode()) { - newPanel = new DockedTabbedPanel(docking, wrapper, anchor); - } - else { - newPanel = new DockedSimplePanel(docking, wrapper, anchor); - } - - if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) { - split.setLeft(this); - split.setRight(newPanel); - dividerProportion = 1.0 - dividerProportion; - } - else { - split.setLeft(newPanel); - split.setRight(this); - } - - if (region == DockingRegion.EAST || region == DockingRegion.WEST) { - split.setOrientation(JSplitPane.HORIZONTAL_SPLIT); - } - else { - split.setOrientation(JSplitPane.VERTICAL_SPLIT); - } - - split.setDividerLocation(dividerProportion); - } - - @Override - public void undock(Dockable dockable) { - } - - @Override - public void replaceChild(DockingPanel child, DockingPanel newChild) { - if (left == child) { - setLeft(newChild); - } - else if (right == child) { - setRight(newChild); - } - } - - @Override - public void removeChild(DockingPanel child) { - // safety against partially configured layout restorations - if (dockedParent == null) { - return; - } - - if (left == child) { - dockedParent.replaceChild(this, right); - } - else if (right == child) { - dockedParent.replaceChild(this, left); - } - } - - public List getChildren() { - return Arrays.asList(left, right); - } - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() >= 2) { - setDividerLocation(splitPane.getResizeWeight()); - } - } - - @Override - public void mousePressed(MouseEvent e) { - } - - @Override - public void mouseReleased(MouseEvent e) { - docking.getAppState().persist(); - } - - @Override - public void mouseEntered(MouseEvent e) { - } - - @Override - public void mouseExited(MouseEvent e) { - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - docking.getAppState().persist(); - } +public class DockedSplitPanel extends DockingPanel { + + static final int DIVIDER_THICKNESS = 5; + + /** + * Ordered child panels. + */ + private final List children = new ArrayList<>(); + + /** + * dividerPositions[i] is the position of the divider between children[i] and + * children[i+1], as a fraction [0,1] of this panel's axis dimension. + * Used for persistence (save/restore). Length equals children.size() - 1. + */ + private final List dividerPositions = new ArrayList<>(); + + /** + * dividerPixels[i] is the absolute pixel position of divider[i] along the + * split axis, measured from the leading edge of this panel. + * A value of -1 means "not yet initialised; compute from dividerPositions on + * the next layout pass". Once set, this value is kept fixed across parent + * resizes so that only the last child absorbs the extra space. + */ + private final List dividerPixels = new ArrayList<>(); + + /** + * One DividerBar per divider, parallel to dividerPositions. + */ + private final List dividerBars = new ArrayList<>(); + + /** + * JSplitPane.HORIZONTAL_SPLIT or VERTICAL_SPLIT. + */ + private int orientation = JSplitPane.HORIZONTAL_SPLIT; + + private DockingPanel dockedParent; + private final DockingAPI docking; + private final Window window; + private String anchor; + + /** + * Guards against re-entrant layoutChildren calls triggered by validate(). + */ + private boolean inLayout = false; + + /** + * Set to true while a divider drag is in progress. During drag, layout uses + * absolute pixel positions so that only the two adjacent children resize and + * all other panels stay stationary. Outside of a drag (e.g. window resize), + * layout uses stored fractions so that all children resize proportionally. + */ + private static boolean isDragging = false; + + /** + * Create a new DockedSplitPanel. Children and orientation must be + * configured before the panel is displayed. + */ + public DockedSplitPanel(DockingAPI docking, Window window, String anchor) { + this.docking = docking; + this.window = window; + this.anchor = anchor; + setLayout(null); + } + + @Override + public void doLayout() { + layoutChildren(); + } + + @Override + public void setBounds(int x, int y, int width, int height) { + super.setBounds(x, y, width, height); + layoutChildren(); + } + + @Override + public Dimension getMinimumSize() { + boolean horiz = (orientation == JSplitPane.HORIZONTAL_SPLIT); + // Split axis: sum of children + dividers between them. + // Perpendicular axis: max of children (they all share the same cross-dimension). + int axisMin = DIVIDER_THICKNESS * Math.max(0, children.size() - 1); + int perpMin = 0; + + for (DockingPanel child : children) { + if (child == null) { + continue; + } + Dimension min = child.getMinimumSize(); + axisMin += horiz ? min.width : min.height; + perpMin = Math.max(perpMin, horiz ? min.height : min.width); + } + return horiz ? new Dimension(axisMin, perpMin) : new Dimension(perpMin, axisMin); + } + + /** + * Minimum pixels needed for children[0..divIndex] plus the dividers between them. + */ + private int minPxBefore(int divIndex) { + boolean horiz = (orientation == JSplitPane.HORIZONTAL_SPLIT); + int px = 0; + + for (int i = 0; i <= divIndex; i++) { + Dimension min = children.get(i).getMinimumSize(); + px += horiz ? min.width : min.height; + + if (i < divIndex) { + px += DIVIDER_THICKNESS; + } + } + return px; + } + + /** + * Minimum pixels needed for children[divIndex+1..n-1] plus the dividers between them. + */ + private int minPxAfter(int divIndex) { + boolean horiz = (orientation == JSplitPane.HORIZONTAL_SPLIT); + int px = 0; + + for (int i = divIndex + 1; i < children.size(); i++) { + Dimension min = children.get(i).getMinimumSize(); + px += horiz ? min.width : min.height; + + if (i < children.size() - 1) { + px += DIVIDER_THICKNESS; + } + } + return px; + } + + // ---------------------------------------------------------------- + // Child management (called during construction and dock operations) + // ---------------------------------------------------------------- + + /** + * Set the left/top child (index 0). + */ + public void setLeft(DockingPanel panel) { + setChildAt(0, panel); + } + + /** + * Set the right/bottom child (index 1). + */ + public void setRight(DockingPanel panel) { + setChildAt(1, panel); + } + + private void setChildAt(int index, DockingPanel panel) { + if (index < children.size()) { + remove(children.get(index)); + children.set(index, panel); + } + else { + children.add(panel); + } + panel.setParent(this); + add(panel); + rebuildDividerBars(); + } + + /** + * Insert {@code panel} beside {@code existingChild}. + * + * @param existingChild the child to split beside + * @param panel the new child to insert + * @param after true = new child goes after (EAST/SOUTH), false = before (WEST/NORTH) + * @param dividerPos fraction of this panel's axis where the new divider sits + */ + public void insertChildBeside(DockingPanel existingChild, DockingPanel panel, + boolean after, double dividerPos) { + int idx = children.indexOf(existingChild); + if (idx < 0) return; + + if (after) { + children.add(idx + 1, panel); + } + else { + children.add(idx, panel); + } + // In both cases the new divider sits at the original index of existingChild. + dividerPositions.add(idx, dividerPos); + dividerPixels.add(idx, -1); + + panel.setParent(this); + add(panel); + rebuildDividerBars(); + layoutChildren(); + } + + /** + * Append a child without creating dividers — used only during layout + * restoration where divider positions are applied afterwards via + * {@link #setDividerPositions(double[])}. + */ + public void addChildForRestore(DockingPanel child) { + child.setParent(this); + children.add(child); + add(child); + } + + public DockingPanel getLeft() { + return children.isEmpty() ? null : children.get(0); + } + + public DockingPanel getRight() { + return children.size() < 2 ? null : children.get(1); + } + + public int getOrientation() { + return orientation; + } + + public void setOrientation(int orientation) { + this.orientation = orientation; + for (DividerBar bar : dividerBars) { + bar.updateCursor(); + } + } + + /** + * Return the index of {@code child} within this panel, or -1. + */ + public int indexOfChild(DockingPanel child) { + return children.indexOf(child); + } + + // ---------------------------------------------------------------- + // Divider position API + // ---------------------------------------------------------------- + + /** + * Set the single divider proportion for a freshly constructed 2-child split. + * Also used by legacy restore code that still calls this method. + */ + public void setDividerLocation(double proportion) { + if (dividerPositions.isEmpty()) { + dividerPositions.add(proportion); + dividerPixels.add(-1); + } + else { + dividerPositions.set(0, proportion); + dividerPixels.set(0, -1); + } + rebuildDividerBarsIfNeeded(); + layoutChildren(); + } + + /** + * Snapshot of all divider positions as a double array — used when + * persisting layouts. + */ + public double[] getDividerPositions() { + double[] out = new double[dividerPositions.size()]; + + for (int i = 0; i < out.length; i++) { + out[i] = dividerPositions.get(i); + } + return out; + } + + /** + * Replace all divider positions at once — used when restoring N-ary + * layouts from disk. + */ + public void setDividerPositions(double[] positions) { + dividerPositions.clear(); + dividerPixels.clear(); + for (double p : positions) { + dividerPositions.add(p); + dividerPixels.add(-1); + } + rebuildDividerBars(); + layoutChildren(); + } + + // ---------------------------------------------------------------- + // Divider bar management + // ---------------------------------------------------------------- + + private void rebuildDividerBars() { + for (DividerBar bar : dividerBars) remove(bar); + dividerBars.clear(); + int count = Math.max(0, children.size() - 1); + for (int i = 0; i < count; i++) { + DividerBar bar = new DividerBar(i); + dividerBars.add(bar); + add(bar); + } + } + + private void rebuildDividerBarsIfNeeded() { + if (dividerBars.size() != Math.max(0, children.size() - 1)) { + rebuildDividerBars(); + } + } + + // ---------------------------------------------------------------- + // Layout pass + // ---------------------------------------------------------------- + + void layoutChildren() { + if (inLayout) { + return; + } + inLayout = true; + try { + layoutChildrenImpl(); + } + finally { + inLayout = false; + } + } + + private void layoutChildrenImpl() { + if (children.isEmpty()) { + return; + } + boolean horiz = (orientation == JSplitPane.HORIZONTAL_SPLIT); + int total = horiz ? getWidth() : getHeight(); + + if (total <= 0) { + return; + } + int prev = 0; + + for (int i = 0; i < children.size(); i++) { + DockingPanel child = children.get(i); + + if (child == null) { + continue; + } + + if (i < dividerPositions.size()) { + // During a drag: use stored pixels so that only the two children adjacent + // to the dragged divider resize; all others stay stationary. + // Outside a drag (window resize, restore, etc.): use fractions so that + // all children resize proportionally. + int px; + + if (isDragging) { + px = dividerPixels.get(i); + + if (px < 0) { + px = (int) Math.round(dividerPositions.get(i) * total); + } + } + else { + px = (int) Math.round(dividerPositions.get(i) * total); + } + // Lower bound: leave at least children[i]'s minimum space before the divider. + // Upper bound: leave enough space for all children after the divider. + Dimension minI = children.get(i).getMinimumSize(); + int lo = prev + (horiz ? minI.width : minI.height); + int divStart = clamp(px, lo, total - DIVIDER_THICKNESS - minPxAfter(i)); + // Update pixel cache so drag has an accurate starting point. + // Fractions are only updated by onDrag / syncFractionsFromPixels. + dividerPixels.set(i, divStart); + + int childSize = divStart - prev; + if (horiz) { + child.setBounds(prev, 0, Math.max(0, childSize), getHeight()); + + if (i < dividerBars.size()) { + dividerBars.get(i).setBounds(divStart, 0, DIVIDER_THICKNESS, getHeight()); + } + } + else { + child.setBounds(0, prev, getWidth(), Math.max(0, childSize)); + + if (i < dividerBars.size()) { + dividerBars.get(i).setBounds(0, divStart, getWidth(), DIVIDER_THICKNESS); + } + } + prev = divStart + DIVIDER_THICKNESS; + } + else { + int childSize = Math.max(0, total - prev); + + if (horiz) { + child.setBounds(prev, 0, childSize, getHeight()); + } + else { + child.setBounds(0, prev, getWidth(), childSize); + } + } + } + if (!docking.getAppState().isPaused()) { + // validate() is synchronous: it calls doLayout() on this (guarded by inLayout + // to prevent re-entry) then recursively validates all children, running their + // layout managers immediately rather than deferring to the EDT queue. + validate(); + repaint(); + } + } + + /** + * Walk this split and all descendant splits, updating every stored fraction + * to match the current pixel position relative to the current panel size. + * Called after a drag ends so that subsequent window resizes proportion from + * the new post-drag layout rather than from stale pre-drag fractions. + */ + private void syncFractionsFromPixels() { + boolean horiz = (orientation == JSplitPane.HORIZONTAL_SPLIT); + int total = horiz ? getWidth() : getHeight(); + + if (total > 0) { + for (int i = 0; i < dividerPixels.size() && i < dividerPositions.size(); i++) { + int px = dividerPixels.get(i); + + if (px >= 0) { + dividerPositions.set(i, (double) px / total); + } + } + } + + for (DockingPanel child : children) { + if (child instanceof DockedSplitPanel) { + ((DockedSplitPanel) child).syncFractionsFromPixels(); + } + } + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } + + // ---------------------------------------------------------------- + // DockingPanel interface + // ---------------------------------------------------------------- + + @Override + public String getAnchor() { + return anchor; + } + + @Override + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + @Override + public void setParent(DockingPanel parent) { + this.dockedParent = parent; + } + + @Override + public void dock(Dockable dockable, DockingRegion region, double dividerProportion) { + DockableWrapper wrapper = DockingInternal.get(docking).getWrapper(dockable); + wrapper.setWindow(window); + + // CENTER has no meaning for a split — redirect to the nearer edge + if (region == DockingRegion.CENTER) { + region = (orientation == JSplitPane.HORIZONTAL_SPLIT) ? DockingRegion.WEST : DockingRegion.NORTH; + } + + boolean after = (region == DockingRegion.EAST || region == DockingRegion.SOUTH); + int newOrientation = orientationForRegion(region); + DockingPanel newPanel = createLeafPanel(docking, wrapper, anchor); + + if (newOrientation == orientation) { + // Same axis: append to the near end of this split. + double newDivPos = appendDividerPosition(after, dividerProportion); + insertChildBeside(children.get(after ? children.size() - 1 : 0), newPanel, after, newDivPos); + } + else { + // Different axis: wrap this split in a new outer split. + dockPanelBeside(this, dockedParent, newPanel, region, dividerProportion, docking, window, anchor); + } + } + + /** + * Computes the position for a new divider appended to the near end of this split. + * When docking after (EAST/SOUTH) the new divider sits inside the space after the + * last existing divider. When docking before (NORTH/WEST) it sits inside the space + * before the first existing divider. + */ + private double appendDividerPosition(boolean after, double proportion) { + if (after) { + double lastEnd = dividerPositions.isEmpty() ? 0.0 : dividerPositions.get(dividerPositions.size() - 1); + return lastEnd + (1.0 - lastEnd) * (1.0 - proportion); + } + double firstEnd = dividerPositions.isEmpty() ? 1.0 : dividerPositions.get(0); + return firstEnd * proportion; + } + + @Override + public void undock(Dockable dockable) { + // Undocking is handled by the leaf panels; this split panel is only a container. + } + + @Override + public void replaceChild(DockingPanel child, DockingPanel newChild) { + int idx = children.indexOf(child); + + if (idx >= 0) { + remove(child); + children.set(idx, newChild); + newChild.setParent(this); + add(newChild); + layoutChildren(); + } + } + + @Override + public void removeChild(DockingPanel child) { + if (dockedParent == null) { + return; + } + + int idx = children.indexOf(child); + + if (idx < 0) { + return; + } + + remove(child); + + // Remove the divider that was adjacent to this child. + // Prefer the divider to the right (index idx); fall back to the left (idx-1). + int divIdx = (idx < dividerPositions.size()) ? idx : idx - 1; + if (divIdx >= 0 && divIdx < dividerBars.size()) { + remove(dividerBars.get(divIdx)); + } + children.remove(idx); + if (divIdx >= 0 && divIdx < dividerPositions.size()) { + dividerPositions.remove(divIdx); + dividerPixels.remove(divIdx); + } + + if (children.size() == 1) { + // Only one child left — collapse this split by promoting the survivor. + dockedParent.replaceChild(this, children.get(0)); + } + else { + rebuildDividerBars(); + layoutChildren(); + } + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(children); + } + + // ---------------------------------------------------------------- + // DividerBar + // ---------------------------------------------------------------- + + final class DividerBar extends javax.swing.JComponent { + + private final int index; + private int dragStartScreen; + private int dragStartPixel; + + DividerBar(int index) { + this.index = index; + Color dividerColor = UIManager.getColor("SplitPane.dividerColor"); + setBackground(dividerColor != null ? dividerColor : new Color(0xaaaaaa)); + setOpaque(true); + updateCursor(); + + MouseAdapter ma = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + isDragging = true; + Point p = e.getLocationOnScreen(); + dragStartScreen = isHoriz() ? p.x : p.y; + int px = dividerPixels.get(index); + + if (px < 0) { + int total = isHoriz() ? DockedSplitPanel.this.getWidth() : DockedSplitPanel.this.getHeight(); + px = (int) Math.round(dividerPositions.get(index) * total); + dividerPixels.set(index, px); + } + dragStartPixel = px; + } + + @Override + public void mouseDragged(MouseEvent e) { + onDrag(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + isDragging = false; + // Sync all fractions from current pixel positions so that the next + // window resize proportions from the new post-drag layout. + syncFractionsFromPixels(); + docking.getAppState().persist(); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() >= 2) { + // Double-click: reset to equal split + int total = isHoriz() ? DockedSplitPanel.this.getWidth() : DockedSplitPanel.this.getHeight(); + int n = children.size(); + int equalPx = (int) Math.round((double) (index + 1) / n * total); + dividerPixels.set(index, equalPx); + dividerPositions.set(index, (double) equalPx / total); + layoutChildren(); + docking.getAppState().persist(); + } + } + }; + addMouseListener(ma); + addMouseMotionListener(ma); + } + + void updateCursor() { + setCursor(Cursor.getPredefinedCursor( + isHoriz() ? Cursor.E_RESIZE_CURSOR : Cursor.N_RESIZE_CURSOR)); + } + + private boolean isHoriz() { + return orientation == JSplitPane.HORIZONTAL_SPLIT; + } + + private void onDrag(MouseEvent e) { + Point p = e.getLocationOnScreen(); + int current = isHoriz() ? p.x : p.y; + int delta = current - dragStartScreen; + int total = isHoriz() ? DockedSplitPanel.this.getWidth() : DockedSplitPanel.this.getHeight(); + if (total <= 0) { + return; + } + + int newPx = dragStartPixel + delta; + + // Clamp to the range that keeps both adjacent children above their minimum + // size. Neighbouring dividers' pixel positions (unchanged by this drag) + // define the outer boundaries; only child[index] and child[index+1] resize. + int prevEdge = (index > 0) + ? (isHoriz() ? dividerBars.get(index - 1).getX() : dividerBars.get(index - 1).getY()) + DIVIDER_THICKNESS + : 0; + Dimension minI = children.get(index).getMinimumSize(); + int lo = prevEdge + (isHoriz() ? minI.width : minI.height); + + int nextEdge = (index < dividerBars.size() - 1) + ? (isHoriz() ? dividerBars.get(index + 1).getX() : dividerBars.get(index + 1).getY()) + : total; + Dimension minNext = children.get(index + 1).getMinimumSize(); + int hi = nextEdge - DIVIDER_THICKNESS - (isHoriz() ? minNext.width : minNext.height); + + int clampedPx = Math.max(lo, Math.min(hi, newPx)); + dividerPixels.set(index, clampedPx); + dividerPositions.set(index, (double) clampedPx / total); + layoutChildren(); + } + } + + // ---------------------------------------------------------------- + // Helpers shared with DockedSimplePanel / DockedTabbedPanel + // ---------------------------------------------------------------- + + /** + * Creates the appropriate leaf panel for {@code wrapper}: anchor, tabbed, or simple. + */ + static DockingPanel createLeafPanel(DockingAPI docking, DockableWrapper wrapper, String anchor) { + if (wrapper.isAnchor()) { + return new DockedAnchorPanel(docking, wrapper); + } + if (Settings.alwaysDisplayTabsMode()) { + return new DockedTabbedPanel(docking, wrapper, anchor); + } + return new DockedSimplePanel(docking, wrapper, anchor); + } + + /** + * Returns the JSplitPane orientation constant that corresponds to {@code region}. + */ + static int orientationForRegion(DockingRegion region) { + if (region == DockingRegion.EAST || region == DockingRegion.WEST) { + return JSplitPane.HORIZONTAL_SPLIT; + } + return JSplitPane.VERTICAL_SPLIT; + } + + /** + * Docks {@code newPanel} beside {@code thisPanel} in the direction indicated by + * {@code region}. If the parent split already runs along the same axis the new + * panel is inserted inline (no extra nesting). Otherwise a new 2-child split is + * created to wrap {@code thisPanel}. + */ + static void dockPanelBeside(DockingPanel thisPanel, DockingPanel parentPanel, + DockingPanel newPanel, DockingRegion region, + double proportion, DockingAPI docking, + Window window, String anchor) { + boolean after = (region == DockingRegion.EAST || region == DockingRegion.SOUTH); + int newOrientation = orientationForRegion(region); + + if (parentPanel instanceof DockedSplitPanel) { + DockedSplitPanel parentSplit = (DockedSplitPanel) parentPanel; + + if (parentSplit.getOrientation() == newOrientation) { + // Insert inline — avoids nesting two splits of the same axis. + double newDivPos = inlineDividerPosition(parentSplit, thisPanel, after, proportion); + parentSplit.insertChildBeside(thisPanel, newPanel, after, newDivPos); + return; + } + } + + // Wrap thisPanel in a new 2-child split. + DockedSplitPanel split = new DockedSplitPanel(docking, window, anchor); + split.setOrientation(newOrientation); + parentPanel.replaceChild(thisPanel, split); + + if (after) { + split.setLeft(thisPanel); + split.setRight(newPanel); + split.setDividerLocation(1.0 - proportion); + } + else { + split.setLeft(newPanel); + split.setRight(thisPanel); + split.setDividerLocation(proportion); + } + } + + /** + * Computes the fraction position for a new divider when inserting inline into + * {@code parentSplit} beside {@code child}. + */ + private static double inlineDividerPosition(DockedSplitPanel parentSplit, DockingPanel child, + boolean after, double proportion) { + int myIndex = parentSplit.indexOfChild(child); + double[] positions = parentSplit.getDividerPositions(); + double childStart = (myIndex > 0) ? positions[myIndex - 1] : 0.0; + double childEnd = (myIndex < positions.length) ? positions[myIndex] : 1.0; + double childSpace = childEnd - childStart; + + if (after) { + return childStart + childSpace * (1.0 - proportion); + } + return childStart + childSpace * proportion; + } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedTabbedPanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedTabbedPanel.java index 92d575dc..0f794075 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedTabbedPanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockedTabbedPanel.java @@ -33,8 +33,10 @@ of this software and associated documentation files (the "Software"), to deal import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.dnd.DragGestureListener; @@ -51,7 +53,6 @@ of this software and associated documentation files (the "Software"), to deal import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; @@ -119,10 +120,10 @@ public DockedTabbedPanel(DockingAPI docking, DockableWrapper dockable, String an boolean usingFlatLaf = tabs.getUI().getClass().getPackageName().startsWith("com.formdev.flatlaf"); if (Settings.alwaysDisplayTabsMode() && usingFlatLaf) { - tabs.setTabPlacement(JTabbedPane.TOP); + tabs.setTabPlacement(SwingConstants.TOP); } else { - tabs.setTabPlacement(JTabbedPane.BOTTOM); + tabs.setTabPlacement(SwingConstants.BOTTOM); } tabs.setTabLayoutPolicy(Settings.getTabLayoutPolicy()); @@ -217,6 +218,36 @@ public void removeNotify() { super.removeNotify(); } + @Override + public Dimension getMinimumSize() { + Insets in = getInsets(); + int w = 0, h = 0; + + for (DockableWrapper wrapper : panels) { + Dimension dm = wrapper.getDisplayPanel().getMinimumSize(); + w = Math.max(w, dm.width); + h = Math.max(h, dm.height); + } + + // Add room for the tab strip itself + boolean topOrBottom = (tabs.getTabPlacement() == SwingConstants.TOP || tabs.getTabPlacement() == SwingConstants.BOTTOM); + Dimension tabsMin = tabs.getMinimumSize(); + + if (topOrBottom) { + w = Math.max(w, tabsMin.width); + h += tabsMin.height; + } + else { + w += tabsMin.width; + h = Math.max(h, tabsMin.height); + } + + // Floor at a practical minimum so panels with scroll-pane content (min=0) can't be squished to nothing + w = Math.max(w, 50); + h = Math.max(h, 50); + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + /** * Add a new panel to this tabbed panel. * @@ -340,39 +371,8 @@ public void dock(Dockable dockable, DockingRegion region, double dividerProporti addPanel(wrapper); } else { - DockedSplitPanel split = new DockedSplitPanel(docking, panels.get(0).getWindow(), anchor); - dockedParent.replaceChild(this, split); - - DockingPanel newPanel; - - if (wrapper.isAnchor()) { - newPanel = new DockedAnchorPanel(docking, wrapper); - } - else if (Settings.alwaysDisplayTabsMode()) { - newPanel = new DockedTabbedPanel(docking, wrapper, anchor); - } - else { - newPanel = new DockedSimplePanel(docking, wrapper, anchor); - } - - if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) { - split.setLeft(this); - split.setRight(newPanel); - dividerProportion = 1.0 - dividerProportion; - } - else { - split.setLeft(newPanel); - split.setRight(this); - } - - if (region == DockingRegion.EAST || region == DockingRegion.WEST) { - split.setOrientation(JSplitPane.HORIZONTAL_SPLIT); - } - else { - split.setOrientation(JSplitPane.VERTICAL_SPLIT); - } - - split.setDividerLocation(dividerProportion); + DockingPanel newPanel = DockedSplitPanel.createLeafPanel(docking, wrapper, anchor); + DockedSplitPanel.dockPanelBeside(this, dockedParent, newPanel, region, dividerProportion, docking, panels.get(0).getWindow(), anchor); } revalidate(); @@ -414,7 +414,7 @@ public void undock(Dockable dockable) { if (remaining.isHidden()) { remaining.setHidden(false); - DockingListeners.fireShownEvent(remaining.getDockable()); + docking.getDockingListeners().fireShownEvent(remaining.getDockable()); } dockedParent.replaceChild(this, new DockedSimplePanel(docking, remaining, anchor)); @@ -476,11 +476,11 @@ public void bringToFront(Dockable dockable) { if (tabs.getSelectedIndex() != -1) { DockableWrapper wrapper = panels.get(tabs.getSelectedIndex()); wrapper.setHidden(true); - DockingListeners.fireHiddenEvent(wrapper.getDockable()); + docking.getDockingListeners().fireHiddenEvent(wrapper.getDockable()); } DockableWrapper wrapper = panels.get(i); wrapper.setHidden(false); - DockingListeners.fireShownEvent(wrapper.getDockable()); + docking.getDockingListeners().fireShownEvent(wrapper.getDockable()); } tabs.setSelectedIndex(i); } @@ -514,12 +514,12 @@ public void stateChanged(ChangeEvent e) { if (previousSelectedIndex != -1 && previousSelectedIndex < panels.size() && previousSelectedIndex != selectedTab) { DockableWrapper previous = panels.get(previousSelectedIndex); previous.setHidden(true); - DockingListeners.fireHiddenEvent(previous.getDockable()); + docking.getDockingListeners().fireHiddenEvent(previous.getDockable()); } DockableWrapper panel = panels.get(selectedTab); panel.setHidden(false); - DockingListeners.fireShownEvent(panel.getDockable()); + docking.getDockingListeners().fireShownEvent(panel.getDockable()); } previousSelectedIndex = selectedTab; diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingComponentUtils.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingComponentUtils.java index dfc590c7..51986ba1 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingComponentUtils.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingComponentUtils.java @@ -44,8 +44,21 @@ public class DockingComponentUtils { private DockingComponentUtils() { } - // - // + /** + * Recomputes and sets the minimum size of {@code window} by delegating to the + * {@link InternalRootDockingPanel} for that window. This bypasses the + * intermediate GridBagLayout / RootPaneLayout chain and computes directly from + * the docking panel hierarchy. + */ + public static void updateWindowMinimumSize(DockingAPI docking, Window window) { + try { + rootForWindow(docking, window).updateWindowMinimumSize(); + } + catch (RootDockingPanelNotFoundException ignored) { + // Window has no root panel yet — nothing to update. + } + } + /** * used to clear all anchors before we undock components. This is done to prevent the anchor from being readded * @@ -297,7 +310,7 @@ public static void removeIllegalFloats(DockingAPI docking, Window window) { if (docking.canDisposeWindow(window) && root != null) { if (shouldUndock(root)) { - undockIllegalFloats(root); + undockIllegalFloats(docking, root); } } @@ -323,7 +336,7 @@ else if (component instanceof Container) { return true; } - private static void undockIllegalFloats(Container container) { + private static void undockIllegalFloats(DockingAPI docking, Container container) { for (Component component : container.getComponents()) { if (component instanceof DisplayPanel) { DisplayPanel panel = (DisplayPanel) component; @@ -332,10 +345,10 @@ private static void undockIllegalFloats(Container container) { Dockable dockable = wrapper.getDockable(); wrapper.getParent().undock(dockable); - DockingListeners.fireUndockedEvent(dockable, false); + docking.getDockingListeners().fireUndockedEvent(dockable, false); } else if (component instanceof Container) { - undockIllegalFloats((Container) component); + undockIllegalFloats(docking, (Container) component); } } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingInternal.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingInternal.java index 4ff4fadc..76fb7eb7 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingInternal.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingInternal.java @@ -65,6 +65,7 @@ public class DockingInternal { private final AppStatePersister appStatePersister; private boolean deregistering = false; + private boolean inFocusedModeTransition = false; /** * Create a new instance of our internal helper for the docking instance @@ -321,8 +322,11 @@ public void undock(Dockable dockable, boolean isTemporary) { Window window = DockingComponentUtils.findWindowForDockable(docking, dockable); - // TODO something about DockingStateAPI.restoreAnchor is causing a null here - Objects.requireNonNull(window); + // if this dockable is being closed while in focused mode, exit focused mode first + // to restore the previous state and allow another dockable to enter focused mode + if (!inFocusedModeTransition && docking.inFocusedMode(dockable)) { + docking.exitFocusedMode(dockable); + } InternalRootDockingPanel root = DockingComponentUtils.rootForWindow(docking, window); @@ -344,18 +348,21 @@ public void undock(Dockable dockable, boolean isTemporary) { } wrapper.setWindow(null); - DockingListeners.fireUndockedEvent(dockable, isTemporary); + docking.getDockingListeners().fireUndockedEvent(dockable, isTemporary); // make sure that can dispose this window, and we're not floating the last dockable in it if (docking.canDisposeWindow(window) && root.isEmpty() && !Floating.isFloating()) { deregisterDockingPanel(window); window.dispose(); } + else { + DockingComponentUtils.updateWindowMinimumSize(docking, window); + } docking.getAppState().persist(); - // force this dockable to dock again if we're not floating it - if (!dockable.isClosable() && !Floating.isFloating() && !isDeregistering(docking)) { + // force this dockable to dock again if we're not floating it and not in a focused mode transition + if (!dockable.isClosable() && !Floating.isFloating() && !isDeregistering(docking) && !inFocusedModeTransition) { docking.dock(dockable, docking.getMainWindow(), DockingRegion.SOUTH); } } @@ -367,7 +374,7 @@ public void fireDockedEventForFrame(Window window) { .collect(Collectors.toList()); for (DockableWrapper wrapper : wrappers) { - DockingListeners.fireDockedEvent(wrapper.getDockable()); + docking.getDockingListeners().fireDockedEvent(wrapper.getDockable()); } } @@ -379,7 +386,7 @@ public void fireDockedEventForFrame(Window window) { public static void fireDockedEventForAll(DockingAPI docking) { for (Dockable dockable : DockingInternal.get(docking).getDockables()) { if (docking.isDocked(dockable)) { - DockingListeners.fireDockedEvent(dockable); + docking.getDockingListeners().fireDockedEvent(dockable); } } } @@ -485,4 +492,12 @@ public static boolean isDeregistering(DockingAPI docking) { public void setDeregistering(boolean deregistering) { this.deregistering = deregistering; } + + public boolean isInFocusedModeTransition() { + return inFocusedModeTransition; + } + + public void setInFocusedModeTransition(boolean inFocusedModeTransition) { + this.inFocusedModeTransition = inFocusedModeTransition; + } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingListeners.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingListeners.java index 96168c12..778f2127 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingListeners.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/DockingListeners.java @@ -32,177 +32,212 @@ of this software and associated documentation files (the "Software"), to deal import javax.swing.JFrame; /** - * Manager class for docking and maximize listeners + * Per-instance listener manager. Each DockingAPI instance owns one of these, + * so events fired by one instance are never delivered to listeners registered + * with a different instance. */ public class DockingListeners { - private static final List maximizeListeners = new ArrayList<>(); - private static final List dockingListeners = new ArrayList<>(); - private static final List newFloatingFrameListeners = new ArrayList<>(); + private final List maximizeListeners = new ArrayList<>(); + private final List dockingListeners = new ArrayList<>(); + private final List newFloatingFrameListeners = new ArrayList<>(); /** - * Unused. All methods are static - */ - private DockingListeners() { - } - - /** - * Add a new maximize listener. Will be called when a dockable is maximized + * Add a new maximize listener. * * @param listener Listener to add + * @deprecated Use {@link #addDockingListener(DockingListener)} and handle + * {@link DockingEvent.ID#FOCUSED_MODE_ENTERED} / {@link DockingEvent.ID#FOCUSED_MODE_EXITED} instead. + * Will be removed in 2.0. */ - public static void addMaximizeListener(MaximizeListener listener) { + @Deprecated(since = "1.5.0", forRemoval = true) + public void addMaximizeListener(MaximizeListener listener) { if (!maximizeListeners.contains(listener)) { maximizeListeners.add(listener); } } /** - * Remove a previously added maximize listener. No-op if the listener isn't in the list + * Remove a previously added maximize listener. * * @param listener Listener to remove + * @deprecated Will be removed in 2.0. */ - public static void removeMaximizeListener(MaximizeListener listener) { + @Deprecated(since = "1.5.0", forRemoval = true) + public void removeMaximizeListener(MaximizeListener listener) { maximizeListeners.remove(listener); } /** - * Fire a new maximize event + * Fire a focused mode entered event. Also notifies deprecated {@link MaximizeListener} registrations. * - * @param dockable Dockable that has changed - * @param maximized New maximized state + * @param dockable Dockable that entered focused mode + */ + public void fireFocusedModeEnteredEvent(Dockable dockable) { + fireDockingEvent(new DockingEvent(DockingEvent.ID.FOCUSED_MODE_ENTERED, dockable)); + + // fire legacy maximize event + List listeners = new ArrayList<>(maximizeListeners); + listeners.forEach(listener -> listener.maximized(dockable, true)); + } + + /** + * Fire a focused mode exited event. Also notifies deprecated {@link MaximizeListener} registrations. + * + * @param dockable Dockable that exited focused mode */ - public static void fireMaximizeEvent(Dockable dockable, boolean maximized) { + public void fireFocusedModeExitedEvent(Dockable dockable) { + fireDockingEvent(new DockingEvent(DockingEvent.ID.FOCUSED_MODE_EXITED, dockable)); + + // fire legacy minimize event List listeners = new ArrayList<>(maximizeListeners); - listeners.forEach(listener -> listener.maximized(dockable, maximized)); + listeners.forEach(listener -> listener.maximized(dockable, false)); + } + + /** + * Fire a maximize event. + * + * @param dockable Dockable that has changed + * @param maximized New maximized state + * @deprecated Use {@link #fireFocusedModeEnteredEvent(Dockable)} / {@link #fireFocusedModeExitedEvent(Dockable)}. + * Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public void fireMaximizeEvent(Dockable dockable, boolean maximized) { + if (maximized) { + fireFocusedModeEnteredEvent(dockable); + } else { + fireFocusedModeExitedEvent(dockable); + } } /** - * Add a new docking listener + * Add a new docking listener. * * @param listener Listener to add */ - public static void addDockingListener(DockingListener listener) { + public void addDockingListener(DockingListener listener) { if (!dockingListeners.contains(listener)) { dockingListeners.add(listener); } } /** - * Remove a docking listener + * Remove a docking listener. * * @param listener Listener to remove */ - public static void removeDockingListener(DockingListener listener) { + public void removeDockingListener(DockingListener listener) { dockingListeners.remove(listener); } /** - * Add a new floating frame listener + * Add a new floating frame listener. * * @param listener Listener to add */ - public static void addNewFloatingFrameListener(NewFloatingFrameListener listener) { + public void addNewFloatingFrameListener(NewFloatingFrameListener listener) { newFloatingFrameListeners.add(listener); } /** - * Remove a floating frame listener + * Remove a floating frame listener. * * @param listener Listener to remove */ - public static void removeNewFloatingFrameListener(NewFloatingFrameListener listener) { + public void removeNewFloatingFrameListener(NewFloatingFrameListener listener) { newFloatingFrameListeners.remove(listener); } /** - * Fire a new floating frame event + * Fire a new floating frame event. * * @param frame The frame that was created - * @param root The root of the frame + * @param root The root of the frame */ - public static void fireNewFloatingFrameEvent(JFrame frame, RootDockingPanelAPI root) { + public void fireNewFloatingFrameEvent(JFrame frame, RootDockingPanelAPI root) { List listeners = new ArrayList<>(newFloatingFrameListeners); listeners.forEach(listener -> listener.newFrameCreated(frame, root)); } /** - * Fire a new floating frame event + * Fire a new floating frame event. * - * @param frame The frame that was created - * @param root The root of the frame + * @param frame The frame that was created + * @param root The root of the frame * @param dockable The dockable in the frame */ - public static void fireNewFloatingFrameEvent(JFrame frame, RootDockingPanelAPI root, Dockable dockable) { + public void fireNewFloatingFrameEvent(JFrame frame, RootDockingPanelAPI root, Dockable dockable) { List listeners = new ArrayList<>(newFloatingFrameListeners); listeners.forEach(listener -> listener.newFrameCreated(frame, root, dockable)); } /** - * Fire a new docked event + * Fire a docked event. * * @param dockable Dockable that was docked */ - public static void fireDockedEvent(Dockable dockable) { + public void fireDockedEvent(Dockable dockable) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.DOCKED, dockable, false))); } /** - * Fire a new undocked event + * Fire an undocked event. * - * @param dockable Dockable that was undocked + * @param dockable Dockable that was undocked + * @param isTemporary Whether the undock is temporary */ - public static void fireUndockedEvent(Dockable dockable, boolean isTemporary) { + public void fireUndockedEvent(Dockable dockable, boolean isTemporary) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.UNDOCKED, dockable, isTemporary))); } /** - * Fire a new auto hide enabled event + * Fire an auto-hide enabled event. * - * @param dockable Dockable that was auto hide enabled + * @param dockable Dockable that was auto-hide enabled */ - public static void fireAutoShownEvent(Dockable dockable) { + public void fireAutoShownEvent(Dockable dockable) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.AUTO_HIDE_ENABLED, dockable, false))); } /** - * Fire a new auto hide disabled event + * Fire an auto-hide disabled event. * - * @param dockable Dockable that was auto hide disabled + * @param dockable Dockable that was auto-hide disabled */ - public static void fireAutoHiddenEvent(Dockable dockable) { + public void fireAutoHiddenEvent(Dockable dockable) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.AUTO_HIDE_DISABLED, dockable, false))); } /** - * Fire a new shown event + * Fire a shown event. * * @param dockable Dockable that was shown */ - public static void fireShownEvent(Dockable dockable) { + public void fireShownEvent(Dockable dockable) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.SHOWN, dockable, false))); } /** - * Fire a new hidden event + * Fire a hidden event. * * @param dockable Dockable that was hidden */ - public static void fireHiddenEvent(Dockable dockable) { + public void fireHiddenEvent(Dockable dockable) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(new DockingEvent(DockingEvent.ID.HIDDEN, dockable, false))); } /** - * Fire a new docking event + * Fire a docking event. * * @param e Docking event to fire */ - public static void fireDockingEvent(DockingEvent e) { + public void fireDockingEvent(DockingEvent e) { List listeners = new ArrayList<>(dockingListeners); listeners.forEach(listener -> listener.dockingChange(e)); } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/InternalRootDockingPanel.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/InternalRootDockingPanel.java index 0f020f00..f876951d 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/InternalRootDockingPanel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/InternalRootDockingPanel.java @@ -22,17 +22,20 @@ of this software and associated documentation files (the "Software"), to deal package io.github.andrewauclair.moderndocking.internal; import io.github.andrewauclair.moderndocking.Dockable; -import io.github.andrewauclair.moderndocking.DockableTabPreference; import io.github.andrewauclair.moderndocking.DockingRegion; import io.github.andrewauclair.moderndocking.api.DockingAPI; import io.github.andrewauclair.moderndocking.api.RootDockingPanelAPI; import io.github.andrewauclair.moderndocking.settings.Settings; import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.Insets; import java.awt.Window; import java.util.Collections; import java.util.List; +import javax.swing.JMenuBar; +import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; /** @@ -154,6 +157,49 @@ private boolean removeExistingPanel() { return false; } + /** + * Computes and sets the minimum size of the containing window directly from + * the docking panel hierarchy, bypassing the GridBagLayout intermediate containers + * that can silently return stale or zero values. + * + * Formula: panel minimum + menu bar height + native window insets (title bar / borders). + */ + public void updateWindowMinimumSize() { + Window w = SwingUtilities.getWindowAncestor(this); + + if (w == null || !w.isDisplayable() || !(w instanceof RootPaneContainer)) { + return; + } + + Dimension panelMin = (panel != null) ? panel.getMinimumSize() : new Dimension(0, 0); + + JMenuBar menuBar = ((RootPaneContainer) w).getRootPane().getJMenuBar(); + int menuH = (menuBar != null && menuBar.isVisible()) ? menuBar.getPreferredSize().height : 0; + + Insets wi = w.getInsets(); + w.setMinimumSize(new Dimension( + panelMin.width + wi.left + wi.right, + panelMin.height + menuH + wi.top + wi.bottom + )); + } + + @Override + public Dimension getMinimumSize() { + // Return the panel's minimum directly so that any caller (GridBagLayout, + // RootPaneLayout, …) gets an accurate value without going through extra layers. + if (panel == null) { + return new Dimension(0, 0); + } + return panel.getMinimumSize(); + } + + @Override + public void addNotify() { + super.addNotify(); + // Defer so the window's insets and decorations are fully established. + SwingUtilities.invokeLater(this::updateWindowMinimumSize); + } + @Override public void removeNotify() { // this class has a default constructor which could be called and docking would be null @@ -184,7 +230,7 @@ public void dock(Dockable dockable, DockingRegion region, double dividerProporti if (panel != null) { panel.dock(dockable, region, dividerProportion); } - else if (Settings.defaultTabPreference() == DockableTabPreference.TOP_ALWAYS) { + else if (Settings.alwaysDisplayTabsMode()) { setPanel(new DockedTabbedPanel(docking, wrapper, "")); wrapper.setWindow(rootPanel.getWindow()); } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DisplayPanelFloatListener.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DisplayPanelFloatListener.java index 062d69d6..cb1925e0 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DisplayPanelFloatListener.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DisplayPanelFloatListener.java @@ -107,10 +107,10 @@ protected JFrame createFloatingFrame() { } @Override - protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Point mousePosOnScreen) { + protected boolean dropPanel(FloatUtils utils, JFrame floatingFrame, Point mousePosOnScreen) { DockableWrapper floatingDockable = panel.getWrapper(); - if (utilsFrame != null) { + if (utils != null) { Window targetWindow = DockingComponentUtils.findRootAtScreenPos(docking, mousePosOnScreen); InternalRootDockingPanel root = DockingComponentUtils.rootForWindow(docking, targetWindow); @@ -121,16 +121,16 @@ protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Po return false; } - if (utilsFrame.isOverRootHandle()) { - docking.dock(floatingDockable.getDockable(), targetWindow, utilsFrame.rootHandleRegion()); + if (utils.isOverRootHandle()) { + docking.dock(floatingDockable.getDockable(), targetWindow, utils.rootHandleRegion()); } - else if (utilsFrame.isOverDockableHandle()) { - docking.dock(floatingDockable.getDockable(), dockableAtPos, utilsFrame.dockableHandle()); + else if (utils.isOverDockableHandle() && dockableAtPos != null) { + docking.dock(floatingDockable.getDockable(), dockableAtPos, utils.dockableHandle()); } - else if (utilsFrame.isOverPinHandle()) { - docking.autoHideDockable(floatingDockable.getDockable(), utilsFrame.pinRegion(), targetWindow); + else if (utils.isOverPinHandle()) { + docking.autoHideDockable(floatingDockable.getDockable(), utils.pinRegion(), targetWindow); } - else if (utilsFrame.isOverTab()) { + else if (utils.isOverTab()) { CustomTabbedPane tabbedPane = DockingComponentUtils.findTabbedPaneAtPos(mousePosOnScreen, targetWindow); DockedTabbedPanel dockingTabPanel = (DockedTabbedPanel) DockingComponentUtils.findDockingPanelAtScreenPos(mousePosOnScreen, targetWindow); @@ -152,7 +152,7 @@ else if (utilsFrame.isOverTab()) { } else if (dockableAtPos != null) { // docking to a dockable region - DockingRegion region = utilsFrame.getDockableRegion(dockableAtPos, panel.getWrapper().getDockable(), mousePosOnScreen); + DockingRegion region = utils.getDockableRegion(dockableAtPos, panel.getWrapper().getDockable(), mousePosOnScreen); docking.dock(floatingDockable.getDockable(), dockableAtPos, region); } @@ -164,7 +164,7 @@ else if (floatingDockable.getDockable().isFloatingAllowed()) { SwingUtilities.invokeLater(() -> { docking.bringToFront(floatingDockable.getDockable()); - DockingListeners.fireNewFloatingFrameEvent(newFloatingFrame, newFloatingFrame.getRoot(), floatingDockable.getDockable()); + docking.getDockingListeners().fireNewFloatingFrameEvent(newFloatingFrame, newFloatingFrame.getRoot(), floatingDockable.getDockable()); }); } else { @@ -180,7 +180,7 @@ else if (floatingDockable.getDockable().isFloatingAllowed()) { SwingUtilities.invokeLater(() -> { docking.bringToFront(floatingDockable.getDockable()); - DockingListeners.fireNewFloatingFrameEvent(newFloatingFrame, newFloatingFrame.getRoot(), floatingDockable.getDockable()); + docking.getDockingListeners().fireNewFloatingFrameEvent(newFloatingFrame, newFloatingFrame.getRoot(), floatingDockable.getDockable()); }); } else { diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockableHandles.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockableHandles.java index e7e71fc1..3d6a4c7b 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockableHandles.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockableHandles.java @@ -27,11 +27,11 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.ui.DockingSettings; import java.awt.Color; import java.awt.Component; +import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; -import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; @@ -47,47 +47,47 @@ public class DockableHandles { private final DockingHandle dockableNorth = new DockingHandle(DockingRegion.NORTH, false); private final DockingHandle dockableEast = new DockingHandle(DockingRegion.EAST, false); private final DockingHandle dockableSouth = new DockingHandle(DockingRegion.SOUTH, false); - private final JFrame frame; + private final Container container; private final Dockable targetDockable; private final Dockable floatingDockable; /** - * Create a new instance for the frame and target dockable + * Create a new instance for the coordinate root and target dockable * - * @param frame The frame to display these handles on + * @param container The container that handles are added to and coordinates are relative to * @param targetDockable The target dockable the mouse is over */ - public DockableHandles(JFrame frame, Dockable targetDockable) { - this.frame = frame; + public DockableHandles(Container container, Dockable targetDockable) { + this.container = container; this.targetDockable = targetDockable; this.floatingDockable = null; - setupHandle(frame, dockableCenter); - setupHandle(frame, dockableWest); - setupHandle(frame, dockableNorth); - setupHandle(frame, dockableEast); - setupHandle(frame, dockableSouth); + setupHandle(dockableCenter); + setupHandle(dockableWest); + setupHandle(dockableNorth); + setupHandle(dockableEast); + setupHandle(dockableSouth); setDockableHandleLocations(); } /** - * Create a new instance for the frame and target dockable + * Create a new instance for the coordinate root and target dockable * - * @param frame The frame to display these handles on + * @param container The container that handles are added to and coordinates are relative to * @param targetDockable The target dockable the mouse is over - * @param floatingDockable The dockable is floating + * @param floatingDockable The dockable being floated */ - public DockableHandles(JFrame frame, Dockable targetDockable, Dockable floatingDockable) { - this.frame = frame; + public DockableHandles(Container container, Dockable targetDockable, Dockable floatingDockable) { + this.container = container; this.targetDockable = targetDockable; this.floatingDockable = floatingDockable; - setupHandle(frame, dockableCenter); - setupHandle(frame, dockableWest); - setupHandle(frame, dockableNorth); - setupHandle(frame, dockableEast); - setupHandle(frame, dockableSouth); + setupHandle(dockableCenter); + setupHandle(dockableWest); + setupHandle(dockableNorth); + setupHandle(dockableEast); + setupHandle(dockableSouth); setDockableHandleLocations(); } @@ -99,7 +99,7 @@ public DockableHandles(JFrame frame, Dockable targetDockable, Dockable floatingD */ public void mouseMoved(Point mousePosOnScreen) { Point framePoint = new Point(mousePosOnScreen); - SwingUtilities.convertPointFromScreen(framePoint, frame); + SwingUtilities.convertPointFromScreen(framePoint, container); dockableCenter.mouseMoved(framePoint); dockableWest.mouseMoved(framePoint); @@ -108,9 +108,9 @@ public void mouseMoved(Point mousePosOnScreen) { dockableSouth.mouseMoved(framePoint); } - private void setupHandle(JFrame frame, DockingHandle label) { + private void setupHandle(DockingHandle label) { label.setVisible(false); - frame.add(label); + container.add(label); } private void setDockableHandleLocations() { @@ -162,7 +162,7 @@ private void setDockableHandleLocations() { location.y -= (int) (HANDLE_ICON_SIZE * (1.75/2)); SwingUtilities.convertPointToScreen(location, ((Component) targetDockable).getParent()); - SwingUtilities.convertPointFromScreen(location, frame); + SwingUtilities.convertPointFromScreen(location, container); setLocation(dockableCenter, location.x, location.y); setLocation(dockableWest, location.x - handleSpacing(dockableWest), location.y); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockedTabbedPanelFloatListener.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockedTabbedPanelFloatListener.java index 27e8a133..8feba276 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockedTabbedPanelFloatListener.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/DockedTabbedPanelFloatListener.java @@ -98,7 +98,7 @@ protected JFrame createFloatingFrame() { } @Override - protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Point mousePosOnScreen) { + protected boolean dropPanel(FloatUtils utils, JFrame floatingFrame, Point mousePosOnScreen) { if (!(floatingFrame instanceof TempFloatingFrame)) { return false; } @@ -110,7 +110,7 @@ protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Po List dockables = new ArrayList<>(tempFloatingFrame.getDockables()); Dockable selectedDockable = dockables.get(tempFloatingFrame.getSelectedIndex()).getDockable(); - if (utilsFrame == null) { + if (utils == null) { for (DockableWrapper dockable : dockables) { // don't allow dockables that are limited to their window to move to another window if (dockable.getDockable().isLimitedToWindow()) { @@ -141,7 +141,7 @@ protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Po final FloatingFrame frame = newFrame; SwingUtilities.invokeLater(() -> { - DockingListeners.fireNewFloatingFrameEvent(frame, frame.getRoot()); + docking.getDockingListeners().fireNewFloatingFrameEvent(frame, frame.getRoot()); }); } @@ -154,10 +154,10 @@ protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Po Window targetWindow = DockingComponentUtils.findRootAtScreenPos(docking, mousePosOnScreen); Dockable dockableAtPos = DockingComponentUtils.findDockableAtScreenPos(mousePosOnScreen, targetWindow); - DockingRegion region = dockableAtPos == null ? DockingRegion.CENTER : utilsFrame.getDockableRegion(dockableAtPos, null, mousePosOnScreen); + DockingRegion region = dockableAtPos == null ? DockingRegion.CENTER : utils.getDockableRegion(dockableAtPos, null, mousePosOnScreen); - if (utilsFrame.isOverDockableHandle()) { - region = utilsFrame.dockableHandle(); + if (utils.isOverDockableHandle()) { + region = utils.dockableHandle(); } for (DockableWrapper dockable : dockables) { @@ -167,8 +167,8 @@ protected boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Po } if (first) { - if (utilsFrame.isOverRootHandle()) { - docking.dock(dockable.getDockable(), targetWindow, utilsFrame.rootHandleRegion()); + if (utils.isOverRootHandle()) { + docking.dock(dockable.getDockable(), targetWindow, utils.rootHandleRegion()); } else if (dockableAtPos != null) { docking.dock(dockable.getDockable(), dockableAtPos, region); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatListener.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatListener.java index adc4ee88..1d222f80 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatListener.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatListener.java @@ -64,7 +64,7 @@ public abstract class FloatListener extends DragSourceAdapter implements DragSou private JFrame floatingFrame; private Window currentUtilWindow; - private FloatUtilsFrame currentUtilFrame; + private FloatUtils currentUtils; private DragGestureRecognizer alternateDragGesture; /** @@ -240,16 +240,16 @@ private void checkForFrameSwitch(Point mousePosOnScreen) { Window frame = DockingComponentUtils.findRootAtScreenPos(docking, mousePosOnScreen); if (frame != currentUtilWindow) { - if (currentUtilFrame != null) { - currentUtilFrame.deactivate(); + if (currentUtils != null) { + currentUtils.deactivate(); } currentUtilWindow = frame; - currentUtilFrame = Floating.frameForWindow(currentUtilWindow); + currentUtils = Floating.utilsForWindow(currentUtilWindow); - if (currentUtilFrame != null) { - currentUtilFrame.activate(this, floatingFrame, dragSource, mousePosOnScreen); + if (currentUtils != null) { + currentUtils.activate(this, floatingFrame, dragSource, mousePosOnScreen); } } } @@ -275,10 +275,10 @@ private void dropFloatingPanel(Point mousePosOnScreen) { return; } - boolean docked = dropPanel(currentUtilFrame, floatingFrame, mousePosOnScreen); + boolean docked = dropPanel(currentUtils, floatingFrame, mousePosOnScreen); - if (currentUtilFrame != null) { - currentUtilFrame.deactivate(); + if (currentUtils != null) { + currentUtils.deactivate(); } if (!docked) { @@ -313,11 +313,11 @@ private void dropFloatingPanel(Point mousePosOnScreen) { /** * Drop the floating panel * - * @param utilsFrame The utils frame that provides the docking handles and overlay + * @param utils The utils frame that provides the docking handles and overlay * @param floatingFrame The floating frame that contains the floating dockable * @param mousePosOnScreen The position of the mouse on screen * * @return Did we successfully dock the floating dockable? */ - protected abstract boolean dropPanel(FloatUtilsFrame utilsFrame, JFrame floatingFrame, Point mousePosOnScreen); + protected abstract boolean dropPanel(FloatUtils utils, JFrame floatingFrame, Point mousePosOnScreen); } \ No newline at end of file diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtils.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtils.java new file mode 100644 index 00000000..9c6329b0 --- /dev/null +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtils.java @@ -0,0 +1,122 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package io.github.andrewauclair.moderndocking.internal.floating; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; + +import java.awt.Point; +import java.awt.dnd.DragSource; +import javax.swing.JFrame; + +/** + * Common interface for the floating overlay implementations. + *

+ * {@link FloatUtilsFrame} renders the docking handles and highlight overlay on a transparent + * top-level window. {@code FloatUtilsLayer} renders the same content on a {@code JPanel} added to + * the host window's {@code JLayeredPane}, which works on platforms that do not support per-pixel + * window translucency. + */ +public interface FloatUtils { + + /** + * Activate the overlay for a new drag operation. + * + * @param floatListener The listener that started the drag + * @param floatingFrame The temporary frame containing the floating dockable + * @param dragSource The drag source for the operation + * @param mousePosOnScreen Initial mouse position in screen coordinates + */ + void activate(FloatListener floatListener, JFrame floatingFrame, DragSource dragSource, Point mousePosOnScreen); + + /** + * Deactivate the overlay when dragging ends. + */ + void deactivate(); + + /** + * Check whether the mouse is currently over a root docking handle. + * + * @return true if over a root handle + */ + boolean isOverRootHandle(); + + /** + * Get the root docking region currently indicated by the mouse. + * + * @return Root region, or null if not over a root handle + */ + DockingRegion rootHandleRegion(); + + /** + * Check whether the mouse is currently over an auto-hide (pin) handle. + * + * @return true if over a pin handle + */ + boolean isOverPinHandle(); + + /** + * Get the auto-hide toolbar location currently indicated by the mouse. + * + * @return Pin region, or null if not over a pin handle + */ + ToolbarLocation pinRegion(); + + /** + * Check whether the mouse is currently over a dockable region handle. + * + * @return true if over a dockable handle + */ + boolean isOverDockableHandle(); + + /** + * Check whether the mouse is currently positioned for a tab drop. + * + * @return true if over a tab target + */ + boolean isOverTab(); + + /** + * Get the dockable region handle currently indicated by the mouse. + * + * @return Region of the target dockable, or null + */ + DockingRegion dockableHandle(); + + /** + * Determine the docking region for a free mouse position over a target dockable. + * + * @param targetDockable The dockable the mouse is over + * @param floatingDockable The dockable being dragged + * @param mousePosOnScreen Mouse position in screen coordinates + * + * @return The docking region + */ + DockingRegion getDockableRegion(Dockable targetDockable, Dockable floatingDockable, Point mousePosOnScreen); + + /** + * Release all resources associated with this overlay. + * Called when the host window is deregistered. + */ + void dispose(); +} diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsFrame.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsFrame.java index a6d20253..1319913a 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsFrame.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsFrame.java @@ -48,7 +48,7 @@ of this software and associated documentation files (the "Software"), to deal /** * A special invisible frame that's used to provide docking handles and overlays */ -public class FloatUtilsFrame extends JFrame implements DragSourceMotionListener, ComponentListener, WindowListener { +public class FloatUtilsFrame extends JFrame implements FloatUtils, DragSourceMotionListener, ComponentListener, WindowListener { /** * The window this utility frame is tied to */ @@ -501,7 +501,6 @@ public void windowDeiconified(WindowEvent e) { @Override public void windowActivated(WindowEvent e) { -// System.out.println("windowActivated " + ((JFrame) e.getWindow()).getTitle()); windowStack.remove(e.getWindow()); windowStack.add(e.getWindow()); @@ -513,26 +512,21 @@ public void windowActivated(WindowEvent e) { if (windowStack.size() == 3 && windowStack.get(0) == referenceDockingWindow && windowStack.get(1) == floatingFrame && windowStack.get(2) == this) { // perfect -// System.out.println("Windows are in proper order 1"); } else if (windowStack.size() == 2 && windowStack.get(0) == referenceDockingWindow && windowStack.get(1) == this) { // perfect -// System.out.println("Windows are in proper order 2"); } else if (windowStack.get(0) != referenceDockingWindow) { // reference window isn't at the bottom, figure out which frame is out of order if (windowStack.get(0) == floatingFrame) { -// System.out.println("first frame is floating frame, bring to front"); SwingUtilities.invokeLater(floatingFrame::toFront); } else if (windowStack.get(0) == this) { -// System.out.println("First frame is utils frame, bring to front"); SwingUtilities.invokeLater(this::toFront); } } else if (windowStack.size() > 1 && windowStack.get(1) != floatingFrame) { -// System.out.println("Second frame is not floating frame, bring utils to front"); SwingUtilities.invokeLater(this::toFront); } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsLayer.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsLayer.java new file mode 100644 index 00000000..b6bee73d --- /dev/null +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatUtilsLayer.java @@ -0,0 +1,357 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package io.github.andrewauclair.moderndocking.internal.floating; + +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.internal.CustomTabbedPane; +import io.github.andrewauclair.moderndocking.internal.DockingComponentUtils; +import io.github.andrewauclair.moderndocking.internal.InternalRootDockingPanel; +import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Window; +import java.awt.dnd.DragSource; +import java.awt.dnd.DragSourceDragEvent; +import java.awt.dnd.DragSourceMotionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import javax.swing.JFrame; +import javax.swing.JLayeredPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/** + * A {@link FloatUtils} implementation that renders docking handles and the drop overlay on a + * transparent {@link JPanel} added to the host window's {@link JLayeredPane}. + *

+ * This approach is used on platforms that do not support per-pixel window translucency, + * where {@link FloatUtilsFrame} cannot make its background transparent. + * Because the panel lives inside the host window there is no separate overlay + * window and no need for z-order management. + */ +class FloatUtilsLayer implements FloatUtils, DragSourceMotionListener, ComponentListener { + + private final Window referenceDockingWindow; + private final InternalRootDockingPanel root; + private final JLayeredPane layeredPane; + private final RootDockingHandles rootHandles; + private final FloatingOverlay overlay; + + private FloatListener floatListener; + private JFrame floatingFrame; + private DragSource dragSource; + private Dockable currentDockable; + private DockableHandles dockableHandles; + + /** + * Proxy panel that hosts the floating dockable's content while the drag is inside the host + * window. Sits below {@link #renderPanel} in the layered pane so the docking handles and + * drop overlay render on top. Its {@code contains()} always returns {@code false} so that + * {@code SwingUtilities.getDeepestComponentAt} passes through to the underlying dockables. + */ + private final JPanel floatingProxy = new JPanel(new BorderLayout()) { + @Override + public boolean contains(int x, int y) { + return false; + } + }; + + /** + * Transparent panel covering the layered pane. Handles are added as children so that + * their bounds are expressed in this panel's coordinate space, making hit-testing and + * coordinate conversion consistent. + */ + private final JPanel renderPanel = new JPanel() { + @Override + public boolean contains(int x, int y) { + // Pass through for SwingUtilities.getDeepestComponentAt so dockables + // underneath the overlay remain discoverable during hit-testing. + return false; + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + rootHandles.paint(g2); + + if (dockableHandles != null) { + dockableHandles.paint(g2); + } + + overlay.paint(g); + g2.dispose(); + } + }; + + /** + * Create a new layer-based overlay tied to the given host window. + * + * @param docking The docking instance + * @param referenceDockingWindow The host window; must be a {@link JFrame} + * @param root The internal root panel within the host window + */ + FloatUtilsLayer(DockingAPI docking, JFrame referenceDockingWindow, InternalRootDockingPanel root) { + this.referenceDockingWindow = referenceDockingWindow; + this.root = root; + this.layeredPane = referenceDockingWindow.getLayeredPane(); + + floatingProxy.setVisible(false); + layeredPane.add(floatingProxy, JLayeredPane.POPUP_LAYER); + + renderPanel.setOpaque(false); + renderPanel.setLayout(null); + + this.rootHandles = new RootDockingHandles(renderPanel, root); + this.overlay = new FloatingOverlay(docking, renderPanel); + + referenceDockingWindow.addComponentListener(this); + SwingUtilities.invokeLater(this::updatePanelBounds); + + layeredPane.add(renderPanel, JLayeredPane.DRAG_LAYER); + renderPanel.setVisible(false); + } + + @Override + public void activate(FloatListener floatListener, JFrame floatingFrame, DragSource dragSource, Point mousePosOnScreen) { + this.floatListener = floatListener; + this.floatingFrame = floatingFrame; + this.dragSource = dragSource; + dragSource.addDragSourceMotionListener(this); + + // Make the panel visible before any coordinate conversion: renderPanel is a JPanel, + // so getLocationOnScreen() (used internally by SwingUtilities.convertPointFromScreen) + // requires isShowing() == true. Unlike FloatUtilsFrame (a JFrame whose screen + // location is valid even before setVisible), renderPanel must be in a visible + // hierarchy first. + renderPanel.setVisible(true); + updatePanelBounds(); + + // Reparent the floating dockable's content into the layered pane proxy so that the + // actual dockable renders inside the host window rather than as a separate OS window. + // A Swing component can only have one parent, so we move it out of the JFrame. + Container contentPane = floatingFrame.getContentPane(); + if (contentPane.getComponentCount() > 0) { + floatingProxy.add(contentPane.getComponent(0), BorderLayout.CENTER); + } + floatingFrame.setVisible(false); + updateFloatingProxyBounds(); + floatingProxy.setVisible(true); + + if (floatListener instanceof DisplayPanelFloatListener) { + Dockable floatingDockable = ((DisplayPanelFloatListener) floatListener).getDockable(); + rootHandles.setFloatingDockable(floatingDockable); + } + + mouseMoved(mousePosOnScreen); + renderPanel.repaint(); + } + + @Override + public void deactivate() { + renderPanel.setVisible(false); + + if (dragSource != null) { + dragSource.removeDragSourceMotionListener(this); + } + floatListener = null; + + // Move the floating dockable content back into its JFrame before making it visible, + // so the user sees the dockable ghost while dragging outside the host window. + if (floatingFrame != null) { + if (floatingProxy.getComponentCount() > 0) { + floatingFrame.getContentPane().add(floatingProxy.getComponent(0), BorderLayout.CENTER); + } + floatingFrame.setVisible(true); + } + floatingProxy.setVisible(false); + floatingProxy.removeAll(); + + floatingFrame = null; + dragSource = null; + currentDockable = null; + dockableHandles = null; + } + + @Override + public void dragMouseMoved(DragSourceDragEvent event) { + SwingUtilities.invokeLater(() -> mouseMoved(event.getLocation())); + } + + private void mouseMoved(Point mousePosOnScreen) { + if (dragSource == null) { + return; + } + + updateFloatingProxyBounds(); + + rootHandles.mouseMoved(mousePosOnScreen); + + if (dockableHandles != null) { + dockableHandles.mouseMoved(mousePosOnScreen); + } + + boolean prevVisible = overlay.isVisible(); + + // Hide the overlay; it will be set visible again if we find a target + overlay.setVisible(false); + + if (!referenceDockingWindow.getBounds().contains(mousePosOnScreen)) { + return; + } + + Dockable dockable = DockingComponentUtils.findDockableAtScreenPos(mousePosOnScreen, referenceDockingWindow); + + Dockable floatingDockable = null; + if (floatListener instanceof DisplayPanelFloatListener) { + floatingDockable = ((DisplayPanelFloatListener) floatListener).getDockable(); + } + + if (dockable != currentDockable) { + if (dockable == null) { + dockableHandles = null; + } + else if (floatListener instanceof DisplayPanelFloatListener) { + dockableHandles = new DockableHandles(renderPanel, dockable, floatingDockable); + } + else { + dockableHandles = new DockableHandles(renderPanel, dockable); + } + } + currentDockable = dockable; + + if (rootHandles.isOverHandle()) { + overlay.updateForRoot(root, rootHandles.getRegion()); + } + else if (dockableHandles != null) { + overlay.updateForDockable(currentDockable, floatingDockable, mousePosOnScreen, dockableHandles.getRegion()); + } + else if (currentDockable == null && floatListener instanceof DisplayPanelFloatListener) { + CustomTabbedPane tabbedPane = DockingComponentUtils.findTabbedPaneAtPos(mousePosOnScreen, referenceDockingWindow); + if (tabbedPane != null) { + overlay.updateForTab(tabbedPane, mousePosOnScreen); + } + } + + renderPanel.repaint(); + if (overlay.requiresRedraw() || prevVisible != overlay.isVisible()) { + renderPanel.repaint(); + overlay.clearRedraw(); + } + } + + @Override + public boolean isOverRootHandle() { + return rootHandles.isOverHandle(); + } + + @Override + public DockingRegion rootHandleRegion() { + return rootHandles.getRegion(); + } + + @Override + public boolean isOverPinHandle() { + return rootHandles.isOverPinHandle(); + } + + @Override + public ToolbarLocation pinRegion() { + return rootHandles.getPinRegion(); + } + + @Override + public boolean isOverDockableHandle() { + return dockableHandles != null && dockableHandles.getRegion() != null; + } + + @Override + public boolean isOverTab() { + return overlay.isOverTab(); + } + + @Override + public DockingRegion dockableHandle() { + return dockableHandles == null ? null : dockableHandles.getRegion(); + } + + @Override + public DockingRegion getDockableRegion(Dockable targetDockable, Dockable floatingDockable, Point mousePosOnScreen) { + return overlay.getRegion(targetDockable, floatingDockable, mousePosOnScreen); + } + + @Override + public void dispose() { + referenceDockingWindow.removeComponentListener(this); + layeredPane.remove(renderPanel); + layeredPane.remove(floatingProxy); + } + + // -- ComponentListener -- + + @Override + public void componentResized(ComponentEvent e) { + SwingUtilities.invokeLater(this::updatePanelBounds); + } + + @Override + public void componentMoved(ComponentEvent e) { + // Panel position is relative to the layered pane; it doesn't need updating on window move + } + + @Override + public void componentShown(ComponentEvent e) { + // Panel position is relative to the layered pane; it doesn't need updating on window shown + } + + @Override + public void componentHidden(ComponentEvent e) { + // Panel position is relative to the layered pane; it doesn't need updating on window hidden + } + + private void updatePanelBounds() { + renderPanel.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); + // updateHandlePositions calls SwingUtilities.convertPointFromScreen against renderPanel, + // which requires renderPanel.isShowing() == true. Skip it here when the panel is hidden + // (e.g. the invokeLater from the constructor); activate() calls updatePanelBounds again + // after making the panel visible. + if (renderPanel.isShowing()) { + rootHandles.updateHandlePositions(); + } + renderPanel.revalidate(); + renderPanel.repaint(); + } + + private void updateFloatingProxyBounds() { + if (floatingFrame == null) { + return; + } + Point pos = new Point(floatingFrame.getLocation()); + SwingUtilities.convertPointFromScreen(pos, layeredPane); + floatingProxy.setBounds(pos.x, pos.y, floatingFrame.getWidth(), floatingFrame.getHeight()); + } +} diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/Floating.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/Floating.java index e4f2a7c8..b4e703ad 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/Floating.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/Floating.java @@ -23,16 +23,18 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.api.DockingAPI; import io.github.andrewauclair.moderndocking.internal.InternalRootDockingPanel; +import io.github.andrewauclair.moderndocking.ui.DockingSettings; import java.awt.Window; import java.util.HashMap; import java.util.Map; +import javax.swing.JFrame; import javax.swing.SwingUtilities; /** * Small utility class for floating feature */ public class Floating { - private static final Map utilFrames = new HashMap<>(); + private static final Map utils = new HashMap<>(); private static boolean isFloating = false; private static boolean isFloatingTabbedPane = false; @@ -50,7 +52,23 @@ private Floating() { * @param root The internal root in the window */ public static void registerDockingWindow(DockingAPI docking, Window window, InternalRootDockingPanel root) { - SwingUtilities.invokeLater(() -> utilFrames.put(window, new FloatUtilsFrame(docking, window, root))); + // Run the probe off the EDT so the full Robot-based empirical check is used, + // then post FloatUtils creation back to the EDT once the result is cached. + Thread probeThread = new Thread(() -> { + TransparencyProbe.isTransparencySupported(); + SwingUtilities.invokeLater(() -> { + FloatUtils utils; + if (window instanceof JFrame && (DockingSettings.isUseLayeredPaneOverlay() || !TransparencyProbe.isTransparencySupported())) { + utils = new FloatUtilsLayer(docking, (JFrame) window, root); + } + else { + utils = new FloatUtilsFrame(docking, window, root); + } + Floating.utils.put(window, utils); + }); + }, "transparency-probe"); + probeThread.setDaemon(true); + probeThread.start(); } /** @@ -59,9 +77,9 @@ public static void registerDockingWindow(DockingAPI docking, Window window, Inte * @param window The window to deregister */ public static void deregisterDockingWindow(Window window) { - FloatUtilsFrame frame = utilFrames.remove(window); - if (frame != null) { - frame.dispose(); + FloatUtils utils = Floating.utils.remove(window); + if (utils != null) { + utils.dispose(); } } @@ -72,8 +90,8 @@ public static void deregisterDockingWindow(Window window) { * * @return Utility frame or null */ - public static FloatUtilsFrame frameForWindow(Window window) { - return utilFrames.get(window); + public static FloatUtils utilsForWindow(Window window) { + return utils.get(window); } /** diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatingOverlay.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatingOverlay.java index 2e8bd820..616f1141 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatingOverlay.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/FloatingOverlay.java @@ -35,7 +35,6 @@ of this software and associated documentation files (the "Software"), to deal import java.awt.Point; import java.awt.Rectangle; import javax.swing.JComponent; -import javax.swing.JFrame; import javax.swing.SwingUtilities; /** @@ -61,20 +60,20 @@ public class FloatingOverlay { private Rectangle targetTab = null; private final DockingAPI docking; - private final JFrame utilFrame; + private final Component container; private Point prevLocation = location; private Dimension prevSize = size; /** - * Create a new overlay, attached to a utility frame + * Create a new overlay. * - * @param docking The docking instance this overlay belongs to - * @param utilFrame The utility frame this overlay is tied to + * @param docking The docking instance this overlay belongs to + * @param container The component that overlay coordinates are relative to */ - public FloatingOverlay(DockingAPI docking, JFrame utilFrame) { + public FloatingOverlay(DockingAPI docking, Component container) { this.docking = docking; - this.utilFrame = utilFrame; + this.container = container; } /** @@ -109,7 +108,7 @@ public void updateForRoot(InternalRootDockingPanel rootPanel, DockingRegion regi Point point = rootPanel.getLocation(); Dimension size = rootPanel.getSize(); - point = SwingUtilities.convertPoint(rootPanel.getParent(), point, utilFrame); + point = SwingUtilities.convertPoint(rootPanel.getParent(), point, container); final double DROP_SIZE = 4; @@ -166,7 +165,7 @@ public void updateForDockable(Dockable targetDockable, Dockable floatingDockable Point point = component.getLocation(); Dimension size = component.getSize(); - point = SwingUtilities.convertPoint(component.getParent(), point, utilFrame); + point = SwingUtilities.convertPoint(component.getParent(), point, container); final double DROP_SIZE = 2; @@ -211,7 +210,7 @@ public void updateForTab(CustomTabbedPane tabbedPane, Point mousePosOnScreen) { location = componentAt.getLocation(); SwingUtilities.convertPointToScreen(location, tabbedPane); - SwingUtilities.convertPointFromScreen(location, utilFrame); + SwingUtilities.convertPointFromScreen(location, container); size = componentAt.getSize(); @@ -222,7 +221,7 @@ public void updateForTab(CustomTabbedPane tabbedPane, Point mousePosOnScreen) { Point p = new Point(targetTab.x, targetTab.y); SwingUtilities.convertPointToScreen(p, tabbedPane); - SwingUtilities.convertPointFromScreen(p, utilFrame); + SwingUtilities.convertPointFromScreen(p, container); targetTab.x = p.x; targetTab.y = p.y; @@ -244,7 +243,7 @@ public void updateForTab(CustomTabbedPane tabbedPane, Point mousePosOnScreen) { targetTab.width = Math.abs((tabPoint.x + tabbedPane.getWidth()) - (boundsPoint.x + targetTab.width)); } - SwingUtilities.convertPointFromScreen(boundsPoint, utilFrame); + SwingUtilities.convertPointFromScreen(boundsPoint, container); targetTab.x = boundsPoint.x + widthToAdd; targetTab.y = boundsPoint.y; @@ -273,12 +272,12 @@ public DockingRegion getRegion(Dockable targetDockable, Dockable floatingDockabl JComponent component = DockingInternal.get(docking).getWrapper(targetDockable).getDisplayPanel(); Point framePoint = new Point(mousePosOnScreen); - SwingUtilities.convertPointFromScreen(framePoint, utilFrame); + SwingUtilities.convertPointFromScreen(framePoint, container); Point point = (component).getLocation(); Dimension size = component.getSize(); - point = SwingUtilities.convertPoint(component.getParent(), point, utilFrame); + point = SwingUtilities.convertPoint(component.getParent(), point, container); double horizontalPct = (framePoint.x - point.x) / (double) size.width; double verticalPct = (framePoint.y - point.y) / (double) size.height; diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/RootDockingHandles.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/RootDockingHandles.java index 31f194db..227e6aaf 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/RootDockingHandles.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/RootDockingHandles.java @@ -28,10 +28,10 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.internal.InternalRootDockingPanel; import io.github.andrewauclair.moderndocking.ui.ToolbarLocation; import java.awt.Component; +import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; -import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.SwingUtilities; @@ -51,7 +51,7 @@ public class RootDockingHandles { private final DockingHandle pinEast = new DockingHandle(DockingRegion.EAST); private final DockingHandle pinSouth = new DockingHandle(DockingRegion.SOUTH); - private final JFrame frame; + private final Container container; private final InternalRootDockingPanel rootPanel; private DockingRegion mouseOverRegion = null; @@ -60,21 +60,21 @@ public class RootDockingHandles { /** * Create a new instance of the root docking handles * - * @param frame The frame this root docking handle belongs to - * @param rootPanel The root panel for the frame + * @param container The container that handles are added to and coordinates are relative to + * @param rootPanel The root panel for the window */ - public RootDockingHandles(JFrame frame, InternalRootDockingPanel rootPanel) { - this.frame = frame; + public RootDockingHandles(Container container, InternalRootDockingPanel rootPanel) { + this.container = container; this.rootPanel = rootPanel; - setupHandle(frame, rootCenter); - setupHandle(frame, rootWest); - setupHandle(frame, rootNorth); - setupHandle(frame, rootEast); - setupHandle(frame, rootSouth); - - setupHandle(frame, pinWest); - setupHandle(frame, pinEast); - setupHandle(frame, pinSouth); + setupHandle(rootCenter); + setupHandle(rootWest); + setupHandle(rootNorth); + setupHandle(rootEast); + setupHandle(rootSouth); + + setupHandle(pinWest); + setupHandle(pinEast); + setupHandle(pinSouth); } /** @@ -122,7 +122,7 @@ public void setFloatingDockable(Dockable dockable) { */ public void mouseMoved(Point mousePosOnScreen) { Point framePoint = new Point(mousePosOnScreen); - SwingUtilities.convertPointFromScreen(framePoint, frame); + SwingUtilities.convertPointFromScreen(framePoint, container); rootCenter.mouseMoved(framePoint); rootWest.mouseMoved(framePoint); @@ -147,9 +147,9 @@ public void mouseMoved(Point mousePosOnScreen) { if (pinSouth.isMouseOver()) mouseOverPin = DockingRegion.SOUTH; } - private void setupHandle(JFrame frame, DockingHandle label) { + private void setupHandle(DockingHandle label) { label.setVisible(true); - frame.add(label); + container.add(label); } private void setRootHandleLocations() { @@ -159,7 +159,7 @@ private void setRootHandleLocations() { location.y += size.height / 2; SwingUtilities.convertPointToScreen(location, rootPanel.getRootPanel().getParent()); - SwingUtilities.convertPointFromScreen(location, frame); + SwingUtilities.convertPointFromScreen(location, container); setLocation(rootCenter, location.x, location.y); setLocation(rootWest, location.x - (size.width / 2) + rootHandleSpacing(rootWest), location.y); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/TransparencyProbe.java b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/TransparencyProbe.java new file mode 100644 index 00000000..e084f2a9 --- /dev/null +++ b/docking-api/src/io/github/andrewauclair/moderndocking/internal/floating/TransparencyProbe.java @@ -0,0 +1,274 @@ +/* +Copyright (c) 2026 Andrew Auclair + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. + */ +package io.github.andrewauclair.moderndocking.internal.floating; + +import java.awt.AWTException; +import java.awt.Color; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.IllegalComponentStateException; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.Window; +import java.lang.reflect.InvocationTargetException; +import javax.swing.JComponent; +import javax.swing.JWindow; +import javax.swing.SwingUtilities; + +/** + * Probes at startup whether the platform supports per-pixel window translucency. + * The result is cached after the first call and reused for all subsequent calls. + *

+ * When transparency is not supported, {@link Floating} uses a JLayeredPane-based + * overlay instead of a transparent JFrame. + */ +class TransparencyProbe { + /** Reference background color used by the Robot-based check. Chosen to be visually + * distinctive (magenta) and unlikely to match typical desktop content. */ + private static final Color PROBE_COLOR = new Color(255, 0, 255); + /** Sentinel color painted on the probe frame before switching to transparent. + * Allows Robot polling to confirm the probe frame is actually on screen. */ + private static final Color SENTINEL_COLOR = new Color(0, 255, 255); + /** Per-channel tolerance for the Robot color comparison. */ + private static final int COLOR_TOLERANCE = 30; + /** How long to poll for the sentinel/transparent frame to appear (ms). */ + private static final int POLL_TIMEOUT_MS = 2000; + /** Polling interval while waiting for a frame to appear (ms). */ + private static final int POLL_INTERVAL_MS = 20; + + private static volatile Boolean cachedResult = null; + + private TransparencyProbe() { + } + + /** + * Returns whether per-pixel window translucency is supported on this platform. + * Runs the probe on the first call; subsequent calls return the cached result. + *

+ * Should be triggered early (e.g., in {@link Floating#registerDockingWindow}) so the + * result is available before any drag operation begins. + * + * @return true if transparent overlay windows will work correctly + */ + static boolean isTransparencySupported() { + if (cachedResult == null) { + cachedResult = probe(); + } + return cachedResult; + } + + private static boolean probe() { + // Over SSH X11 forwarding every Robot.getPixelColor() call is a round-trip + // to the remote X server, making the empirical check extremely slow. + // Transparency is also unreliable in that environment, so return false + // immediately and let Floating fall back to the JLayeredPane overlay. + if (isRemoteX11Display()) { + return false; + } + + GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + + // Fast path: platform declares per-pixel translucency unsupported + if (!gd.isWindowTranslucencySupported(GraphicsDevice.WindowTranslucency.PERPIXEL_TRANSLUCENT)) { + return false; + } + + return probeOffEDT(gd); + } + + /** + * Returns true when running inside an SSH session with X11 forwarding. + * Detected by the presence of {@code SSH_CONNECTION} or {@code SSH_CLIENT} + * environment variables together with a non-null {@code DISPLAY}. + */ + private static boolean isRemoteX11Display() { + if (System.getenv("DISPLAY") == null) { + return false; + } + return System.getenv("SSH_CONNECTION") != null || System.getenv("SSH_CLIENT") != null; + } + + /** + * Full empirical check for when the probe runs off the EDT. + *

+ * Phase 1: shows the probe frame as an opaque sentinel color (cyan) and polls + * Robot until that color appears at the sample point, confirming the probe is + * composited on screen and on top of the magenta reference frame. + *

+ * Phase 2: switches the probe frame to fully transparent and samples again. + * If the underlying magenta reference frame shows through, per-pixel translucency + * works. If the pixel stays black (or the sentinel color), it does not. + */ + private static boolean probeOffEDT(GraphicsDevice gd) { + Rectangle screen = gd.getDefaultConfiguration().getBounds(); + int sampleX = screen.x + 4; + int sampleY = screen.y + 4; + + // holder[0] = reference window, holder[1] = probe window (null if setup failed) + JWindow[] holder = { null, null }; + + // --- Phase 1: create reference (magenta) + probe (opaque sentinel cyan) --- + // JWindow is used instead of JFrame so neither window appears in the taskbar. + try { + SwingUtilities.invokeAndWait(() -> { + JWindow reference = new JWindow(); + reference.setType(Window.Type.UTILITY); + reference.getContentPane().setBackground(PROBE_COLOR); + reference.setSize(8, 8); + reference.setLocation(screen.x, screen.y); + reference.setVisible(true); + holder[0] = reference; + + JWindow probe = new JWindow(); + probe.setType(Window.Type.UTILITY); + // Start opaque with sentinel color so we can confirm it is on screen + // before switching to transparent. + probe.getContentPane().setBackground(SENTINEL_COLOR); + probe.setSize(8, 8); + probe.setLocation(screen.x, screen.y); + probe.setVisible(true); + holder[1] = probe; + }); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + catch (InvocationTargetException e) { + return false; + } + + Robot robot; + try { + robot = new Robot(gd); + // Drain the event queue once after frame creation; subsequent polls use + // only Thread.sleep to avoid hammering the EDT every 20 ms. + robot.waitForIdle(); + } + catch (AWTException e) { + SwingUtilities.invokeLater(() -> { + if (holder[0] != null) holder[0].dispose(); + if (holder[1] != null) holder[1].dispose(); + }); + return false; + } + + // Poll until the sentinel color is visible, confirming the probe frame is rendered + // and on top before we attempt the transparency switch. + boolean sentinelSeen = pollForColor(robot, sampleX, sampleY, SENTINEL_COLOR); + + if (!sentinelSeen) { + // Can't confirm the probe frame appeared — conservative fallback + SwingUtilities.invokeLater(() -> { + if (holder[0] != null) holder[0].dispose(); + if (holder[1] != null) holder[1].dispose(); + }); + return false; + } + + // --- Phase 2: switch probe to transparent, then check for magenta reference --- + try { + SwingUtilities.invokeAndWait(() -> { + JWindow probe = holder[1]; + try { + probe.setBackground(new Color(0, 0, 0, 0)); + probe.getRootPane().setBackground(new Color(0, 0, 0, 0)); + if (probe.getContentPane() instanceof JComponent) { + ((JComponent) probe.getContentPane()).setOpaque(false); + } + probe.repaint(); + } + catch (IllegalComponentStateException e) { + // Transparency setup failed — signal via null + holder[1] = null; + } + }); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + SwingUtilities.invokeLater(() -> { + if (holder[0] != null) holder[0].dispose(); + }); + return false; + } + catch (InvocationTargetException e) { + SwingUtilities.invokeLater(() -> { + if (holder[0] != null) holder[0].dispose(); + }); + return false; + } + + if (holder[1] == null) { + SwingUtilities.invokeLater(() -> { + if (holder[0] != null) holder[0].dispose(); + }); + return false; + } + + // Drain the EDT once after the transparency switch before polling. + robot.waitForIdle(); + + // Poll for the magenta reference to become visible through the now-transparent probe + boolean result; + try { + result = pollForColor(robot, sampleX, sampleY, PROBE_COLOR); + } + finally { + JWindow ref = holder[0]; + JWindow prob = holder[1]; + SwingUtilities.invokeLater(() -> { + if (ref != null) ref.dispose(); + if (prob != null) prob.dispose(); + }); + } + return result; + } + + /** + * Polls {@link Robot#getPixelColor} at (x, y) until the sampled color is within + * {@link #COLOR_TOLERANCE} of {@code target}, or {@link #POLL_TIMEOUT_MS} elapses. + * + * @return true if the target color was observed within the timeout + */ + private static boolean pollForColor(Robot robot, int x, int y, Color target) { + long deadline = System.currentTimeMillis() + POLL_TIMEOUT_MS; + while (System.currentTimeMillis() < deadline) { + if (isCloseTo(robot.getPixelColor(x, y), target)) { + return true; + } + try { + Thread.sleep(POLL_INTERVAL_MS); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + private static boolean isCloseTo(Color c, Color target) { + return Math.abs(c.getRed() - target.getRed()) < COLOR_TOLERANCE + && Math.abs(c.getGreen() - target.getGreen()) < COLOR_TOLERANCE + && Math.abs(c.getBlue() - target.getBlue()) < COLOR_TOLERANCE; + } +} \ No newline at end of file diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingAnchorPanelNode.java b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingAnchorPanelNode.java index 4d5aa83c..44f4fce1 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingAnchorPanelNode.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingAnchorPanelNode.java @@ -21,11 +21,15 @@ of this software and associated documentation files (the "Software"), to deal */ package io.github.andrewauclair.moderndocking.layouts; +import io.github.andrewauclair.moderndocking.Dockable; import io.github.andrewauclair.moderndocking.DockingRegion; import io.github.andrewauclair.moderndocking.Property; import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.internal.DockingInternal; +import io.github.andrewauclair.moderndocking.settings.Settings; import java.util.HashMap; import java.util.Map; +import javax.swing.JSplitPane; /** * Layout node representing a docking anchor for Window and Application layouts @@ -59,9 +63,48 @@ public DockingLayoutNode findNode(String persistentID) { return null; } + // does not match DockingAnchorPanel on purpose. The below approach (supporting region) allows for more flexibility + // when building layouts @Override public void dock(String persistentID, DockingRegion region, double dividerProportion) { - // TODO I think we need to handle something here + Dockable newDockable = DockingInternal.get(docking).getDockable(persistentID); + String newClassName = newDockable.getClass().getTypeName(); + String newTitleText = newDockable.getTitleText(); + String newTabText = newDockable.getTabText(); + + int orientation = (region == DockingRegion.EAST || region == DockingRegion.WEST) + ? JSplitPane.HORIZONTAL_SPLIT : JSplitPane.VERTICAL_SPLIT; + + DockingLayoutNode left; + DockingLayoutNode right; + + if (Settings.alwaysDisplayTabsMode()) { + if (orientation == JSplitPane.HORIZONTAL_SPLIT) { + left = region == DockingRegion.EAST ? this : new DockingTabPanelNode(docking, persistentID, "", "", newTitleText, newTabText); + right = region == DockingRegion.EAST ? new DockingTabPanelNode(docking, persistentID, "", "", newTitleText, newTabText) : this; + } + else { + left = region == DockingRegion.SOUTH ? this : new DockingTabPanelNode(docking, persistentID, "", "", newTitleText, newTabText); + right = region == DockingRegion.SOUTH ? new DockingTabPanelNode(docking, persistentID, "", "", newTitleText, newTabText) : this; + } + } else { + if (orientation == JSplitPane.HORIZONTAL_SPLIT) { + left = region == DockingRegion.EAST ? this : new DockingSimplePanelNode(docking, persistentID, newClassName, "", newTitleText, newTabText); + right = region == DockingRegion.EAST ? new DockingSimplePanelNode(docking, persistentID, newClassName, "", newTitleText, newTabText) : this; + } + else { + left = region == DockingRegion.SOUTH ? this : new DockingSimplePanelNode(docking, persistentID, newClassName, "", newTitleText, newTabText); + right = region == DockingRegion.SOUTH ? new DockingSimplePanelNode(docking, persistentID, newClassName, "", newTitleText, newTabText) : this; + } + } + + if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) { + dividerProportion = 1.0 - dividerProportion; + } + + DockingLayoutNode oldParent = parent; + DockingSplitPanelNode split = new DockingSplitPanelNode(docking, left, right, orientation, dividerProportion, ""); + oldParent.replaceChild(this, split); } @Override diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayoutRootNode.java b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayoutRootNode.java index fd958a81..d6884d67 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayoutRootNode.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayoutRootNode.java @@ -57,6 +57,7 @@ else if (DockingInternal.get(docking).hasAnchor(persistentID)) { String className = DockingInternal.get(docking).getDockable(persistentID).getClass().getTypeName(); node = new DockingAnchorPanelNode(docking, persistentID, className); + node.setParent(this); } else if (Settings.alwaysDisplayTabsMode()) { node = new DockingTabPanelNode(docking, persistentID, "", "", "", ""); diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayouts.java b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayouts.java index ef388c06..a6481dca 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayouts.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingLayouts.java @@ -42,7 +42,6 @@ of this software and associated documentation files (the "Software"), to deal import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.swing.JSplitPane; /** * Manage storage, persistence and restoration of application layouts @@ -217,17 +216,11 @@ else if (panel == null) { } private static DockingLayoutNode splitPanelToNode(DockingAPI docking, DockedSplitPanel panel) { - JSplitPane splitPane = panel.getSplitPane(); - - int orientation = splitPane.getOrientation(); - int height = splitPane.getHeight(); - int dividerSize = splitPane.getDividerSize(); - int dividerLocation = splitPane.getDividerLocation(); - int width = splitPane.getWidth(); - double dividerProportion = orientation == JSplitPane.VERTICAL_SPLIT ? dividerLocation / (float) (height - dividerSize) : - dividerLocation / (float) (width - dividerSize); - - return new DockingSplitPanelNode(docking, panelToNode(docking, panel.getLeft()), panelToNode(docking, panel.getRight()), splitPane.getOrientation(), dividerProportion, panel.getAnchor()); + List childNodes = new ArrayList<>(); + for (DockingPanel child : panel.getChildren()) { + childNodes.add(panelToNode(docking, child)); + } + return new DockingSplitPanelNode(docking, childNodes, panel.getOrientation(), panel.getDividerPositions(), panel.getAnchor()); } private static DockingLayoutNode tabbedPanelToNode(DockingAPI docking, DockedTabbedPanel panel) { diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingSplitPanelNode.java b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingSplitPanelNode.java index 731f9739..2f9482f3 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingSplitPanelNode.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/DockingSplitPanelNode.java @@ -1,4 +1,4 @@ - /* +/* Copyright (c) 2022-2023 Andrew Auclair Permission is hereby granted, free of charge, to any person obtaining a copy @@ -21,155 +21,177 @@ of this software and associated documentation files (the "Software"), to deal */ package io.github.andrewauclair.moderndocking.layouts; - import io.github.andrewauclair.moderndocking.Dockable; - import io.github.andrewauclair.moderndocking.DockingRegion; - import io.github.andrewauclair.moderndocking.api.DockingAPI; - import io.github.andrewauclair.moderndocking.internal.DockingInternal; - import io.github.andrewauclair.moderndocking.settings.Settings; - import javax.swing.JSplitPane; - - /** - * Layout node that represents a splitpane - */ - public class DockingSplitPanelNode implements DockingLayoutNode { - private final DockingAPI docking; - private DockingLayoutNode left; - private DockingLayoutNode right; - private final int orientation; - private final double dividerProportion; - private String anchor; - - private DockingLayoutNode parent; - - /** - * Create a new DockingSplitPanelNode for a layout - * - * @param docking The docking instance this node belongs to - * @param left The left component of the split - * @param right The right component of the split - * @param orientation The orientation of the split - * @param dividerProportion The divider proportion of the split - * @param anchor The anchor associated with this node - */ - public DockingSplitPanelNode(DockingAPI docking, DockingLayoutNode left, DockingLayoutNode right, int orientation, double dividerProportion, String anchor) { - this.docking = docking; - this.left = left; - this.right = right; - this.orientation = orientation; - this.dividerProportion = dividerProportion; +import io.github.andrewauclair.moderndocking.Dockable; +import io.github.andrewauclair.moderndocking.DockingRegion; +import io.github.andrewauclair.moderndocking.api.DockingAPI; +import io.github.andrewauclair.moderndocking.internal.DockingInternal; +import io.github.andrewauclair.moderndocking.settings.Settings; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.swing.JSplitPane; + +/** + * Layout node representing a flat N-ary split (N >= 2 children). + * Stores divider positions as fractions of this node's own rect so that + * each divider's absolute position is independent of its siblings. + */ +public class DockingSplitPanelNode implements DockingLayoutNode { + + private final DockingAPI docking; + private final List children; + private final int orientation; + /** + * Length is children.size() - 1. + * dividerPositions[i] is the fraction of this node's axis dimension where + * the divider between children[i] and children[i+1] sits. + */ + private final double[] dividerPositions; + private String anchor; + private DockingLayoutNode parent; + + /** + * N-ary constructor. + */ + public DockingSplitPanelNode(DockingAPI docking, List children, + int orientation, double[] dividerPositions, String anchor) { + this.docking = docking; + this.children = new ArrayList<>(children); + this.orientation = orientation; + this.dividerPositions = dividerPositions.clone(); this.anchor = anchor; - - if (this.left != null) { - this.left.setParent(this); - } - if (this.right != null) { - this.right.setParent(this); - } - } - - @Override - public DockingLayoutNode getParent() { - return parent; - } - - @Override - public void setParent(DockingLayoutNode parent) { - this.parent = parent; - } - - @Override - public DockingLayoutNode findNode(String persistentID) { - if (this.left != null) { - DockingLayoutNode left = this.left.findNode(persistentID); - - if (left != null) { - return left; - } - } - - if (this.right == null) { - return null; - } - return this.right.findNode(persistentID); - } - - @Override - public void dock(String persistentID, DockingRegion region, double dividerProportion) { - if (region != DockingRegion.CENTER) { - int orientation = region == DockingRegion.EAST || region == DockingRegion.WEST ? JSplitPane.HORIZONTAL_SPLIT : JSplitPane.VERTICAL_SPLIT; - - DockingLayoutNode left; - DockingLayoutNode right; - - Dockable dockable = DockingInternal.get(docking).getDockable(persistentID); - String className = dockable.getClass().getTypeName(); - - if (Settings.alwaysDisplayTabsMode()) { - left = region == DockingRegion.NORTH || region == DockingRegion.WEST ? new DockingTabPanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()) : this; - right = region == DockingRegion.NORTH || region == DockingRegion.WEST ? this : new DockingTabPanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()); - } - else { - - - left = region == DockingRegion.NORTH || region == DockingRegion.WEST ? new DockingSimplePanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()) : this; - right = region == DockingRegion.NORTH || region == DockingRegion.WEST ? this : new DockingSimplePanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()); - } - - if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) { - dividerProportion = 1.0 - dividerProportion; - } - - DockingLayoutNode oldParent = parent; - DockingSplitPanelNode split = new DockingSplitPanelNode(docking, left, right, orientation, dividerProportion, anchor); - oldParent.replaceChild(this, split); - } - } - - @Override - public void replaceChild(DockingLayoutNode child, DockingLayoutNode newChild) { - if (left == child) { - left = newChild; - left.setParent(this); - } - else if (right == child) { - right = newChild; - right.setParent(this); - } - } - - /** - * Get the left component of the split - * - * @return Left component - */ - public DockingLayoutNode getLeft() { - return left; - } - - /** - * Get the right component of the split - * - * @return Right component - */ - public DockingLayoutNode getRight() { - return right; - } - - /** - * Get the orientation - * - * @return The orientation of the JSplitPane - */ - public int getOrientation() { - return orientation; - } - - /** - * Get the divider proportion - * - * @return Proportion of the JSPlitPane Divider - */ - public double getDividerProportion() { - return dividerProportion; - } + for (DockingLayoutNode child : this.children) { + if (child != null) { + child.setParent(this); + } + } + } + + /** + * Binary constructor kept for callers that still create 2-child splits + * (e.g. the dock() helper below and XML loading of old layouts). + */ + public DockingSplitPanelNode(DockingAPI docking, DockingLayoutNode left, + DockingLayoutNode right, int orientation, + double dividerProportion, String anchor) { + this(docking, Arrays.asList(left, right), orientation, + new double[]{dividerProportion}, anchor); + } + + // ---------------------------------------------------------------- + // Accessors + // ---------------------------------------------------------------- + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public double[] getDividerPositions() { + return dividerPositions.clone(); + } + + /** Convenience for binary splits and XML serialisation. */ + public DockingLayoutNode getLeft() { + return children.get(0); + } + + /** Convenience for binary splits and XML serialisation. */ + public DockingLayoutNode getRight() { + return children.get(1); + } + + public int getOrientation() { + return orientation; + } + + /** Convenience for binary splits and XML serialisation. */ + public double getDividerProportion() { + return dividerPositions[0]; + } + + public String getAnchor() { + return anchor; + } + + // ---------------------------------------------------------------- + // DockingLayoutNode interface + // ---------------------------------------------------------------- + + @Override + public DockingLayoutNode getParent() { + return parent; + } + + @Override + public void setParent(DockingLayoutNode parent) { + this.parent = parent; + } + + @Override + public DockingLayoutNode findNode(String persistentID) { + for (DockingLayoutNode child : children) { + if (child != null) { + DockingLayoutNode found = child.findNode(persistentID); + + if (found != null) { + return found; + } + } + } + return null; + } + + @Override + public void dock(String persistentID, DockingRegion region, double dividerProportion) { + if (region == DockingRegion.CENTER) { + return; + } + + int newOrientation; + + if (region == DockingRegion.EAST || region == DockingRegion.WEST) { + newOrientation = JSplitPane.HORIZONTAL_SPLIT; + } + else { + newOrientation = JSplitPane.VERTICAL_SPLIT; + } + + Dockable dockable = DockingInternal.get(docking).getDockable(persistentID); + String className = dockable.getClass().getTypeName(); + DockingLayoutNode newNode; + + if (Settings.alwaysDisplayTabsMode()) { + newNode = new DockingTabPanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()); + } + else { + newNode = new DockingSimplePanelNode(docking, persistentID, className, anchor, dockable.getTitleText(), dockable.getTabText()); + } + + DockingLayoutNode left; + DockingLayoutNode right; + + if (region == DockingRegion.NORTH || region == DockingRegion.WEST) { + left = newNode; + right = this; + } + else { + left = this; + right = newNode; + dividerProportion = 1.0 - dividerProportion; + } + + DockingLayoutNode oldParent = parent; + DockingSplitPanelNode split = new DockingSplitPanelNode(docking, left, right, newOrientation, dividerProportion, anchor); + oldParent.replaceChild(this, split); + } + + @Override + public void replaceChild(DockingLayoutNode child, DockingLayoutNode newChild) { + int idx = children.indexOf(child); + if (idx >= 0) { + children.set(idx, newChild); + newChild.setParent(this); + } + } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/WindowLayout.java b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/WindowLayout.java index b0a712b0..97a66c73 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/layouts/WindowLayout.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/layouts/WindowLayout.java @@ -189,23 +189,39 @@ public DockingLayoutNode getRootNode() { } /** - * Set the dockable that is maximized + * Set the dockable that is in focused mode * - * @param persistentID Persistent ID of maximized dockable or null + * @param persistentID Persistent ID of focused mode dockable or null */ - public void setMaximizedDockable(String persistentID) { + public void setFocusedModeDockable(String persistentID) { maximizedDockable = persistentID; } /** - * Get the persistent ID of the dockable which is currently maximized + * Get the persistent ID of the dockable which is currently in focused mode * - * @return Name of maximized dockable. Empty string if none. + * @return Persistent ID of focused mode dockable, or null if none. */ - public String getMaximizedDockable() { + public String getFocusedModeDockable() { return maximizedDockable; } + /** + * @deprecated Use {@link #setFocusedModeDockable(String)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public void setMaximizedDockable(String persistentID) { + setFocusedModeDockable(persistentID); + } + + /** + * @deprecated Use {@link #getFocusedModeDockable()} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public String getMaximizedDockable() { + return getFocusedModeDockable(); + } + /** * Set a list of all the dockables on the west toolbar * diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/ui/DefaultHeaderUI.java b/docking-api/src/io/github/andrewauclair/moderndocking/ui/DefaultHeaderUI.java index cc6b6ec3..d982c7fe 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/ui/DefaultHeaderUI.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/ui/DefaultHeaderUI.java @@ -34,7 +34,6 @@ of this software and associated documentation files (the "Software"), to deal import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; -import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; @@ -42,13 +41,11 @@ of this software and associated documentation files (the "Software"), to deal import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.UIManager; -import javax.swing.event.AncestorEvent; -import javax.swing.event.AncestorListener; /** * This can be replaced by the user or with the docking-ui FlatLaf header UI */ -public class DefaultHeaderUI extends JPanel implements DockingHeaderUI, AncestorListener { +public class DefaultHeaderUI extends JPanel implements DockingHeaderUI { /** * Header controller which determines what menu options are enabled */ @@ -71,9 +68,9 @@ public class DefaultHeaderUI extends JPanel implements DockingHeaderUI, Ancestor */ protected final JButton close = new JButton(); /** - * Label that is displayed when the dockable is maximized + * Label that is displayed when the dockable is in focused mode */ - protected final JLabel maximizedIndicator = new JLabel("Maximized"); + protected final JLabel maximizedIndicator = new JLabel("Focused"); /** * Popup menu that is displayed when the settings button is pressed @@ -92,12 +89,7 @@ public class DefaultHeaderUI extends JPanel implements DockingHeaderUI, Ancestor /** * Option to maximize the dockable */ - private final JCheckBoxMenuItem maximizeOption = new JCheckBoxMenuItem("Maximize"); - - /** - * Used to ensure that the header UI is only initialized once when added to its parent - */ - protected boolean initialized = false; + private final JCheckBoxMenuItem maximizeOption = new JCheckBoxMenuItem("Focused Mode"); /** * Override the background color of the header with a new color @@ -120,26 +112,6 @@ public DefaultHeaderUI(HeaderController headerController, HeaderModel headerMode setOpaque(true); - // delay the actual init of the UI in case the dockable object is partially constructed - JComponent component = (JComponent) headerModel.dockable; - component.addAncestorListener(this); - } - - @Override - public void displaySettingsMenu(JButton settings) { - SwingUtilities.updateComponentTreeUI(settingsMenu); - settingsMenu.show(settings, settings.getWidth(), settings.getHeight()); - } - - /** - * Initialize the header UI components - */ - protected void init() { - if (initialized) { - return; - } - initialized = true; - try { settings.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/api_icons/settings.png")))); close.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/api_icons/close.png")))); @@ -148,7 +120,6 @@ protected void init() { } settings.addActionListener(e -> displaySettingsMenu(settings)); - settings.addActionListener(e -> this.settingsMenu.show(settings, settings.getWidth(), settings.getHeight())); close.addActionListener(e -> headerController.close()); setupButton(settings); @@ -183,6 +154,7 @@ protected void init() { gbc.gridx++; gbc.weightx = 0.2; + // Default to hidden; update() will set the correct state once the dockable is registered maximizedIndicator.setVisible(false); maximizedIndicator.setFont(maximizedIndicator.getFont().deriveFont(Font.BOLD)); @@ -191,7 +163,9 @@ protected void init() { gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0; - if (headerModel.hasMoreOptions() || headerModel.isMaximizeAllowed() || headerModel.isAutoHideAllowed() || (headerModel.isFloatingAllowed() && !headerModel.isDockableAloneInWindow())) { + // Skip isDockableAloneInWindow() — the dockable has no window at construction time. + // The "Window" menu item is enabled/disabled by update() instead. + if (headerModel.hasMoreOptions() || headerModel.isMaximizeAllowed() || headerModel.isAutoHideAllowed() || headerModel.isFloatingAllowed()) { addOptions(); add(settings, gbc); @@ -203,6 +177,12 @@ protected void init() { } } + @Override + public void displaySettingsMenu(JButton settings) { + SwingUtilities.updateComponentTreeUI(settingsMenu); + settingsMenu.show(settings, settings.getWidth(), settings.getHeight()); + } + /** * Configure the background and foreground colors of the header */ @@ -253,16 +233,16 @@ private void addOptions() { settingsMenu.add(maximizeOption); maximizeOption.addActionListener(e -> { - boolean maxed = headerModel.isMaximized(); + boolean focused = headerModel.isInFocusedMode(); - maximizeOption.setSelected(!maxed); - maximizedIndicator.setVisible(!maxed); + maximizeOption.setSelected(!focused); + maximizedIndicator.setVisible(!focused); - if (maxed) { - headerController.minimize(); + if (focused) { + headerController.exitFocusedMode(); } else { - headerController.maximize(); + headerController.enterFocusedMode(); } }); } @@ -319,6 +299,7 @@ public void setForeground(Color fg) { super.setForeground(fg); if (titleLabel != null) titleLabel.setForeground(fg); + if (maximizedIndicator != null) maximizedIndicator.setForeground(fg); if (close != null) close.setForeground(fg); if (settings != null) settings.setForeground(fg); @@ -328,8 +309,8 @@ public void setForeground(Color fg) { public void update() { titleLabel.setText(headerModel.titleText()); - maximizedIndicator.setVisible(headerModel.isMaximized()); - maximizeOption.setSelected(headerModel.isMaximized()); + maximizedIndicator.setVisible(headerModel.isInFocusedMode()); + maximizeOption.setSelected(headerModel.isInFocusedMode()); maximizeOption.setEnabled(headerModel.isMaximizeAllowed()); autoHide.setEnabled(headerModel.isAutoHideAllowed()); @@ -337,17 +318,4 @@ public void update() { window.setEnabled(headerModel.isFloatingAllowed() && !headerModel.isDockableAloneInWindow()); } - - @Override - public void ancestorAdded(AncestorEvent event) { - init(); - } - - @Override - public void ancestorRemoved(AncestorEvent event) { - } - - @Override - public void ancestorMoved(AncestorEvent event) { - } } diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/ui/DockingSettings.java b/docking-api/src/io/github/andrewauclair/moderndocking/ui/DockingSettings.java index a8651927..c750b747 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/ui/DockingSettings.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/ui/DockingSettings.java @@ -71,6 +71,8 @@ public class DockingSettings { private static final Color defaultHeaderBackground = Color.white; private static final Color defaultHeaderForeground = Color.black; + private static boolean useLayeredPaneOverlay = false; + private static String currentHandleBackground = themeHandleBackground; private static String currentHandleForeground = themeHandleForeground; private static String currentHighlightSelectedBorder = themeHighlighterSelectedBorder; @@ -85,6 +87,29 @@ public class DockingSettings { private DockingSettings() { } + /** + * Override the automatic transparency detection and force use of the JLayeredPane-based + * overlay. Set this before registering any docking windows. + *

+ * When {@code true}, the layered-pane overlay is always used regardless of what the + * {@code TransparencyProbe} reports. When {@code false} (the default), the overlay + * strategy is chosen automatically based on platform transparency support. + * + * @param use true to force the layered-pane overlay + */ + public static void setUseLayeredPaneOverlay(boolean use) { + useLayeredPaneOverlay = use; + } + + /** + * Returns whether the JLayeredPane-based overlay has been explicitly requested. + * + * @return true if the layered-pane overlay is forced on + */ + public static boolean isUseLayeredPaneOverlay() { + return useLayeredPaneOverlay; + } + /** * Set a new property to use for the background color on Docking Handles * diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderController.java b/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderController.java index 60ab0438..aeb2532d 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderController.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderController.java @@ -31,6 +31,7 @@ of this software and associated documentation files (the "Software"), to deal /** * Controller for the header of dockables. Responsible for controlling the state of all buttons on the header. */ +@SuppressWarnings("deprecation") public class HeaderController implements MaximizeListener, DockingListener { /** * The dockable this header controller references @@ -63,8 +64,8 @@ public HeaderController(Dockable dockable, DockingAPI docking, HeaderModel model this.docking = docking; this.model = model; - DockingListeners.addMaximizeListener(this); - DockingListeners.addDockingListener(this); + docking.getDockingListeners().addMaximizeListener(this); + docking.getDockingListeners().addDockingListener(this); } /** @@ -80,8 +81,8 @@ public void setUI(DockingHeaderUI ui) { * Remove the docking listeners that we've added */ public void removeListeners() { - DockingListeners.removeMaximizeListener(this); - DockingListeners.removeDockingListener(this); + docking.getDockingListeners().removeMaximizeListener(this); + docking.getDockingListeners().removeDockingListener(this); } /** @@ -104,17 +105,33 @@ public void newWindow() { } /** - * Minimize the dockable + * Exit focused mode for the dockable */ + public void exitFocusedMode() { + docking.exitFocusedMode(dockable); + } + + /** + * Enter focused mode for the dockable + */ + public void enterFocusedMode() { + docking.enterFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #exitFocusedMode()} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) public void minimize() { - docking.minimize(dockable); + exitFocusedMode(); } /** - * Maximize the dockable + * @deprecated Use {@link #enterFocusedMode()} instead. Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public void maximize() { - docking.maximize(dockable); + enterFocusedMode(); } /** @@ -126,9 +143,16 @@ public void close() { } } + /** + * @deprecated {@link MaximizeListener} is deprecated. Focused mode changes are now delivered via + * {@link #dockingChange(DockingEvent)} using {@link DockingEvent.ID#FOCUSED_MODE_ENTERED} / + * {@link DockingEvent.ID#FOCUSED_MODE_EXITED}. This override is retained for binary compatibility. + * Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) @Override public void maximized(Dockable dockable, boolean maximized) { - ui.update(); + // handled via dockingChange — no-op here to avoid double update } @Override diff --git a/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderModel.java b/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderModel.java index a40f687b..524b6376 100644 --- a/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderModel.java +++ b/docking-api/src/io/github/andrewauclair/moderndocking/ui/HeaderModel.java @@ -88,21 +88,29 @@ public boolean isAutoHideEnabled() { } /** - * helper function to determine if the header min/max option should be enabled + * helper function to determine if the header focused mode option should be enabled * - * @return Is min/max allowed for this dockable + * @return Is focused mode allowed for this dockable */ public boolean isMaximizeAllowed() { return dockable.isMinMaxAllowed(); } /** - * Check if the dockable is currently maximized + * Check if the dockable is currently in focused mode * - * @return Is dockable maximized? + * @return Is dockable in focused mode? */ + public boolean isInFocusedMode() { + return docking.inFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #isInFocusedMode()} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) public boolean isMaximized() { - return docking.isMaximized(dockable); + return isInFocusedMode(); } /** diff --git a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/AppState.java b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/AppState.java index d82213c2..2b7a081b 100644 --- a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/AppState.java +++ b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/AppState.java @@ -25,6 +25,8 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.Property; import io.github.andrewauclair.moderndocking.api.AppStateAPI; import io.github.andrewauclair.moderndocking.exception.DockingLayoutException; +import io.github.andrewauclair.moderndocking.internal.DockableWrapper; +import io.github.andrewauclair.moderndocking.internal.DockingInternal; import io.github.andrewauclair.moderndocking.layouts.ApplicationLayout; import java.io.File; @@ -32,7 +34,9 @@ of this software and associated documentation files (the "Software"), to deal * Handle persistence and restoration of Application layouts */ public class AppState { - private static final AppStateAPI instance = new AppStateAPI(Docking.getSingleInstance()){}; + private static AppStateAPI instance() { + return Docking.getSingleInstance().getAppState(); + } /** * This class should not be instantiated @@ -47,7 +51,7 @@ private AppState() { * @param autoPersist Should the framework auto persist the application layout to a file? */ public static void setAutoPersist(boolean autoPersist) { - instance.setAutoPersist(autoPersist); + instance().setAutoPersist(autoPersist); } /** @@ -56,7 +60,7 @@ public static void setAutoPersist(boolean autoPersist) { * @return True - we are auto persisting, False - we are not auto persisting */ public static boolean isAutoPersist() { - return instance.isAutoPersist(); + return instance().isAutoPersist(); } /** @@ -65,7 +69,7 @@ public static boolean isAutoPersist() { * @param file File to persist layout to */ public static void setPersistFile(File file) { - instance.setPersistFile(file); + instance().setPersistFile(file); } /** @@ -74,7 +78,7 @@ public static void setPersistFile(File file) { * @return The file we are currently persisting to */ public static File getPersistFile() { - return instance.getPersistFile(); + return instance().getPersistFile(); } /** @@ -83,7 +87,7 @@ public static File getPersistFile() { * @param paused Whether auto persistence should be enabled */ public static void setPaused(boolean paused) { - instance.setPaused(paused); + instance().setPaused(paused); } /** @@ -92,7 +96,7 @@ public static void setPaused(boolean paused) { * @return Whether auto persistence is enabled */ public static boolean isPaused() { - return instance.isPaused(); + return instance().isPaused(); } /** @@ -100,7 +104,7 @@ public static boolean isPaused() { * This is a no-op if auto persistence is turned off, it's paused or there is no file */ public static void persist() { - instance.persist(); + instance().persist(); } /** @@ -110,7 +114,7 @@ public static void persist() { * @throws DockingLayoutException Thrown for any issues with the layout file. */ public static boolean restore() throws DockingLayoutException { - return instance.restore(); + return instance().restore(); } /** @@ -120,7 +124,7 @@ public static boolean restore() throws DockingLayoutException { * @param layout Default layout */ public static void setDefaultApplicationLayout(ApplicationLayout layout) { - instance.setDefaultApplicationLayout(layout); + instance().setDefaultApplicationLayout(layout); } /** @@ -132,7 +136,7 @@ public static void setDefaultApplicationLayout(ApplicationLayout layout) { * @return The property instance of the dockable, or null if not found */ public static Property getProperty(Dockable dockable, String propertyName) { - return instance.getProperty(dockable, propertyName); + return instance().getProperty(dockable, propertyName); } /** @@ -143,7 +147,18 @@ public static Property getProperty(Dockable dockable, String propertyName) { * @param value The value of the property */ public static void setProperty(Dockable dockable, String propertyName, String value) { - instance.setProperty(dockable, propertyName, new Property.StringProperty(propertyName, value)); + instance().setProperty(dockable, propertyName, new Property.StringProperty(propertyName, value)); + } + + /** + * Set the value of a property on the dockable. If the property does not exist, it will be created + * + * @param dockable The dockable to set a property for + * @param propertyName The name of the property we're setting + * @param value The value of the property + */ + public static void setProperty(Dockable dockable, String propertyName, Property value) { + instance().setProperty(dockable, propertyName, value); } /** @@ -153,6 +168,6 @@ public static void setProperty(Dockable dockable, String propertyName, String va * @param propertyName The property to remove */ public static void removeProperty(Dockable dockable, String propertyName) { - instance.removeProperty(dockable, propertyName); + instance().removeProperty(dockable, propertyName); } } diff --git a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/Docking.java b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/Docking.java index 45e49cbe..81b683bc 100644 --- a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/Docking.java +++ b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/Docking.java @@ -25,6 +25,7 @@ of this software and associated documentation files (the "Software"), to deal import io.github.andrewauclair.moderndocking.DockingRegion; import io.github.andrewauclair.moderndocking.api.DockingAPI; import io.github.andrewauclair.moderndocking.api.RootDockingPanelAPI; +import io.github.andrewauclair.moderndocking.event.DockingEvent; import io.github.andrewauclair.moderndocking.event.DockingListener; import io.github.andrewauclair.moderndocking.event.MaximizeListener; import io.github.andrewauclair.moderndocking.event.NewFloatingFrameListener; @@ -469,62 +470,86 @@ public static boolean canDisposeWindow(Window window) { } /** - * checks if a dockable is currently maximized + * Checks if a dockable is currently in focused mode * * @param dockable The dockable to check - * @return Whether the dockable is maximized + * @return Whether the dockable is in focused mode */ - public static boolean isMaximized(Dockable dockable) { - return instance.isMaximized(dockable); + public static boolean inFocusedMode(Dockable dockable) { + return instance.inFocusedMode(dockable); } /** - * maximizes a dockable + * Enter focused mode for a dockable, undocking all others in the same root * - * @param dockable Dockable to maximize + * @param dockable Dockable to enter focused mode */ - public static void maximize(Dockable dockable) { - instance.maximize(dockable); + public static void enterFocusedMode(Dockable dockable) { + instance.enterFocusedMode(dockable); } /** - * minimize a dockable if it is currently maximized + * Exit focused mode for a dockable, restoring the previous layout * - * @param dockable Dockable to minimize + * @param dockable Dockable to exit focused mode + */ + public static void exitFocusedMode(Dockable dockable) { + instance.exitFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #inFocusedMode(Dockable)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public static boolean isMaximized(Dockable dockable) { + return instance.inFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #enterFocusedMode(Dockable)} instead. Will be removed in 2.0. + */ + @Deprecated(since = "1.5.0", forRemoval = true) + public static void maximize(Dockable dockable) { + instance.enterFocusedMode(dockable); + } + + /** + * @deprecated Use {@link #exitFocusedMode(Dockable)} instead. Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public static void minimize(Dockable dockable) { - instance.minimize(dockable); + instance.exitFocusedMode(dockable); } - public void autoShowDockable(Dockable dockable) { + public static void autoShowDockable(Dockable dockable) { instance.autoShowDockable(dockable); } - public void autoShowDockable(String persistentID) { + public static void autoShowDockable(String persistentID) { instance.autoShowDockable(persistentID); } - public void autoHideDockable(Dockable dockable) { + public static void autoHideDockable(Dockable dockable) { instance.autoHideDockable(dockable); } - public void autoHideDockable(String persistentID) { + public static void autoHideDockable(String persistentID) { instance.autoHideDockable(persistentID); } - public void autoHideDockable(Dockable dockable, ToolbarLocation location) { + public static void autoHideDockable(Dockable dockable, ToolbarLocation location) { instance.autoHideDockable(dockable, location); } - public void autoHideDockable(String persistentID, ToolbarLocation location) { + public static void autoHideDockable(String persistentID, ToolbarLocation location) { instance.autoHideDockable(persistentID, location); } - public void autoHideDockable(Dockable dockable, ToolbarLocation location, Window window) { + public static void autoHideDockable(Dockable dockable, ToolbarLocation location, Window window) { instance.autoHideDockable(dockable, location, window); } - public void autoHideDockable(String persistentID, ToolbarLocation location, Window window) { + public static void autoHideDockable(String persistentID, ToolbarLocation location, Window window) { instance.autoHideDockable(persistentID, location, window); } @@ -568,19 +593,25 @@ public static void updateTabInfo(Dockable dockable) { } /** - * Add a new maximize listener. Will be called when a dockable is maximized + * Add a new maximize listener. Will be called when a dockable enters or exits focused mode. * * @param listener Listener to add + * @deprecated Use {@link #addDockingListener(DockingListener)} and handle + * {@link DockingEvent.ID#FOCUSED_MODE_ENTERED} / {@link DockingEvent.ID#FOCUSED_MODE_EXITED} instead. + * Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public static void addMaximizeListener(MaximizeListener listener) { instance.addMaximizeListener(listener); } /** - * Remove a previously added maximize listener. No-op if the listener isn't in the list + * Remove a previously added maximize listener. No-op if the listener isn't in the list. * * @param listener Listener to remove + * @deprecated Will be removed in 2.0. */ + @Deprecated(since = "1.5.0", forRemoval = true) public static void removeMaximizeListener(MaximizeListener listener) { instance.removeMaximizeListener(listener); } @@ -609,7 +640,7 @@ public static void removeDockingListener(DockingListener listener) { * @param listener Listener to add */ public static void addNewFloatingFrameListener(NewFloatingFrameListener listener) { - DockingListeners.addNewFloatingFrameListener(listener); + instance.getDockingListeners().addNewFloatingFrameListener(listener); } /** @@ -618,7 +649,7 @@ public static void addNewFloatingFrameListener(NewFloatingFrameListener listener * @param listener Listener to remove */ public static void removeNewFloatingFrameListener(NewFloatingFrameListener listener) { - DockingListeners.removeNewFloatingFrameListener(listener); + instance.getDockingListeners().removeNewFloatingFrameListener(listener); } public static void setUserDynamicDockableCreationListener(DynamicDockableCreationListener listener) { diff --git a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/LayoutPersistence.java b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/LayoutPersistence.java index 08253733..1fdd4f84 100644 --- a/docking-single-app/src/io/github/andrewauclair/moderndocking/app/LayoutPersistence.java +++ b/docking-single-app/src/io/github/andrewauclair/moderndocking/app/LayoutPersistence.java @@ -31,7 +31,9 @@ of this software and associated documentation files (the "Software"), to deal * Persist and restore Application and Window layouts to/from files */ public class LayoutPersistence { - private static final LayoutPersistenceAPI instance = new LayoutPersistenceAPI(Docking.getSingleInstance()){}; + private static LayoutPersistenceAPI instance() { + return Docking.getSingleInstance().getLayoutPersistence(); + } private LayoutPersistence() { } @@ -44,7 +46,7 @@ private LayoutPersistence() { * @throws DockingLayoutException Thrown if we failed to save the layout to the file */ public static void saveLayoutToFile(File file, ApplicationLayout layout) throws DockingLayoutException { - instance.saveLayoutToFile(file, layout); + instance().saveLayoutToFile(file, layout); } /** @@ -55,11 +57,11 @@ public static void saveLayoutToFile(File file, ApplicationLayout layout) throws * @throws DockingLayoutException Thrown if we failed to read from the file or something went wrong with loading the layout */ public static ApplicationLayout loadApplicationLayoutFromFile(File file) throws DockingLayoutException { - return instance.loadApplicationLayoutFromFile(file); + return instance().loadApplicationLayoutFromFile(file); } public static boolean saveWindowLayoutToFile(File file, WindowLayout layout) { - return instance.saveWindowLayoutToFile(file, layout); + return instance().saveWindowLayoutToFile(file, layout); } /** @@ -69,6 +71,6 @@ public static boolean saveWindowLayoutToFile(File file, WindowLayout layout) { * @return The loaded WindowLayout */ public static WindowLayout loadWindowLayoutFromFile(File file) { - return instance.loadWindowLayoutFromFile(file); + return instance().loadWindowLayoutFromFile(file); } } diff --git a/docking-ui/src/io/github/andrewauclair/moderndocking/ext/ui/FlatLafHeaderUI.java b/docking-ui/src/io/github/andrewauclair/moderndocking/ext/ui/FlatLafHeaderUI.java index 1c6b8042..8736b79c 100644 --- a/docking-ui/src/io/github/andrewauclair/moderndocking/ext/ui/FlatLafHeaderUI.java +++ b/docking-ui/src/io/github/andrewauclair/moderndocking/ext/ui/FlatLafHeaderUI.java @@ -65,12 +65,8 @@ public FlatLafHeaderUI(HeaderController headerController, HeaderModel headerMode settingsIcon.setColorFilter(new FlatSVGIcon.ColorFilter(color -> foreground)); closeIcon.setColorFilter(new FlatSVGIcon.ColorFilter(color -> foreground)); - } - - @Override - protected void init() { - super.init(); + // Replace the PNG icons set by the parent constructor with sharper SVG icons settings.setIcon(settingsIcon); close.setIcon(closeIcon); } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c..8bdaf60c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2a84e188..c61a118f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf93008..ef07e016 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a218..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle b/settings.gradle index c5bc4d48..ff903456 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,5 +4,6 @@ include ':docking-api' include ':docking-ui' include ':docking-single-app' include ':docking-multi-app' +include ':demo-common' include ':demo-single-app' include ':demo-multi-app' \ No newline at end of file