diff --git a/.gitignore b/.gitignore
index cb0209d..d8674a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,6 @@ dockfxdemo.jar
*.db
.git/modules
/bin
-target
\ No newline at end of file
+target
+.gradle
+/build/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..876d724
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,179 @@
+plugins { id "com.jfrog.bintray" version "1.2" }
+
+apply plugin: 'java'
+apply plugin: 'eclipse'
+apply plugin: 'idea'
+apply plugin: 'maven'
+apply plugin: 'maven-publish'
+
+
+//***********************************************************************************
+// JAVA CODE BUILDING
+
+sourceSets
+{
+ main
+ {
+ java
+ { srcDir 'src/main/java' }
+ resources
+ { srcDir 'src/main/resources' }
+ }
+ test
+ {
+ java
+ { srcDir 'src/test/java' }
+ resources
+ { srcDir 'src/test/resources' }
+ }
+}
+
+sourceCompatibility = 1.8
+
+test
+{
+ testLogging.showStandardStreams = true
+ testLogging
+ { events "passed", "skipped", "failed" }
+
+ exclude '**/demo/**'
+ exclude '**/run/**'
+
+ maxHeapSize = "4G"
+}
+
+dependencies
+{
+
+
+}
+
+repositories
+{
+ mavenCentral()
+ maven { url "http://oss.sonatype.org/content/groups/public" }
+ maven {url "http://dl.bintray.com/clearcontrol/ClearControl" }
+}
+
+
+task sourcesJar(type: Jar, dependsOn:classes) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+}
+
+task javadocJar(type: Jar, dependsOn:javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+if (JavaVersion.current().isJava8Compatible()) {
+ allprojects {
+ tasks.withType(Javadoc) {
+ options.addStringOption('Xdoclint:none', '-quiet')
+ }
+ }
+}
+
+//***********************************************************************************
+// PUBLISHING
+
+
+/*
+ * Gets the version name from the latest Git tag
+ */
+def getVersionName = {
+ ->
+ try
+ {
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine 'git', 'describe', '--tags'
+ standardOutput = stdout
+ }
+ return stdout.toString().trim()
+ }
+ catch(Throwable e)
+ {
+ println e
+ }
+}
+
+group = 'org.dockfx'
+version = getVersionName()
+
+
+artifacts
+{
+ archives sourcesJar
+ archives javadocJar
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ from components.java
+ artifact sourcesJar { classifier "sources" }
+ }
+ }
+}
+
+if(hasProperty('bintray_user') && hasProperty('bintray_key') )
+{
+ bintray {
+
+ // property must be set in ~/.gradle/gradle.properties
+ user = bintray_user
+ key = bintray_key
+
+ publications = [
+ 'maven'] //When uploading configuration files
+ dryRun = false //Whether to run this as dry-run, without deploying
+ publish = true //If version should be auto published after an upload
+ pkg {
+ repo = 'ClearControl'
+ userOrg = 'clearcontrol' //An optional organization name when the repo belongs to one of the user's orgs
+ name = 'DockFX'
+ desc = 'DockFX'
+ websiteUrl = 'https://github.com/ClearControl/DockFX'
+ issueTrackerUrl = 'https://github.com/ClearControl/DockFX/issues'
+ vcsUrl = 'https://github.com/ClearControl/DockFX.git'
+ licenses = ['GPL-3.0']
+ labels = [
+ 'DockFX',
+ 'GUI'
+ ]
+ publicDownloadNumbers = true
+ //attributes= ['a': ['ay1', 'ay2'], 'b': ['bee'], c: 'cee'] //Optional package-level attributes
+ //Optional version descriptor
+ version {
+ name = project.version //Bintray logical version name
+ desc = '.'
+ released = new java.util.Date()
+ vcsTag = 'v' + project.version
+ /*attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin'] //Optional version-level attributes
+ gpg {
+ sign = false //Determines whether to GPG sign the files. The default is false
+ passphrase = 'passphrase' //Optional. The passphrase for GPG signing'
+ }
+ mavenCentralSync {
+ sync = false //Optional (true by default). Determines whether to sync the version to Maven Central.
+ user = 'userToken' //OSS user token
+ password = 'paasword' //OSS user password
+ close = '1' //Optional property. By default the staging repository is closed and artifacts are released to Maven Central. You can optionally turn this behaviour off (by puting 0 as value) and release the version manually.
+ } /**/
+ }
+ }
+ /**/
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..053c82b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Apr 05 15:27:24 CEST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip
diff --git a/pom.xml b/pom.xml
index 1bd0e58..6f8d968 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,11 +4,11 @@
http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.dockfx
- dockfx
+ DockFX
jar
- 0.1-SNAPSHOT
+ 0.1.7
DockFX
- https://github.com/RobertBColton/DockFX.git
+ https://github.com/ClearControl/DockFX.git
1.8
@@ -73,6 +73,14 @@
+
+
+ bintray-clearcontrol-ClearControl
+ clearcontrol-ClearControl
+ https://api.bintray.com/maven/clearcontrol/ClearControl/DockFX/;publish=1
+
+
+
diff --git a/src/main/java/org/dockfx/ContentHolder.java b/src/main/java/org/dockfx/ContentHolder.java
new file mode 100644
index 0000000..79796be
--- /dev/null
+++ b/src/main/java/org/dockfx/ContentHolder.java
@@ -0,0 +1,106 @@
+package org.dockfx;
+
+import java.util.LinkedList;
+import java.util.Properties;
+
+/**
+ * ContentHolder has common functions for storing persistent object for node
+ *
+ * @author HongKee Moon
+ */
+public class ContentHolder
+{
+ /**
+ * The enum ContentHolder Type.
+ */
+ public enum Type {
+ /**
+ * The SplitPane.
+ */
+ SplitPane,
+ /**
+ * The TabPane.
+ */
+ TabPane,
+ /**
+ * The Collection.
+ */
+ Collection,
+ /**
+ * The FloatingNode.
+ */
+ FloatingNode,
+ /**
+ * The DockNode.
+ */
+ DockNode
+ }
+
+ String name;
+ Properties properties;
+ LinkedList children;
+ Type type;
+
+ public ContentHolder()
+ {
+
+ }
+
+ public ContentHolder( String name, Type type )
+ {
+ this.name = name;
+ this.properties = new Properties();
+ this.children = new LinkedList();
+ this.type = type;
+ }
+
+ public void addProperty( Object key, Object value )
+ {
+ properties.put( key, value );
+ }
+
+ public void addChild( Object child )
+ {
+ children.add( child );
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName( String name )
+ {
+ this.name = name;
+ }
+
+ public Properties getProperties()
+ {
+ return properties;
+ }
+
+ public void setProperties( Properties properties )
+ {
+ this.properties = properties;
+ }
+
+ public LinkedList getChildren()
+ {
+ return children;
+ }
+
+ public void setChildren( LinkedList children )
+ {
+ this.children = children;
+ }
+
+ public Type getType()
+ {
+ return type;
+ }
+
+ public void setType( Type type )
+ {
+ this.type = type;
+ }
+}
diff --git a/src/main/java/org/dockfx/DelayOpenHandler.java b/src/main/java/org/dockfx/DelayOpenHandler.java
new file mode 100644
index 0000000..424f161
--- /dev/null
+++ b/src/main/java/org/dockfx/DelayOpenHandler.java
@@ -0,0 +1,8 @@
+package org.dockfx;
+
+/**
+ * To support the delayed open process for some specific applications, this interface implementation is used.
+ */
+public interface DelayOpenHandler {
+ public DockNode open(String nodeName);
+}
diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java
index 3dc097d..9d225d8 100644
--- a/src/main/java/org/dockfx/DockNode.java
+++ b/src/main/java/org/dockfx/DockNode.java
@@ -4,776 +4,1042 @@
*
* @section License
*
- * This file is a part of the DockFX Library. Copyright (C) 2015 Robert B. Colton
+ * This file is a part of the DockFX Library. Copyright (C) 2015 Robert B. Colton
*
- * This program is free software: you can redistribute it and/or modify it under the terms
- * of the GNU Lesser General Public License as published by the Free Software Foundation,
- * either version 3 of the License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
*
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
*
- * You should have received a copy of the GNU Lesser General Public License along with this
- * program. If not, see .
- **/
-
+ * You should have received a copy of the GNU Lesser General Public License along with this program.
+ * If not, see .
+ *
+ */
package org.dockfx;
+import org.dockfx.viewControllers.DockFXViewController;
+
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
+import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
+import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
+import org.dockfx.pane.DockNodeTab;
/**
* Base class for a dock node that provides the layout of the content along with a title bar and a
* styled border. The dock node can be detached and floated or closed and removed from the layout.
* Dragging behavior is implemented through the title bar.
- *
+ *
* @since DockFX 0.1
*/
public class DockNode extends VBox implements EventHandler {
- /**
- * The style this dock node should use on its stage when set to floating.
- */
- private StageStyle stageStyle = StageStyle.TRANSPARENT;
- /**
- * The stage that this dock node is currently using when floating.
- */
- private Stage stage;
-
- /**
- * The contents of the dock node, i.e. a TreeView or ListView.
- */
- private Node contents;
- /**
- * The title bar that implements our dragging and state manipulation.
- */
- private DockTitleBar dockTitleBar;
- /**
- * The border pane used when floating to provide a styled custom border.
- */
- private BorderPane borderPane;
-
- /**
- * The dock pane this dock node belongs to when not floating.
- */
- private DockPane dockPane;
-
- /**
- * CSS pseudo class selector representing whether this node is currently floating.
- */
- private static final PseudoClass FLOATING_PSEUDO_CLASS = PseudoClass.getPseudoClass("floating");
- /**
- * CSS pseudo class selector representing whether this node is currently docked.
- */
- private static final PseudoClass DOCKED_PSEUDO_CLASS = PseudoClass.getPseudoClass("docked");
- /**
- * CSS pseudo class selector representing whether this node is currently maximized.
- */
- private static final PseudoClass MAXIMIZED_PSEUDO_CLASS = PseudoClass.getPseudoClass("maximized");
-
- /**
- * Boolean property maintaining whether this node is currently maximized.
- *
- * @defaultValue false
- */
- private BooleanProperty maximizedProperty = new SimpleBooleanProperty(false) {
- @Override
- protected void invalidated() {
- DockNode.this.pseudoClassStateChanged(MAXIMIZED_PSEUDO_CLASS, get());
- if (borderPane != null) {
- borderPane.pseudoClassStateChanged(MAXIMIZED_PSEUDO_CLASS, get());
- }
+ /**
+ * The style this dock node should use on its stage when set to floating.
+ */
+ private StageStyle stageStyle = StageStyle.TRANSPARENT;
+ /**
+ * The stage that this dock node is currently using when floating.
+ */
+ private Stage stage;
+
+ /**
+ * The contents of the dock node, i.e. a TreeView or ListView.
+ */
+ private Node contents;
+ /**
+ * The title bar that implements our dragging and state manipulation.
+ */
+ private DockTitleBar dockTitleBar;
+ /**
+ * The border pane used when floating to provide a styled custom border.
+ */
+ private BorderPane borderPane;
+
+ /**
+ * The dock pane this dock node belongs to when not floating.
+ */
+ private DockPane dockPane;
+
+ /**
+ * View controller of node inside this DockNode
+ */
+ private DockFXViewController viewController;
+
+ /**
+ * CSS pseudo class selector representing whether this node is currently floating.
+ */
+ private static final PseudoClass FLOATING_PSEUDO_CLASS = PseudoClass.getPseudoClass("floating");
+ /**
+ * CSS pseudo class selector representing whether this node is currently docked.
+ */
+ private static final PseudoClass DOCKED_PSEUDO_CLASS = PseudoClass.getPseudoClass("docked");
+ /**
+ * CSS pseudo class selector representing whether this node is currently maximized.
+ */
+ private static final PseudoClass MAXIMIZED_PSEUDO_CLASS = PseudoClass.getPseudoClass("maximized");
+
+ private double widthBeforeMaximizing;
+ private double heightBeforeMaximizing;
+ private double xPosBeforeMaximizing;
+ private double yPosBeforeMaximizing;
+
+ /**
+ * Boolean property maintaining whether this node is currently maximized.
+ *
+ * @defaultValue false
+ */
+ private BooleanProperty maximizedProperty = new SimpleBooleanProperty(false) {
+
+ @Override
+ protected void invalidated() {
+ DockNode.this.pseudoClassStateChanged(MAXIMIZED_PSEUDO_CLASS, get());
+ if (borderPane != null) {
+ borderPane.pseudoClassStateChanged(MAXIMIZED_PSEUDO_CLASS, get());
+ }
+
+ if (!get()) {
+
+ stage.setX(xPosBeforeMaximizing);
+ stage.setY(yPosBeforeMaximizing);
+ stage.setWidth(widthBeforeMaximizing);
+ stage.setHeight(heightBeforeMaximizing);
+
+ //for minimizing
+ //stage.setIconified(true);
+ } else {
+ widthBeforeMaximizing = stage.getWidth();
+ heightBeforeMaximizing = stage.getHeight();
+ xPosBeforeMaximizing = stage.getX();
+ yPosBeforeMaximizing = stage.getY();
+ }
+
+ // TODO: This is a work around to fill the screen bounds and not overlap the task bar when
+ // the window is undecorated as in Visual Studio. A similar work around needs applied for
+ // JFrame in Swing. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4737788
+ // Bug report filed:
+ // https://bugs.openjdk.java.net/browse/JDK-8133330
+ if (this.get()) {
+ Screen screen = Screen
+ .getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())
+ .get(0);
+ Rectangle2D bounds = screen.getVisualBounds();
+
+ stage.setX(bounds.getMinX());
+ stage.setY(bounds.getMinY());
+
+ stage.setWidth(bounds.getWidth());
+ stage.setHeight(bounds.getHeight());
+ }
+ }
- stage.setMaximized(get());
+ @Override
+ public String getName() {
+ return "maximized";
+ }
+ };
- // TODO: This is a work around to fill the screen bounds and not overlap the task bar when
- // the window is undecorated as in Visual Studio. A similar work around needs applied for
- // JFrame in Swing. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4737788
- // Bug report filed:
- // https://bugs.openjdk.java.net/browse/JDK-8133330
- if (this.get()) {
- Screen screen = Screen
- .getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())
- .get(0);
- Rectangle2D bounds = screen.getVisualBounds();
+ public DockNode(Node contents, String title, Node graphic, DockFXViewController controller) {
+ initializeDockNode(contents, title, graphic, controller);
+ }
- stage.setX(bounds.getMinX());
- stage.setY(bounds.getMinY());
+ /**
+ * Creates a default DockNode with a default title bar and layout.
+ *
+ * @param contents The contents of the dock node which may be a tree or another scene graph
+ * node.
+ * @param title The caption title of this dock node which maintains bidirectional state with the
+ * title bar and stage.
+ * @param graphic The caption graphic of this dock node which maintains bidirectional state with
+ * the title bar and stage.
+ */
+ public DockNode(Node contents, String title, Node graphic) {
+ this(contents, title, graphic, null);
+ }
- stage.setWidth(bounds.getWidth());
- stage.setHeight(bounds.getHeight());
- }
+ /**
+ * Creates a default DockNode with a default title bar and layout.
+ *
+ * @param contents The contents of the dock node which may be a tree or another scene graph
+ * node.
+ * @param title The caption title of this dock node which maintains bidirectional state with the
+ * title bar and stage.
+ */
+ public DockNode(Node contents, String title) {
+ this(contents, title, null);
}
- @Override
- public String getName() {
- return "maximized";
- }
- };
-
- /**
- * Creates a default DockNode with a default title bar and layout.
- *
- * @param contents The contents of the dock node which may be a tree or another scene graph node.
- * @param title The caption title of this dock node which maintains bidirectional state with the
- * title bar and stage.
- * @param graphic The caption graphic of this dock node which maintains bidirectional state with
- * the title bar and stage.
- */
- public DockNode(Node contents, String title, Node graphic) {
- this.titleProperty.setValue(title);
- this.graphicProperty.setValue(graphic);
- this.contents = contents;
-
- dockTitleBar = new DockTitleBar(this);
-
- getChildren().addAll(dockTitleBar, contents);
- VBox.setVgrow(contents, Priority.ALWAYS);
-
- this.getStyleClass().add("dock-node");
- }
-
- /**
- * Creates a default DockNode with a default title bar and layout.
- *
- * @param contents The contents of the dock node which may be a tree or another scene graph node.
- * @param title The caption title of this dock node which maintains bidirectional state with the
- * title bar and stage.
- */
- public DockNode(Node contents, String title) {
- this(contents, title, null);
- }
-
- /**
- * Creates a default DockNode with a default title bar and layout.
- *
- * @param contents The contents of the dock node which may be a tree or another scene graph node.
- */
- public DockNode(Node contents) {
- this(contents, null, null);
- }
-
- /**
- * The stage style that will be used when the dock node is floating. This must be set prior to
- * setting the dock node to floating.
- *
- * @param stageStyle The stage style that will be used when the node is floating.
- */
- public void setStageStyle(StageStyle stageStyle) {
- this.stageStyle = stageStyle;
- }
-
- /**
- * Changes the contents of the dock node.
- *
- * @param contents The new contents of this dock node.
- */
- public void setContents(Node contents) {
- this.getChildren().set(this.getChildren().indexOf(this.contents), contents);
- this.contents = contents;
- }
-
- /**
- * Changes the title bar in the layout of this dock node. This can be used to remove the dock
- * title bar from the dock node by passing null.
- *
- * @param dockTitleBar null The new title bar of this dock node, can be set null indicating no
- * title bar is used.
- */
- public void setDockTitleBar(DockTitleBar dockTitleBar) {
- if (dockTitleBar != null) {
- if (this.dockTitleBar != null) {
- this.getChildren().set(this.getChildren().indexOf(this.dockTitleBar), dockTitleBar);
- } else {
- this.getChildren().add(0, dockTitleBar);
- }
- } else {
- this.getChildren().remove(this.dockTitleBar);
- }
-
- this.dockTitleBar = dockTitleBar;
- }
-
- /**
- * Whether the node is currently maximized.
- *
- * @param maximized Whether the node is currently maximized.
- */
- public final void setMaximized(boolean maximized) {
- maximizedProperty.set(maximized);
- }
-
- /**
- * Whether the node is currently floating.
- *
- * @param floating Whether the node is currently floating.
- * @param translation null The offset of the node after being set floating. Used for aligning it
- * with its layout bounds inside the dock pane when it becomes detached. Can be null
- * indicating no translation.
- */
- public void setFloating(boolean floating, Point2D translation) {
- if (floating && !this.isFloating()) {
- // position the new stage relative to the old scene offset
- Point2D floatScene = this.localToScene(0, 0);
- Point2D floatScreen = this.localToScreen(0, 0);
-
- // setup window stage
- dockTitleBar.setVisible(this.isCustomTitleBar());
- dockTitleBar.setManaged(this.isCustomTitleBar());
-
- if (this.isDocked()) {
- this.undock();
- }
-
- stage = new Stage();
- stage.titleProperty().bind(titleProperty);
- if (dockPane != null && dockPane.getScene() != null
- && dockPane.getScene().getWindow() != null) {
- stage.initOwner(dockPane.getScene().getWindow());
- }
-
- stage.initStyle(stageStyle);
-
- // offset the new stage to cover exactly the area the dock was local to the scene
- // this is useful for when the user presses the + sign and we have no information
- // on where the mouse was clicked
- Point2D stagePosition;
- if (this.isDecorated()) {
- Window owner = stage.getOwner();
- stagePosition = floatScene.add(new Point2D(owner.getX(), owner.getY()));
- } else {
- stagePosition = floatScreen;
- }
- if (translation != null) {
- stagePosition = stagePosition.add(translation);
- }
-
- // the border pane allows the dock node to
- // have a drop shadow effect on the border
- // but also maintain the layout of contents
- // such as a tab that has no content
- borderPane = new BorderPane();
- borderPane.getStyleClass().add("dock-node-border");
- borderPane.setCenter(this);
-
- Scene scene = new Scene(borderPane);
-
- // apply the floating property so we can get its padding size
- // while it is floating to offset it by the drop shadow
- // this way it pops out above exactly where it was when docked
- this.floatingProperty.set(floating);
- this.applyCss();
-
- // apply the border pane css so that we can get the insets and
- // position the stage properly
- borderPane.applyCss();
- Insets insetsDelta = borderPane.getInsets();
-
- double insetsWidth = insetsDelta.getLeft() + insetsDelta.getRight();
- double insetsHeight = insetsDelta.getTop() + insetsDelta.getBottom();
-
- stage.setX(stagePosition.getX() - insetsDelta.getLeft());
- stage.setY(stagePosition.getY() - insetsDelta.getTop());
-
- stage.setMinWidth(borderPane.minWidth(this.getHeight()) + insetsWidth);
- stage.setMinHeight(borderPane.minHeight(this.getWidth()) + insetsHeight);
-
- borderPane.setPrefSize(this.getWidth() + insetsWidth, this.getHeight() + insetsHeight);
-
- stage.setScene(scene);
-
- if (stageStyle == StageStyle.TRANSPARENT) {
- scene.setFill(null);
- }
-
- stage.setResizable(this.isStageResizable());
- if (this.isStageResizable()) {
- stage.addEventFilter(MouseEvent.MOUSE_PRESSED, this);
- stage.addEventFilter(MouseEvent.MOUSE_MOVED, this);
- stage.addEventFilter(MouseEvent.MOUSE_DRAGGED, this);
- }
-
- // we want to set the client area size
- // without this it subtracts the native border sizes from the scene
- // size
- stage.sizeToScene();
-
- stage.show();
- } else if (!floating && this.isFloating()) {
- this.floatingProperty.set(floating);
-
- stage.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
- stage.removeEventFilter(MouseEvent.MOUSE_MOVED, this);
- stage.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
-
- stage.close();
- }
- }
-
- /**
- * Whether the node is currently floating.
- *
- * @param floating Whether the node is currently floating.
- */
- public void setFloating(boolean floating) {
- setFloating(floating, null);
- }
-
- /**
- * The dock pane that was last associated with this dock node. Either the dock pane that it is
- * currently docked to or the one it was detached from. Can be null if the node was never docked.
- *
- * @return The dock pane that was last associated with this dock node.
- */
- public final DockPane getDockPane() {
- return dockPane;
- }
-
- /**
- * The dock title bar associated with this dock node.
- *
- * @return The dock title bar associated with this node.
- */
- public final DockTitleBar getDockTitleBar() {
- return this.dockTitleBar;
- }
-
- /**
- * The stage associated with this dock node. Can be null if the dock node was never set to
- * floating.
- *
- * @return The stage associated with this node.
- */
- public final Stage getStage() {
- return stage;
- }
-
- /**
- * The border pane used to parent this dock node when floating. Can be null if the dock node was
- * never set to floating.
- *
- * @return The stage associated with this node.
- */
- public final BorderPane getBorderPane() {
- return borderPane;
- }
-
- /**
- * The contents managed by this dock node.
- *
- * @return The contents managed by this dock node.
- */
- public final Node getContents() {
- return contents;
- }
-
- /**
- * Object property maintaining bidirectional state of the caption graphic for this node with the
- * dock title bar or stage.
- *
- * @defaultValue null
- */
- public final ObjectProperty graphicProperty() {
- return graphicProperty;
- }
-
- private ObjectProperty graphicProperty = new SimpleObjectProperty() {
- @Override
- public String getName() {
- return "graphic";
- }
- };
-
- public final Node getGraphic() {
- return graphicProperty.get();
- }
-
- public final void setGraphic(Node graphic) {
- this.graphicProperty.setValue(graphic);
- }
-
- /**
- * Boolean property maintaining bidirectional state of the caption title for this node with the
- * dock title bar or stage.
- *
- * @defaultValue "Dock"
- */
- public final StringProperty titleProperty() {
- return titleProperty;
- }
-
- private StringProperty titleProperty = new SimpleStringProperty("Dock") {
- @Override
- public String getName() {
- return "title";
- }
- };
-
- public final String getTitle() {
- return titleProperty.get();
- }
-
- public final void setTitle(String title) {
- this.titleProperty.setValue(title);
- }
-
- /**
- * Boolean property maintaining whether this node is currently using a custom title bar. This can
- * be used to force the default title bar to show when the dock node is set to floating instead of
- * using native window borders.
- *
- * @defaultValue true
- */
- public final BooleanProperty customTitleBarProperty() {
- return customTitleBarProperty;
- }
-
- private BooleanProperty customTitleBarProperty = new SimpleBooleanProperty(true) {
- @Override
- public String getName() {
- return "customTitleBar";
+ /**
+ * Creates a default DockNode with a default title bar and layout.
+ *
+ * @param contents The contents of the dock node which may be a tree or another scene graph
+ * node.
+ */
+ public DockNode(Node contents) {
+ this(contents, null, null);
}
- };
- public final boolean isCustomTitleBar() {
- return customTitleBarProperty.get();
- }
+ /**
+ *
+ * Creates a default DockNode with contents loaded from FXMLFile at provided path.
+ *
+ * @param FXMLPath path to fxml file.
+ * @param title The caption title of this dock node which maintains bidirectional state with the
+ * title bar and stage.
+ * @param graphic The caption title of this dock node which maintains bidirectional state with
+ * the title bar and stage.
+ */
+ public DockNode(String FXMLPath, String title, Node graphic) {
+ FXMLLoader loader = loadNode(FXMLPath);
+ initializeDockNode(loader.getRoot(), title, graphic, loader.getController());
+ }
- public final void setUseCustomTitleBar(boolean useCustomTitleBar) {
- if (this.isFloating()) {
- dockTitleBar.setVisible(useCustomTitleBar);
- dockTitleBar.setManaged(useCustomTitleBar);
+ /**
+ * Creates a default DockNode with contents loaded from FXMLFile at provided path.
+ *
+ * @param FXMLPath path to fxml file.
+ * @param title The caption title of this dock node which maintains bidirectional state with the
+ * title bar and stage.
+ */
+ public DockNode(String FXMLPath, String title) {
+ this(FXMLPath, title, null);
}
- this.customTitleBarProperty.set(useCustomTitleBar);
- }
- /**
- * Boolean property maintaining whether this node is currently floating.
- *
- * @defaultValue false
- */
- public final BooleanProperty floatingProperty() {
- return floatingProperty;
- }
+ /**
+ * Creates a default DockNode with contents loaded from FXMLFile at provided path with default
+ * title bar.
+ *
+ * @param FXMLPath path to fxml file.
+ */
+ public DockNode(String FXMLPath) {
+ this(FXMLPath, null, null);
+ }
- private BooleanProperty floatingProperty = new SimpleBooleanProperty(false) {
- @Override
- protected void invalidated() {
- DockNode.this.pseudoClassStateChanged(FLOATING_PSEUDO_CLASS, get());
- if (borderPane != null) {
- borderPane.pseudoClassStateChanged(FLOATING_PSEUDO_CLASS, get());
- }
+ /**
+ * Loads Node from fxml file located at FXMLPath and returns it.
+ *
+ * @param FXMLPath Path to fxml file.
+ * @return Node loaded from fxml file or StackPane with Label with error message.
+ */
+ private static FXMLLoader loadNode(String FXMLPath) {
+ FXMLLoader loader = new FXMLLoader();
+ try {
+ loader.load(DockNode.class.getResourceAsStream(FXMLPath));
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ loader.setRoot(new StackPane(new Label("Could not load FXML file")));
+ }
+ return loader;
}
- @Override
- public String getName() {
- return "floating";
+ /**
+ * Sets DockNodes contents, title and title bar graphic
+ *
+ * @param contents The contents of the dock node which may be a tree or another scene graph
+ * node.
+ * @param title The caption title of this dock node which maintains bidirectional state with the
+ * title bar and stage.
+ * @param graphic The caption title of this dock node which maintains bidirectional state with
+ * the title bar and stage.
+ */
+ private void initializeDockNode(Node contents, String title, Node graphic, DockFXViewController controller) {
+ this.titleProperty.setValue(title);
+ this.graphicProperty.setValue(graphic);
+ this.contents = contents;
+ this.viewController = controller;
+
+ dockTitleBar = new DockTitleBar(this);
+ if (viewController != null) {
+ viewController.setDockTitleBar(dockTitleBar);
+ }
+
+ getChildren().addAll(dockTitleBar, contents);
+ VBox.setVgrow(contents, Priority.ALWAYS);
+
+ this.getStyleClass().add("dock-node");
+ setMinimizable(false);
}
- };
- public final boolean isFloating() {
- return floatingProperty.get();
- }
+ /**
+ * The stage style that will be used when the dock node is floating. This must be set prior to
+ * setting the dock node to floating.
+ *
+ * @param stageStyle The stage style that will be used when the node is floating.
+ */
+ public void setStageStyle(StageStyle stageStyle) {
+ this.stageStyle = stageStyle;
+ }
- /**
- * Boolean property maintaining whether this node is currently floatable.
- *
- * @defaultValue true
- */
- public final BooleanProperty floatableProperty() {
- return floatableProperty;
- }
+ /**
+ * Changes the contents of the dock node.
+ *
+ * @param contents The new contents of this dock node.
+ */
+ public void setContents(Node contents) {
+ this.getChildren().set(this.getChildren().indexOf(this.contents), contents);
+ this.contents = contents;
+ }
- private BooleanProperty floatableProperty = new SimpleBooleanProperty(true) {
- @Override
- public String getName() {
- return "floatable";
+ /**
+ * Changes the title bar in the layout of this dock node. This can be used to remove the dock
+ * title bar from the dock node by passing null.
+ *
+ * @param dockTitleBar null The new title bar of this dock node, can be set null indicating no
+ * title bar is used.
+ */
+ public void setDockTitleBar(DockTitleBar dockTitleBar) {
+ if (dockTitleBar != null) {
+ if (this.dockTitleBar != null) {
+ this.getChildren().set(this.getChildren().indexOf(this.dockTitleBar), dockTitleBar);
+ } else {
+ this.getChildren().add(0, dockTitleBar);
+ }
+ } else {
+ this.getChildren().remove(this.dockTitleBar);
+ }
+
+ this.dockTitleBar = dockTitleBar;
}
- };
- public final boolean isFloatable() {
- return floatableProperty.get();
- }
+ /**
+ * Whether the node is currently maximized.
+ *
+ * @param maximized Whether the node is currently maximized.
+ */
+ public final void setMaximized(boolean maximized) {
+ maximizedProperty.set(maximized);
+ }
- public final void setFloatable(boolean floatable) {
- if (!floatable && this.isFloating()) {
- this.setFloating(false);
+ /**
+ * Whether the node is currently floating.
+ *
+ * @param floating Whether the node is currently floating.
+ * @param translation null The offset of the node after being set floating. Used for aligning it
+ * with its layout bounds inside the dock pane when it becomes detached. Can be null indicating
+ * no translation.
+ */
+ public void setFloating(boolean floating, Point2D translation) {
+ if (floating && !this.isFloating()) {
+ // position the new stage relative to the old scene offset
+ Point2D floatScene = this.localToScene(0, 0);
+ Point2D floatScreen = this.localToScreen(0, 0);
+
+ // setup window stage
+ dockTitleBar.setVisible(this.isCustomTitleBar());
+ dockTitleBar.setManaged(this.isCustomTitleBar());
+
+ if (this.isDocked()) {
+ this.undock();
+ }
+
+ stage = new Stage();
+ stage.titleProperty().bind(titleProperty);
+ if (dockPane != null && dockPane.getScene() != null
+ && dockPane.getScene().getWindow() != null) {
+ //stage.initOwner(dockPane.getScene().getWindow());
+ }
+
+ stage.initStyle(stageStyle);
+
+ // offset the new stage to cover exactly the area the dock was local to the scene
+ // this is useful for when the user presses the + sign and we have no information
+ // on where the mouse was clicked
+ Point2D stagePosition;
+ if (this.isDecorated()) {
+ Window owner = dockPane.getScene().getWindow();
+ stagePosition = floatScene.add(new Point2D(owner.getX(), owner.getY()));
+ } else if (floatScreen != null) {
+ // using coordinates the component was previously in (if available)
+ stagePosition = floatScreen;
+ } else {
+ // using the center of the screen if no relative position is available
+ Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
+ double centerX = (primScreenBounds.getWidth() - Math.max(getWidth(), getMinWidth())) / 2;
+ double centerY = (primScreenBounds.getHeight() - Math.max(getHeight(), getMinHeight())) / 2;
+ stagePosition = new Point2D(centerX, centerY);
+ }
+
+ if (translation != null) {
+ stagePosition = stagePosition.add(translation);
+ }
+
+ // the border pane allows the dock node to
+ // have a drop shadow effect on the border
+ // but also maintain the layout of contents
+ // such as a tab that has no content
+ borderPane = new BorderPane();
+ borderPane.getStyleClass().add("dock-node-border");
+ borderPane.setCenter(this);
+
+ Scene scene = new Scene(borderPane);
+
+ // apply the floating property so we can get its padding size
+ // while it is floating to offset it by the drop shadow
+ // this way it pops out above exactly where it was when docked
+ this.floatingProperty.set(floating);
+ this.applyCss();
+
+ // apply the border pane css so that we can get the insets and
+ // position the stage properly
+ borderPane.applyCss();
+ Insets insetsDelta = borderPane.getInsets();
+
+ double insetsWidth = insetsDelta.getLeft() + insetsDelta.getRight();
+ double insetsHeight = insetsDelta.getTop() + insetsDelta.getBottom();
+
+ stage.setX(stagePosition.getX() - insetsDelta.getLeft());
+ stage.setY(stagePosition.getY() - insetsDelta.getTop());
+
+ //stage.setMinWidth(borderPane.minWidth(this.getHeight()) + insetsWidth);
+ //stage.setMinHeight(borderPane.minHeight(this.getWidth()) + insetsHeight);
+ borderPane.setPrefSize(this.getWidth() + insetsWidth, this.getHeight() + insetsHeight);
+
+ stage.setScene(scene);
+
+ if (stageStyle == StageStyle.TRANSPARENT) {
+ scene.setFill(null);
+ }
+
+ stage.setResizable(this.isStageResizable());
+ if (this.isStageResizable()) {
+ stage.addEventFilter(MouseEvent.MOUSE_PRESSED, this);
+ stage.addEventFilter(MouseEvent.MOUSE_MOVED, this);
+ stage.addEventFilter(MouseEvent.MOUSE_DRAGGED, this);
+ }
+
+ // we want to set the client area size
+ // without this it subtracts the native border sizes from the scene
+ // size
+ stage.sizeToScene();
+
+ stage.show();
+ } else if (!floating && this.isFloating()) {
+ this.floatingProperty.set(floating);
+
+ stage.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
+ stage.removeEventFilter(MouseEvent.MOUSE_MOVED, this);
+ stage.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
+
+ stage.close();
+ }
}
- this.floatableProperty.set(floatable);
- }
- /**
- * Boolean property maintaining whether this node is currently closable.
- *
- * @defaultValue true
- */
- public final BooleanProperty closableProperty() {
- return closableProperty;
- }
+ /**
+ * Whether the node is currently floating.
+ *
+ * @param floating Whether the node is currently floating.
+ */
+ public void setFloating(boolean floating) {
+ setFloating(floating, null);
+ setMinimizable(floating);
+ }
- private BooleanProperty closableProperty = new SimpleBooleanProperty(true) {
- @Override
- public String getName() {
- return "closable";
+ /**
+ * The dock pane that was last associated with this dock node. Either the dock pane that it is
+ * currently docked to or the one it was detached from. Can be null if the node was never
+ * docked.
+ *
+ * @return The dock pane that was last associated with this dock node.
+ */
+ public final DockPane getDockPane() {
+ return dockPane;
}
- };
- public final boolean isClosable() {
- return closableProperty.get();
- }
+ /**
+ * ViewController associated with this dock nodes contents, might be null
+ *
+ * @return ViewController associated with this dock nodes contents
+ */
+ public final DockFXViewController getViewController() {
+ return viewController;
+ }
- public final void setClosable(boolean closable) {
- this.closableProperty.set(closable);
- }
+ /**
+ * The dock title bar associated with this dock node.
+ *
+ * @return The dock title bar associated with this node.
+ */
+ public final DockTitleBar getDockTitleBar() {
+ return this.dockTitleBar;
+ }
- /**
- * Boolean property maintaining whether this node is currently resizable.
- *
- * @defaultValue true
- */
- public final BooleanProperty resizableProperty() {
- return stageResizableProperty;
- }
+ /**
+ * The stage associated with this dock node. Can be null if the dock node was never set to
+ * floating.
+ *
+ * @return The stage associated with this node.
+ */
+ public final Stage getStage() {
+ return stage;
+ }
- private BooleanProperty stageResizableProperty = new SimpleBooleanProperty(true) {
- @Override
- public String getName() {
- return "resizable";
- }
- };
-
- public final boolean isStageResizable() {
- return stageResizableProperty.get();
- }
-
- public final void setStageResizable(boolean resizable) {
- stageResizableProperty.set(resizable);
- }
-
- /**
- * Boolean property maintaining whether this node is currently docked. This is used by the dock
- * pane to inform the dock node whether it is currently docked.
- *
- * @defaultValue false
- */
- public final BooleanProperty dockedProperty() {
- return dockedProperty;
- }
-
- private BooleanProperty dockedProperty = new SimpleBooleanProperty(false) {
- @Override
- protected void invalidated() {
- if (get()) {
- if (dockTitleBar != null) {
- dockTitleBar.setVisible(true);
- dockTitleBar.setManaged(true);
+ /**
+ * The border pane used to parent this dock node when floating. Can be null if the dock node was
+ * never set to floating.
+ *
+ * @return The stage associated with this node.
+ */
+ public final BorderPane getBorderPane() {
+ return borderPane;
+ }
+
+ /**
+ * The contents managed by this dock node.
+ *
+ * @return The contents managed by this dock node.
+ */
+ public final Node getContents() {
+ return contents;
+ }
+
+ /**
+ * Object property maintaining bidirectional state of the caption graphic for this node with the
+ * dock title bar or stage.
+ *
+ * @defaultValue null
+ */
+ public final ObjectProperty graphicProperty() {
+ return graphicProperty;
+ }
+
+ private ObjectProperty graphicProperty = new SimpleObjectProperty() {
+ @Override
+ public String getName() {
+ return "graphic";
+ }
+ };
+
+ public final Node getGraphic() {
+ return graphicProperty.get();
+ }
+
+ public final void setGraphic(Node graphic) {
+ this.graphicProperty.setValue(graphic);
+ }
+
+ /**
+ * Boolean property maintaining bidirectional state of the caption title for this node with the
+ * dock title bar or stage.
+ *
+ * @defaultValue "Dock"
+ */
+ public final StringProperty titleProperty() {
+ return titleProperty;
+ }
+
+ private StringProperty titleProperty = new SimpleStringProperty("Dock") {
+ @Override
+ public String getName() {
+ return "title";
}
- }
+ };
+
+ public final String getTitle() {
+ return titleProperty.get();
+ }
+
+ public final void setTitle(String title) {
+ this.titleProperty.setValue(title);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently using a custom title bar. This
+ * can be used to force the default title bar to show when the dock node is set to floating
+ * instead of using native window borders.
+ *
+ * @defaultValue true
+ */
+ public final BooleanProperty customTitleBarProperty() {
+ return customTitleBarProperty;
+ }
+
+ private BooleanProperty customTitleBarProperty = new SimpleBooleanProperty(true) {
+ @Override
+ public String getName() {
+ return "customTitleBar";
+ }
+ };
+
+ public final boolean isCustomTitleBar() {
+ return customTitleBarProperty.get();
+ }
+
+ public final void setUseCustomTitleBar(boolean useCustomTitleBar) {
+ if (this.isFloating()) {
+ dockTitleBar.setVisible(useCustomTitleBar);
+ dockTitleBar.setManaged(useCustomTitleBar);
+ }
+ this.customTitleBarProperty.set(useCustomTitleBar);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently floating.
+ *
+ * @defaultValue false
+ */
+ public final BooleanProperty floatingProperty() {
+ return floatingProperty;
+ }
+
+ private BooleanProperty floatingProperty = new SimpleBooleanProperty(false) {
+ @Override
+ protected void invalidated() {
+ DockNode.this.pseudoClassStateChanged(FLOATING_PSEUDO_CLASS, get());
+ if (borderPane != null) {
+ borderPane.pseudoClassStateChanged(FLOATING_PSEUDO_CLASS, get());
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "floating";
+ }
+ };
+
+ public final boolean isFloating() {
+ return floatingProperty.get();
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently floatable.
+ *
+ * @defaultValue true
+ */
+ public final BooleanProperty floatableProperty() {
+ return floatableProperty;
+ }
+
+ private BooleanProperty floatableProperty = new SimpleBooleanProperty(true) {
+ @Override
+ public String getName() {
+ return "floatable";
+ }
+ };
+
+ public final boolean isFloatable() {
+ return floatableProperty.get();
+ }
+
+ public final void setFloatable(boolean floatable) {
+ if (!floatable && this.isFloating()) {
+ this.setFloating(false);
+ }
+ this.floatableProperty.set(floatable);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently closable.
+ *
+ * @defaultValue true
+ */
+ public final BooleanProperty closableProperty() {
+ return closableProperty;
+ }
+
+ private BooleanProperty closableProperty = new SimpleBooleanProperty(true) {
+ @Override
+ public String getName() {
+ return "closable";
+ }
+ };
+
+ public final boolean isClosable() {
+ return closableProperty.get();
+ }
- DockNode.this.pseudoClassStateChanged(DOCKED_PSEUDO_CLASS, get());
+ public final void setClosable(boolean closable) {
+ this.closableProperty.set(closable);
+ }
+
+ private BooleanProperty minimizedProperty = new SimpleBooleanProperty(false) {
+ @Override
+ public String getName() {
+ return "minimized";
+ }
+ ;
+
+ };
+
+ public final boolean isMinimized() {
+ return minimizedProperty.get();
+ }
+
+ public final void setMinimized(boolean minimized) {
+
+ if (minimized) {
+ stage.setIconified(true);
+ }
+
+ this.minimizedProperty.set(minimized);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is minimizable.
+ *
+ * @defaultValue true
+ */
+ public final BooleanProperty minimizableProperty() {
+ return minimizableProperty;
+ }
+
+ private BooleanProperty minimizableProperty = new SimpleBooleanProperty(true) {
+ @Override
+ public String getName() {
+ return "minimizable";
+ }
+ };
+
+ public final boolean isMinimizable() {
+ return minimizableProperty.get();
+ }
+
+ public final void setMinimizable(boolean minimizable) {
+ if (minimizable && !this.isMinimizable()) {
+ dockTitleBar.getChildren().add(2, dockTitleBar.getMinimizeButton());
+ } else if (!minimizable && this.isMinimizable()) {
+ dockTitleBar.getChildren().remove(dockTitleBar.getMinimizeButton());
+ }
+ this.minimizableProperty.set(minimizable);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently resizable.
+ *
+ * @defaultValue true
+ */
+ public final BooleanProperty resizableProperty() {
+ return stageResizableProperty;
+ }
+
+ private BooleanProperty stageResizableProperty = new SimpleBooleanProperty(true) {
+ @Override
+ public String getName() {
+ return "resizable";
+ }
+ };
+
+ public final boolean isStageResizable() {
+ return stageResizableProperty.get();
+ }
+
+ public final void setStageResizable(boolean resizable) {
+ stageResizableProperty.set(resizable);
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently docked. This is used by the dock
+ * pane to inform the dock node whether it is currently docked.
+ *
+ * @defaultValue false
+ */
+ public final BooleanProperty dockedProperty() {
+ return dockedProperty;
+ }
+
+ private BooleanProperty dockedProperty = new SimpleBooleanProperty(false) {
+ @Override
+ protected void invalidated() {
+ if (get()) {
+ if (dockTitleBar != null) {
+ dockTitleBar.setVisible(true);
+ dockTitleBar.setManaged(true);
+ }
+ }
+
+ DockNode.this.pseudoClassStateChanged(DOCKED_PSEUDO_CLASS, get());
+ }
+
+ @Override
+ public String getName() {
+ return "docked";
+ }
+ };
+
+ public final boolean isDocked() {
+ return dockedProperty.get();
+ }
+
+ public final BooleanProperty maximizedProperty() {
+ return maximizedProperty;
+ }
+
+ public final boolean isMaximized() {
+ return maximizedProperty.get();
+ }
+
+ public final boolean isDecorated() {
+ return stageStyle != StageStyle.TRANSPARENT && stageStyle != StageStyle.UNDECORATED;
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently tabbed.
+ *
+ * @defaultValue false
+ */
+ public final BooleanProperty tabbedProperty() {
+ return tabbedProperty;
+ }
+
+ private BooleanProperty tabbedProperty = new SimpleBooleanProperty(false) {
+ @Override
+ protected void invalidated() {
+
+ if (getChildren() != null) {
+ if (get()) {
+ getChildren().remove(dockTitleBar);
+ } else {
+ getChildren().add(0, dockTitleBar);
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "tabbed";
+ }
+ };
+
+ public final boolean isTabbed() {
+ return tabbedProperty.get();
+ }
+
+ /**
+ * Boolean property maintaining whether this node is currently closed.
+ */
+ public final BooleanProperty closedProperty() {
+ return closedProperty;
+ }
+
+ private BooleanProperty closedProperty = new SimpleBooleanProperty(false) {
+ @Override
+ protected void invalidated() {
+ }
+
+ @Override
+ public String getName() {
+ return "closed";
+ }
+ };
+
+ public final boolean isClosed() {
+ return closedProperty.get();
+ }
+
+ private DockPos lastDockPos;
+
+ public DockPos getLastDockPos() {
+ return lastDockPos;
+ }
+
+ private Node lastDockSibling;
+
+ public Node getLastDockSibling() {
+ return lastDockSibling;
+ }
+
+ /**
+ * Dock this node into a dock pane.
+ *
+ * @param dockPane The dock pane to dock this node into.
+ * @param dockPos The docking position relative to the sibling of the dock pane.
+ * @param sibling The sibling node to dock this node relative to.
+ */
+ public void dock(DockPane dockPane, DockPos dockPos, Node sibling) {
+ dockImpl(dockPane);
+ dockPane.dock(this, dockPos, sibling);
+ this.lastDockPos = dockPos;
+ this.lastDockSibling = sibling;
+ }
+
+ /**
+ * Dock this node into a dock pane.
+ *
+ * @param dockPane The dock pane to dock this node into.
+ * @param dockPos The docking position relative to the sibling of the dock pane.
+ */
+ public void dock(DockPane dockPane, DockPos dockPos) {
+ dockImpl(dockPane);
+ dockPane.dock(this, dockPos);
+ this.lastDockPos = dockPos;
+ }
+
+ private final void dockImpl(DockPane dockPane) {
+ if (isFloating()) {
+ setFloating(false);
+ }
+ this.dockPane = dockPane;
+ this.dockedProperty.set(true);
+ this.closedProperty.set(false);
+ }
+
+ /**
+ * Detach this node from its previous dock pane if it was previously docked.
+ */
+ public void undock() {
+ if (dockPane != null) {
+ dockPane.undock(this);
+ }
+ this.dockedProperty.set(false);
+ this.tabbedProperty.set(false);
+ }
+
+ /**
+ * Close this dock node by setting it to not floating and making sure it is detached from any
+ * dock pane.
+ */
+ public void close() {
+ this.closedProperty.set(true);
+ if (isFloating()) {
+ setFloating(false);
+ } else if (isDocked()) {
+ undock();
+ }
+ }
+
+ private DockNodeTab dockNodeTab;
+
+ public void setNodeTab(DockNodeTab nodeTab) {
+ this.dockNodeTab = nodeTab;
+ }
+
+ public void focus() {
+ if (tabbedProperty().get()) {
+ dockNodeTab.select();
+ }
+ }
+
+ /**
+ * The last position of the mouse that was within the minimum layout bounds.
+ */
+ private Point2D sizeLast;
+ /**
+ * Whether we are currently resizing in a given direction.
+ */
+ private boolean sizeWest = false, sizeEast = false, sizeNorth = false, sizeSouth = false;
+
+ /**
+ * Gets whether the mouse is currently in this dock node's resize zone.
+ *
+ * @return Whether the mouse is currently in this dock node's resize zone.
+ */
+ public boolean isMouseResizeZone() {
+ return sizeWest || sizeEast || sizeNorth || sizeSouth;
}
@Override
- public String getName() {
- return "docked";
- }
- };
-
- public final boolean isDocked() {
- return dockedProperty.get();
- }
-
- public final BooleanProperty maximizedProperty() {
- return maximizedProperty;
- }
-
- public final boolean isMaximized() {
- return maximizedProperty.get();
- }
-
- public final boolean isDecorated() {
- return stageStyle != StageStyle.TRANSPARENT && stageStyle != StageStyle.UNDECORATED;
- }
-
- /**
- * Dock this node into a dock pane.
- *
- * @param dockPane The dock pane to dock this node into.
- * @param dockPos The docking position relative to the sibling of the dock pane.
- * @param sibling The sibling node to dock this node relative to.
- */
- public void dock(DockPane dockPane, DockPos dockPos, Node sibling) {
- dockImpl(dockPane);
- dockPane.dock(this, dockPos, sibling);
- }
-
- /**
- * Dock this node into a dock pane.
- *
- * @param dockPane The dock pane to dock this node into.
- * @param dockPos The docking position relative to the sibling of the dock pane.
- */
- public void dock(DockPane dockPane, DockPos dockPos) {
- dockImpl(dockPane);
- dockPane.dock(this, dockPos);
- }
-
- private final void dockImpl(DockPane dockPane) {
- if (isFloating()) {
- setFloating(false);
- }
- this.dockPane = dockPane;
- this.dockedProperty.set(true);
- }
-
- /**
- * Detach this node from its previous dock pane if it was previously docked.
- */
- public void undock() {
- if (dockPane != null) {
- dockPane.undock(this);
- }
- this.dockedProperty.set(false);
- }
-
- /**
- * Close this dock node by setting it to not floating and making sure it is detached from any dock
- * pane.
- */
- public void close() {
- if (isFloating()) {
- setFloating(false);
- } else if (isDocked()) {
- undock();
- }
- }
-
- /**
- * The last position of the mouse that was within the minimum layout bounds.
- */
- private Point2D sizeLast;
- /**
- * Whether we are currently resizing in a given direction.
- */
- private boolean sizeWest = false, sizeEast = false, sizeNorth = false, sizeSouth = false;
-
- /**
- * Gets whether the mouse is currently in this dock node's resize zone.
- *
- * @return Whether the mouse is currently in this dock node's resize zone.
- */
- public boolean isMouseResizeZone() {
- return sizeWest || sizeEast || sizeNorth || sizeSouth;
- }
-
- @Override
- public void handle(MouseEvent event) {
- Cursor cursor = Cursor.DEFAULT;
-
- // TODO: use escape to cancel resize/drag operation like visual studio
- if (!this.isFloating() || !this.isStageResizable()) {
- return;
- }
-
- if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
- sizeLast = new Point2D(event.getScreenX(), event.getScreenY());
- } else if (event.getEventType() == MouseEvent.MOUSE_MOVED) {
- Insets insets = borderPane.getPadding();
-
- sizeWest = event.getX() < insets.getLeft();
- sizeEast = event.getX() > borderPane.getWidth() - insets.getRight();
- sizeNorth = event.getY() < insets.getTop();
- sizeSouth = event.getY() > borderPane.getHeight() - insets.getBottom();
-
- if (sizeWest) {
- if (sizeNorth) {
- cursor = Cursor.NW_RESIZE;
- } else if (sizeSouth) {
- cursor = Cursor.SW_RESIZE;
- } else {
- cursor = Cursor.W_RESIZE;
+ public void handle(MouseEvent event) {
+ Cursor cursor = Cursor.DEFAULT;
+
+ // TODO: use escape to cancel resize/drag operation like visual studio
+ if (!this.isFloating() || !this.isStageResizable()) {
+ return;
}
- } else if (sizeEast) {
- if (sizeNorth) {
- cursor = Cursor.NE_RESIZE;
- } else if (sizeSouth) {
- cursor = Cursor.SE_RESIZE;
- } else {
- cursor = Cursor.E_RESIZE;
- }
- } else if (sizeNorth) {
- cursor = Cursor.N_RESIZE;
- } else if (sizeSouth) {
- cursor = Cursor.S_RESIZE;
- }
-
- this.getScene().setCursor(cursor);
- } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED && this.isMouseResizeZone()) {
- Point2D sizeCurrent = new Point2D(event.getScreenX(), event.getScreenY());
- Point2D sizeDelta = sizeCurrent.subtract(sizeLast);
-
- double newX = stage.getX(), newY = stage.getY(), newWidth = stage.getWidth(),
- newHeight = stage.getHeight();
-
- if (sizeNorth) {
- newHeight -= sizeDelta.getY();
- newY += sizeDelta.getY();
- } else if (sizeSouth) {
- newHeight += sizeDelta.getY();
- }
-
- if (sizeWest) {
- newWidth -= sizeDelta.getX();
- newX += sizeDelta.getX();
- } else if (sizeEast) {
- newWidth += sizeDelta.getX();
- }
-
- // TODO: find a way to do this synchronously and eliminate the flickering of moving the stage
- // around, also file a bug report for this feature if a work around can not be found this
- // primarily occurs when dragging north/west but it also appears in native windows and Visual
- // Studio, so not that big of a concern.
- // Bug report filed:
- // https://bugs.openjdk.java.net/browse/JDK-8133332
- double currentX = sizeLast.getX(), currentY = sizeLast.getY();
- if (newWidth >= stage.getMinWidth()) {
- stage.setX(newX);
- stage.setWidth(newWidth);
- currentX = sizeCurrent.getX();
- }
-
- if (newHeight >= stage.getMinHeight()) {
- stage.setY(newY);
- stage.setHeight(newHeight);
- currentY = sizeCurrent.getY();
- }
- sizeLast = new Point2D(currentX, currentY);
- // we do not want the title bar getting these events
- // while we are actively resizing
- if (sizeNorth || sizeSouth || sizeWest || sizeEast) {
- event.consume();
- }
- }
- }
+
+ if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
+ sizeLast = new Point2D(event.getScreenX(), event.getScreenY());
+ } else if (event.getEventType() == MouseEvent.MOUSE_MOVED) {
+ Insets insets = borderPane.getPadding();
+
+ sizeWest = event.getX() < insets.getLeft();
+ sizeEast = event.getX() > borderPane.getWidth() - insets.getRight();
+ sizeNorth = event.getY() < insets.getTop();
+ sizeSouth = event.getY() > borderPane.getHeight() - insets.getBottom();
+
+ if (sizeWest) {
+ if (sizeNorth) {
+ cursor = Cursor.NW_RESIZE;
+ } else if (sizeSouth) {
+ cursor = Cursor.SW_RESIZE;
+ } else {
+ cursor = Cursor.W_RESIZE;
+ }
+ } else if (sizeEast) {
+ if (sizeNorth) {
+ cursor = Cursor.NE_RESIZE;
+ } else if (sizeSouth) {
+ cursor = Cursor.SE_RESIZE;
+ } else {
+ cursor = Cursor.E_RESIZE;
+ }
+ } else if (sizeNorth) {
+ cursor = Cursor.N_RESIZE;
+ } else if (sizeSouth) {
+ cursor = Cursor.S_RESIZE;
+ }
+
+ this.getScene().setCursor(cursor);
+ } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED && this.isMouseResizeZone()) {
+ Point2D sizeCurrent = new Point2D(event.getScreenX(), event.getScreenY());
+ Point2D sizeDelta = sizeCurrent.subtract(sizeLast);
+
+ double newX = stage.getX(), newY = stage.getY(), newWidth = stage.getWidth(),
+ newHeight = stage.getHeight();
+
+ if (sizeNorth) {
+ newHeight -= sizeDelta.getY();
+ newY += sizeDelta.getY();
+ } else if (sizeSouth) {
+ newHeight += sizeDelta.getY();
+ }
+
+ if (sizeWest) {
+ newWidth -= sizeDelta.getX();
+ newX += sizeDelta.getX();
+ } else if (sizeEast) {
+ newWidth += sizeDelta.getX();
+ }
+
+ // TODO: find a way to do this synchronously and eliminate the flickering of moving the stage
+ // around, also file a bug report for this feature if a work around can not be found this
+ // primarily occurs when dragging north/west but it also appears in native windows and Visual
+ // Studio, so not that big of a concern.
+ // Bug report filed:
+ // https://bugs.openjdk.java.net/browse/JDK-8133332
+ double currentX = sizeLast.getX(), currentY = sizeLast.getY();
+ if (newWidth >= stage.getMinWidth()) {
+ stage.setX(newX);
+ stage.setWidth(newWidth);
+ currentX = sizeCurrent.getX();
+ }
+
+ if (newHeight >= stage.getMinHeight()) {
+ stage.setY(newY);
+ stage.setHeight(newHeight);
+ currentY = sizeCurrent.getY();
+ }
+ sizeLast = new Point2D(currentX, currentY);
+ // we do not want the title bar getting these events
+ // while we are actively resizing
+ if (sizeNorth || sizeSouth || sizeWest || sizeEast) {
+ event.consume();
+ }
+ }
+ }
}
diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java
index 7ea7a94..18224a5 100644
--- a/src/main/java/org/dockfx/DockPane.java
+++ b/src/main/java/org/dockfx/DockPane.java
@@ -20,10 +20,36 @@
package org.dockfx;
+import java.beans.XMLDecoder;
+import java.beans.XMLEncoder;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Scanner;
import java.util.Stack;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import com.sun.javafx.css.StyleManager;
+import javafx.geometry.Bounds;
+import javafx.scene.control.Tab;
+import javafx.stage.Stage;
+
+import org.dockfx.pane.ContentPane;
+import org.dockfx.pane.ContentSplitPane;
+import org.dockfx.pane.ContentTabPane;
+import org.dockfx.pane.DockNodeTab;
+
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
@@ -38,7 +64,6 @@
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
-import javafx.scene.control.SplitPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
@@ -49,10 +74,11 @@
* Base class for a dock pane that provides the layout of the dock nodes. Stacking the dock nodes to
* the center in a TabPane will be added in a future release. For now the DockPane uses the relative
* sizes of the dock nodes and lays them out in a tree of SplitPanes.
- *
+ *
* @since DockFX 0.1
*/
public class DockPane extends StackPane implements EventHandler {
+
/**
* The current root node of this dock pane's layout.
*/
@@ -104,13 +130,15 @@ public class DockPane extends StackPane implements EventHandler {
*/
private Popup dockIndicatorPopup;
+
/**
* Base class for a dock indicator button that allows it to be displayed during a dock event and
* continue to receive input.
- *
+ *
* @since DockFX 0.1
*/
public class DockPosButton extends Button {
+
/**
* Whether this dock indicator button is used for docking a node relative to the root of the
* dock pane.
@@ -133,9 +161,9 @@ public DockPosButton(boolean dockRoot, DockPos dockPos) {
/**
* Whether this dock indicator button is used for docking a node relative to the root of the
* dock pane.
- *
+ *
* @param dockRoot Whether this indicator button is used for docking a node relative to the root
- * of the dock pane.
+ * of the dock pane.
*/
public final void setDockRoot(boolean dockRoot) {
this.dockRoot = dockRoot;
@@ -143,7 +171,7 @@ public final void setDockRoot(boolean dockRoot) {
/**
* The docking position indicated by this button.
- *
+ *
* @param dockPos The docking position indicated by this button.
*/
public final void setDockPos(DockPos dockPos) {
@@ -152,7 +180,7 @@ public final void setDockPos(DockPos dockPos) {
/**
* The docking position indicated by this button.
- *
+ *
* @return The docking position indicated by this button.
*/
public final DockPos getDockPos() {
@@ -162,9 +190,9 @@ public final DockPos getDockPos() {
/**
* Whether this dock indicator button is used for docking a node relative to the root of the
* dock pane.
- *
+ *
* @return Whether this indicator button is used for docking a node relative to the root of the
- * dock pane.
+ * dock pane.
*/
public final boolean isDockRoot() {
return dockRoot;
@@ -177,6 +205,7 @@ public final boolean isDockRoot() {
*/
private ObservableList dockPosButtons;
+ private ObservableList undockedNodes;
/**
* Creates a new DockPane adding event handlers for dock events and creating the indicator
@@ -253,18 +282,19 @@ public void handle(DockEvent event) {
dockLeftRoot.getStyleClass().add("dock-left-root");
// TODO: dockCenter goes first when tabs are added in a future version
- dockPosButtons = FXCollections.observableArrayList(dockTop, dockRight, dockBottom, dockLeft,
- dockTopRoot, dockRightRoot, dockBottomRoot, dockLeftRoot);
+ dockPosButtons =
+ FXCollections.observableArrayList(dockCenter, dockTop, dockRight, dockBottom, dockLeft,
+ dockTopRoot, dockRightRoot, dockBottomRoot, dockLeftRoot);
dockPosIndicator = new GridPane();
dockPosIndicator.add(dockTop, 1, 0);
dockPosIndicator.add(dockRight, 2, 1);
dockPosIndicator.add(dockBottom, 1, 2);
dockPosIndicator.add(dockLeft, 0, 1);
- // dockPosIndicator.add(dockCenter, 1, 1);
+ dockPosIndicator.add(dockCenter, 1, 1);
dockRootPane.getChildren().addAll(dockAreaIndicator, dockTopRoot, dockRightRoot, dockBottomRoot,
- dockLeftRoot);
+ dockLeftRoot);
dockIndicatorOverlay.getContent().add(dockRootPane);
dockIndicatorPopup.getContent().addAll(dockPosIndicator);
@@ -273,14 +303,16 @@ public void handle(DockEvent event) {
dockRootPane.getStyleClass().add("dock-root-pane");
dockPosIndicator.getStyleClass().add("dock-pos-indicator");
dockAreaIndicator.getStyleClass().add("dock-area-indicator");
+
+ undockedNodes = FXCollections.observableArrayList();
}
/**
* The Timeline used to animate the docking area indicator in the dock indicator overlay for this
* dock pane.
- *
+ *
* @return The Timeline used to animate the docking area indicator in the dock indicator overlay
- * for this dock pane.
+ * for this dock pane.
*/
public final Timeline getDockAreaStrokeTimeline() {
return dockAreaStrokeTimeline;
@@ -288,7 +320,7 @@ public final Timeline getDockAreaStrokeTimeline() {
/**
* Helper function to retrieve the URL of the default style sheet used by DockFX.
- *
+ *
* @return The URL of the default style sheet used by DockFX.
*/
public final static String getDefaultUserAgentStyleheet() {
@@ -314,10 +346,11 @@ public final static void initializeDefaultUserAgentStylesheet() {
* A wrapper to the type parameterized generic EventHandler that allows us to remove it from its
* listener when the dock node becomes detached. It is specifically used to monitor which dock
* node in this dock pane's layout we are currently dragging over.
- *
+ *
* @since DockFX 0.1
*/
private class DockNodeEventHandler implements EventHandler {
+
/**
* The node associated with this event handler that reports to the encapsulating dock pane.
*/
@@ -326,9 +359,9 @@ private class DockNodeEventHandler implements EventHandler {
/**
* Creates a default dock node event handler that will help this dock pane track the current
* docking area.
- *
+ *
* @param node The node that is to listen for docking events and report to the encapsulating
- * docking pane.
+ * docking pane.
*/
public DockNodeEventHandler(Node node) {
this.node = node;
@@ -344,142 +377,138 @@ public void handle(DockEvent event) {
* Dock the node into this dock pane at the given docking position relative to the sibling in the
* layout. This is used to relatively position the dock nodes to other nodes given their preferred
* size.
- *
- * @param node The node that is to be docked into this dock pane.
+ *
+ * @param node The node that is to be docked into this dock pane.
* @param dockPos The docking position of the node relative to the sibling.
* @param sibling The sibling of this node in the layout.
*/
- public void dock(Node node, DockPos dockPos, Node sibling) {
+ void dock(Node node, DockPos dockPos, Node sibling) {
DockNodeEventHandler dockNodeEventHandler = new DockNodeEventHandler(node);
dockNodeEventFilters.put(node, dockNodeEventHandler);
node.addEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler);
- SplitPane split = (SplitPane) root;
- if (split == null) {
- split = new SplitPane();
- split.getItems().add(node);
- root = split;
+ ContentPane pane = (ContentPane) root;
+ if (pane == null) {
+ pane = new ContentSplitPane(node);
+ root = (Node) pane;
this.getChildren().add(root);
return;
}
- // find the parent of the sibling
if (sibling != null && sibling != root) {
- Stack stack = new Stack();
+ Stack stack = new Stack<>();
stack.push((Parent) root);
- while (!stack.isEmpty()) {
- Parent parent = stack.pop();
-
- ObservableList children = parent.getChildrenUnmodifiable();
-
- if (parent instanceof SplitPane) {
- SplitPane splitPane = (SplitPane) parent;
- children = splitPane.getItems();
- }
-
- for (int i = 0; i < children.size(); i++) {
- if (children.get(i) == sibling) {
- split = (SplitPane) parent;
- } else if (children.get(i) instanceof Parent) {
- stack.push((Parent) children.get(i));
- }
- }
- }
+ pane = pane.getSiblingParent(stack, sibling);
}
- Orientation requestedOrientation = (dockPos == DockPos.LEFT || dockPos == DockPos.RIGHT)
- ? Orientation.HORIZONTAL : Orientation.VERTICAL;
-
- // if the orientation is different then reparent the split pane
- if (split.getOrientation() != requestedOrientation) {
- if (split.getItems().size() > 1) {
- SplitPane splitPane = new SplitPane();
- if (split == root && sibling == root) {
- this.getChildren().set(this.getChildren().indexOf(root), splitPane);
- splitPane.getItems().add(split);
- root = splitPane;
- } else {
- split.getItems().set(split.getItems().indexOf(sibling), splitPane);
- splitPane.getItems().add(sibling);
- }
-
- split = splitPane;
- }
- split.setOrientation(requestedOrientation);
+ if (pane == null) {
+ sibling = root;
+ dockPos = DockPos.RIGHT;
+ pane = (ContentPane) root;
}
- // finally dock the node to the correct split pane
- ObservableList splitItems = split.getItems();
+ if (dockPos == DockPos.CENTER) {
+ if (pane instanceof ContentSplitPane) {
+ // Create a ContentTabPane with two nodes
+ DockNode siblingNode = (DockNode) sibling;
+ DockNode newNode = (DockNode) node;
- double magnitude = 0;
+ ContentTabPane tabPane = new ContentTabPane();
- if (splitItems.size() > 0) {
- if (split.getOrientation() == Orientation.HORIZONTAL) {
- for (Node splitItem : splitItems) {
- magnitude += splitItem.prefWidth(0);
- }
- } else {
- for (Node splitItem : splitItems) {
- magnitude += splitItem.prefHeight(0);
- }
- }
- }
+ tabPane.addDockNodeTab(new DockNodeTab(siblingNode));
+ tabPane.addDockNodeTab(new DockNodeTab(newNode));
- if (dockPos == DockPos.LEFT || dockPos == DockPos.TOP) {
- int relativeIndex = 0;
- if (sibling != null && sibling != root) {
- relativeIndex = splitItems.indexOf(sibling);
- }
+ tabPane.setContentParent(pane);
- splitItems.add(relativeIndex, node);
+ double[] pos = ((ContentSplitPane) pane).getDividerPositions();
+ pane.set(sibling, tabPane);
+ ((ContentSplitPane) pane).setDividerPositions(pos);
+ }
+ } else {
+ // Otherwise, SplitPane is assumed.
+ Orientation requestedOrientation = (dockPos == DockPos.LEFT || dockPos == DockPos.RIGHT)
+ ? Orientation.HORIZONTAL : Orientation.VERTICAL;
+
+ if (pane instanceof ContentSplitPane) {
+ ContentSplitPane split = (ContentSplitPane) pane;
+
+ // if the orientation is different then reparent the split pane
+ if (split.getOrientation() != requestedOrientation) {
+ if (split.getItems().size() > 1) {
+ ContentSplitPane splitPane = new ContentSplitPane();
+
+ if (split == root && sibling == root) {
+ this.getChildren().set(this.getChildren().indexOf(root), splitPane);
+ splitPane.getItems().add(split);
+ root = splitPane;
+ } else {
+ split.set(sibling, splitPane);
+ splitPane.setContentParent(split);
+ splitPane.getItems().add(sibling);
+ }
- if (splitItems.size() > 1) {
- if (split.getOrientation() == Orientation.HORIZONTAL) {
- split.setDividerPosition(relativeIndex,
- node.prefWidth(0) / (magnitude + node.prefWidth(0)));
- } else {
- split.setDividerPosition(relativeIndex,
- node.prefHeight(0) / (magnitude + node.prefHeight(0)));
+ split = splitPane;
+ }
+ split.setOrientation(requestedOrientation);
+ pane = split;
}
- }
- } else if (dockPos == DockPos.RIGHT || dockPos == DockPos.BOTTOM) {
- int relativeIndex = splitItems.size();
- if (sibling != null && sibling != root) {
- relativeIndex = splitItems.indexOf(sibling) + 1;
- }
- splitItems.add(relativeIndex, node);
- if (splitItems.size() > 1) {
- if (split.getOrientation() == Orientation.HORIZONTAL) {
- split.setDividerPosition(relativeIndex - 1,
- 1 - node.prefWidth(0) / (magnitude + node.prefWidth(0)));
+ } else if (pane instanceof ContentTabPane) {
+ ContentSplitPane split = (ContentSplitPane) pane.getContentParent();
+
+ // if the orientation is different then reparent the split pane
+ if (split.getOrientation() != requestedOrientation) {
+ ContentSplitPane splitPane = new ContentSplitPane();
+ if (split == root && sibling == root) {
+ this.getChildren().set(this.getChildren().indexOf(root), splitPane);
+ splitPane.getItems().add(split);
+ root = splitPane;
+ } else {
+ pane.setContentParent(splitPane);
+ sibling = (Node) pane;
+ split.set(sibling, splitPane);
+ splitPane.setContentParent(split);
+ splitPane.getItems().add(sibling);
+ }
+ split = splitPane;
} else {
- split.setDividerPosition(relativeIndex - 1,
- 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0)));
+ sibling = (Node) pane;
}
+
+ split.setOrientation(requestedOrientation);
+ pane = split;
}
}
+ // Add a node to the proper pane
+ pane.addNode(root, sibling, node, dockPos);
+
+ if (undockedNodes.contains(node)) {
+ undockedNodes.remove(node);
+ }
}
/**
* Dock the node into this dock pane at the given docking position relative to the root in the
* layout. This is used to relatively position the dock nodes to other nodes given their preferred
* size.
- *
- * @param node The node that is to be docked into this dock pane.
+ *
+ * @param node The node that is to be docked into this dock pane.
* @param dockPos The docking position of the node relative to the sibling.
*/
- public void dock(Node node, DockPos dockPos) {
+ void dock(Node node, DockPos dockPos) {
dock(node, dockPos, root);
}
/**
* Detach the node from this dock pane removing it from the layout.
- *
+ *
* @param node The node that is to be removed from this dock pane.
*/
- public void undock(DockNode node) {
+ void undock(DockNode node) {
+ if(!node.closedProperty().get())
+ undockedNodes.add(node);
+
DockNodeEventHandler dockNodeEventHandler = dockNodeEventFilters.get(node);
node.removeEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler);
dockNodeEventFilters.remove(node);
@@ -487,61 +516,39 @@ public void undock(DockNode node) {
// depth first search to find the parent of the node
Stack findStack = new Stack();
findStack.push((Parent) root);
+
while (!findStack.isEmpty()) {
Parent parent = findStack.pop();
- ObservableList children = parent.getChildrenUnmodifiable();
-
- if (parent instanceof SplitPane) {
- SplitPane split = (SplitPane) parent;
- children = split.getItems();
- }
-
- for (int i = 0; i < children.size(); i++) {
- if (children.get(i) == node) {
- children.remove(i);
-
- // start from the root again and remove any SplitPane's with no children in them
- Stack clearStack = new Stack();
- clearStack.push((Parent) root);
- while (!clearStack.isEmpty()) {
- parent = clearStack.pop();
-
- children = parent.getChildrenUnmodifiable();
-
- if (parent instanceof SplitPane) {
- SplitPane split = (SplitPane) parent;
- children = split.getItems();
- }
+ if (parent instanceof ContentPane) {
+ ContentPane pane = (ContentPane) parent;
+ pane.removeNode(findStack, node);
- for (i = 0; i < children.size(); i++) {
- if (children.get(i) instanceof SplitPane) {
- SplitPane split = (SplitPane) children.get(i);
- if (split.getItems().size() < 1) {
- children.remove(i);
- continue;
- } else {
- clearStack.push(split);
- }
- }
+ // if there is only 1-tab left, we replace it with the SplitPane
+ if (pane.getChildrenList().size() == 1 &&
+ pane instanceof ContentTabPane &&
+ pane.getChildrenList().get(0) instanceof DockNode) {
- }
- }
+ List children = pane.getChildrenList();
+ Node sibling = children.get(0);
+ ContentPane contentParent = pane.getContentParent();
- return;
- } else if (children.get(i) instanceof Parent) {
- findStack.push((Parent) children.get(i));
+ contentParent.set((Node) pane, sibling);
+ ((DockNode) sibling).tabbedProperty().setValue(false);
}
}
}
+
}
@Override
public void handle(DockEvent event) {
if (event.getEventType() == DockEvent.DOCK_ENTER) {
if (!dockIndicatorOverlay.isShowing()) {
- Point2D topLeft = DockPane.this.localToScreen(0, 0);
- dockIndicatorOverlay.show(DockPane.this, topLeft.getX(), topLeft.getY());
+ Point2D originToScreen = root.localToScreen(0, 0);
+
+ dockIndicatorOverlay
+ .show(DockPane.this, originToScreen.getX(), originToScreen.getY());
}
} else if (event.getEventType() == DockEvent.DOCK_OVER) {
this.receivedEnter = false;
@@ -563,11 +570,12 @@ public void handle(DockEvent event) {
}
}
- if (dockPosDrag != null) {
- Point2D originToScene = dockAreaDrag.localToScene(0, 0);
+ if (dockPosDrag != null && dockAreaDrag != null) {
+ Point2D originToScene = dockAreaDrag.localToScreen(0, 0);
dockAreaIndicator.setVisible(true);
- dockAreaIndicator.relocate(originToScene.getX(), originToScene.getY());
+ dockAreaIndicator.relocate(originToScene.getX() - dockIndicatorOverlay.getAnchorX(),
+ originToScene.getY() - dockIndicatorOverlay.getAnchorY());
if (dockPosDrag == DockPos.RIGHT) {
dockAreaIndicator.setTranslateX(dockAreaDrag.getLayoutBounds().getWidth() / 2);
} else {
@@ -598,9 +606,9 @@ public void handle(DockEvent event) {
Point2D originToScreen = dockNodeDrag.localToScreen(0, 0);
double posX = originToScreen.getX() + dockNodeDrag.getLayoutBounds().getWidth() / 2
- - dockPosIndicator.getWidth() / 2;
+ - dockPosIndicator.getWidth() / 2;
double posY = originToScreen.getY() + dockNodeDrag.getLayoutBounds().getHeight() / 2
- - dockPosIndicator.getHeight() / 2;
+ - dockPosIndicator.getHeight() / 2;
if (!dockIndicatorPopup.isShowing()) {
dockIndicatorPopup.show(DockPane.this, posX, posY);
@@ -625,10 +633,285 @@ public void handle(DockEvent event) {
if ((event.getEventType() == DockEvent.DOCK_EXIT && !this.receivedEnter)
|| event.getEventType() == DockEvent.DOCK_RELEASED) {
- if (dockIndicatorPopup.isShowing()) {
+ if (dockIndicatorOverlay.isShowing()) {
dockIndicatorOverlay.hide();
+ }
+ if (dockIndicatorPopup.isShowing()) {
dockIndicatorPopup.hide();
}
}
}
+
+ public void storePreference(String filePath) {
+ ContentPane pane = (ContentPane) root;
+
+ HashMap contents = new HashMap<>();
+
+ // Floating Nodes collection
+ contents
+ .put("_FloatingNodes", new ContentHolder("_FloatingNodes", ContentHolder.Type.Collection));
+
+ List floatingNodes = new LinkedList<>(undockedNodes);
+
+ for (int i = 0; i < floatingNodes.size(); i++) {
+// System.out.println(floatingNodes.get(i).getTitle());
+ ContentHolder
+ floatingNode =
+ new ContentHolder(floatingNodes.get(i).getTitle(), ContentHolder.Type.FloatingNode);
+ floatingNode.addProperty("Title", floatingNodes.get(i).getTitle());
+ floatingNode.addProperty("Size", new Double[]{
+ floatingNodes.get(i).getLayoutBounds().getWidth(),
+ floatingNodes.get(i).getLayoutBounds().getHeight()
+ });
+
+ Point2D
+ loc =
+ floatingNodes.get(i).localToScreen(floatingNodes.get(i).getLayoutBounds().getMinX(),
+ floatingNodes.get(i).getLayoutBounds().getMinY());
+
+ floatingNode.addProperty("Position", new Double[]{
+ loc.getX(), loc.getY()
+ });
+
+ contents.get("_FloatingNodes").addChild(floatingNode);
+ }
+
+ // Prepare Docking Nodes collection
+ List dockingNodes = new LinkedList<>();
+
+ Integer count = 0;
+
+ checkPane(contents, pane, dockingNodes, count);
+
+ contents.get("0").addProperty("Size", new Double[]{this.getScene().getWindow().getWidth(),
+ this.getScene().getWindow().getHeight()});
+ contents.get("0").addProperty("Position", new Double[]{this.getScene().getWindow().getX(),
+ this.getScene().getWindow().getY()});
+
+ storeCollection(filePath, contents);
+ }
+
+ private Object loadCollection(String fileName) {
+ XMLDecoder e = null;
+ try {
+ e = new XMLDecoder(
+ new BufferedInputStream(
+ new FileInputStream(fileName)));
+ } catch (FileNotFoundException e1) {
+ e1.printStackTrace();
+ }
+
+ Object collection = e.readObject();
+
+ e.close();
+
+ return collection;
+ }
+
+ private void storeCollection(String fileName, Object collection) {
+ XMLEncoder e = null;
+ try {
+ e = new XMLEncoder(
+ new BufferedOutputStream(
+ new FileOutputStream(fileName)));
+ } catch (FileNotFoundException e1) {
+ e1.printStackTrace();
+ }
+
+ e.writeObject(collection);
+
+ e.close();
+ }
+
+ private ContentHolder checkPane(HashMap contents, ContentPane pane,
+ List dockingNodes, Integer count) {
+ ContentHolder holder = null;
+ if (pane instanceof ContentSplitPane) {
+ final String contentSplitPaneName = "" + count++;
+ ContentSplitPane splitPane = (ContentSplitPane) pane;
+
+ holder = new ContentHolder(contentSplitPaneName, ContentHolder.Type.SplitPane);
+ contents.put(contentSplitPaneName, holder);
+
+ holder.addProperty("Orientation", splitPane.getOrientation());
+ holder.addProperty("DividerPositions", splitPane.getDividerPositions());
+ } else if (pane instanceof ContentTabPane) {
+ final String contentTabPaneName = "" + count++;
+ ContentTabPane tabPane = (ContentTabPane) pane;
+
+ holder = new ContentHolder(contentTabPaneName, ContentHolder.Type.TabPane);
+ contents.put(contentTabPaneName, holder);
+
+ holder.addProperty("SelectedIndex", tabPane.getSelectionModel().getSelectedIndex());
+ }
+
+ for (Node node : pane.getChildrenList()) {
+ if (node instanceof DockNode) {
+ dockingNodes.add((DockNode) node);
+ holder.addChild(((DockNode) node).getTitle());
+// System.out.println( ( ( DockNode ) node ).getTitle() );
+ }
+// else
+// {
+// System.out.println( node.getClass().getCanonicalName() );
+// }
+
+ if (node instanceof ContentPane) {
+ holder.addChild(checkPane(contents, (ContentPane) node, dockingNodes, count));
+ }
+ }
+
+ return holder;
+ }
+
+ public void loadPreference(String filePath) {
+ loadPreference(filePath, null);
+ }
+
+ public void loadPreference(String filePath, DelayOpenHandler delayOpenHandler)
+ {
+ HashMap
+ contents =
+ (HashMap) loadCollection(filePath);
+
+ applyPane(contents, (ContentPane) root, delayOpenHandler);
+ }
+
+ private void collectDockNodes(HashMap dockNodes, ContentPane pane) {
+ for (Node node : pane.getChildrenList()) {
+ if (node instanceof DockNode) {
+ dockNodes.put(((DockNode) node).getTitle(), (DockNode) node);
+ }
+
+ if (node instanceof ContentPane) {
+ collectDockNodes(dockNodes, (ContentPane) node);
+ }
+ }
+ }
+
+ private void applyPane(HashMap contents, ContentPane root, DelayOpenHandler delayOpenHandler) {
+ // Collect the current pane information
+ HashMap dockNodes = new HashMap<>();
+
+ // undockNodes
+ for (DockNode node : undockedNodes) {
+ dockNodes.put(node.getTitle(), node);
+ }
+
+ collectDockNodes(dockNodes, root);
+
+ Double[] windowSize = (Double[]) contents.get("0").getProperties().get("Size");
+ Double[] windowPosition = (Double[]) contents.get("0").getProperties().get("Position");
+
+ Stage currentStage = (Stage) this.getScene().getWindow();
+ currentStage.setX(windowPosition[0]);
+ currentStage.setY(windowPosition[1]);
+
+ currentStage.setWidth(windowSize[0]);
+ currentStage.setHeight(windowSize[1]);
+
+ // Set floating docks according to the preference data
+ for (Object item : contents.get("_FloatingNodes").getChildren()) {
+ ContentHolder holder = (ContentHolder) item;
+ String title = holder.getProperties().getProperty("Title");
+ Double[] size = (Double[]) holder.getProperties().get("Size");
+ Double[] position = (Double[]) holder.getProperties().get("Position");
+ DockNode node = dockNodes.get(title);
+ if(null == node && null != delayOpenHandler)
+ node = delayOpenHandler.open(title);
+
+ if(null != node) {
+ node.setFloating(true, null);
+
+ node.getStage().setX(position[0]);
+ node.getStage().setY(position[1]);
+
+ node.getStage().setWidth(size[0]);
+ node.getStage().setHeight(size[1]);
+
+ dockNodes.remove(title);
+ }
+ else {
+ System.err.println(item + " is not present.");
+ }
+ }
+
+ // Restore dock location based on the preferences
+ // Make it sorted
+ ContentHolder rootHolder = contents.get("0");
+ Node newRoot = buildPane(rootHolder, dockNodes, delayOpenHandler);
+
+ this.root = newRoot;
+ this.getChildren().set(0, this.root);
+ }
+
+ private Node buildPane(ContentHolder holder, HashMap dockNodes, DelayOpenHandler delayOpenHandler) {
+ Node pane = null;
+ if (holder.getType().equals(ContentHolder.Type.SplitPane)) {
+ ContentSplitPane splitPane = new ContentSplitPane();
+ splitPane.setOrientation((Orientation) holder.getProperties().get("Orientation"));
+ splitPane.setDividerPositions((double[]) holder.getProperties().get("DividerPositions"));
+
+ for (Object item : holder.getChildren()) {
+ if (item instanceof String) {
+ // Use dock node
+ if(dockNodes.containsKey(item)) {
+ DockNode n = dockNodes.get(item);
+ if (n.tabbedProperty().get()) {
+ n.tabbedProperty().set(false);
+ }
+
+ splitPane.getItems().add(dockNodes.get(item));
+ }
+ else
+ {
+ // If delayOpenHandler is provided, we call it
+ if(delayOpenHandler != null) {
+ DockNode newNode = delayOpenHandler.open((String) item);
+ if (newNode.tabbedProperty().get()) {
+ newNode.tabbedProperty().set(false);
+ }
+
+ newNode.dockedProperty().set(true);
+ splitPane.getItems().add(newNode);
+ } else
+ System.err.println(item + " is not present.");
+ }
+ } else if (item instanceof ContentHolder) {
+ // Call this function recursively
+ splitPane.getItems().add(buildPane((ContentHolder) item, dockNodes, delayOpenHandler));
+ }
+ }
+
+ pane = splitPane;
+ } else if (holder.getType().equals(ContentHolder.Type.TabPane)) {
+ ContentTabPane tabPane = new ContentTabPane();
+
+ for (Object item : holder.getChildren()) {
+ if (item instanceof String) {
+ // Use dock node
+ if(dockNodes.containsKey(item)) {
+ tabPane.addDockNodeTab(new DockNodeTab(dockNodes.get(item)));
+ }
+ else
+ {
+ // If delayOpenHandler is provided, we call it
+ if(null != delayOpenHandler)
+ {
+ DockNode newNode = delayOpenHandler.open((String) item);
+ newNode.dockedProperty().set(true);
+ tabPane.addDockNodeTab(new DockNodeTab(newNode));
+ }
+ else
+ System.err.println(item + " is not present.");
+ }
+ }
+ }
+
+ tabPane.getSelectionModel().select((int) holder.getProperties().get("SelectedIndex"));
+ pane = tabPane;
+ }
+
+ return pane;
+ }
}
diff --git a/src/main/java/org/dockfx/DockTitleBar.java b/src/main/java/org/dockfx/DockTitleBar.java
index 4b1a2df..cd52f52 100644
--- a/src/main/java/org/dockfx/DockTitleBar.java
+++ b/src/main/java/org/dockfx/DockTitleBar.java
@@ -4,20 +4,20 @@
*
* @section License
*
- * This file is a part of the DockFX Library. Copyright (C) 2015 Robert B. Colton
+ * This file is a part of the DockFX Library. Copyright (C) 2015 Robert B. Colton
*
- * This program is free software: you can redistribute it and/or modify it under the terms
- * of the GNU Lesser General Public License as published by the Free Software Foundation,
- * either version 3 of the License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
*
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
*
- * You should have received a copy of the GNU Lesser General Public License along with this
- * program. If not, see .
- **/
-
+ * You should have received a copy of the GNU Lesser General Public License along with this program.
+ * If not, see .
+ *
+ */
package org.dockfx;
import java.util.HashMap;
@@ -25,6 +25,8 @@
import com.sun.javafx.stage.StageHelper;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
@@ -47,385 +49,416 @@
/**
* Base class for a dock node title bar that provides the mouse dragging functionality, captioning,
* docking, and state manipulation.
- *
+ *
* @since DockFX 0.1
*/
public class DockTitleBar extends HBox implements EventHandler {
- /**
- * The DockNode this node is a title bar for.
- */
- private DockNode dockNode;
- /**
- * The label node used for captioning and the graphic.
- */
- private Label label;
- /**
- * State manipulation buttons including close, maximize, detach, and restore.
- */
- private Button closeButton, stateButton;
-
- /**
- * Creates a default DockTitleBar with captions and dragging behavior.
- *
- * @param dockNode The docking node that requires a title bar.
- */
- public DockTitleBar(DockNode dockNode) {
- this.dockNode = dockNode;
-
- label = new Label("Dock Title Bar");
- label.textProperty().bind(dockNode.titleProperty());
- label.graphicProperty().bind(dockNode.graphicProperty());
-
- stateButton = new Button();
- stateButton.setOnAction(new EventHandler() {
- @Override
- public void handle(ActionEvent event) {
- if (dockNode.isFloating()) {
- dockNode.setMaximized(!dockNode.isMaximized());
- } else {
- dockNode.setFloating(true);
- }
- }
- });
-
- closeButton = new Button();
- closeButton.setOnAction(new EventHandler() {
-
- @Override
- public void handle(ActionEvent event) {
- dockNode.close();
- }
- });
- closeButton.visibleProperty().bind(dockNode.closableProperty());
-
- // create a pane that will stretch to make the buttons right aligned
- Pane fillPane = new Pane();
- HBox.setHgrow(fillPane, Priority.ALWAYS);
-
- getChildren().addAll(label, fillPane, stateButton, closeButton);
-
- this.addEventHandler(MouseEvent.MOUSE_PRESSED, this);
- this.addEventHandler(MouseEvent.DRAG_DETECTED, this);
- this.addEventHandler(MouseEvent.MOUSE_DRAGGED, this);
- this.addEventHandler(MouseEvent.MOUSE_RELEASED, this);
-
- label.getStyleClass().add("dock-title-label");
- closeButton.getStyleClass().add("dock-close-button");
- stateButton.getStyleClass().add("dock-state-button");
- this.getStyleClass().add("dock-title-bar");
-
- }
-
- /**
- * Whether this title bar is currently being dragged.
- *
- * @return Whether this title bar is currently being dragged.
- */
- public final boolean isDragging() {
- return dragging;
- }
-
- /**
- * The label used for captioning and to provide a graphic.
- *
- * @return The label used for captioning and to provide a graphic.
- */
- public final Label getLabel() {
- return label;
- }
-
- /**
- * The button used for closing this title bar and its associated dock node.
- *
- * @return The button used for closing this title bar and its associated dock node.
- */
- public final Button getCloseButton() {
- return closeButton;
- }
-
- /**
- * The button used for detaching, maximizing, or restoring this title bar and its associated dock
- * node.
- *
- * @return The button used for detaching, maximizing, or restoring this title bar and its
- * associated dock node.
- */
- public final Button getStateButton() {
- return stateButton;
- }
-
- /**
- * The dock node that is associated with this title bar.
- *
- * @return The dock node that is associated with this title bar.
- */
- public final DockNode getDockNode() {
- return dockNode;
- }
-
- /**
- * The mouse location of the original click which we can use to determine the offset during drag.
- * Title bar dragging is asynchronous so it will not be negatively impacted by less frequent or
- * lagging mouse events as in the case of most current JavaFX implementations on Linux.
- */
- private Point2D dragStart;
- /**
- * Whether this title bar is currently being dragged.
- */
- private boolean dragging = false;
- /**
- * The current node being dragged over for each window so we can keep track of enter/exit events.
- */
- private HashMap dragNodes = new HashMap();
-
- /**
- * The task that is to be executed when the dock event target is picked. This provides context for
- * what specific events and what order the events should be fired.
- *
- * @since DockFX 0.1
- */
- private abstract class EventTask {
/**
- * The number of times this task has been executed.
+ * The DockNode this node is a title bar for.
+ */
+ private DockNode dockNode;
+ /**
+ * The label node used for captioning and the graphic.
+ */
+ private Label label;
+ /**
+ * State manipulation buttons including close, maximize, detach, and restore.
*/
- protected int executions = 0;
+ private Button closeButton, stateButton, minimizeButton;
/**
* Creates a default DockTitleBar with captions and dragging behavior.
- *
- * @param node The node that was chosen as the event target.
- * @param dragNode The node that was last event target.
+ *
+ * @param dockNode The docking node that requires a title bar.
*/
- public abstract void run(Node node, Node dragNode);
+ public DockTitleBar(DockNode dockNode) {
+ this.dockNode = dockNode;
+
+ label = new Label("Dock Title Bar");
+ label.textProperty().bind(dockNode.titleProperty());
+ label.graphicProperty().bind(dockNode.graphicProperty());
+
+
+
+ stateButton = new Button();
+ stateButton.setOnAction(new EventHandler() {
+ @Override
+ public void handle(ActionEvent event) {
+ if (dockNode.isFloating()) {
+ dockNode.setMaximized(!dockNode.isMaximized());
+ } else {
+ dockNode.setFloating(true);
+ }
+ }
+ });
+
+ closeButton = new Button();
+ closeButton.setOnAction(new EventHandler() {
+
+ @Override
+ public void handle(ActionEvent event) {
+ dockNode.close();
+ }
+ });
+
+ minimizeButton = new Button();
+ minimizeButton.setOnAction(new EventHandler() {
+ public void handle(ActionEvent event) {
+ dockNode.setMinimized(true);
+ }
+ });
+
+ this.addEventHandler(MouseEvent.MOUSE_PRESSED, this);
+ this.addEventHandler(MouseEvent.DRAG_DETECTED, this);
+ this.addEventHandler(MouseEvent.MOUSE_DRAGGED, this);
+ this.addEventHandler(MouseEvent.MOUSE_RELEASED, this);
+
+ label.getStyleClass().add("dock-title-label");
+ closeButton.getStyleClass().add("dock-close-button");
+ stateButton.getStyleClass().add("dock-state-button");
+ minimizeButton.getStyleClass().add("dock-minimize-button");
+ this.getStyleClass().add("dock-title-bar");
+
+ // create a pane that will stretch to make the buttons right aligned
+ Pane fillPane = new Pane();
+ HBox.setHgrow(fillPane, Priority.ALWAYS);
+
+ super.getChildren().addAll(label, fillPane, minimizeButton, stateButton, closeButton);
+
+ dockNode.closableProperty().addListener(new ChangeListener< Boolean>() {
+ @Override
+ public void changed(ObservableValue< ? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+ if (newValue) {
+ if (!getChildren().contains(closeButton)) {
+ getChildren().add(closeButton);
+ }
+ } else {
+ getChildren().removeIf(c -> c.equals(closeButton));
+ }
+ }
+ });
+
+ }
/**
- * The number of times this task has been executed.
+ * Whether this title bar is currently being dragged.
*
- * @return The number of times this task has been executed.
+ * @return Whether this title bar is currently being dragged.
*/
- public int getExecutions() {
- return executions;
+ public final boolean isDragging() {
+ return dragging;
}
/**
- * Reset the execution count to zero.
+ * The label used for captioning and to provide a graphic.
+ *
+ * @return The label used for captioning and to provide a graphic.
*/
- public void reset() {
- executions = 0;
+ public final Label getLabel() {
+ return label;
}
- }
-
- /**
- * Traverse the scene graph for all open stages and pick an event target for a dock event based on
- * the location. Once the event target is chosen run the event task with the target and the
- * previous target of the last dock event if one is cached. If an event target is not found fire
- * the explicit dock event on the stage root if one is provided.
- *
- * @param location The location of the dock event in screen coordinates.
- * @param eventTask The event task to be run when the event target is found.
- * @param explicit The explicit event to be fired on the stage root when no event target is found.
- */
- private void pickEventTarget(Point2D location, EventTask eventTask, Event explicit) {
- // RFE for public scene graph traversal API filed but closed:
- // https://bugs.openjdk.java.net/browse/JDK-8133331
-
- ObservableList stages =
- FXCollections.unmodifiableObservableList(StageHelper.getStages());
- // fire the dock over event for the active stages
- for (Stage targetStage : stages) {
- // obviously this title bar does not need to receive its own events
- // though users of this library may want to know when their
- // dock node is being dragged by subclassing it or attaching
- // an event listener in which case a new event can be defined or
- // this continue behavior can be removed
- if (targetStage == this.dockNode.getStage())
- continue;
-
- eventTask.reset();
-
- Node dragNode = dragNodes.get(targetStage);
-
- Parent root = targetStage.getScene().getRoot();
- Stack stack = new Stack();
- if (root.contains(root.screenToLocal(location.getX(), location.getY()))
- && !root.isMouseTransparent()) {
- stack.push(root);
- }
- // depth first traversal to find the deepest node or parent with no children
- // that intersects the point of interest
- while (!stack.isEmpty()) {
- Parent parent = stack.pop();
- // if this parent contains the mouse click in screen coordinates in its local bounds
- // then traverse its children
- boolean notFired = true;
- for (Node node : parent.getChildrenUnmodifiable()) {
- if (node.contains(node.screenToLocal(location.getX(), location.getY()))
- && !node.isMouseTransparent()) {
- if (node instanceof Parent) {
- stack.push((Parent) node);
- } else {
- eventTask.run(node, dragNode);
- }
- notFired = false;
- break;
- }
- }
- // if none of the children fired the event or there were no children
- // fire it with the parent as the target to receive the event
- if (notFired) {
- eventTask.run(parent, dragNode);
- }
- }
- if (explicit != null && dragNode != null && eventTask.getExecutions() < 1) {
- Event.fireEvent(dragNode, explicit.copyFor(this, dragNode));
- dragNodes.put(targetStage, null);
- }
+ /**
+ * The button used for closing this title bar and its associated dock node.
+ *
+ * @return The button used for closing this title bar and its associated dock node.
+ */
+ public final Button getCloseButton() {
+ return closeButton;
}
- }
-
- @Override
- public void handle(MouseEvent event) {
- if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
- if (dockNode.isFloating() && event.getClickCount() == 2
- && event.getButton() == MouseButton.PRIMARY) {
- dockNode.setMaximized(!dockNode.isMaximized());
- } else {
- // drag detected is used in place of mouse pressed so there is some threshold for the
- // dragging which is determined by the default drag detection threshold
- dragStart = new Point2D(event.getX(), event.getY());
- }
- } else if (event.getEventType() == MouseEvent.DRAG_DETECTED) {
- if (!dockNode.isFloating()) {
- // if we are not using a custom title bar and the user
- // is not forcing the default one for floating and
- // the dock node does have native window decorations
- // then we need to offset the stage position by
- // the height of this title bar
- if (!dockNode.isCustomTitleBar() && dockNode.isDecorated()) {
- dockNode.setFloating(true, new Point2D(0, DockTitleBar.this.getHeight()));
- } else {
- dockNode.setFloating(true);
+
+ /**
+ * The button used for detaching, maximizing, or restoring this title bar and its associated
+ * dock node.
+ *
+ * @return The button used for detaching, maximizing, or restoring this title bar and its
+ * associated dock node.
+ */
+ public final Button getStateButton() {
+ return stateButton;
+ }
+
+ public final Button getMinimizeButton() {
+ return minimizeButton;
+ }
+
+ /**
+ * The dock node that is associated with this title bar.
+ *
+ * @return The dock node that is associated with this title bar.
+ */
+ public final DockNode getDockNode() {
+ return dockNode;
+ }
+
+ /**
+ * The mouse location of the original click which we can use to determine the offset during
+ * drag. Title bar dragging is asynchronous so it will not be negatively impacted by less
+ * frequent or lagging mouse events as in the case of most current JavaFX implementations on
+ * Linux.
+ */
+ private Point2D dragStart;
+ /**
+ * Whether this title bar is currently being dragged.
+ */
+ private boolean dragging = false;
+ /**
+ * The current node being dragged over for each window so we can keep track of enter/exit
+ * events.
+ */
+ private HashMap dragNodes = new HashMap();
+
+ /**
+ * The task that is to be executed when the dock event target is picked. This provides context
+ * for what specific events and what order the events should be fired.
+ *
+ * @since DockFX 0.1
+ */
+ private abstract class EventTask {
+
+ /**
+ * The number of times this task has been executed.
+ */
+ protected int executions = 0;
+
+ /**
+ * Creates a default DockTitleBar with captions and dragging behavior.
+ *
+ * @param node The node that was chosen as the event target.
+ * @param dragNode The node that was last event target.
+ */
+ public abstract void run(Node node, Node dragNode);
+
+ /**
+ * The number of times this task has been executed.
+ *
+ * @return The number of times this task has been executed.
+ */
+ public int getExecutions() {
+ return executions;
}
- // TODO: Find a better solution.
- // Temporary work around for nodes losing the drag event when removed from
- // the scene graph.
- // A possible alternative is to use "ghost" panes in the DockPane layout
- // while making DockNode simply an overlay stage that is always shown.
- // However since flickering when popping out was already eliminated that would
- // be overkill and is not a suitable solution for native decorations.
- // Bug report open: https://bugs.openjdk.java.net/browse/JDK-8133335
- DockPane dockPane = this.getDockNode().getDockPane();
- if (dockPane != null) {
- dockPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, this);
- dockPane.addEventFilter(MouseEvent.MOUSE_RELEASED, this);
+ /**
+ * Reset the execution count to zero.
+ */
+ public void reset() {
+ executions = 0;
}
- } else if (dockNode.isMaximized()) {
- double ratioX = event.getX() / this.getDockNode().getWidth();
- double ratioY = event.getY() / this.getDockNode().getHeight();
-
- // Please note that setMaximized is ruined by width and height changes occurring on the
- // stage and there is currently a bug report filed for this though I did not give them an
- // accurate test case which I should and wish I would have. This was causing issues in the
- // original release requiring maximized behavior to be implemented manually by saving the
- // restored bounds. The problem was that the resize functionality in DockNode.java was
- // executing at the same time canceling the maximized change.
- // https://bugs.openjdk.java.net/browse/JDK-8133334
-
- // restore/minimize the window after we have obtained its dimensions
- dockNode.setMaximized(false);
-
- // scale the drag start location by our restored dimensions
- dragStart = new Point2D(ratioX * dockNode.getWidth(), ratioY * dockNode.getHeight());
- }
- dragging = true;
- event.consume();
- } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
- if (dockNode.isFloating() && event.getClickCount() == 2
- && event.getButton() == MouseButton.PRIMARY) {
- event.setDragDetect(false);
- event.consume();
- return;
- }
-
- if (!dragging)
- return;
-
- Stage stage = dockNode.getStage();
- Insets insetsDelta = this.getDockNode().getBorderPane().getInsets();
-
- // dragging this way makes the interface more responsive in the event
- // the system is lagging as is the case with most current JavaFX
- // implementations on Linux
- stage.setX(event.getScreenX() - dragStart.getX() - insetsDelta.getLeft());
- stage.setY(event.getScreenY() - dragStart.getY() - insetsDelta.getTop());
-
- // TODO: change the pick result by adding a copyForPick()
- DockEvent dockEnterEvent =
- new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_ENTER, event.getX(),
- event.getY(), event.getScreenX(), event.getScreenY(), null);
- DockEvent dockOverEvent =
- new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_OVER, event.getX(),
- event.getY(), event.getScreenX(), event.getScreenY(), null);
- DockEvent dockExitEvent =
- new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_EXIT, event.getX(),
- event.getY(), event.getScreenX(), event.getScreenY(), null);
-
- EventTask eventTask = new EventTask() {
- @Override
- public void run(Node node, Node dragNode) {
- executions++;
-
- if (dragNode != node) {
- Event.fireEvent(node, dockEnterEvent.copyFor(DockTitleBar.this, node));
-
- if (dragNode != null) {
- // fire the dock exit first so listeners
- // can actually keep track of the node we
- // are currently over and know when we
- // aren't over any which DOCK_OVER
- // does not provide
- Event.fireEvent(dragNode, dockExitEvent.copyFor(DockTitleBar.this, dragNode));
+ }
+
+ /**
+ * Traverse the scene graph for all open stages and pick an event target for a dock event based
+ * on the location. Once the event target is chosen run the event task with the target and the
+ * previous target of the last dock event if one is cached. If an event target is not found fire
+ * the explicit dock event on the stage root if one is provided.
+ *
+ * @param location The location of the dock event in screen coordinates.
+ * @param eventTask The event task to be run when the event target is found.
+ * @param explicit The explicit event to be fired on the stage root when no event target is
+ * found.
+ */
+ private void pickEventTarget(Point2D location, EventTask eventTask, Event explicit) {
+ // RFE for public scene graph traversal API filed but closed:
+ // https://bugs.openjdk.java.net/browse/JDK-8133331
+
+ ObservableList stages
+ = FXCollections.unmodifiableObservableList(StageHelper.getStages());
+ // fire the dock over event for the active stages
+ for (Stage targetStage : stages) {
+ // obviously this title bar does not need to receive its own events
+ // though users of this library may want to know when their
+ // dock node is being dragged by subclassing it or attaching
+ // an event listener in which case a new event can be defined or
+ // this continue behavior can be removed
+ if (targetStage == this.dockNode.getStage()) {
+ continue;
}
- dragNodes.put(node.getScene().getWindow(), node);
- }
- Event.fireEvent(node, dockOverEvent.copyFor(DockTitleBar.this, node));
- }
- };
-
- this.pickEventTarget(new Point2D(event.getScreenX(), event.getScreenY()), eventTask,
- dockExitEvent);
- } else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) {
- dragging = false;
-
- DockEvent dockReleasedEvent =
- new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_RELEASED, event.getX(),
- event.getY(), event.getScreenX(), event.getScreenY(), null, this.getDockNode());
-
- EventTask eventTask = new EventTask() {
- @Override
- public void run(Node node, Node dragNode) {
- executions++;
- if (dragNode != node) {
- Event.fireEvent(node, dockReleasedEvent.copyFor(DockTitleBar.this, node));
- }
- Event.fireEvent(node, dockReleasedEvent.copyFor(DockTitleBar.this, node));
+ eventTask.reset();
+
+ Node dragNode = dragNodes.get(targetStage);
+
+ Parent root = targetStage.getScene().getRoot();
+ Stack stack = new Stack();
+ if (root.contains(root.screenToLocal(location.getX(), location.getY()))
+ && !root.isMouseTransparent()) {
+ stack.push(root);
+ }
+ // depth first traversal to find the deepest node or parent with no children
+ // that intersects the point of interest
+ while (!stack.isEmpty()) {
+ Parent parent = stack.pop();
+ // if this parent contains the mouse click in screen coordinates in its local bounds
+ // then traverse its children
+ boolean notFired = true;
+ for (Node node : parent.getChildrenUnmodifiable()) {
+ if (node.contains(node.screenToLocal(location.getX(), location.getY()))
+ && !node.isMouseTransparent()) {
+ if (node instanceof Parent) {
+ stack.push((Parent) node);
+ } else {
+ eventTask.run(node, dragNode);
+ }
+ notFired = false;
+ break;
+ }
+ }
+ // if none of the children fired the event or there were no children
+ // fire it with the parent as the target to receive the event
+ if (notFired) {
+ eventTask.run(parent, dragNode);
+ }
+ }
+
+ if (explicit != null && dragNode != null && eventTask.getExecutions() < 1) {
+ Event.fireEvent(dragNode, explicit.copyFor(this, dragNode));
+ dragNodes.put(targetStage, null);
+ }
}
- };
+ }
- this.pickEventTarget(new Point2D(event.getScreenX(), event.getScreenY()), eventTask, null);
+ @Override
+ public void handle(MouseEvent event) {
+ if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
+ if (dockNode.isFloating() && event.getClickCount() == 2
+ && event.getButton() == MouseButton.PRIMARY) {
+ dockNode.setMaximized(!dockNode.isMaximized());
+ } else {
+ // drag detected is used in place of mouse pressed so there is some threshold for the
+ // dragging which is determined by the default drag detection threshold
+ dragStart = new Point2D(event.getX(), event.getY());
+ }
+ } else if (event.getEventType() == MouseEvent.DRAG_DETECTED) {
+ if (!dockNode.isFloating()) {
+ // if we are not using a custom title bar and the user
+ // is not forcing the default one for floating and
+ // the dock node does have native window decorations
+ // then we need to offset the stage position by
+ // the height of this title bar
+ if (!dockNode.isCustomTitleBar() && dockNode.isDecorated()) {
+ dockNode.setFloating(true, new Point2D(0, DockTitleBar.this.getHeight()));
+ } else {
+ dockNode.setFloating(true);
+ }
+
+ // TODO: Find a better solution.
+ // Temporary work around for nodes losing the drag event when removed from
+ // the scene graph.
+ // A possible alternative is to use "ghost" panes in the DockPane layout
+ // while making DockNode simply an overlay stage that is always shown.
+ // However since flickering when popping out was already eliminated that would
+ // be overkill and is not a suitable solution for native decorations.
+ // Bug report open: https://bugs.openjdk.java.net/browse/JDK-8133335
+ DockPane dockPane = this.getDockNode().getDockPane();
+ if (dockPane != null) {
+ dockPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, this);
+ dockPane.addEventFilter(MouseEvent.MOUSE_RELEASED, this);
+ }
+ } else if (dockNode.isMaximized()) {
+ double ratioX = event.getX() / this.getDockNode().getWidth();
+ double ratioY = event.getY() / this.getDockNode().getHeight();
+
+ // Please note that setMaximized is ruined by width and height changes occurring on the
+ // stage and there is currently a bug report filed for this though I did not give them an
+ // accurate test case which I should and wish I would have. This was causing issues in the
+ // original release requiring maximized behavior to be implemented manually by saving the
+ // restored bounds. The problem was that the resize functionality in DockNode.java was
+ // executing at the same time canceling the maximized change.
+ // https://bugs.openjdk.java.net/browse/JDK-8133334
+ // restore/minimize the window after we have obtained its dimensions
+ dockNode.setMaximized(false);
+
+ // scale the drag start location by our restored dimensions
+ dragStart = new Point2D(ratioX * dockNode.getWidth(), ratioY * dockNode.getHeight());
+ }
+ dragging = true;
+ event.consume();
+ } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
+ if (dockNode.isFloating() && event.getClickCount() == 2
+ && event.getButton() == MouseButton.PRIMARY) {
+ event.setDragDetect(false);
+ event.consume();
+ return;
+ }
- dragNodes.clear();
+ if (!dragging) {
+ return;
+ }
- // Remove temporary event handler for bug mentioned above.
- DockPane dockPane = this.getDockNode().getDockPane();
- if (dockPane != null) {
- dockPane.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
- dockPane.removeEventFilter(MouseEvent.MOUSE_RELEASED, this);
- }
+ Stage stage = dockNode.getStage();
+ Insets insetsDelta = this.getDockNode().getBorderPane().getInsets();
+
+ // dragging this way makes the interface more responsive in the event
+ // the system is lagging as is the case with most current JavaFX
+ // implementations on Linux
+ stage.setX(event.getScreenX() - dragStart.getX() - insetsDelta.getLeft());
+ stage.setY(event.getScreenY() - dragStart.getY() - insetsDelta.getTop());
+
+ // TODO: change the pick result by adding a copyForPick()
+ DockEvent dockEnterEvent
+ = new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_ENTER, event.getX(),
+ event.getY(), event.getScreenX(), event.getScreenY(), null);
+ DockEvent dockOverEvent
+ = new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_OVER, event.getX(),
+ event.getY(), event.getScreenX(), event.getScreenY(), null);
+ DockEvent dockExitEvent
+ = new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_EXIT, event.getX(),
+ event.getY(), event.getScreenX(), event.getScreenY(), null);
+
+ EventTask eventTask = new EventTask() {
+ @Override
+ public void run(Node node, Node dragNode) {
+ executions++;
+
+ if (dragNode != node) {
+ Event.fireEvent(node, dockEnterEvent.copyFor(DockTitleBar.this, node));
+
+ if (dragNode != null) {
+ // fire the dock exit first so listeners
+ // can actually keep track of the node we
+ // are currently over and know when we
+ // aren't over any which DOCK_OVER
+ // does not provide
+ Event.fireEvent(dragNode, dockExitEvent.copyFor(DockTitleBar.this, dragNode));
+ }
+
+ dragNodes.put(node.getScene().getWindow(), node);
+ }
+ Event.fireEvent(node, dockOverEvent.copyFor(DockTitleBar.this, node));
+ }
+ };
+
+ this.pickEventTarget(new Point2D(event.getScreenX(), event.getScreenY()), eventTask,
+ dockExitEvent);
+ } else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) {
+ dragging = false;
+
+ DockEvent dockReleasedEvent
+ = new DockEvent(this, DockEvent.NULL_SOURCE_TARGET, DockEvent.DOCK_RELEASED, event.getX(),
+ event.getY(), event.getScreenX(), event.getScreenY(), null, this.getDockNode());
+
+ EventTask eventTask = new EventTask() {
+ @Override
+ public void run(Node node, Node dragNode) {
+ executions++;
+ if (dragNode != node) {
+ Event.fireEvent(node, dockReleasedEvent.copyFor(DockTitleBar.this, node));
+ }
+ Event.fireEvent(node, dockReleasedEvent.copyFor(DockTitleBar.this, node));
+ }
+ };
+
+ this.pickEventTarget(new Point2D(event.getScreenX(), event.getScreenY()), eventTask, null);
+
+ dragNodes.clear();
+
+ // Remove temporary event handler for bug mentioned above.
+ DockPane dockPane = this.getDockNode().getDockPane();
+ if (dockPane != null) {
+ dockPane.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
+ dockPane.removeEventFilter(MouseEvent.MOUSE_RELEASED, this);
+ }
+ }
}
- }
}
diff --git a/src/main/java/org/dockfx/demo/DockFX.java b/src/main/java/org/dockfx/demo/DockFX.java
index 6810fff..109700c 100644
--- a/src/main/java/org/dockfx/demo/DockFX.java
+++ b/src/main/java/org/dockfx/demo/DockFX.java
@@ -21,11 +21,20 @@
package org.dockfx.demo;
+import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Random;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.layout.BorderPane;
import org.dockfx.DockNode;
import org.dockfx.DockPane;
import org.dockfx.DockPos;
@@ -40,8 +49,11 @@
import javafx.scene.control.TreeView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
import javafx.scene.web.HTMLEditor;
import javafx.stage.Stage;
+import javafx.stage.StageStyle;
public class DockFX extends Application {
@@ -98,20 +110,63 @@ public void start(Stage primaryStage) {
tableDock.setPrefSize(300, 100);
tableDock.dock(dockPane, DockPos.BOTTOM);
- primaryStage.setScene(new Scene(dockPane, 800, 500));
- primaryStage.sizeToScene();
+ MenuItem saveMenuItem = new MenuItem("Save");
+ saveMenuItem.setOnAction(new EventHandler() {
+ @Override
+ public void handle(ActionEvent event) {
+ if(dirExist(getUserDataDirectory()))
+ dockPane.storePreference(getUserDataDirectory() + "dock.pref");
+ }
+ });
+ MenuItem restoreMenuItem = new MenuItem("Restore");
+ restoreMenuItem.setOnAction(new EventHandler() {
+ @Override
+ public void handle(ActionEvent event) {
+ dockPane.loadPreference(getUserDataDirectory() + "dock.pref");
+ }
+ });
+
+ Menu fileMenu = new Menu("File");
+ MenuBar menuBar = new MenuBar(fileMenu);
+ fileMenu.getItems().addAll(saveMenuItem, restoreMenuItem);
+
+ BorderPane mainBorderPane = new BorderPane();
+ mainBorderPane.setTop(menuBar);
+ mainBorderPane.setCenter(dockPane);
+
+ // show that overlays are relative to the docking area
+ mainBorderPane.setLeft(new AnchorPane(generateRandomTree()));
+
+ primaryStage.setScene(new Scene(mainBorderPane, 800, 500));
+ primaryStage.sizeToScene();
+
primaryStage.show();
+
+ //Stage s = new Stage(StageStyle.DECORATED);
+ //s.initOwner(primaryStage);
+
+ //s.show();
// can be created and docked before or after the scene is created
// and the stage is shown
- DockNode treeDock = new DockNode(generateRandomTree(), "Tree Dock", new ImageView(dockImage));
+ DockNode treeDock = new DockNode(generateRandomTree(), "Tree Dock1", new ImageView(dockImage));
treeDock.setPrefSize(100, 100);
treeDock.dock(dockPane, DockPos.LEFT);
- treeDock = new DockNode(generateRandomTree(), "Tree Dock", new ImageView(dockImage));
+ treeDock = new DockNode(generateRandomTree(), "Tree Dock2", new ImageView(dockImage));
treeDock.setPrefSize(100, 100);
treeDock.dock(dockPane, DockPos.RIGHT);
+ // If you want to get notified when the docknode is closed. You can add ChangeListener to DockNode's closedProperty()
+ treeDock.closedProperty().addListener( new ChangeListener< Boolean >()
+ {
+ @Override public void changed( ObservableValue< ? extends Boolean > observable, Boolean oldValue, Boolean newValue )
+ {
+ if(newValue)
+ System.out.println("TreeDock(DockPos.RIGHT) is closed.");
+ }
+ } );
+
// test the look and feel with both Caspian and Modena
Application.setUserAgentStylesheet(Application.STYLESHEET_MODENA);
// initialize the default styles for the dock pane and undocked nodes using the DockFX
@@ -144,4 +199,23 @@ private TreeView generateRandomTree() {
return treeView;
}
+
+ public static boolean dirExist(String dir)
+ {
+ String path = getUserDataDirectory();
+ if(!new File(path).exists())
+ return new File(path).mkdirs();
+ else
+ return true;
+ }
+
+ public static String getUserDataDirectory() {
+ return System.getProperty("user.home") + File.separator + ".dockfx" + File.separator
+ + getApplicationVersionString() + File.separator;
+ }
+
+ public static String getApplicationVersionString() {
+ return "1.0";
+ }
+
}
diff --git a/src/main/java/org/dockfx/pane/ContentPane.java b/src/main/java/org/dockfx/pane/ContentPane.java
new file mode 100644
index 0000000..78dd745
--- /dev/null
+++ b/src/main/java/org/dockfx/pane/ContentPane.java
@@ -0,0 +1,103 @@
+package org.dockfx.pane;
+
+import org.dockfx.DockPos;
+
+import java.util.List;
+import java.util.Stack;
+
+import javafx.scene.Node;
+import javafx.scene.Parent;
+
+/**
+ * ContentPane interface has common functions for Content window
+ *
+ * @author HongKee Moon
+ */
+public interface ContentPane {
+
+ /**
+ * The enum ContentPane Type.
+ */
+ public enum Type {
+ /**
+ * The SplitPane.
+ */
+ SplitPane,
+ /**
+ * The TabPane.
+ */
+ TabPane
+ }
+
+ /**
+ * Gets type of ContentPane.
+ *
+ * @return the type
+ */
+ Type getType();
+
+ /**
+ * Add a node.
+ *
+ * @param root the root
+ * @param sibling the sibling
+ * @param node the node
+ * @param dockPos the dock pos
+ */
+ void addNode(Node root, Node sibling, Node node, DockPos dockPos);
+
+ /**
+ * Remove the node.
+ *
+ * @param stack the stack
+ * @param node the node
+ * @return true if the node removed successfully, otherwise false
+ */
+ boolean removeNode(Stack stack, Node node);
+
+ /**
+ * Gets sibling's parent.
+ *
+ * @param stack the stack
+ * @param sibling the sibling
+ * @return the sibling parent
+ */
+ ContentPane getSiblingParent(Stack stack, Node sibling);
+
+ /**
+ * Gets children list.
+ *
+ * @return the children list
+ */
+ List getChildrenList();
+
+ /**
+ * Replace the previous element with new one
+ *
+ * @param sibling the sibling
+ * @param node the node
+ */
+ void set(Node sibling, Node node);
+
+ /**
+ * Replace the previous element with new one by index id
+ *
+ * @param idx the idx
+ * @param node the node
+ */
+ void set(int idx, Node node);
+
+ /**
+ * Gets content parent.
+ *
+ * @return the content parent
+ */
+ ContentPane getContentParent();
+
+ /**
+ * Sets content parent.
+ *
+ * @param pane the pane
+ */
+ void setContentParent(ContentPane pane);
+}
diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java
new file mode 100644
index 0000000..0563a5a
--- /dev/null
+++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java
@@ -0,0 +1,191 @@
+package org.dockfx.pane;
+
+import java.util.Comparator;
+import org.dockfx.DockNode;
+import org.dockfx.DockPos;
+
+import java.util.List;
+import java.util.Stack;
+
+import javafx.collections.ObservableList;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.control.SplitPane;
+
+/**
+ * ContentSplitPane contains multiple SplitPane
+ *
+ * @author Robert B. Colton
+ * @author HongKee Moon
+ */
+public class ContentSplitPane extends SplitPane implements ContentPane {
+
+ /**
+ * The Parent.
+ */
+ ContentPane parent;
+
+ public Type getType() {
+ return Type.SplitPane;
+ }
+
+ public void setContentParent(ContentPane pane) {
+ parent = pane;
+ }
+
+ public ContentPane getContentParent() {
+ return parent;
+ }
+
+ /**
+ * Instantiates a new ContentSplitPane
+ */
+ public ContentSplitPane() {
+ }
+
+ /**
+ * Instantiates a new ContentSplitPane.
+ *
+ * @param node the node
+ */
+ public ContentSplitPane(Node node) {
+ getItems().add(node);
+ }
+
+ public ContentPane getSiblingParent(Stack stack, Node sibling) {
+ ContentPane pane = null;
+
+ while (!stack.isEmpty()) {
+ Parent parent = stack.pop();
+
+ List children = parent.getChildrenUnmodifiable();
+
+ if (parent instanceof ContentPane) {
+ children = ((ContentPane) parent).getChildrenList();
+ }
+
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i) == sibling) {
+ pane = (ContentPane) parent;
+ } else if (children.get(i) instanceof Parent) {
+ stack.push((Parent) children.get(i));
+ }
+ }
+ }
+ return pane;
+ }
+
+
+ public boolean removeNode(Stack stack, Node node) {
+ ContentPane pane;
+
+ List children = getChildrenList();
+
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i) == node) {
+ getItems().remove(i);
+ return true;
+ }
+ else if (children.get(i) instanceof ContentPane) {
+ pane = (ContentPane) children.get(i);
+ if(pane.removeNode(stack, node))
+ {
+ if( pane.getChildrenList().size() < 1) {
+ getItems().remove(i);
+ return true;
+ }
+ else if(pane.getChildrenList().size() == 1 &&
+ pane instanceof ContentTabPane &&
+ pane.getChildrenList().get(0) instanceof DockNode)
+ {
+ List childrenList = pane.getChildrenList();
+ Node sibling = childrenList.get(0);
+ ContentPane contentParent = pane.getContentParent();
+
+ contentParent.set((Node) pane, sibling);
+ ((DockNode)sibling).tabbedProperty().setValue(false);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public List getChildrenList() {
+ return getItems();
+ }
+
+ public void set(int idx, Node node) {
+ getItems().set(idx, node);
+ }
+
+ public void set(Node sibling, Node node) {
+ set(getItems().indexOf(sibling), node);
+ }
+
+ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) {
+ // finally dock the node to the correct split pane
+ ObservableList splitItems = getItems();
+
+ double magnitude = 0;
+
+ if (splitItems.size() > 0) {
+ if (getOrientation() == Orientation.HORIZONTAL) {
+ for (Node splitItem : splitItems) {
+ magnitude += splitItem.prefWidth(0);
+ }
+ } else {
+ for (Node splitItem : splitItems) {
+ magnitude += splitItem.prefHeight(0);
+ }
+ }
+ }
+
+ if (dockPos == DockPos.LEFT || dockPos == DockPos.TOP) {
+ int relativeIndex = 0;
+ if (sibling != null && sibling != root) {
+ relativeIndex = splitItems.indexOf(sibling);
+ }
+
+ splitItems.add(relativeIndex, node);
+
+ if (splitItems.size() > 1) {
+ if (getOrientation() == Orientation.HORIZONTAL) {
+ setDividerPosition(relativeIndex,
+ node.prefWidth(0) / (magnitude + node.prefWidth(0)));
+ } else {
+ setDividerPosition(relativeIndex,
+ node.prefHeight(0) / (magnitude + node.prefHeight(0)));
+ }
+ }
+ } else if (dockPos == DockPos.RIGHT || dockPos == DockPos.BOTTOM) {
+ int relativeIndex = splitItems.size();
+ if (sibling != null && sibling != root) {
+ relativeIndex = splitItems.indexOf(sibling) + 1;
+ }
+
+ splitItems.add(relativeIndex, node);
+ if (splitItems.size() > 1) {
+ if (getOrientation() == Orientation.HORIZONTAL) {
+ setDividerPosition(relativeIndex - 1,
+ 1 - node.prefWidth(0) / (magnitude + node.prefWidth(0)));
+ } else {
+ setDividerPosition(relativeIndex - 1,
+ 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0)));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected double computeMaxWidth(double height) {
+ if (getOrientation() == Orientation.VERTICAL) {
+ return getItems().stream().map(i -> i.maxWidth(height)).min(Comparator.naturalOrder()).get();
+ }
+
+ return super.computeMaxWidth(height);
+ }
+}
diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java
new file mode 100644
index 0000000..36eba39
--- /dev/null
+++ b/src/main/java/org/dockfx/pane/ContentTabPane.java
@@ -0,0 +1,107 @@
+package org.dockfx.pane;
+
+import java.util.Comparator;
+import org.dockfx.DockNode;
+import org.dockfx.DockPos;
+
+import java.util.List;
+import java.util.Stack;
+
+import java.util.stream.Collectors;
+
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.control.TabPane;
+
+/**
+ * ContentTabPane holds multiple tabs
+ *
+ * @author HongKee Moon
+ */
+public class ContentTabPane extends TabPane implements ContentPane {
+
+ ContentPane parent;
+
+ public ContentTabPane() {
+ this.setStyle("-fx-skin: \"org.dockfx.pane.skin.ContentTabPaneSkin\";");
+ }
+
+ public Type getType() {
+ return Type.TabPane;
+ }
+
+ public void setContentParent(ContentPane pane) {
+ parent = pane;
+ }
+
+ public ContentPane getContentParent() {
+ return parent;
+ }
+
+ public ContentPane getSiblingParent(Stack stack, Node sibling) {
+ ContentPane pane = null;
+
+ while (!stack.isEmpty()) {
+ Parent parent = stack.pop();
+
+ List children = parent.getChildrenUnmodifiable();
+
+ if (parent instanceof ContentPane) {
+ children = ((ContentPane) parent).getChildrenList();
+ }
+
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i) == sibling) {
+ pane = (ContentPane) parent;
+ } else if (children.get(i) instanceof Parent) {
+ stack.push((Parent) children.get(i));
+ }
+ }
+ }
+ return pane;
+ }
+
+ public boolean removeNode(Stack stack, Node node) {
+ List children = getChildrenList();
+
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i) == node) {
+ getTabs().remove(i);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void set(int idx, Node node) {
+ DockNode newNode = (DockNode) node;
+ getTabs().set(idx, new DockNodeTab(newNode));
+ getSelectionModel().select( idx );
+ }
+
+ public void set(Node sibling, Node node) {
+ set(getChildrenList().indexOf(sibling), node);
+ }
+
+ public List getChildrenList() {
+ return getTabs().stream().map(i -> i.getContent()).collect(Collectors.toList());
+ }
+
+ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) {
+ DockNode newNode = (DockNode) node;
+ DockNodeTab t = new DockNodeTab(newNode);
+ addDockNodeTab( t );
+ }
+
+ public void addDockNodeTab(DockNodeTab dockNodeTab)
+ {
+ getTabs().add(dockNodeTab);
+ getSelectionModel().select( dockNodeTab );
+ }
+
+ @Override
+ protected double computeMaxWidth(double height) {
+ return getTabs().stream().map(i -> i.getContent().maxWidth(height)).min(Comparator.naturalOrder()).get();
+ }
+}
diff --git a/src/main/java/org/dockfx/pane/DockNodeTab.java b/src/main/java/org/dockfx/pane/DockNodeTab.java
new file mode 100644
index 0000000..f877575
--- /dev/null
+++ b/src/main/java/org/dockfx/pane/DockNodeTab.java
@@ -0,0 +1,46 @@
+package org.dockfx.pane;
+
+import org.dockfx.DockNode;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.Tab;
+
+/**
+ * DockNodeTab class holds Tab for ContentTabPane
+ *
+ * @author HongKee Moon
+ */
+public class DockNodeTab extends Tab {
+
+ final private DockNode dockNode;
+
+ final private SimpleStringProperty title;
+
+ public DockNodeTab(DockNode node) {
+ this.dockNode = node;
+ setClosable(false);
+
+ title = new SimpleStringProperty("");
+ title.bind(dockNode.titleProperty());
+
+ setGraphic(dockNode.getDockTitleBar());
+ setContent(dockNode);
+ dockNode.tabbedProperty().set(true);
+ dockNode.setNodeTab( this );
+ }
+
+ public String getTitle()
+ {
+ return title.getValue();
+ }
+
+ public SimpleStringProperty titleProperty()
+ {
+ return title;
+ }
+
+ public void select()
+ {
+ getTabPane().getSelectionModel().select( this );
+ }
+}
diff --git a/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java
new file mode 100644
index 0000000..3ffe333
--- /dev/null
+++ b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java
@@ -0,0 +1,1820 @@
+
+package org.dockfx.pane.skin;
+
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+import com.sun.javafx.util.Utils;
+import javafx.animation.Animation;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.value.WritableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.WeakListChangeListener;
+import javafx.css.CssMetaData;
+import javafx.css.PseudoClass;
+import javafx.css.Styleable;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.Pos;
+import javafx.geometry.Side;
+import javafx.geometry.VPos;
+import javafx.scene.AccessibleAction;
+import javafx.scene.AccessibleAttribute;
+import javafx.scene.AccessibleRole;
+import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.RadioMenuItem;
+import javafx.scene.control.SkinBase;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.TabPane.TabClosingPolicy;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.Tooltip;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.ContextMenuEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.input.SwipeEvent;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.transform.Rotate;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.sun.javafx.css.converters.EnumConverter;
+import com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler;
+import com.sun.javafx.scene.control.behavior.TabPaneBehavior;
+import com.sun.javafx.scene.traversal.Direction;
+import com.sun.javafx.scene.traversal.TraversalEngine;
+
+import org.dockfx.pane.DockNodeTab;
+
+import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
+
+/**
+ * This class source is copied from com.sun.javafx.scene.control.skin.TabPaneSkin in jfxrt.jar 1.8 version
+ * In the different java version, this source might not be working properly.
+ * The inner class TabMenuItem is modified to handle DockNodeTab title because the tab uses customized graphics
+ */
+public class ContentTabPaneSkin extends BehaviorSkinBase {
+private static enum TabAnimation {
+ NONE,
+ GROW
+ // In future we could add FADE, ...
+}
+
+private enum TabAnimationState {
+ SHOWING, HIDING, NONE;
+}
+
+private ObjectProperty openTabAnimation = new StyleableObjectProperty(TabAnimation.GROW) {
+ @Override public CssMetaData getCssMetaData() {
+ return StyleableProperties.OPEN_TAB_ANIMATION;
+ }
+
+ @Override public Object getBean() {
+ return ContentTabPaneSkin.this;
+ }
+
+ @Override public String getName() {
+ return "openTabAnimation";
+ }
+};
+
+private ObjectProperty closeTabAnimation = new StyleableObjectProperty(TabAnimation.GROW) {
+ @Override public CssMetaData getCssMetaData() {
+ return StyleableProperties.CLOSE_TAB_ANIMATION;
+ }
+
+ @Override public Object getBean() {
+ return ContentTabPaneSkin.this;
+ }
+
+ @Override public String getName() {
+ return "closeTabAnimation";
+ }
+};
+
+ private static int getRotation(Side pos) {
+ switch (pos) {
+ case TOP:
+ return 0;
+ case BOTTOM:
+ return 180;
+ case LEFT:
+ return -90;
+ case RIGHT:
+ return 90;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a
+ * Tab and the tabs menu at the same time.
+ */
+ private static Node clone(Node n) {
+ if (n == null) {
+ return null;
+ }
+ if (n instanceof ImageView) {
+ ImageView iv = (ImageView) n;
+ ImageView imageview = new ImageView();
+ imageview.setImage(iv.getImage());
+ return imageview;
+ }
+ if (n instanceof Label) {
+ Label l = (Label)n;
+ Label label = new Label(l.getText(), l.getGraphic());
+ return label;
+ }
+ return null;
+ }
+private static final double ANIMATION_SPEED = 150;
+private static final int SPACER = 10;
+
+private TabHeaderArea tabHeaderArea;
+private ObservableList tabContentRegions;
+private Rectangle clipRect;
+private Rectangle tabHeaderAreaClipRect;
+private Tab selectedTab;
+private boolean isSelectingTab;
+
+ public ContentTabPaneSkin(TabPane tabPane) {
+ super(tabPane, new TabPaneBehavior(tabPane));
+
+ clipRect = new Rectangle(tabPane.getWidth(), tabPane.getHeight());
+ getSkinnable().setClip(clipRect);
+
+ tabContentRegions = FXCollections.observableArrayList();
+
+ for (Tab tab : getSkinnable().getTabs()) {
+ addTabContent(tab);
+ }
+
+ tabHeaderAreaClipRect = new Rectangle();
+ tabHeaderArea = new TabHeaderArea();
+ tabHeaderArea.setClip(tabHeaderAreaClipRect);
+ getChildren().add(tabHeaderArea);
+ if (getSkinnable().getTabs().size() == 0) {
+ tabHeaderArea.setVisible(false);
+ }
+
+ initializeTabListener();
+
+ registerChangeListener(tabPane.getSelectionModel().selectedItemProperty(), "SELECTED_TAB");
+ registerChangeListener(tabPane.sideProperty(), "SIDE");
+ registerChangeListener(tabPane.widthProperty(), "WIDTH");
+ registerChangeListener(tabPane.heightProperty(), "HEIGHT");
+
+ selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
+ // Could not find the selected tab try and get the selected tab using the selected index
+ if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) {
+ getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex());
+ selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
+ }
+ if (selectedTab == null) {
+ // getSelectedItem and getSelectedIndex failed select the first.
+ getSkinnable().getSelectionModel().selectFirst();
+ }
+ selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
+ isSelectingTab = false;
+
+ initializeSwipeHandlers();
+ }
+
+ public StackPane getSelectedTabContentRegion() {
+ for (TabContentRegion contentRegion : tabContentRegions) {
+ if (contentRegion.getTab().equals(selectedTab)) {
+ return contentRegion;
+ }
+ }
+ return null;
+ }
+
+ @Override protected void handleControlPropertyChanged(String property) {
+ super.handleControlPropertyChanged(property);
+ if ("SELECTED_TAB".equals(property)) {
+ isSelectingTab = true;
+ selectedTab = getSkinnable().getSelectionModel().getSelectedItem();
+ getSkinnable().requestLayout();
+ } else if ("SIDE".equals(property)) {
+ updateTabPosition();
+ } else if ("WIDTH".equals(property)) {
+ clipRect.setWidth(getSkinnable().getWidth());
+ } else if ("HEIGHT".equals(property)) {
+ clipRect.setHeight(getSkinnable().getHeight());
+ }
+ }
+ private void removeTabs(List extends Tab> removedList) {
+ for (final Tab tab : removedList) {
+ stopCurrentAnimation(tab);
+ // Animate the tab removal
+ final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
+ if (tabRegion != null) {
+ tabRegion.isClosing = true;
+
+ tabRegion.removeListeners(tab);
+ removeTabContent(tab);
+
+ // remove the menu item from the popup menu
+ ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
+ TabMenuItem tabItem = null;
+ if (popupMenu != null) {
+ for (MenuItem item : popupMenu.getItems()) {
+ tabItem = (TabMenuItem) item;
+ if (tab == tabItem.getTab()) {
+ break;
+ }
+ tabItem = null;
+ }
+ }
+ if (tabItem != null) {
+ tabItem.dispose();
+ popupMenu.getItems().remove(tabItem);
+ }
+ // end of removing menu item
+
+ EventHandler cleanup = ae -> {
+ tabRegion.animationState = TabAnimationState.NONE;
+
+ tabHeaderArea.removeTab(tab);
+ tabHeaderArea.requestLayout();
+ if (getSkinnable().getTabs().isEmpty()) {
+ tabHeaderArea.setVisible(false);
+ }
+ };
+
+ if (closeTabAnimation.get() == TabAnimation.GROW) {
+ tabRegion.animationState = TabAnimationState.HIDING;
+ Timeline closedTabTimeline = tabRegion.currentAnimation =
+ createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 0.0F, cleanup);
+ closedTabTimeline.play();
+ } else {
+ cleanup.handle(null);
+ }
+ }
+ }
+ }
+
+ private void stopCurrentAnimation(Tab tab) {
+ final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
+ if (tabRegion != null) {
+ // Execute the code immediately, don't wait for the animation to finish.
+ Timeline timeline = tabRegion.currentAnimation;
+ if (timeline != null && timeline.getStatus() == Animation.Status.RUNNING) {
+ timeline.getOnFinished().handle(null);
+ timeline.stop();
+ tabRegion.currentAnimation = null;
+ }
+ }
+ }
+
+ private void addTabs(List extends Tab> addedList, int from) {
+ int i = 0;
+
+ // RT-39984: check if any other tabs are animating - they must be completed first.
+ List headers = new ArrayList<>(tabHeaderArea.headersRegion.getChildren());
+ for (Node n : headers) {
+ TabHeaderSkin header = (TabHeaderSkin) n;
+ if (header.animationState == TabAnimationState.HIDING) {
+ stopCurrentAnimation(header.tab);
+ }
+ }
+ // end of fix for RT-39984
+
+ for (final Tab tab : addedList) {
+ stopCurrentAnimation(tab); // Note that this must happen before addTab() call below
+ // A new tab was added - animate it out
+ if (!tabHeaderArea.isVisible()) {
+ tabHeaderArea.setVisible(true);
+ }
+ int index = from + i++;
+ tabHeaderArea.addTab(tab, index);
+ addTabContent(tab);
+ final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab);
+ if (tabRegion != null) {
+ if (openTabAnimation.get() == TabAnimation.GROW) {
+ tabRegion.animationState = TabAnimationState.SHOWING;
+ tabRegion.animationTransition.setValue(0.0);
+ tabRegion.setVisible(true);
+ tabRegion.currentAnimation = createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 1.0, event -> {
+ tabRegion.animationState = TabAnimationState.NONE;
+ tabRegion.setVisible(true);
+ tabRegion.inner.requestLayout();
+ });
+ tabRegion.currentAnimation.play();
+ } else {
+ tabRegion.setVisible(true);
+ tabRegion.inner.requestLayout();
+ }
+ }
+ }
+ }
+
+ private void initializeTabListener() {
+ getSkinnable().getTabs().addListener((ListChangeListener) c -> {
+ List tabsToRemove = new ArrayList<>();
+ List tabsToAdd = new ArrayList<>();
+ int insertPos = -1;
+
+ while (c.next()) {
+ if (c.wasPermutated()) {
+ TabPane tabPane = getSkinnable();
+ List tabs = tabPane.getTabs();
+
+ // tabs sorted : create list of permutated tabs.
+ // clear selection, set tab animation to NONE
+ // remove permutated tabs, add them back in correct order.
+ // restore old selection, and old tab animation states.
+ int size = c.getTo() - c.getFrom();
+ Tab selTab = tabPane.getSelectionModel().getSelectedItem();
+ List permutatedTabs = new ArrayList(size);
+ getSkinnable().getSelectionModel().clearSelection();
+
+ // save and set tab animation to none - as it is not a good idea
+ // to animate on the same data for open and close.
+ TabAnimation prevOpenAnimation = openTabAnimation.get();
+ TabAnimation prevCloseAnimation = closeTabAnimation.get();
+ openTabAnimation.set(TabAnimation.NONE);
+ closeTabAnimation.set(TabAnimation.NONE);
+ for (int i = c.getFrom(); i < c.getTo(); i++) {
+ permutatedTabs.add(tabs.get(i));
+ }
+
+ removeTabs(permutatedTabs);
+ addTabs(permutatedTabs, c.getFrom());
+ openTabAnimation.set(prevOpenAnimation);
+ closeTabAnimation.set(prevCloseAnimation);
+ getSkinnable().getSelectionModel().select(selTab);
+ }
+
+ if (c.wasRemoved()) {
+ tabsToRemove.addAll(c.getRemoved());
+ }
+
+ if (c.wasAdded()) {
+ tabsToAdd.addAll(c.getAddedSubList());
+ insertPos = c.getFrom();
+ }
+ }
+
+ // now only remove the tabs that are not in the tabsToAdd list
+ tabsToRemove.removeAll(tabsToAdd);
+ removeTabs(tabsToRemove);
+
+ // and add in any new tabs (that we don't already have showing)
+ if (! tabsToAdd.isEmpty()) {
+ for (TabContentRegion tabContentRegion : tabContentRegions) {
+ Tab tab = tabContentRegion.getTab();
+ TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab);
+ if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) {
+ tabsToAdd.remove(tabContentRegion.getTab());
+ }
+ }
+
+ addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos);
+ }
+
+ // Fix for RT-34692
+ getSkinnable().requestLayout();
+ });
+ }
+
+ private void addTabContent(Tab tab) {
+ TabContentRegion tabContentRegion = new TabContentRegion(tab);
+ tabContentRegion.setClip(new Rectangle());
+ tabContentRegions.add(tabContentRegion);
+ // We want the tab content to always sit below the tab headers
+ getChildren().add(0, tabContentRegion);
+ }
+
+ private void removeTabContent(Tab tab) {
+ for (TabContentRegion contentRegion : tabContentRegions) {
+ if (contentRegion.getTab().equals(tab)) {
+ contentRegion.removeListeners(tab);
+ getChildren().remove(contentRegion);
+ tabContentRegions.remove(contentRegion);
+ break;
+ }
+ }
+ }
+
+ private void updateTabPosition() {
+ tabHeaderArea.setScrollOffset(0.0F);
+ getSkinnable().applyCss();
+ getSkinnable().requestLayout();
+ }
+
+ private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler func) {
+ Timeline timeline = new Timeline();
+ timeline.setCycleCount(1);
+
+ KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR);
+ timeline.getKeyFrames().clear();
+ timeline.getKeyFrames().add(new KeyFrame(duration, keyValue));
+
+ timeline.setOnFinished(func);
+ return timeline;
+ }
+
+ private boolean isHorizontal() {
+ Side tabPosition = getSkinnable().getSide();
+ return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition);
+ }
+
+ private void initializeSwipeHandlers() {
+ if (IS_TOUCH_SUPPORTED) {
+ getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> {
+ getBehavior().selectNextTab();
+ });
+
+ getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> {
+ getBehavior().selectPreviousTab();
+ });
+ }
+ }
+
+ //TODO need to cache this.
+ private boolean isFloatingStyleClass() {
+ return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING);
+ }
+
+private double maxw = 0.0d;
+ @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+ // The TabPane can only be as wide as it widest content width.
+ for (TabContentRegion contentRegion: tabContentRegions) {
+ maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1)));
+ }
+
+ final boolean isHorizontal = isHorizontal();
+ final double tabHeaderAreaSize = snapSize(isHorizontal ?
+ tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1));
+
+ double prefWidth = isHorizontal ?
+ Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize;
+ return snapSize(prefWidth) + rightInset + leftInset;
+ }
+
+private double maxh = 0.0d;
+ @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+ // The TabPane can only be as high as it highest content height.
+ for (TabContentRegion contentRegion: tabContentRegions) {
+ maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1)));
+ }
+
+ final boolean isHorizontal = isHorizontal();
+ final double tabHeaderAreaSize = snapSize(isHorizontal ?
+ tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1));
+
+ double prefHeight = isHorizontal ?
+ maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize);
+ return snapSize(prefHeight) + topInset + bottomInset;
+ }
+
+ @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
+ Side tabPosition = getSkinnable().getSide();
+ if (tabPosition == Side.TOP) {
+ return tabHeaderArea.getBaselineOffset() + topInset;
+ }
+ return 0;
+ }
+
+ @Override protected void layoutChildren(final double x, final double y,
+ final double w, final double h) {
+ TabPane tabPane = getSkinnable();
+ Side tabPosition = tabPane.getSide();
+
+ double headerHeight = snapSize(tabHeaderArea.prefHeight(-1));
+ double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x;
+ double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y;
+
+ if (tabPosition == Side.TOP) {
+ tabHeaderArea.resize(w, headerHeight);
+ tabHeaderArea.relocate(tabsStartX, tabsStartY);
+ tabHeaderArea.getTransforms().clear();
+ tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP)));
+ } else if (tabPosition == Side.BOTTOM) {
+ tabHeaderArea.resize(w, headerHeight);
+ tabHeaderArea.relocate(w, tabsStartY - headerHeight);
+ tabHeaderArea.getTransforms().clear();
+ tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight));
+ } else if (tabPosition == Side.LEFT) {
+ tabHeaderArea.resize(h, headerHeight);
+ tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight);
+ tabHeaderArea.getTransforms().clear();
+ tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight));
+ } else if (tabPosition == Side.RIGHT) {
+ tabHeaderArea.resize(h, headerHeight);
+ tabHeaderArea.relocate(tabsStartX, y - headerHeight);
+ tabHeaderArea.getTransforms().clear();
+ tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight));
+ }
+
+ tabHeaderAreaClipRect.setX(0);
+ tabHeaderAreaClipRect.setY(0);
+ if (isHorizontal()) {
+ tabHeaderAreaClipRect.setWidth(w);
+ } else {
+ tabHeaderAreaClipRect.setWidth(h);
+ }
+ tabHeaderAreaClipRect.setHeight(headerHeight);
+
+ // ==================================
+ // position the tab content for the selected tab only
+ // ==================================
+ // if the tabs are on the left, the content needs to be indented
+ double contentStartX = 0;
+ double contentStartY = 0;
+
+ if (tabPosition == Side.TOP) {
+ contentStartX = x;
+ contentStartY = y + headerHeight;
+ if (isFloatingStyleClass()) {
+ // This is to hide the top border content
+ contentStartY -= 1;
+ }
+ } else if (tabPosition == Side.BOTTOM) {
+ contentStartX = x;
+ contentStartY = y;
+ if (isFloatingStyleClass()) {
+ // This is to hide the bottom border content
+ contentStartY = 1;
+ }
+ } else if (tabPosition == Side.LEFT) {
+ contentStartX = x + headerHeight;
+ contentStartY = y;
+ if (isFloatingStyleClass()) {
+ // This is to hide the left border content
+ contentStartX -= 1;
+ }
+ } else if (tabPosition == Side.RIGHT) {
+ contentStartX = x;
+ contentStartY = y;
+ if (isFloatingStyleClass()) {
+ // This is to hide the right border content
+ contentStartX = 1;
+ }
+ }
+
+ double contentWidth = w - (isHorizontal() ? 0 : headerHeight);
+ double contentHeight = h - (isHorizontal() ? headerHeight: 0);
+
+ for (int i = 0, max = tabContentRegions.size(); i < max; i++) {
+ TabContentRegion tabContent = tabContentRegions.get(i);
+
+ tabContent.setAlignment(Pos.TOP_LEFT);
+ if (tabContent.getClip() != null) {
+ ((Rectangle)tabContent.getClip()).setWidth(contentWidth);
+ ((Rectangle)tabContent.getClip()).setHeight(contentHeight);
+ }
+
+ // we need to size all tabs, even if they aren't visible. For example,
+ // see RT-29167
+ tabContent.resize(contentWidth, contentHeight);
+ tabContent.relocate(contentStartX, contentStartY);
+ }
+ }
+
+
+/**
+ * Super-lazy instantiation pattern from Bill Pugh.
+ * @treatAsPrivate implementation detail
+ */
+private static class StyleableProperties {
+ private static final List> STYLEABLES;
+
+ private final static CssMetaData OPEN_TAB_ANIMATION =
+ new CssMetaData("-fx-open-tab-animation",
+ new EnumConverter(TabAnimation.class), TabAnimation.GROW) {
+
+ @Override public boolean isSettable(TabPane node) {
+ return true;
+ }
+
+ @Override public StyleableProperty getStyleableProperty(TabPane node) {
+ ContentTabPaneSkin skin = (ContentTabPaneSkin) node.getSkin();
+ return (StyleableProperty)(WritableValue)skin.openTabAnimation;
+ }
+ };
+
+ private final static CssMetaData CLOSE_TAB_ANIMATION =
+ new CssMetaData("-fx-close-tab-animation",
+ new EnumConverter(TabAnimation.class), TabAnimation.GROW) {
+
+ @Override public boolean isSettable(TabPane node) {
+ return true;
+ }
+
+ @Override public StyleableProperty getStyleableProperty(TabPane node) {
+ ContentTabPaneSkin skin = (ContentTabPaneSkin) node.getSkin();
+ return (StyleableProperty)(WritableValue)skin.closeTabAnimation;
+ }
+ };
+
+ static {
+
+ final List> styleables =
+ new ArrayList>(SkinBase.getClassCssMetaData());
+ styleables.add(OPEN_TAB_ANIMATION);
+ styleables.add(CLOSE_TAB_ANIMATION);
+ STYLEABLES = Collections.unmodifiableList(styleables);
+
+ }
+}
+
+ /**
+ * @return The CssMetaData associated with this class, which may include the
+ * CssMetaData of its super classes.
+ */
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.STYLEABLES;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override public List> getCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+/**************************************************************************
+ *
+ * TabHeaderArea: Area responsible for painting all tabs
+ *
+ **************************************************************************/
+class TabHeaderArea extends StackPane {
+ private Rectangle headerClip;
+ private StackPane headersRegion;
+ private StackPane headerBackground;
+ private TabControlButtons controlButtons;
+
+ private boolean measureClosingTabs = false;
+
+ private double scrollOffset;
+
+ public TabHeaderArea() {
+ getStyleClass().setAll("tab-header-area");
+ setManaged(false);
+ final TabPane tabPane = getSkinnable();
+
+ headerClip = new Rectangle();
+
+ headersRegion = new StackPane() {
+ @Override protected double computePrefWidth(double height) {
+ double width = 0.0F;
+ for (Node child : getChildren()) {
+ TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child;
+ if (tabHeaderSkin.isVisible() && (measureClosingTabs || ! tabHeaderSkin.isClosing)) {
+ width += tabHeaderSkin.prefWidth(height);
+ }
+ }
+ return snapSize(width) + snappedLeftInset() + snappedRightInset();
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ double height = 0.0F;
+ for (Node child : getChildren()) {
+ TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child;
+ height = Math.max(height, tabHeaderSkin.prefHeight(width));
+ }
+ return snapSize(height) + snappedTopInset() + snappedBottomInset();
+ }
+
+ @Override protected void layoutChildren() {
+ if (tabsFit()) {
+ setScrollOffset(0.0);
+ } else {
+ if (!removeTab.isEmpty()) {
+ double offset = 0;
+ double w = tabHeaderArea.getWidth() - snapSize(controlButtons.prefWidth(-1)) - firstTabIndent() - SPACER;
+ Iterator i = getChildren().iterator();
+ while (i.hasNext()) {
+ TabHeaderSkin tabHeader = (TabHeaderSkin)i.next();
+ double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
+ if (removeTab.contains(tabHeader)) {
+ if (offset < w) {
+ isSelectingTab = true;
+ }
+ i.remove();
+ removeTab.remove(tabHeader);
+ if (removeTab.isEmpty()) {
+ break;
+ }
+ }
+ offset += tabHeaderPrefWidth;
+ }
+// } else {
+// isSelectingTab = true;
+ }
+ }
+
+ if (isSelectingTab) {
+ ensureSelectedTabIsVisible();
+ isSelectingTab = false;
+ } else {
+ validateScrollOffset();
+ }
+
+ Side tabPosition = getSkinnable().getSide();
+ double tabBackgroundHeight = snapSize(prefHeight(-1));
+ double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ?
+ snapSize(getWidth()) - getScrollOffset() : getScrollOffset();
+
+ updateHeaderClip();
+ for (Node node : getChildren()) {
+ TabHeaderSkin tabHeader = (TabHeaderSkin)node;
+
+ // size and position the header relative to the other headers
+ double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get());
+ double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1));
+ tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight);
+
+ // This ensures that the tabs are located in the correct position
+ // when there are tabs of differing heights.
+ double startY = tabPosition.equals(Side.BOTTOM) ?
+ 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset();
+ if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
+ // build from the right
+ tabX -= tabHeaderPrefWidth;
+ tabHeader.relocate(tabX, startY);
+ } else {
+ // build from the left
+ tabHeader.relocate(tabX, startY);
+ tabX += tabHeaderPrefWidth;
+ }
+ }
+ }
+
+ };
+ headersRegion.getStyleClass().setAll("headers-region");
+ headersRegion.setClip(headerClip);
+
+ headerBackground = new StackPane();
+ headerBackground.getStyleClass().setAll("tab-header-background");
+
+ int i = 0;
+ for (Tab tab: tabPane.getTabs()) {
+ addTab(tab, i++);
+ }
+
+ controlButtons = new TabControlButtons();
+ controlButtons.setVisible(false);
+ if (controlButtons.isVisible()) {
+ controlButtons.setVisible(true);
+ }
+ getChildren().addAll(headerBackground, headersRegion, controlButtons);
+
+ // support for mouse scroll of header area (for when the tabs exceed
+ // the available space)
+ addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> {
+ Side side = getSkinnable().getSide();
+ side = side == null ? Side.TOP : side;
+ switch (side) {
+ default:
+ case TOP:
+ case BOTTOM:
+ setScrollOffset(scrollOffset - e.getDeltaY());
+ break;
+ case LEFT:
+ case RIGHT:
+ setScrollOffset(scrollOffset + e.getDeltaY());
+ break;
+ }
+
+ });
+ }
+
+ private void updateHeaderClip() {
+ Side tabPosition = getSkinnable().getSide();
+
+ double x = 0;
+ double y = 0;
+ double clipWidth = 0;
+ double clipHeight = 0;
+ double maxWidth = 0;
+ double shadowRadius = 0;
+ double clipOffset = firstTabIndent();
+ double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1));
+
+ measureClosingTabs = true;
+ double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
+ measureClosingTabs = false;
+
+ double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));
+
+ // Add the spacer if isShowTabsMenu is true.
+ if (controlButtonPrefWidth > 0) {
+ controlButtonPrefWidth = controlButtonPrefWidth + SPACER;
+ }
+
+ if (headersRegion.getEffect() instanceof DropShadow) {
+ DropShadow shadow = (DropShadow)headersRegion.getEffect();
+ shadowRadius = shadow.getRadius();
+ }
+
+ maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset;
+ if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
+ if (headersPrefWidth < maxWidth) {
+ clipWidth = headersPrefWidth + shadowRadius;
+ } else {
+ x = headersPrefWidth - maxWidth;
+ clipWidth = maxWidth + shadowRadius;
+ }
+ clipHeight = headersPrefHeight;
+ } else {
+ // If x = 0 the header region's drop shadow is clipped.
+ x = -shadowRadius;
+ clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius;
+ clipHeight = headersPrefHeight;
+ }
+
+ headerClip.setX(x);
+ headerClip.setY(y);
+ headerClip.setWidth(clipWidth);
+ headerClip.setHeight(clipHeight);
+ }
+
+ private void addTab(Tab tab, int addToIndex) {
+ TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab);
+ headersRegion.getChildren().add(addToIndex, tabHeaderSkin);
+ }
+
+ private List removeTab = new ArrayList<>();
+ private void removeTab(Tab tab) {
+ TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab);
+ if (tabHeaderSkin != null) {
+ if (tabsFit()) {
+ headersRegion.getChildren().remove(tabHeaderSkin);
+ } else {
+ // The tab will be removed during layout because
+ // we need its width to compute the scroll offset.
+ removeTab.add(tabHeaderSkin);
+ tabHeaderSkin.removeListeners(tab);
+ }
+ }
+ }
+
+ private TabHeaderSkin getTabHeaderSkin(Tab tab) {
+ for (Node child: headersRegion.getChildren()) {
+ TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child;
+ if (tabHeaderSkin.getTab().equals(tab)) {
+ return tabHeaderSkin;
+ }
+ }
+ return null;
+ }
+
+ private boolean tabsFit() {
+ double headerPrefWidth = snapSize(headersRegion.prefWidth(-1));
+ double controlTabWidth = snapSize(controlButtons.prefWidth(-1));
+ double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER;
+ return visibleWidth < getWidth();
+ }
+
+ private void ensureSelectedTabIsVisible() {
+ // work out the visible width of the tab header
+ double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
+ double controlTabWidth = snapSize(controlButtons.getWidth());
+ double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;
+
+ // and get where the selected tab is in the header area
+ double offset = 0.0;
+ double selectedTabOffset = 0.0;
+ double selectedTabWidth = 0.0;
+ for (Node node : headersRegion.getChildren()) {
+ TabHeaderSkin tabHeader = (TabHeaderSkin)node;
+
+ double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
+
+ if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) {
+ selectedTabOffset = offset;
+ selectedTabWidth = tabHeaderPrefWidth;
+ }
+ offset += tabHeaderPrefWidth;
+ }
+
+ final double scrollOffset = getScrollOffset();
+ final double selectedTabStartX = selectedTabOffset;
+ final double selectedTabEndX = selectedTabOffset + selectedTabWidth;
+
+ final double visibleAreaEndX = visibleWidth;
+
+ if (selectedTabStartX < -scrollOffset) {
+ setScrollOffset(-selectedTabStartX);
+ } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) {
+ setScrollOffset(visibleAreaEndX - selectedTabEndX);
+ }
+ }
+
+ public double getScrollOffset() {
+ return scrollOffset;
+ }
+
+ private void validateScrollOffset() {
+ setScrollOffset(getScrollOffset());
+ }
+
+ private void setScrollOffset(double newScrollOffset) {
+ // work out the visible width of the tab header
+ double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
+ double controlTabWidth = snapSize(controlButtons.getWidth());
+ double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;
+
+ // measure the width of all tabs
+ double offset = 0.0;
+ for (Node node : headersRegion.getChildren()) {
+ TabHeaderSkin tabHeader = (TabHeaderSkin)node;
+ double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
+ offset += tabHeaderPrefWidth;
+ }
+
+ double actualNewScrollOffset;
+
+ if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) {
+ // need to make sure the right-most tab is attached to the
+ // right-hand side of the tab header (e.g. if the tab header area width
+ // is expanded), and if it isn't modify the scroll offset to bring
+ // it into line. See RT-35194 for a test case.
+ actualNewScrollOffset = visibleWidth - offset;
+ } else if (newScrollOffset > 0) {
+ // need to prevent the left-most tab from becoming detached
+ // from the left-hand side of the tab header.
+ actualNewScrollOffset = 0;
+ } else {
+ actualNewScrollOffset = newScrollOffset;
+ }
+
+ if (actualNewScrollOffset != scrollOffset) {
+ scrollOffset = actualNewScrollOffset;
+ headersRegion.requestLayout();
+ }
+ }
+
+ private double firstTabIndent() {
+ switch (getSkinnable().getSide()) {
+ case TOP:
+ case BOTTOM:
+ return snappedLeftInset();
+ case RIGHT:
+ case LEFT:
+ return snappedTopInset();
+ default:
+ return 0;
+ }
+ }
+
+ @Override protected double computePrefWidth(double height) {
+ double padding = isHorizontal() ?
+ snappedLeftInset() + snappedRightInset() :
+ snappedTopInset() + snappedBottomInset();
+ return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) +
+ firstTabIndent() + SPACER + padding;
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ double padding = isHorizontal() ?
+ snappedTopInset() + snappedBottomInset() :
+ snappedLeftInset() + snappedRightInset();
+ return snapSize(headersRegion.prefHeight(-1)) + padding;
+ }
+
+ @Override public double getBaselineOffset() {
+ if (getSkinnable().getSide() == Side.TOP) {
+ return headersRegion.getBaselineOffset() + snappedTopInset();
+ }
+ return 0;
+ }
+
+ @Override protected void layoutChildren() {
+ final double leftInset = snappedLeftInset();
+ final double rightInset = snappedRightInset();
+ final double topInset = snappedTopInset();
+ final double bottomInset = snappedBottomInset();
+ double w = snapSize(getWidth()) - (isHorizontal() ?
+ leftInset + rightInset : topInset + bottomInset);
+ double h = snapSize(getHeight()) - (isHorizontal() ?
+ topInset + bottomInset : leftInset + rightInset);
+ double tabBackgroundHeight = snapSize(prefHeight(-1));
+ double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
+ double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));
+
+ controlButtons.showTabsMenu(! tabsFit());
+
+ updateHeaderClip();
+ headersRegion.requestLayout();
+
+ // RESIZE CONTROL BUTTONS
+ double btnWidth = snapSize(controlButtons.prefWidth(-1));
+ final double btnHeight = controlButtons.prefHeight(btnWidth);
+ controlButtons.resize(btnWidth, btnHeight);
+
+ // POSITION TABS
+ headersRegion.resize(headersPrefWidth, headersPrefHeight);
+
+ if (isFloatingStyleClass()) {
+ headerBackground.setVisible(false);
+ } else {
+ headerBackground.resize(snapSize(getWidth()), snapSize(getHeight()));
+ headerBackground.setVisible(true);
+ }
+
+ double startX = 0;
+ double startY = 0;
+ double controlStartX = 0;
+ double controlStartY = 0;
+ Side tabPosition = getSkinnable().getSide();
+
+ if (tabPosition.equals(Side.TOP)) {
+ startX = leftInset;
+ startY = tabBackgroundHeight - headersPrefHeight - bottomInset;
+ controlStartX = w - btnWidth + leftInset;
+ controlStartY = snapSize(getHeight()) - btnHeight - bottomInset;
+ } else if (tabPosition.equals(Side.RIGHT)) {
+ startX = topInset;
+ startY = tabBackgroundHeight - headersPrefHeight - leftInset;
+ controlStartX = w - btnWidth + topInset;
+ controlStartY = snapSize(getHeight()) - btnHeight - leftInset;
+ } else if (tabPosition.equals(Side.BOTTOM)) {
+ startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
+ startY = tabBackgroundHeight - headersPrefHeight - topInset;
+ controlStartX = rightInset;
+ controlStartY = snapSize(getHeight()) - btnHeight - topInset;
+ } else if (tabPosition.equals(Side.LEFT)) {
+ startX = snapSize(getWidth()) - headersPrefWidth - topInset;
+ startY = tabBackgroundHeight - headersPrefHeight - rightInset;
+ controlStartX = leftInset;
+ controlStartY = snapSize(getHeight()) - btnHeight - rightInset;
+ }
+ if (headerBackground.isVisible()) {
+ positionInArea(headerBackground, 0, 0,
+ snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
+ }
+ positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER);
+ positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight,
+ /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
+ }
+} /* End TabHeaderArea */
+
+static int CLOSE_BTN_SIZE = 16;
+
+/**************************************************************************
+ *
+ * TabHeaderSkin: skin for each tab
+ *
+ **************************************************************************/
+
+class TabHeaderSkin extends StackPane {
+ private final Tab tab;
+ public Tab getTab() {
+ return tab;
+ }
+ private Label label;
+ private StackPane closeBtn;
+ private StackPane inner;
+ private Tooltip oldTooltip;
+ private Tooltip tooltip;
+ private Rectangle clip;
+
+ private boolean isClosing = false;
+
+ private MultiplePropertyChangeListenerHandler listener =
+ new MultiplePropertyChangeListenerHandler(param -> {
+ handlePropertyChanged(param);
+ return null;
+ });
+
+ private final ListChangeListener styleClassListener = new ListChangeListener() {
+ @Override
+ public void onChanged(Change extends String> c) {
+ getStyleClass().setAll(tab.getStyleClass());
+ }
+ };
+
+ private final WeakListChangeListener weakStyleClassListener =
+ new WeakListChangeListener<>(styleClassListener);
+
+ public TabHeaderSkin(final Tab tab) {
+ getStyleClass().setAll(tab.getStyleClass());
+ setId(tab.getId());
+ setStyle(tab.getStyle());
+ setAccessibleRole(AccessibleRole.TAB_ITEM);
+
+ this.tab = tab;
+ clip = new Rectangle();
+ setClip(clip);
+
+ label = new Label(tab.getText(), tab.getGraphic());
+ label.getStyleClass().setAll("tab-label");
+
+ closeBtn = new StackPane() {
+ @Override protected double computePrefWidth(double h) {
+ return CLOSE_BTN_SIZE;
+ }
+ @Override protected double computePrefHeight(double w) {
+ return CLOSE_BTN_SIZE;
+ }
+ @Override
+ public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
+ switch (action) {
+ case FIRE: {
+ Tab tab = getTab();
+ TabPaneBehavior behavior = getBehavior();
+ if (behavior.canCloseTab(tab)) {
+ behavior.closeTab(tab);
+ setOnMousePressed(null);
+ }
+ }
+ default: super.executeAccessibleAction(action, parameters);
+ }
+ }
+ };
+ closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
+ closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
+ closeBtn.getStyleClass().setAll("tab-close-button");
+ closeBtn.setOnMousePressed(new EventHandler() {
+ @Override public void handle(MouseEvent me) {
+ Tab tab = getTab();
+ TabPaneBehavior behavior = getBehavior();
+ if (behavior.canCloseTab(tab)) {
+ behavior.closeTab(tab);
+ setOnMousePressed(null);
+ }
+ }
+ });
+
+ updateGraphicRotation();
+
+ final Region focusIndicator = new Region();
+ focusIndicator.setMouseTransparent(true);
+ focusIndicator.getStyleClass().add("focus-indicator");
+
+ inner = new StackPane() {
+ @Override protected void layoutChildren() {
+ final TabPane skinnable = getSkinnable();
+
+ final double paddingTop = snappedTopInset();
+ final double paddingRight = snappedRightInset();
+ final double paddingBottom = snappedBottomInset();
+ final double paddingLeft = snappedLeftInset();
+ final double w = getWidth() - (paddingLeft + paddingRight);
+ final double h = getHeight() - (paddingTop + paddingBottom);
+
+ final double prefLabelWidth = snapSize(label.prefWidth(-1));
+ final double prefLabelHeight = snapSize(label.prefHeight(-1));
+
+ final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0;
+ final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0;
+ final double minWidth = snapSize(skinnable.getTabMinWidth());
+ final double maxWidth = snapSize(skinnable.getTabMaxWidth());
+ final double maxHeight = snapSize(skinnable.getTabMaxHeight());
+
+ double labelAreaWidth = prefLabelWidth;
+ double labelWidth = prefLabelWidth;
+ double labelHeight = prefLabelHeight;
+
+ final double childrenWidth = labelAreaWidth + closeBtnWidth;
+ final double childrenHeight = Math.max(labelHeight, closeBtnHeight);
+
+ if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) {
+ labelAreaWidth = maxWidth - closeBtnWidth;
+ labelWidth = maxWidth - closeBtnWidth;
+ } else if (childrenWidth < minWidth) {
+ labelAreaWidth = minWidth - closeBtnWidth;
+ }
+
+ if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) {
+ labelHeight = maxHeight;
+ }
+
+ if (animationState != TabAnimationState.NONE) {
+// if (prefWidth.getValue() < labelAreaWidth) {
+// labelAreaWidth = prefWidth.getValue();
+// }
+ labelAreaWidth *= animationTransition.get();
+ closeBtn.setVisible(false);
+ } else {
+ closeBtn.setVisible(showCloseButton());
+ }
+
+
+ label.resize(labelWidth, labelHeight);
+
+
+ double labelStartX = paddingLeft;
+
+ // If maxWidth is less than Double.MAX_VALUE, the user has
+ // clamped the max width, but we should
+ // position the close button at the end of the tab,
+ // which may not necessarily be the entire width of the
+ // provided max width.
+ double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth;
+
+ positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h,
+ /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
+
+ if (closeBtn.isVisible()) {
+ closeBtn.resize(closeBtnWidth, closeBtnHeight);
+ positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h,
+ /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
+ }
+
+ // Magic numbers regretfully introduced for RT-28944 (so that
+ // the focus rect appears as expected on Windows and Mac).
+ // In short we use the vPadding to shift the focus rect down
+ // into the content area (whereas previously it was being clipped
+ // on Windows, whilst it still looked fine on Mac). In the
+ // future we may want to improve this code to remove the
+ // magic number. Similarly, the hPadding differs on Mac.
+ final int vPadding = Utils.isMac() ? 2 : 3;
+ final int hPadding = Utils.isMac() ? 2 : 1;
+ focusIndicator.resizeRelocate(
+ paddingLeft - hPadding,
+ paddingTop + vPadding,
+ w + 2 * hPadding,
+ h - 2 * vPadding);
+ }
+ };
+ inner.getStyleClass().add("tab-container");
+ inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
+ inner.getChildren().addAll(label, closeBtn, focusIndicator);
+
+ getChildren().addAll(inner);
+
+ tooltip = tab.getTooltip();
+ if (tooltip != null) {
+ Tooltip.install(this, tooltip);
+ oldTooltip = tooltip;
+ }
+
+ listener.registerChangeListener(tab.closableProperty(), "CLOSABLE");
+ listener.registerChangeListener(tab.selectedProperty(), "SELECTED");
+ listener.registerChangeListener(tab.textProperty(), "TEXT");
+ listener.registerChangeListener(tab.graphicProperty(), "GRAPHIC");
+ listener.registerChangeListener(tab.contextMenuProperty(), "CONTEXT_MENU");
+ listener.registerChangeListener(tab.tooltipProperty(), "TOOLTIP");
+ listener.registerChangeListener(tab.disableProperty(), "DISABLE");
+ listener.registerChangeListener(tab.styleProperty(), "STYLE");
+
+ tab.getStyleClass().addListener(weakStyleClassListener);
+
+ listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(), "TAB_CLOSING_POLICY");
+ listener.registerChangeListener(getSkinnable().sideProperty(), "SIDE");
+ listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), "ROTATE_GRAPHIC");
+ listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), "TAB_MIN_WIDTH");
+ listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), "TAB_MAX_WIDTH");
+ listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), "TAB_MIN_HEIGHT");
+ listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), "TAB_MAX_HEIGHT");
+
+ getProperties().put(Tab.class, tab);
+ getProperties().put(ContextMenu.class, tab.getContextMenu());
+
+ setOnContextMenuRequested((ContextMenuEvent me) -> {
+ if (getTab().getContextMenu() != null) {
+ getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
+ me.consume();
+ }
+ });
+ setOnMousePressed(new EventHandler() {
+ @Override public void handle(MouseEvent me) {
+ if (getTab().isDisable()) {
+ return;
+ }
+ if (me.getButton().equals(MouseButton.MIDDLE)) {
+ if (showCloseButton()) {
+ Tab tab = getTab();
+ TabPaneBehavior behavior = getBehavior();
+ if (behavior.canCloseTab(tab)) {
+ removeListeners(tab);
+ behavior.closeTab(tab);
+ }
+ }
+ } else if (me.getButton().equals(MouseButton.PRIMARY)) {
+ getBehavior().selectTab(getTab());
+ }
+ }
+ });
+
+ // initialize pseudo-class state
+ pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
+ pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
+ final Side side = getSkinnable().getSide();
+ pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
+ pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
+ pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
+ pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
+ }
+
+ private void handlePropertyChanged(final String p) {
+ // --- Tab properties
+ if ("CLOSABLE".equals(p)) {
+ inner.requestLayout();
+ requestLayout();
+ } else if ("SELECTED".equals(p)) {
+ pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
+ // Need to request a layout pass for inner because if the width
+ // and height didn't not change the label or close button may have
+ // changed.
+ inner.requestLayout();
+ requestLayout();
+ } else if ("TEXT".equals(p)) {
+ label.setText(getTab().getText());
+ } else if ("GRAPHIC".equals(p)) {
+ label.setGraphic(getTab().getGraphic());
+ } else if ("CONTEXT_MENU".equals(p)) {
+ // todo
+ } else if ("TOOLTIP".equals(p)) {
+ // uninstall the old tooltip
+ if (oldTooltip != null) {
+ Tooltip.uninstall(this, oldTooltip);
+ }
+ tooltip = tab.getTooltip();
+ if (tooltip != null) {
+ // install new tooltip and save as old tooltip.
+ Tooltip.install(this, tooltip);
+ oldTooltip = tooltip;
+ }
+ } else if ("DISABLE".equals(p)) {
+ pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable());
+ inner.requestLayout();
+ requestLayout();
+ } else if ("STYLE".equals(p)) {
+ setStyle(tab.getStyle());
+ }
+
+ // --- Skinnable properties
+ else if ("TAB_CLOSING_POLICY".equals(p)) {
+ inner.requestLayout();
+ requestLayout();
+ } else if ("SIDE".equals(p)) {
+ final Side side = getSkinnable().getSide();
+ pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
+ pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
+ pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
+ pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
+ inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
+ if (getSkinnable().isRotateGraphic()) {
+ updateGraphicRotation();
+ }
+ } else if ("ROTATE_GRAPHIC".equals(p)) {
+ updateGraphicRotation();
+ } else if ("TAB_MIN_WIDTH".equals(p)) {
+ requestLayout();
+ getSkinnable().requestLayout();
+ } else if ("TAB_MAX_WIDTH".equals(p)) {
+ requestLayout();
+ getSkinnable().requestLayout();
+ } else if ("TAB_MIN_HEIGHT".equals(p)) {
+ requestLayout();
+ getSkinnable().requestLayout();
+ } else if ("TAB_MAX_HEIGHT".equals(p)) {
+ requestLayout();
+ getSkinnable().requestLayout();
+ }
+ }
+
+ private void updateGraphicRotation() {
+ if (label.getGraphic() != null) {
+ label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :
+ (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F :
+ (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
+ }
+ }
+
+ private boolean showCloseButton() {
+ return tab.isClosable() &&
+ (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) ||
+ getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
+ }
+
+ private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
+ @Override protected void invalidated() {
+ requestLayout();
+ }
+ };
+
+ private void removeListeners(Tab tab) {
+ listener.dispose();
+ inner.getChildren().clear();
+ getChildren().clear();
+ }
+
+ private TabAnimationState animationState = TabAnimationState.NONE;
+ private Timeline currentAnimation;
+
+ @Override protected double computePrefWidth(double height) {
+// if (animating) {
+// return prefWidth.getValue();
+// }
+ double minWidth = snapSize(getSkinnable().getTabMinWidth());
+ double maxWidth = snapSize(getSkinnable().getTabMaxWidth());
+ double paddingRight = snappedRightInset();
+ double paddingLeft = snappedLeftInset();
+ double tmpPrefWidth = snapSize(label.prefWidth(-1));
+
+ // only include the close button width if it is relevant
+ if (showCloseButton()) {
+ tmpPrefWidth += snapSize(closeBtn.prefWidth(-1));
+ }
+
+ if (tmpPrefWidth > maxWidth) {
+ tmpPrefWidth = maxWidth;
+ } else if (tmpPrefWidth < minWidth) {
+ tmpPrefWidth = minWidth;
+ }
+ tmpPrefWidth += paddingRight + paddingLeft;
+// prefWidth.setValue(tmpPrefWidth);
+ return tmpPrefWidth;
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ double minHeight = snapSize(getSkinnable().getTabMinHeight());
+ double maxHeight = snapSize(getSkinnable().getTabMaxHeight());
+ double paddingTop = snappedTopInset();
+ double paddingBottom = snappedBottomInset();
+ double tmpPrefHeight = snapSize(label.prefHeight(width));
+
+ if (tmpPrefHeight > maxHeight) {
+ tmpPrefHeight = maxHeight;
+ } else if (tmpPrefHeight < minHeight) {
+ tmpPrefHeight = minHeight;
+ }
+ tmpPrefHeight += paddingTop + paddingBottom;
+ return tmpPrefHeight;
+ }
+
+ @Override protected void layoutChildren() {
+ double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue();
+ inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset());
+ inner.relocate(snappedLeftInset(), snappedTopInset());
+ }
+
+ @Override protected void setWidth(double value) {
+ super.setWidth(value);
+ clip.setWidth(value);
+ }
+
+ @Override protected void setHeight(double value) {
+ super.setHeight(value);
+ clip.setHeight(value);
+ }
+
+ @Override
+ public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ switch (attribute) {
+ case TEXT: return getTab().getText();
+ case SELECTED: return selectedTab == getTab();
+ default: return super.queryAccessibleAttribute(attribute, parameters);
+ }
+ }
+
+ @Override
+ public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
+ switch (action) {
+ case REQUEST_FOCUS:
+ getSkinnable().getSelectionModel().select(getTab());
+ break;
+ default: super.executeAccessibleAction(action, parameters);
+ }
+ }
+
+} /* End TabHeaderSkin */
+
+private static final PseudoClass SELECTED_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("selected");
+private static final PseudoClass TOP_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("top");
+private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("bottom");
+private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("left");
+private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("right");
+private static final PseudoClass DISABLED_PSEUDOCLASS_STATE =
+ PseudoClass.getPseudoClass("disabled");
+
+
+/**************************************************************************
+ *
+ * TabContentRegion: each tab has one to contain the tab's content node
+ *
+ **************************************************************************/
+class TabContentRegion extends StackPane {
+
+ private TraversalEngine engine;
+ private Direction direction = Direction.NEXT;
+ private Tab tab;
+
+ private InvalidationListener tabContentListener = valueModel -> {
+ updateContent();
+ };
+ private InvalidationListener tabSelectedListener = new InvalidationListener() {
+ @Override public void invalidated(Observable valueModel) {
+ setVisible(tab.isSelected());
+ }
+ };
+
+ private WeakInvalidationListener weakTabContentListener =
+ new WeakInvalidationListener(tabContentListener);
+ private WeakInvalidationListener weakTabSelectedListener =
+ new WeakInvalidationListener(tabSelectedListener);
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ public TabContentRegion(Tab tab) {
+ getStyleClass().setAll("tab-content-area");
+ setManaged(false);
+ this.tab = tab;
+ updateContent();
+ setVisible(tab.isSelected());
+
+ tab.selectedProperty().addListener(weakTabSelectedListener);
+ tab.contentProperty().addListener(weakTabContentListener);
+ }
+
+ private void updateContent() {
+ Node newContent = getTab().getContent();
+ if (newContent == null) {
+ getChildren().clear();
+ } else {
+ getChildren().setAll(newContent);
+ }
+ }
+
+ private void removeListeners(Tab tab) {
+ tab.selectedProperty().removeListener(weakTabSelectedListener);
+ tab.contentProperty().removeListener(weakTabContentListener);
+ }
+
+} /* End TabContentRegion */
+
+/**************************************************************************
+ *
+ * TabControlButtons: controls to manipulate tab interaction
+ *
+ **************************************************************************/
+class TabControlButtons extends StackPane {
+ private StackPane inner;
+ private StackPane downArrow;
+ private Pane downArrowBtn;
+ private boolean showControlButtons;
+ private ContextMenu popup;
+
+ public TabControlButtons() {
+ getStyleClass().setAll("control-buttons-tab");
+
+ TabPane tabPane = getSkinnable();
+
+ downArrowBtn = new Pane();
+ downArrowBtn.getStyleClass().setAll("tab-down-button");
+ downArrowBtn.setVisible(isShowTabsMenu());
+ downArrow = new StackPane();
+ downArrow.setManaged(false);
+ downArrow.getStyleClass().setAll("arrow");
+ downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
+ downArrowBtn.getChildren().add(downArrow);
+ downArrowBtn.setOnMouseClicked(me -> {
+ showPopupMenu();
+ });
+
+ setupPopupMenu();
+
+ inner = new StackPane() {
+ @Override protected double computePrefWidth(double height) {
+ double pw;
+ double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight()));
+ pw = 0.0F;
+ if (isShowTabsMenu()) {
+ pw += maxArrowWidth;
+ }
+ if (pw > 0) {
+ pw += snappedLeftInset() + snappedRightInset();
+ }
+ return pw;
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ double height = 0.0F;
+ if (isShowTabsMenu()) {
+ height = Math.max(height, snapSize(downArrowBtn.prefHeight(width)));
+ }
+ if (height > 0) {
+ height += snappedTopInset() + snappedBottomInset();
+ }
+ return height;
+ }
+
+ @Override protected void layoutChildren() {
+ if (isShowTabsMenu()) {
+ double x = 0;
+ double y = snappedTopInset();
+ double w = snapSize(getWidth()) - x + snappedLeftInset();
+ double h = snapSize(getHeight()) - y + snappedBottomInset();
+ positionArrow(downArrowBtn, downArrow, x, y, w, h);
+ }
+ }
+
+ private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) {
+ btn.resize(width, height);
+ positionInArea(btn, x, y, width, height, /*baseline ignored*/0,
+ HPos.CENTER, VPos.CENTER);
+ // center arrow region within arrow button
+ double arrowWidth = snapSize(arrow.prefWidth(-1));
+ double arrowHeight = snapSize(arrow.prefHeight(-1));
+ arrow.resize(arrowWidth, arrowHeight);
+ positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(),
+ width - btn.snappedLeftInset() - btn.snappedRightInset(),
+ height - btn.snappedTopInset() - btn.snappedBottomInset(),
+ /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
+ }
+ };
+ inner.getStyleClass().add("container");
+ inner.getChildren().add(downArrowBtn);
+
+ getChildren().add(inner);
+
+ tabPane.sideProperty().addListener(valueModel -> {
+ Side tabPosition = getSkinnable().getSide();
+ downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F);
+ });
+ tabPane.getTabs().addListener((ListChangeListener) c -> setupPopupMenu());
+ showControlButtons = false;
+ if (isShowTabsMenu()) {
+ showControlButtons = true;
+ requestLayout();
+ }
+ getProperties().put(ContextMenu.class, popup);
+ }
+
+ private boolean showTabsMenu = false;
+
+ private void showTabsMenu(boolean value) {
+ final boolean wasTabsMenuShowing = isShowTabsMenu();
+ this.showTabsMenu = value;
+
+ if (showTabsMenu && !wasTabsMenuShowing) {
+ downArrowBtn.setVisible(true);
+ showControlButtons = true;
+ inner.requestLayout();
+ tabHeaderArea.requestLayout();
+ } else if (!showTabsMenu && wasTabsMenuShowing) {
+ hideControlButtons();
+ }
+ }
+
+ private boolean isShowTabsMenu() {
+ return showTabsMenu;
+ }
+
+ @Override protected double computePrefWidth(double height) {
+ double pw = snapSize(inner.prefWidth(height));
+ if (pw > 0) {
+ pw += snappedLeftInset() + snappedRightInset();
+ }
+ return pw;
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) +
+ snappedTopInset() + snappedBottomInset();
+ }
+
+ @Override protected void layoutChildren() {
+ double x = snappedLeftInset();
+ double y = snappedTopInset();
+ double w = snapSize(getWidth()) - x + snappedRightInset();
+ double h = snapSize(getHeight()) - y + snappedBottomInset();
+
+ if (showControlButtons) {
+ showControlButtons();
+ showControlButtons = false;
+ }
+
+ inner.resize(w, h);
+ positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM);
+ }
+
+ private void showControlButtons() {
+ setVisible(true);
+ if (popup == null) {
+ setupPopupMenu();
+ }
+ }
+
+ private void hideControlButtons() {
+ // If the scroll arrows or tab menu is still visible we don't want
+ // to hide it animate it back it.
+ if (isShowTabsMenu()) {
+ showControlButtons = true;
+ } else {
+ setVisible(false);
+ popup.getItems().clear();
+ popup = null;
+ }
+
+ // This needs to be called when we are in the left tabPosition
+ // to allow for the clip offset to move properly (otherwise
+ // it jumps too early - before the animation is done).
+ requestLayout();
+ }
+
+ private void setupPopupMenu() {
+ if (popup == null) {
+ popup = new ContextMenu();
+ }
+ popup.getItems().clear();
+ ToggleGroup group = new ToggleGroup();
+ ObservableList menuitems = FXCollections.observableArrayList();
+ for (final Tab tab : getSkinnable().getTabs()) {
+ TabMenuItem item = new TabMenuItem(tab);
+ item.setToggleGroup(group);
+ item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab));
+ menuitems.add(item);
+ }
+ popup.getItems().addAll(menuitems);
+ }
+
+ private void showPopupMenu() {
+ for (MenuItem mi: popup.getItems()) {
+ TabMenuItem tmi = (TabMenuItem)mi;
+ if (selectedTab.equals(tmi.getTab())) {
+ tmi.setSelected(true);
+ break;
+ }
+ }
+ popup.show(downArrowBtn, Side.BOTTOM, 0, 0);
+ }
+} /* End TabControlButtons*/
+
+class TabMenuItem extends RadioMenuItem {
+ DockNodeTab tab;
+
+ private InvalidationListener disableListener = new InvalidationListener() {
+ @Override public void invalidated(Observable o) {
+ setDisable(tab.isDisable());
+ }
+ };
+
+ private WeakInvalidationListener weakDisableListener =
+ new WeakInvalidationListener(disableListener);
+
+ public TabMenuItem(final Tab tab) {
+ this((DockNodeTab) tab);
+ }
+
+ public TabMenuItem(final DockNodeTab tab) {
+ super(tab.getTitle(), ContentTabPaneSkin.clone(tab.getGraphic()));
+ this.tab = tab;
+ setDisable(tab.isDisable());
+ tab.disableProperty().addListener(weakDisableListener);
+ textProperty().bind(tab.titleProperty());
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ public void dispose() {
+ tab.disableProperty().removeListener(weakDisableListener);
+ }
+}
+
+ @Override
+ public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ switch (attribute) {
+ case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab);
+ case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size();
+ case ITEM_AT_INDEX: {
+ Integer index = (Integer)parameters[0];
+ if (index == null) return null;
+ return tabHeaderArea.headersRegion.getChildren().get(index);
+ }
+ default: return super.queryAccessibleAttribute(attribute, parameters);
+ }
+ }
+}
diff --git a/src/main/java/org/dockfx/viewControllers/DockFXViewController.java b/src/main/java/org/dockfx/viewControllers/DockFXViewController.java
new file mode 100644
index 0000000..cd9d3ba
--- /dev/null
+++ b/src/main/java/org/dockfx/viewControllers/DockFXViewController.java
@@ -0,0 +1,42 @@
+/**
+ * @file DockFXViewController.java
+ * @brief Abstract class for View Controllers
+ *
+ * @section License
+ *
+ * This file is a part of the DockFX Library. Copyright (C) 2015 Robert B. Colton
+ *
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this program.
+ * If not, see .
+ *
+ */
+package org.dockfx.viewControllers;
+
+import org.dockfx.DockTitleBar;
+
+/**
+ * Abstract base class for View Controllers
+ *
+ * @since DockFX 0.1
+ */
+abstract public class DockFXViewController {
+
+ private DockTitleBar dockTitleBar;
+
+ public void setDockTitleBar(DockTitleBar dockTitleBar) {
+ this.dockTitleBar = dockTitleBar;
+ }
+
+ public DockTitleBar getDockTitleBar() {
+ return dockTitleBar;
+ }
+
+}
diff --git a/src/main/resources/org/dockfx/default.css b/src/main/resources/org/dockfx/default.css
index f64aa2a..96d872d 100644
--- a/src/main/resources/org/dockfx/default.css
+++ b/src/main/resources/org/dockfx/default.css
@@ -127,15 +127,18 @@
-fx-background-color: -fx-background;
}
+/* In order to support TabPane, titleBar's background color is transparent
+ */
.dock-title-bar {
- -fx-background-color: -fx-background;
+ -fx-background-color: transparent;
-fx-padding: 2;
-fx-spacing: 3;
- -fx-border-width: 1;
+ -fx-border-width: 0;
-fx-border-color: -fx-outer-border;
}
.dock-title-label {
+ -fx-background-color: transparent;
-fx-graphic: url(docknode.png);
-fx-padding: 0 0 3 0;
-fx-text-fill: -fx-text-base-color;
@@ -169,3 +172,7 @@
.dock-close-button {
-fx-graphic: url(close.png);
}
+
+.dock-minimize-button {
+ -fx-graphic: url(minimize.png);
+}
\ No newline at end of file
diff --git a/src/main/resources/org/dockfx/minimize.png b/src/main/resources/org/dockfx/minimize.png
new file mode 100644
index 0000000..84dd6c6
Binary files /dev/null and b/src/main/resources/org/dockfx/minimize.png differ