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 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 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 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