From 5369a9c1ec5d4fd89a92e8cd9e8f1ca8412a7f04 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 13 Jan 2016 14:11:03 +0100 Subject: [PATCH 01/51] WIP --- src/main/java/org/dockfx/DockPane.java | 313 ++++++++++++++++--------- 1 file changed, 197 insertions(+), 116 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 7ea7a94..368524c 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -39,6 +39,8 @@ import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; @@ -253,7 +255,7 @@ 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, + dockPosButtons = FXCollections.observableArrayList(dockCenter, dockTop, dockRight, dockBottom, dockLeft, dockTopRoot, dockRightRoot, dockBottomRoot, dockLeftRoot); dockPosIndicator = new GridPane(); @@ -261,7 +263,7 @@ public void handle(DockEvent event) { 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); @@ -387,79 +389,115 @@ public void dock(Node node, DockPos dockPos, Node 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); - } - - // finally dock the node to the correct split pane - ObservableList splitItems = split.getItems(); - - double magnitude = 0; - - 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); - } - } - } - - 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 (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))); - } - } - } 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 { - split.setDividerPosition(relativeIndex - 1, - 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0))); - } - } - } - + if(dockPos == DockPos.CENTER) + { + if(split.getItems().size() > 0) + { + DockNode siblingNode = (DockNode) sibling; + DockNode newNode = (DockNode) node; + + TabPane tabPane = new TabPane(); + + split.getItems().set( split.getItems().indexOf( sibling ), tabPane ); + tabPane.getTabs().add( new Tab( siblingNode.getTitle(), sibling ) ); + tabPane.getTabs().add( new Tab( newNode.getTitle(), node ) ); + //((SplitPane) root).getItems().add( tabPane ); + } + } + else + { + 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 ); + } + + // finally dock the node to the correct split pane + ObservableList splitItems = split.getItems(); + + double magnitude = 0; + + 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 ); + } + } + } + + 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 (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 )) ); + } + } + } 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 + { + split.setDividerPosition( relativeIndex - 1, + 1 - node.prefHeight( 0 ) / (magnitude + node.prefHeight( 0 )) ); + } + } + } + } } /** @@ -490,49 +528,92 @@ public void undock(DockNode node) { while (!findStack.isEmpty()) { Parent parent = findStack.pop(); - ObservableList children = parent.getChildrenUnmodifiable(); - if (parent instanceof SplitPane) { - SplitPane split = (SplitPane) parent; - children = split.getItems(); - } + SplitPane splitPane = (SplitPane) parent; + ObservableList children = splitPane.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(); + } + + 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); + } + } + + } + } + + return; + } else if (children.get(i) instanceof Parent) { + findStack.push((Parent) children.get(i)); + } + } + + } + else if (parent instanceof TabPane) + { + TabPane tabPane = (TabPane) parent; + ObservableList tabChildren = tabPane.getTabs(); + + for (int i = 0; i < tabChildren.size(); i++) { + if (tabChildren.get(i).getContent() == node) { + tabChildren.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(); + + ObservableList children = parent.getChildrenUnmodifiable(); + + if (parent instanceof SplitPane) { + SplitPane split = (SplitPane) parent; + children = split.getItems(); + } + + 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); + } + } + + } + } + + return; + } else if (tabChildren.get(i).getContent() instanceof Parent) { + findStack.push((Parent) tabChildren.get(i).getContent()); + } + } + + } - 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(); - } - - 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); - } - } - - } - } - - return; - } else if (children.get(i) instanceof Parent) { - findStack.push((Parent) children.get(i)); - } - } } } From d0c6a8f7d064202962882551a4fe68be0202ec9f Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Jan 2016 15:11:57 +0100 Subject: [PATCH 02/51] Support multi tab in DockFX --- src/main/java/org/dockfx/DockNode.java | 39 +++ src/main/java/org/dockfx/DockPane.java | 273 ++++-------------- .../java/org/dockfx/pane/ContentPane.java | 87 ++++++ .../org/dockfx/pane/ContentSplitPane.java | 196 +++++++++++++ .../java/org/dockfx/pane/ContentTabPane.java | 128 ++++++++ .../java/org/dockfx/pane/DockNodeTab.java | 24 ++ src/main/resources/org/dockfx/default.css | 7 +- 7 files changed, 530 insertions(+), 224 deletions(-) create mode 100644 src/main/java/org/dockfx/pane/ContentPane.java create mode 100644 src/main/java/org/dockfx/pane/ContentSplitPane.java create mode 100644 src/main/java/org/dockfx/pane/ContentTabPane.java create mode 100644 src/main/java/org/dockfx/pane/DockNodeTab.java diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 3dc097d..89a0493 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -616,6 +616,44 @@ 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().clear(); + getChildren().addAll(dockTitleBar, contents); + } + } + } + + @Override + public String getName() { + return "tabbed"; + } + }; + + public final boolean isTabbed() { + return floatingProperty.get(); + } + + /** * Dock this node into a dock pane. * @@ -655,6 +693,7 @@ public void undock() { dockPane.undock(this); } this.dockedProperty.set(false); + this.tabbedProperty.set(false); } /** diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 368524c..aa5934c 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -20,10 +20,16 @@ package org.dockfx; +import java.util.List; import java.util.Stack; import com.sun.javafx.css.StyleManager; +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; @@ -32,15 +38,11 @@ import javafx.collections.ObservableMap; import javafx.css.PseudoClass; import javafx.event.EventHandler; -import javafx.geometry.Orientation; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Button; -import javafx.scene.control.SplitPane; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; @@ -356,148 +358,46 @@ public void dock(Node node, DockPos dockPos, Node sibling) { 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(); + if(sibling != null && sibling != root) { + Stack stack = new Stack<>(); stack.push((Parent) root); - while (!stack.isEmpty()) { - Parent parent = stack.pop(); + pane = pane.getSiblingParent(stack, sibling); + } - ObservableList children = parent.getChildrenUnmodifiable(); + if(dockPos == DockPos.CENTER) + { + if(pane instanceof ContentTabPane) + { + pane.addNode(root, sibling, node, dockPos); + } + else + { + // Create a ContentTabPane with two nodes + DockNode siblingNode = (DockNode) sibling; + DockNode newNode = (DockNode) node; - if (parent instanceof SplitPane) { - SplitPane splitPane = (SplitPane) parent; - children = splitPane.getItems(); - } + ContentTabPane tabPane = new ContentTabPane(); - 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)); - } - } + tabPane.getTabs().add( new DockNodeTab( siblingNode ) ); + tabPane.getTabs().add( new DockNodeTab( newNode ) ); + + tabPane.setContentParent(pane); + pane.set(sibling, tabPane); } } - - if(dockPos == DockPos.CENTER) - { - if(split.getItems().size() > 0) - { - DockNode siblingNode = (DockNode) sibling; - DockNode newNode = (DockNode) node; - - TabPane tabPane = new TabPane(); - - split.getItems().set( split.getItems().indexOf( sibling ), tabPane ); - tabPane.getTabs().add( new Tab( siblingNode.getTitle(), sibling ) ); - tabPane.getTabs().add( new Tab( newNode.getTitle(), node ) ); - //((SplitPane) root).getItems().add( tabPane ); - } - } - else - { - 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 ); - } - - // finally dock the node to the correct split pane - ObservableList splitItems = split.getItems(); - - double magnitude = 0; - - 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 ); - } - } - } - - 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 (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 )) ); - } - } - } 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 - { - split.setDividerPosition( relativeIndex - 1, - 1 - node.prefHeight( 0 ) / (magnitude + node.prefHeight( 0 )) ); - } - } - } - } + else + { + pane.addNode(root, sibling, node, dockPos); + } } /** @@ -525,95 +425,24 @@ 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(); - if (parent instanceof SplitPane) { - SplitPane splitPane = (SplitPane) parent; - ObservableList children = splitPane.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(); - } - - 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); - } - } - - } - } - - return; - } else if (children.get(i) instanceof Parent) { - findStack.push((Parent) children.get(i)); - } - } - - } - else if (parent instanceof TabPane) - { - TabPane tabPane = (TabPane) parent; - ObservableList tabChildren = tabPane.getTabs(); - - for (int i = 0; i < tabChildren.size(); i++) { - if (tabChildren.get(i).getContent() == node) { - tabChildren.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(); - - ObservableList children = parent.getChildrenUnmodifiable(); - - if (parent instanceof SplitPane) { - SplitPane split = (SplitPane) parent; - children = split.getItems(); - } - - 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); - } - } - - } - } - - return; - } else if (tabChildren.get(i).getContent() instanceof Parent) { - findStack.push((Parent) tabChildren.get(i).getContent()); - } - } - - } - + if(parent instanceof ContentPane) + { + ContentPane pane = (ContentPane) parent; + pane.removeNode(findStack, node); + + // if there is only 1-tab left, we replace it with the SplitPane + if (pane.getChildrenList().size() == 1 && pane instanceof ContentTabPane) { + List children = pane.getChildrenList(); + DockNode sibling = (DockNode) children.get(0); + ContentPane contentParent = ((ContentTabPane) pane).getContentParent(); + contentParent.set((Node)pane, sibling); + sibling.tabbedProperty().setValue(false); + } + } } } 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..8402982 --- /dev/null +++ b/src/main/java/org/dockfx/pane/ContentPane.java @@ -0,0 +1,87 @@ +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 + */ + void 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); +} 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..29f3751 --- /dev/null +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -0,0 +1,196 @@ +package org.dockfx.pane; + +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 { + + public Type getType() { + return Type.SplitPane; + } + + /** + * 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 void removeNode(Stack stack, Node node) + { + ContentPane pane = null; + + List children = getChildrenList(); + for(int i = 0; i < children.size(); i++) + { + if(children.get(i) == node) { + getItems().remove(i); + + while (!stack.isEmpty()) { + Parent parent = stack.pop(); + + children = parent.getChildrenUnmodifiable(); + + for (i = 0; i < children.size(); i++) { + if (children.get(i) instanceof ContentPane) { + pane = (ContentPane) children.get(i); + if (pane.getChildrenList().size() < 1) { + children.remove(i); + continue; + } else { + stack.push((Parent) pane); + } + } + } + + } + return; + } else if(children.get(i) instanceof Parent) + { + stack.push((Parent) children.get(i)); + } + } + + } + + 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) + { + Orientation requestedOrientation = (dockPos == DockPos.LEFT || dockPos == DockPos.RIGHT) + ? Orientation.HORIZONTAL : Orientation.VERTICAL; + + ContentSplitPane split = this; + + // 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.set(root, splitPane); + splitPane.getItems().add(split); + root = splitPane; + } else { + split.set(sibling, splitPane); + splitPane.getItems().add(sibling); + } + + split = splitPane; + } + split.setOrientation(requestedOrientation); + } + + // finally dock the node to the correct split pane + ObservableList splitItems = split.getItems(); + + double magnitude = 0; + + 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); + } + } + } + + 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 (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))); + } + } + } 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 { + split.setDividerPosition(relativeIndex - 1, + 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0))); + } + } + } + } +} 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..fdc8524 --- /dev/null +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -0,0 +1,128 @@ +package org.dockfx.pane; + +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.Tab; +import javafx.scene.control.TabPane; + +/** + * ContentTabPane holds multiple tabs + * @author HongKee Moon + */ +public class ContentTabPane extends TabPane implements ContentPane { + + ContentPane parent; + + public ContentTabPane() + { + this.getStyleClass().add("dock-tab-pane"); + } + + 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 void removeNode(Stack stack, Node node) + { + ContentPane pane = null; + + List children = getChildrenList(); + for(int i = 0; i < children.size(); i++) + { + if(children.get(i) == node) { + getTabs().remove(i); + + while (!stack.isEmpty()) { + Parent parent = stack.pop(); + + children = parent.getChildrenUnmodifiable(); + + if (parent instanceof ContentPane) { + children = ((ContentPane) parent).getChildrenList(); + } + + for (i = 0; i < children.size(); i++) { + if (children.get(i) instanceof ContentPane) { + pane = (ContentPane) children.get(i); + if (pane.getChildrenList().size() < 1) { + children.remove(i); + continue; + } else { + stack.push((Parent) pane); + } + } + } + } + return; + } else if(children.get(i) instanceof Parent) + { + stack.push((Parent) children.get(i)); + } + } + + } + + public void set(int idx, Node node) + { + DockNode newNode = (DockNode) node; + getTabs().set(idx, new DockNodeTab(newNode)); + } + + 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; + getTabs().add( new Tab( newNode.getTitle(), node ) ); + } +} 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..eae3ca3 --- /dev/null +++ b/src/main/java/org/dockfx/pane/DockNodeTab.java @@ -0,0 +1,24 @@ +package org.dockfx.pane; + +import org.dockfx.DockNode; + +import javafx.scene.control.Tab; + +/** + * DockNodeTab class holds Tab for ContentTabPane + * @author HongKee Moon + */ +public class DockNodeTab extends Tab { + + final private DockNode dockNode; + + public DockNodeTab(DockNode node) + { + this.dockNode = node; + setClosable(false); + + setGraphic(dockNode.getDockTitleBar()); + setContent(dockNode); + dockNode.tabbedProperty().set(true); + } +} diff --git a/src/main/resources/org/dockfx/default.css b/src/main/resources/org/dockfx/default.css index f64aa2a..803e943 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; From fd2a7b1527523781c5e9e33059e8e18640190d8d Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Jan 2016 16:43:20 +0100 Subject: [PATCH 03/51] Use simpler way to add a component --- src/main/java/org/dockfx/DockNode.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 89a0493..4cca747 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -637,8 +637,7 @@ protected void invalidated() { } else { - getChildren().clear(); - getChildren().addAll(dockTitleBar, contents); + getChildren().add(0, dockTitleBar); } } } From e54816708fcead40a7f918df13e1ee44ca9e12c7 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Jan 2016 10:59:28 +0100 Subject: [PATCH 04/51] Fix the bug that TabPane confuses orientation during docking behavior --- src/main/java/org/dockfx/DockPane.java | 154 ++++++++++++------ .../java/org/dockfx/pane/ContentPane.java | 30 +++- .../org/dockfx/pane/ContentSplitPane.java | 125 ++++++-------- .../java/org/dockfx/pane/ContentTabPane.java | 70 ++------ .../java/org/dockfx/pane/DockNodeTab.java | 4 +- 5 files changed, 196 insertions(+), 187 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index aa5934c..8d94308 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -38,6 +38,7 @@ import javafx.collections.ObservableMap; import javafx.css.PseudoClass; import javafx.event.EventHandler; +import javafx.geometry.Orientation; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.Node; @@ -53,10 +54,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. */ @@ -108,13 +110,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. @@ -137,9 +141,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; @@ -147,7 +151,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) { @@ -156,7 +160,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() { @@ -166,9 +170,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; @@ -257,8 +261,9 @@ 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(dockCenter, 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); @@ -268,7 +273,7 @@ public void handle(DockEvent event) { dockPosIndicator.add(dockCenter, 1, 1); dockRootPane.getChildren().addAll(dockAreaIndicator, dockTopRoot, dockRightRoot, dockBottomRoot, - dockLeftRoot); + dockLeftRoot); dockIndicatorOverlay.getContent().add(dockRootPane); dockIndicatorPopup.getContent().addAll(dockPosIndicator); @@ -282,9 +287,9 @@ public void handle(DockEvent event) { /** * 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; @@ -292,7 +297,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() { @@ -318,10 +323,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. */ @@ -330,9 +336,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; @@ -348,8 +354,8 @@ 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. */ @@ -359,53 +365,101 @@ public void dock(Node node, DockPos dockPos, Node sibling) { node.addEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler); ContentPane pane = (ContentPane) root; - if (pane == null) - { + if (pane == null) { pane = new ContentSplitPane(node); root = (Node) pane; this.getChildren().add(root); return; } - if(sibling != null && sibling != root) { + if (sibling != null && sibling != root) { Stack stack = new Stack<>(); stack.push((Parent) root); pane = pane.getSiblingParent(stack, sibling); } - if(dockPos == DockPos.CENTER) - { - if(pane instanceof ContentTabPane) - { - pane.addNode(root, sibling, node, dockPos); - } - else - { + if (dockPos == DockPos.CENTER) { + if (pane instanceof ContentSplitPane) { // Create a ContentTabPane with two nodes DockNode siblingNode = (DockNode) sibling; DockNode newNode = (DockNode) node; ContentTabPane tabPane = new ContentTabPane(); - tabPane.getTabs().add( new DockNodeTab( siblingNode ) ); - tabPane.getTabs().add( new DockNodeTab( newNode ) ); + tabPane.getTabs().add(new DockNodeTab(siblingNode)); + tabPane.getTabs().add(new DockNodeTab(newNode)); tabPane.setContentParent(pane); pane.set(sibling, tabPane); } + } 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); + } + + split = splitPane; + } + split.setOrientation(requestedOrientation); + pane = split; + } + + } + 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 { + sibling = (Node) pane; + } + + split.setOrientation(requestedOrientation); + pane = split; + } } - else - { - pane.addNode(root, sibling, node, dockPos); - } + + // Add a node to the proper pane + pane.addNode(root, sibling, node, dockPos); } /** * 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) { @@ -414,7 +468,7 @@ public void dock(Node node, DockPos dockPos) { /** * 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) { @@ -429,21 +483,25 @@ public void undock(DockNode node) { while (!findStack.isEmpty()) { Parent parent = findStack.pop(); - if(parent instanceof ContentPane) - { + if (parent instanceof ContentPane) { ContentPane pane = (ContentPane) parent; pane.removeNode(findStack, node); // if there is only 1-tab left, we replace it with the SplitPane - if (pane.getChildrenList().size() == 1 && pane instanceof ContentTabPane) { + if (pane.getChildrenList().size() == 1 && + pane instanceof ContentTabPane && + pane.getChildrenList().get(0) instanceof DockNode) { + List children = pane.getChildrenList(); - DockNode sibling = (DockNode) children.get(0); - ContentPane contentParent = ((ContentTabPane) pane).getContentParent(); - contentParent.set((Node)pane, sibling); - sibling.tabbedProperty().setValue(false); + Node sibling = children.get(0); + ContentPane contentParent = pane.getContentParent(); + + contentParent.set((Node) pane, sibling); + ((DockNode)sibling).tabbedProperty().setValue(false); } } } + } @Override @@ -508,9 +566,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); diff --git a/src/main/java/org/dockfx/pane/ContentPane.java b/src/main/java/org/dockfx/pane/ContentPane.java index 8402982..78dd745 100644 --- a/src/main/java/org/dockfx/pane/ContentPane.java +++ b/src/main/java/org/dockfx/pane/ContentPane.java @@ -10,6 +10,7 @@ /** * ContentPane interface has common functions for Content window + * * @author HongKee Moon */ public interface ContentPane { @@ -38,9 +39,9 @@ public enum Type { /** * Add a node. * - * @param root the root + * @param root the root * @param sibling the sibling - * @param node the node + * @param node the node * @param dockPos the dock pos */ void addNode(Node root, Node sibling, Node node, DockPos dockPos); @@ -49,14 +50,15 @@ public enum Type { * Remove the node. * * @param stack the stack - * @param node the node + * @param node the node + * @return true if the node removed successfully, otherwise false */ - void removeNode(Stack stack, Node node); + boolean removeNode(Stack stack, Node node); /** * Gets sibling's parent. * - * @param stack the stack + * @param stack the stack * @param sibling the sibling * @return the sibling parent */ @@ -73,15 +75,29 @@ public enum Type { * Replace the previous element with new one * * @param sibling the sibling - * @param node the node + * @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 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 index 29f3751..a03495a 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -13,20 +13,33 @@ /** * 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() - { + public ContentSplitPane() { } /** @@ -34,13 +47,11 @@ public ContentSplitPane() * * @param node the node */ - public ContentSplitPane(Node node) - { + public ContentSplitPane(Node node) { getItems().add(node); } - public ContentPane getSiblingParent(Stack stack, Node sibling) - { + public ContentPane getSiblingParent(Stack stack, Node sibling) { ContentPane pane = null; while (!stack.isEmpty()) { @@ -64,90 +75,48 @@ public ContentPane getSiblingParent(Stack stack, Node sibling) } - public void removeNode(Stack stack, Node node) - { - ContentPane pane = null; + 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); - - while (!stack.isEmpty()) { - Parent parent = stack.pop(); - - children = parent.getChildrenUnmodifiable(); - - for (i = 0; i < children.size(); i++) { - if (children.get(i) instanceof ContentPane) { - pane = (ContentPane) children.get(i); - if (pane.getChildrenList().size() < 1) { - children.remove(i); - continue; - } else { - stack.push((Parent) pane); - } - } - } + 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) && pane.getChildrenList().size() < 1) { + getItems().remove(i); + return true; } - return; - } else if(children.get(i) instanceof Parent) - { - stack.push((Parent) children.get(i)); } } + return false; } - public List getChildrenList() - { + public List getChildrenList() { return getItems(); } - public void set(int idx, Node node) - { + 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 set(Node sibling, Node node) { + set(getItems().indexOf(sibling), node); } - public void addNode(Node root, Node sibling, Node node, DockPos dockPos) - { - Orientation requestedOrientation = (dockPos == DockPos.LEFT || dockPos == DockPos.RIGHT) - ? Orientation.HORIZONTAL : Orientation.VERTICAL; - - ContentSplitPane split = this; - - // 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.set(root, splitPane); - splitPane.getItems().add(split); - root = splitPane; - } else { - split.set(sibling, splitPane); - splitPane.getItems().add(sibling); - } - - split = splitPane; - } - split.setOrientation(requestedOrientation); - } - + public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { // finally dock the node to the correct split pane - ObservableList splitItems = split.getItems(); + ObservableList splitItems = getItems(); double magnitude = 0; if (splitItems.size() > 0) { - if (split.getOrientation() == Orientation.HORIZONTAL) { + if (getOrientation() == Orientation.HORIZONTAL) { for (Node splitItem : splitItems) { magnitude += splitItem.prefWidth(0); } @@ -167,12 +136,12 @@ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) splitItems.add(relativeIndex, node); if (splitItems.size() > 1) { - if (split.getOrientation() == Orientation.HORIZONTAL) { - split.setDividerPosition(relativeIndex, - node.prefWidth(0) / (magnitude + node.prefWidth(0))); + if (getOrientation() == Orientation.HORIZONTAL) { + setDividerPosition(relativeIndex, + node.prefWidth(0) / (magnitude + node.prefWidth(0))); } else { - split.setDividerPosition(relativeIndex, - node.prefHeight(0) / (magnitude + node.prefHeight(0))); + setDividerPosition(relativeIndex, + node.prefHeight(0) / (magnitude + node.prefHeight(0))); } } } else if (dockPos == DockPos.RIGHT || dockPos == DockPos.BOTTOM) { @@ -183,12 +152,12 @@ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) 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))); + if (getOrientation() == Orientation.HORIZONTAL) { + setDividerPosition(relativeIndex - 1, + 1 - node.prefWidth(0) / (magnitude + node.prefWidth(0))); } else { - split.setDividerPosition(relativeIndex - 1, - 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0))); + setDividerPosition(relativeIndex - 1, + 1 - node.prefHeight(0) / (magnitude + node.prefHeight(0))); } } } diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index fdc8524..3ac13a1 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -15,14 +15,14 @@ /** * ContentTabPane holds multiple tabs + * * @author HongKee Moon */ public class ContentTabPane extends TabPane implements ContentPane { ContentPane parent; - public ContentTabPane() - { + public ContentTabPane() { this.getStyleClass().add("dock-tab-pane"); } @@ -30,18 +30,15 @@ public Type getType() { return Type.TabPane; } - public void setContentParent(ContentPane pane) - { + public void setContentParent(ContentPane pane) { parent = pane; } - public ContentPane getContentParent() - { + public ContentPane getContentParent() { return parent; } - public ContentPane getSiblingParent(Stack stack, Node sibling) - { + public ContentPane getSiblingParent(Stack stack, Node sibling) { ContentPane pane = null; while (!stack.isEmpty()) { @@ -64,65 +61,34 @@ public ContentPane getSiblingParent(Stack stack, Node sibling) return pane; } - public void removeNode(Stack stack, Node node) - { - ContentPane pane = null; - + 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); - while (!stack.isEmpty()) { - Parent parent = stack.pop(); - - children = parent.getChildrenUnmodifiable(); - - if (parent instanceof ContentPane) { - children = ((ContentPane) parent).getChildrenList(); - } - - for (i = 0; i < children.size(); i++) { - if (children.get(i) instanceof ContentPane) { - pane = (ContentPane) children.get(i); - if (pane.getChildrenList().size() < 1) { - children.remove(i); - continue; - } else { - stack.push((Parent) pane); - } - } - } - } - return; - } else if(children.get(i) instanceof Parent) - { - stack.push((Parent) children.get(i)); + 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) - { + public void set(int idx, Node node) { DockNode newNode = (DockNode) node; getTabs().set(idx, new DockNodeTab(newNode)); } - public void set(Node sibling, Node node) - { - set( getChildrenList().indexOf(sibling), node); + 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 List getChildrenList() { + return getTabs().stream().map(i -> i.getContent()).collect(Collectors.toList()); } - public void addNode(Node root, Node sibling, Node node, DockPos dockPos) - { + public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { DockNode newNode = (DockNode) node; - getTabs().add( new Tab( newNode.getTitle(), node ) ); + getTabs().add(new DockNodeTab(newNode)); } } diff --git a/src/main/java/org/dockfx/pane/DockNodeTab.java b/src/main/java/org/dockfx/pane/DockNodeTab.java index eae3ca3..415c2da 100644 --- a/src/main/java/org/dockfx/pane/DockNodeTab.java +++ b/src/main/java/org/dockfx/pane/DockNodeTab.java @@ -6,14 +6,14 @@ /** * DockNodeTab class holds Tab for ContentTabPane + * * @author HongKee Moon */ public class DockNodeTab extends Tab { final private DockNode dockNode; - public DockNodeTab(DockNode node) - { + public DockNodeTab(DockNode node) { this.dockNode = node; setClosable(false); From 6608dba82f14ebea84e43c45f250f8dc44318598 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Jan 2016 21:19:37 +0100 Subject: [PATCH 05/51] Fix to get the title string rather than textProperty Modified TabMenuItem class in the skin. --- .../java/org/dockfx/pane/ContentTabPane.java | 2 +- .../java/org/dockfx/pane/DockNodeTab.java | 22 + .../dockfx/pane/skin/ContentTabPaneSkin.java | 1817 +++++++++++++++++ 3 files changed, 1840 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index 3ac13a1..ec3c4b9 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -23,7 +23,7 @@ public class ContentTabPane extends TabPane implements ContentPane { ContentPane parent; public ContentTabPane() { - this.getStyleClass().add("dock-tab-pane"); + this.setStyle("-fx-skin: \"org.dockfx.pane.skin.ContentTabPaneSkin\";"); } public Type getType() { diff --git a/src/main/java/org/dockfx/pane/DockNodeTab.java b/src/main/java/org/dockfx/pane/DockNodeTab.java index 415c2da..e26eae1 100644 --- a/src/main/java/org/dockfx/pane/DockNodeTab.java +++ b/src/main/java/org/dockfx/pane/DockNodeTab.java @@ -1,7 +1,14 @@ package org.dockfx.pane; +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; import org.dockfx.DockNode; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.Label; import javafx.scene.control.Tab; /** @@ -13,12 +20,27 @@ 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); } + + public String getTitle() + { + return title.getValue(); + } + + public SimpleStringProperty titleProperty() + { + return title; + } } 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..06c2740 --- /dev/null +++ b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java @@ -0,0 +1,1817 @@ +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; + +/** + * Created by moon on 1/20/16. + */ +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); + } + } +} From 0bac703e88c0f768c6bc450f7e76f8d3a3a0eaab Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 21 Jan 2016 09:53:46 +0100 Subject: [PATCH 06/51] Change the header comment to contain the reference and modification detail. --- src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java index 06c2740..3ffe333 100644 --- a/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java +++ b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java @@ -1,3 +1,4 @@ + package org.dockfx.pane.skin; import com.sun.javafx.scene.control.skin.BehaviorSkinBase; @@ -73,7 +74,9 @@ import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString; /** - * Created by moon on 1/20/16. + * 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 { From 19f96c232b4e5123fb5932c04343251cac851ac9 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Fri, 22 Jan 2016 15:17:03 +0100 Subject: [PATCH 07/51] Fix the bug of the dockpane is not responding when the node is detaching too fast --- src/main/java/org/dockfx/DockPane.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 8d94308..3128183 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -593,10 +593,10 @@ public void handle(DockEvent event) { if ((event.getEventType() == DockEvent.DOCK_EXIT && !this.receivedEnter) || event.getEventType() == DockEvent.DOCK_RELEASED) { - if (dockIndicatorPopup.isShowing()) { - dockIndicatorOverlay.hide(); - dockIndicatorPopup.hide(); - } + if (dockIndicatorOverlay.isShowing()) + dockIndicatorOverlay.hide(); + if (dockIndicatorPopup.isShowing()) + dockIndicatorPopup.hide(); } } } From e7a8fc9757b0973a1c62a0c93a5fdb307ea614aa Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 27 Jan 2016 10:56:33 +0100 Subject: [PATCH 08/51] Add bintray distribution --- pom.xml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1bd0e58..df331c0 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.1 DockFX - https://github.com/RobertBColton/DockFX.git + https://github.com/hkmoon/DockFX.git 1.8 @@ -73,6 +73,14 @@ + + + bintray-rtlib-Halcyon + rtlib-Halcyon + https://api.bintray.com/maven/rtlib/Halcyon/DockFX/;publish=1 + + + From 2890457cd4a2e1015c555a7896c403c02dbe9c43 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 15 Mar 2016 17:53:49 +0100 Subject: [PATCH 09/51] Add a tab and select it --- src/main/java/org/dockfx/pane/ContentTabPane.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index ec3c4b9..f7c33e3 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -77,6 +77,7 @@ public boolean removeNode(Stack stack, Node node) { 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) { @@ -89,6 +90,8 @@ public List getChildrenList() { public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { DockNode newNode = (DockNode) node; - getTabs().add(new DockNodeTab(newNode)); + DockNodeTab t = new DockNodeTab(newNode); + getTabs().add(t); + getSelectionModel().select( t ); } } From a42d45751a3e63e2e482424c68186f7e7436cb32 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 15 Mar 2016 18:00:32 +0100 Subject: [PATCH 10/51] Add a tab and select it --- src/main/java/org/dockfx/pane/ContentTabPane.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index f7c33e3..de40775 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -77,7 +77,7 @@ public boolean removeNode(Stack stack, Node node) { public void set(int idx, Node node) { DockNode newNode = (DockNode) node; getTabs().set(idx, new DockNodeTab(newNode)); - getSelectionModel().select( idx ); + getSelectionModel().select(idx); } public void set(Node sibling, Node node) { @@ -92,6 +92,6 @@ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { DockNode newNode = (DockNode) node; DockNodeTab t = new DockNodeTab(newNode); getTabs().add(t); - getSelectionModel().select( t ); + getSelectionModel().select(t); } } From 3ffdebc004d76a9f1ba217e357e5bfd73d08ef32 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 16 Mar 2016 09:46:24 +0100 Subject: [PATCH 11/51] Update to 0.1.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index df331c0..5f7d214 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.dockfx DockFX jar - 0.1.1 + 0.1.2 DockFX https://github.com/hkmoon/DockFX.git From ec05663dc6a08125708cf11655b9d8814f76e08b Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Apr 2016 13:40:14 +0200 Subject: [PATCH 12/51] Newly added tab is focused --- src/main/java/org/dockfx/DockPane.java | 5 ++-- .../java/org/dockfx/pane/ContentTabPane.java | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 3128183..3ad9d01 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -386,11 +386,10 @@ public void dock(Node node, DockPos dockPos, Node sibling) { ContentTabPane tabPane = new ContentTabPane(); - tabPane.getTabs().add(new DockNodeTab(siblingNode)); - tabPane.getTabs().add(new DockNodeTab(newNode)); + tabPane.addDockNodeTab( new DockNodeTab( siblingNode ) ); + tabPane.addDockNodeTab( new DockNodeTab( newNode ) ); tabPane.setContentParent(pane); - pane.set(sibling, tabPane); } } else { // Otherwise, SplitPane is assumed. diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index de40775..a4fcdc9 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -1,5 +1,7 @@ package org.dockfx.pane; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import org.dockfx.DockNode; import org.dockfx.DockPos; @@ -77,7 +79,7 @@ public boolean removeNode(Stack stack, Node node) { public void set(int idx, Node node) { DockNode newNode = (DockNode) node; getTabs().set(idx, new DockNodeTab(newNode)); - getSelectionModel().select(idx); + getSelectionModel().select( idx ); } public void set(Node sibling, Node node) { @@ -91,7 +93,25 @@ public List getChildrenList() { public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { DockNode newNode = (DockNode) node; DockNodeTab t = new DockNodeTab(newNode); - getTabs().add(t); - getSelectionModel().select(t); + addDockNodeTab( t ); + } + + public void addDockNodeTab(DockNodeTab dockNodeTab) + { + dockNodeTab.getGraphic().visibleProperty().addListener( new ChangeListener< Boolean >() + { + @Override public void changed( ObservableValue< ? extends Boolean > observable, Boolean oldValue, Boolean newValue ) + { + if(newValue) + getSelectionModel().select( dockNodeTab ); + } + } ); + getTabs().add(dockNodeTab); + getSelectionModel().select( dockNodeTab ); + } + + public void select(DockNodeTab nodeTab) + { + getSelectionModel().select(nodeTab); } } From 42a80dcebdf764d716d271e0afca8864360e3d43 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Apr 2016 13:42:21 +0200 Subject: [PATCH 13/51] The split size is preserved when changed to ContentTabPane --- src/main/java/org/dockfx/DockPane.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 3ad9d01..2632222 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -390,6 +390,10 @@ public void dock(Node node, DockPos dockPos, Node sibling) { tabPane.addDockNodeTab( new DockNodeTab( newNode ) ); tabPane.setContentParent(pane); + + double[] pos = ((ContentSplitPane) pane).getDividerPositions(); + pane.set( sibling, tabPane ); + ( ( ContentSplitPane ) pane ).setDividerPositions( pos ); } } else { // Otherwise, SplitPane is assumed. From 2d24ea54cc191bde85674294a6206091b44cabb4 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Apr 2016 14:05:22 +0200 Subject: [PATCH 14/51] When only one tab is left, convert to SplitContentPane --- .../org/dockfx/pane/ContentSplitPane.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java index a03495a..aff3441 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -1,5 +1,6 @@ package org.dockfx.pane; +import org.dockfx.DockNode; import org.dockfx.DockPos; import java.util.List; @@ -87,10 +88,23 @@ public boolean removeNode(Stack stack, Node node) { } else if (children.get(i) instanceof ContentPane) { pane = (ContentPane) children.get(i); - if(pane.removeNode(stack, node) && pane.getChildrenList().size() < 1) { - getItems().remove(i); - return true; - } + if(pane.removeNode(stack, node)) + { + if( pane.getChildrenList().size() < 1) { + getItems().remove(i); + return true; + } + else if(pane.getChildrenList().size() == 1) + { + 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; + } + } } } From 757638fa1849a6c520d83178f5cd375d11173a21 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Apr 2016 14:16:42 +0200 Subject: [PATCH 15/51] It is more stable for splitContentPane conversion When only one tab is left, convert to SplitContentPane. --- src/main/java/org/dockfx/pane/ContentSplitPane.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java index aff3441..b816af5 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -94,7 +94,9 @@ else if (children.get(i) instanceof ContentPane) { getItems().remove(i); return true; } - else if(pane.getChildrenList().size() == 1) + else if(pane.getChildrenList().size() == 1 && + pane instanceof ContentTabPane && + pane.getChildrenList().get(0) instanceof DockNode) { List childrenList = pane.getChildrenList(); Node sibling = childrenList.get(0); From 2146558477d51e96ee11c21c30d171ddeb6236b9 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 19 Apr 2016 15:24:40 +0200 Subject: [PATCH 16/51] Hide DockPane.dock()/DockPane.undock() from outside access Please, use DockNode.dock()/DockNode.undock() functions in order to keep the properties consistent. --- src/main/java/org/dockfx/DockPane.java | 7 +++---- src/main/java/org/dockfx/pane/ContentSplitPane.java | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 2632222..b355810 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -359,7 +359,7 @@ public void handle(DockEvent event) { * @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); @@ -465,7 +465,7 @@ else if( pane instanceof ContentTabPane ) { * @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); } @@ -474,7 +474,7 @@ public void dock(Node node, DockPos dockPos) { * * @param node The node that is to be removed from this dock pane. */ - public void undock(DockNode node) { + void undock(DockNode node) { DockNodeEventHandler dockNodeEventHandler = dockNodeEventFilters.get(node); node.removeEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler); dockNodeEventFilters.remove(node); @@ -500,7 +500,6 @@ public void undock(DockNode node) { ContentPane contentParent = pane.getContentParent(); contentParent.set((Node) pane, sibling); - ((DockNode)sibling).tabbedProperty().setValue(false); } } } diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java index b816af5..f91c41b 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -103,7 +103,6 @@ else if(pane.getChildrenList().size() == 1 && ContentPane contentParent = pane.getContentParent(); contentParent.set((Node) pane, sibling); - ((DockNode)sibling).tabbedProperty().setValue(false); return true; } } From 59799b878ae627a375b96e025367495bca7e220f Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Apr 2016 09:28:08 +0200 Subject: [PATCH 17/51] Fixes the splitContentPane conversion --- src/main/java/org/dockfx/DockPane.java | 1 + src/main/java/org/dockfx/pane/ContentSplitPane.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index b355810..873eea6 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -500,6 +500,7 @@ void undock(DockNode node) { ContentPane contentParent = pane.getContentParent(); contentParent.set((Node) pane, sibling); + ((DockNode)sibling).tabbedProperty().setValue(false); } } } diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java index f91c41b..b816af5 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -103,6 +103,7 @@ else if(pane.getChildrenList().size() == 1 && ContentPane contentParent = pane.getContentParent(); contentParent.set((Node) pane, sibling); + ((DockNode)sibling).tabbedProperty().setValue(false); return true; } } From 22bc024344e752eda686a1fb5d1ebaea6d250372 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Apr 2016 09:56:20 +0200 Subject: [PATCH 18/51] Correct isTabbed value --- src/main/java/org/dockfx/DockNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 4cca747..12d847e 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -649,8 +649,8 @@ public String getName() { }; public final boolean isTabbed() { - return floatingProperty.get(); - } + return tabbedProperty.get(); + } /** From 75ffa7722b597850a2bafaf875e0c6b7f3aa8cc3 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Apr 2016 11:03:32 +0200 Subject: [PATCH 19/51] Handle cases when all the sibling nodes are closed --- src/main/java/org/dockfx/DockNode.java | 40 +++++++++++++++++++++++++- src/main/java/org/dockfx/DockPane.java | 6 ++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 12d847e..fdb5e7d 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -652,8 +652,41 @@ 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. @@ -663,6 +696,8 @@ public final boolean isTabbed() { public void dock(DockPane dockPane, DockPos dockPos, Node sibling) { dockImpl(dockPane); dockPane.dock(this, dockPos, sibling); + this.lastDockPos = dockPos; + this.lastDockSibling = sibling; } /** @@ -674,6 +709,7 @@ public void dock(DockPane dockPane, DockPos dockPos, Node sibling) { public void dock(DockPane dockPane, DockPos dockPos) { dockImpl(dockPane); dockPane.dock(this, dockPos); + this.lastDockPos = dockPos; } private final void dockImpl(DockPane dockPane) { @@ -682,6 +718,7 @@ private final void dockImpl(DockPane dockPane) { } this.dockPane = dockPane; this.dockedProperty.set(true); + this.closedProperty.set(false); } /** @@ -705,6 +742,7 @@ public void close() { } else if (isDocked()) { undock(); } + this.closedProperty.set(true); } /** diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 873eea6..47e23d0 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -378,6 +378,12 @@ void dock(Node node, DockPos dockPos, Node sibling) { pane = pane.getSiblingParent(stack, sibling); } + if (pane == null) { + sibling = root; + dockPos = DockPos.LEFT; + pane = (ContentPane) root; + } + if (dockPos == DockPos.CENTER) { if (pane instanceof ContentSplitPane) { // Create a ContentTabPane with two nodes From 3aa7a500847d67f9679cae7f87dcd26829cfd11d Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Apr 2016 11:25:44 +0200 Subject: [PATCH 20/51] Double click focuses the tab --- src/main/java/org/dockfx/DockNode.java | 14 ++++++++++++++ src/main/java/org/dockfx/pane/DockNodeTab.java | 12 ++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index fdb5e7d..83819aa 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -33,6 +33,7 @@ import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; @@ -42,6 +43,7 @@ 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 @@ -745,6 +747,18 @@ public void close() { this.closedProperty.set(true); } + 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. */ diff --git a/src/main/java/org/dockfx/pane/DockNodeTab.java b/src/main/java/org/dockfx/pane/DockNodeTab.java index e26eae1..f877575 100644 --- a/src/main/java/org/dockfx/pane/DockNodeTab.java +++ b/src/main/java/org/dockfx/pane/DockNodeTab.java @@ -1,14 +1,8 @@ package org.dockfx.pane; -import javafx.beans.InvalidationListener; -import javafx.beans.binding.Bindings; import org.dockfx.DockNode; import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.scene.control.Label; import javafx.scene.control.Tab; /** @@ -32,6 +26,7 @@ public DockNodeTab(DockNode node) { setGraphic(dockNode.getDockTitleBar()); setContent(dockNode); dockNode.tabbedProperty().set(true); + dockNode.setNodeTab( this ); } public String getTitle() @@ -43,4 +38,9 @@ public SimpleStringProperty titleProperty() { return title; } + + public void select() + { + getTabPane().getSelectionModel().select( this ); + } } From f32b38604257bb2d42745dacef61feacf502e69d Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 20 Apr 2016 11:31:31 +0200 Subject: [PATCH 21/51] Version 0.1.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f7d214..e3d469e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.dockfx DockFX jar - 0.1.2 + 0.1.3 DockFX https://github.com/hkmoon/DockFX.git From d2df9401b45aa135ec769e0cb71266caadb870c4 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 21 Apr 2016 17:23:52 +0200 Subject: [PATCH 22/51] Focus login moved into DockNodeTab --- src/main/java/org/dockfx/pane/ContentTabPane.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index a4fcdc9..9216861 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -98,20 +98,7 @@ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { public void addDockNodeTab(DockNodeTab dockNodeTab) { - dockNodeTab.getGraphic().visibleProperty().addListener( new ChangeListener< Boolean >() - { - @Override public void changed( ObservableValue< ? extends Boolean > observable, Boolean oldValue, Boolean newValue ) - { - if(newValue) - getSelectionModel().select( dockNodeTab ); - } - } ); getTabs().add(dockNodeTab); getSelectionModel().select( dockNodeTab ); } - - public void select(DockNodeTab nodeTab) - { - getSelectionModel().select(nodeTab); - } } From 1eb3dafd11b8ddb43601f05303d9b3004f58fcfb Mon Sep 17 00:00:00 2001 From: Loic Alain Royer Date: Sat, 30 Apr 2016 20:36:10 +0200 Subject: [PATCH 23/51] added gradle build --- build.gradle | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 build.gradle diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3c8bcc1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,172 @@ +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/ressources' } + } + test + { + java + { srcDir 'src/main/java' } + resources + { srcDir 'src/main/ressources' } + } +} + +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 = { + -> + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'describe', '--tags' + standardOutput = stdout + } + return stdout.toString().trim() +} + +group = 'org.dockfx' +version = '0.1.3' + + +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. + } /**/ + } + } + /**/ + } +} + + + + + + + + + + + From 05c886dd9d0dfb0f00f04d1da229e7e421e9c5de Mon Sep 17 00:00:00 2001 From: Loic Alain Royer Date: Mon, 2 May 2016 12:12:24 +0200 Subject: [PATCH 24/51] new version number --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3c8bcc1..c09c3f3 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ def getVersionName = { } group = 'org.dockfx' -version = '0.1.3' +version = '0.1.4' artifacts From d7dcd77ebf2713d368cdf7324992e40c2ad07f40 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Mon, 9 May 2016 14:37:39 +0200 Subject: [PATCH 25/51] Change the default position of dock RIGHT when there is no sibling --- src/main/java/org/dockfx/DockPane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 47e23d0..9b98592 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -380,7 +380,7 @@ void dock(Node node, DockPos dockPos, Node sibling) { if (pane == null) { sibling = root; - dockPos = DockPos.LEFT; + dockPos = DockPos.RIGHT; pane = (ContentPane) root; } From e17e61e654f63eedcc22fc26e528c26e72594c6f Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Mon, 9 May 2016 15:10:28 +0200 Subject: [PATCH 26/51] Enable Closable Property changed with nice layout of TitleBar --- src/main/java/org/dockfx/DockTitleBar.java | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/DockTitleBar.java b/src/main/java/org/dockfx/DockTitleBar.java index 4b1a2df..bf7bbf5 100644 --- a/src/main/java/org/dockfx/DockTitleBar.java +++ b/src/main/java/org/dockfx/DockTitleBar.java @@ -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; @@ -97,13 +99,28 @@ 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); + 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 ) ); + } + } + } ); + + getChildren().addAll(label, fillPane, stateButton, closeButton); this.addEventHandler(MouseEvent.MOUSE_PRESSED, this); this.addEventHandler(MouseEvent.DRAG_DETECTED, this); From 2cb0210d4e9bf8da6743ec1b61b699ff6355d497 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Mon, 9 May 2016 15:26:37 +0200 Subject: [PATCH 27/51] Version 0.1.5 with some changes --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index e3d469e..e41ac76 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.dockfx DockFX jar - 0.1.3 + 0.1.5 DockFX https://github.com/hkmoon/DockFX.git @@ -75,9 +75,9 @@ - bintray-rtlib-Halcyon - rtlib-Halcyon - https://api.bintray.com/maven/rtlib/Halcyon/DockFX/;publish=1 + bintray-clearcontrol-Halcyon + clearcontrol-Halcyon + https://api.bintray.com/maven/clearcontrol/ClearControl/DockFX/;publish=1 From fd78dffef80344b384db249dcf7598be5a58701c Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Mon, 9 May 2016 15:39:04 +0200 Subject: [PATCH 28/51] Upgrade to 0.1.5 version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c09c3f3..4197a88 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ def getVersionName = { } group = 'org.dockfx' -version = '0.1.4' +version = '0.1.5' artifacts From 1677db2d48ef69334da33a124007b3b9f2af0a66 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 10 May 2016 18:37:44 +0200 Subject: [PATCH 29/51] Update pom.xml according to version 0.1.5 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index e41ac76..7d9752f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ jar 0.1.5 DockFX - https://github.com/hkmoon/DockFX.git + https://github.com/ClearControl/DockFX.git 1.8 @@ -75,8 +75,8 @@ - bintray-clearcontrol-Halcyon - clearcontrol-Halcyon + bintray-clearcontrol-ClearControl + clearcontrol-ClearControl https://api.bintray.com/maven/clearcontrol/ClearControl/DockFX/;publish=1 From 8c12834258092bd70e5bc00562b1c3c49bd81e3d Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 12 May 2016 16:55:46 +0200 Subject: [PATCH 30/51] Fixed the wrong positioned dockIndicatorOverlay in some case When a menubar is included, the proposed rectangle was wrongly located. --- src/main/java/org/dockfx/DockPane.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 9b98592..905927d 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -25,6 +25,7 @@ import com.sun.javafx.css.StyleManager; +import javafx.geometry.Bounds; import org.dockfx.pane.ContentPane; import org.dockfx.pane.ContentSplitPane; import org.dockfx.pane.ContentTabPane; @@ -517,8 +518,11 @@ void undock(DockNode node) { 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()); + Bounds bounds = DockPane.this.getBoundsInParent(); + Bounds localBounds = DockPane.this.getBoundsInLocal(); + Bounds screenBounds = DockPane.this.localToScreen(localBounds); + + dockIndicatorOverlay.show(DockPane.this, screenBounds.getMinX(), screenBounds.getMinY() - bounds.getMinY()); } } else if (event.getEventType() == DockEvent.DOCK_OVER) { this.receivedEnter = false; From 157cddb9f56ffa81b94d6deacf6e746e44bc1fd5 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 18 May 2016 10:03:01 +0200 Subject: [PATCH 31/51] Apply @clumsy's fix for "Create a new undocked DockNode" --- src/main/java/org/dockfx/DockNode.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 83819aa..77c82c2 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -33,7 +33,6 @@ import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; @@ -264,9 +263,17 @@ public void setFloating(boolean floating, Point2D translation) { if (this.isDecorated()) { Window owner = stage.getOwner(); stagePosition = floatScene.add(new Point2D(owner.getX(), owner.getY())); - } else { - stagePosition = floatScreen; - } + } 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); } @@ -298,8 +305,8 @@ public void setFloating(boolean floating, Point2D translation) { 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); + //stage.setMinWidth(borderPane.minWidth(this.getHeight()) + insetsWidth); + //stage.setMinHeight(borderPane.minHeight(this.getWidth()) + insetsHeight); borderPane.setPrefSize(this.getWidth() + insetsWidth, this.getHeight() + insetsHeight); From 3fc4689f01e37f6cf06093de87f3571519b2eec7 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 18 May 2016 10:03:46 +0200 Subject: [PATCH 32/51] Remove unused imports --- src/main/java/org/dockfx/pane/ContentTabPane.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/dockfx/pane/ContentTabPane.java b/src/main/java/org/dockfx/pane/ContentTabPane.java index 9216861..ddeb34b 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -1,7 +1,5 @@ package org.dockfx.pane; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import org.dockfx.DockNode; import org.dockfx.DockPos; @@ -12,7 +10,6 @@ import javafx.scene.Node; import javafx.scene.Parent; -import javafx.scene.control.Tab; import javafx.scene.control.TabPane; /** From 8b732de7ab7fc1522d7a7cf5af27ca5fc78df283 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 18 May 2016 16:10:52 +0200 Subject: [PATCH 33/51] Add null check for dockAreaDrag --- src/main/java/org/dockfx/DockPane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 905927d..f1c537d 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -544,7 +544,7 @@ public void handle(DockEvent event) { } } - if (dockPosDrag != null) { + if (dockPosDrag != null && dockAreaDrag != null) { Point2D originToScene = dockAreaDrag.localToScene(0, 0); dockAreaIndicator.setVisible(true); From e5e596f48f036a652463b567ae8285aac27267f0 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 19 May 2016 16:03:32 +0200 Subject: [PATCH 34/51] v0.1.6 --- build.gradle | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4197a88..49ae3a5 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ def getVersionName = { } group = 'org.dockfx' -version = '0.1.5' +version = '0.1.6' artifacts diff --git a/pom.xml b/pom.xml index 7d9752f..ab1ef80 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.dockfx DockFX jar - 0.1.5 + 0.1.6 DockFX https://github.com/ClearControl/DockFX.git From 07f3c7dd72feceb151a3e266c49474d19906f5dc Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 19 May 2016 16:48:30 +0200 Subject: [PATCH 35/51] update build.gradle for publishing --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/build.gradle b/build.gradle index 49ae3a5..de9995d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,14 +17,14 @@ sourceSets java { srcDir 'src/main/java' } resources - { srcDir 'src/main/ressources' } + { srcDir 'src/main/resources' } } test { java - { srcDir 'src/main/java' } + { srcDir 'src/test/java' } resources - { srcDir 'src/main/ressources' } + { srcDir 'src/test/resources' } } } 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 From 5e09c12a50359a7c5e95c74b7338f8c9ba708acc Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 19 May 2016 17:00:27 +0200 Subject: [PATCH 36/51] Use git tag version number for release in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index de9995d..48c756b 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ def getVersionName = { } group = 'org.dockfx' -version = '0.1.6' +version = getVersionName() artifacts From 63b0d8d9be64a37f64977fddc2bb3742194b56f0 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 19 May 2016 17:02:47 +0200 Subject: [PATCH 37/51] Include .gradle in .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cb0209d..fb52a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ dockfxdemo.jar *.db .git/modules /bin -target \ No newline at end of file +target +.gradle \ No newline at end of file From 1580438543c34f81f6ccc5aa770a90b24f772689 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 19 May 2016 17:16:57 +0200 Subject: [PATCH 38/51] Add a usecase of using closedProperty() of DockNode --- src/main/java/org/dockfx/demo/DockFX.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/dockfx/demo/DockFX.java b/src/main/java/org/dockfx/demo/DockFX.java index 6810fff..41bb757 100644 --- a/src/main/java/org/dockfx/demo/DockFX.java +++ b/src/main/java/org/dockfx/demo/DockFX.java @@ -26,6 +26,8 @@ import java.nio.file.Paths; import java.util.Random; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import org.dockfx.DockNode; import org.dockfx.DockPane; import org.dockfx.DockPos; @@ -112,6 +114,16 @@ public void start(Stage primaryStage) { 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 From e4b68e8e618faa13fdae25fa994beaea672dd15f Mon Sep 17 00:00:00 2001 From: Sam Cooper Date: Thu, 14 Jul 2016 16:33:11 +0100 Subject: [PATCH 39/51] Merged FXML support from https://github.com/Kladimir/DockFX --- src/main/java/org/dockfx/DockNode.java | 114 ++++++++++++++++-- .../viewControllers/DockFXViewController.java | 42 +++++++ 2 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/dockfx/viewControllers/DockFXViewController.java diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 77c82c2..64e4d5c 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -20,6 +20,8 @@ package org.dockfx; +import org.dockfx.viewControllers.DockFXViewController; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -28,15 +30,18 @@ import javafx.beans.property.StringProperty; 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; @@ -79,6 +84,11 @@ public class DockNode extends VBox implements EventHandler { */ private DockPane dockPane; + /** + * View controller of node inside this DockNode + */ + private DockFXViewController viewController; + /** * CSS pseudo class selector representing whether this node is currently floating. */ @@ -133,6 +143,10 @@ public String getName() { } }; + public DockNode(Node contents, String title, Node graphic, DockFXViewController controller) { + initializeDockNode(contents, title, graphic, controller); + } + /** * Creates a default DockNode with a default title bar and layout. * @@ -143,16 +157,7 @@ public String getName() { * 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"); + this(contents, title, graphic, null); } /** @@ -175,6 +180,86 @@ public DockNode(Node contents) { this(contents, null, null); } + /** + * + * 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()); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * 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"); + } + /** * 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. @@ -359,6 +444,15 @@ public final DockPane getDockPane() { return dockPane; } + /** + * ViewController associated with this dock nodes contents, might be null + * + * @return ViewController associated with this dock nodes contents + */ + public final DockFXViewController getViewController() { + return viewController; + } + /** * The dock title bar associated with this dock node. * 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; + } + +} From 86729b54d13049e75569162ba1fd4c9e4835ce86 Mon Sep 17 00:00:00 2001 From: Sam Cooper Date: Thu, 21 Jul 2016 13:37:43 +0100 Subject: [PATCH 40/51] Fix for incorrect calculation of max width of split/tab panes. --- src/main/java/org/dockfx/pane/ContentSplitPane.java | 10 ++++++++++ src/main/java/org/dockfx/pane/ContentTabPane.java | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/java/org/dockfx/pane/ContentSplitPane.java b/src/main/java/org/dockfx/pane/ContentSplitPane.java index b816af5..0563a5a 100644 --- a/src/main/java/org/dockfx/pane/ContentSplitPane.java +++ b/src/main/java/org/dockfx/pane/ContentSplitPane.java @@ -1,5 +1,6 @@ package org.dockfx.pane; +import java.util.Comparator; import org.dockfx.DockNode; import org.dockfx.DockPos; @@ -178,4 +179,13 @@ public void addNode(Node root, Node sibling, Node node, DockPos dockPos) { } } } + + @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 index ddeb34b..36eba39 100644 --- a/src/main/java/org/dockfx/pane/ContentTabPane.java +++ b/src/main/java/org/dockfx/pane/ContentTabPane.java @@ -1,5 +1,6 @@ package org.dockfx.pane; +import java.util.Comparator; import org.dockfx.DockNode; import org.dockfx.DockPos; @@ -98,4 +99,9 @@ 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(); + } } From 712a4ed651b65384a4952301fb65c9e1983e635c Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 18 Aug 2016 17:58:12 +0200 Subject: [PATCH 41/51] Add preference management for dock position/size --- src/main/java/org/dockfx/ContentHolder.java | 106 +++++++ src/main/java/org/dockfx/DockPane.java | 321 +++++++++++++++++--- src/main/java/org/dockfx/demo/DockFX.java | 57 +++- 3 files changed, 446 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/dockfx/ContentHolder.java 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/DockPane.java b/src/main/java/org/dockfx/DockPane.java index f1c537d..858985a 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -20,12 +20,30 @@ 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.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; @@ -186,6 +204,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 @@ -283,6 +302,8 @@ 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(); } /** @@ -379,11 +400,11 @@ void dock(Node node, DockPos dockPos, Node sibling) { pane = pane.getSiblingParent(stack, sibling); } - if (pane == null) { - sibling = root; - dockPos = DockPos.RIGHT; - pane = (ContentPane) root; - } + if (pane == null) { + sibling = root; + dockPos = DockPos.RIGHT; + pane = (ContentPane) root; + } if (dockPos == DockPos.CENTER) { if (pane instanceof ContentSplitPane) { @@ -393,14 +414,14 @@ void dock(Node node, DockPos dockPos, Node sibling) { ContentTabPane tabPane = new ContentTabPane(); - tabPane.addDockNodeTab( new DockNodeTab( siblingNode ) ); - tabPane.addDockNodeTab( new DockNodeTab( newNode ) ); + tabPane.addDockNodeTab(new DockNodeTab(siblingNode)); + tabPane.addDockNodeTab(new DockNodeTab(newNode)); tabPane.setContentParent(pane); double[] pos = ((ContentSplitPane) pane).getDividerPositions(); - pane.set( sibling, tabPane ); - ( ( ContentSplitPane ) pane ).setDividerPositions( pos ); + pane.set(sibling, tabPane); + ((ContentSplitPane) pane).setDividerPositions(pos); } } else { // Otherwise, SplitPane is assumed. @@ -431,27 +452,25 @@ void dock(Node node, DockPos dockPos, Node sibling) { pane = split; } - } - else if( pane instanceof ContentTabPane ) { + } 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 { + 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 { sibling = (Node) pane; } @@ -462,6 +481,10 @@ else if( pane instanceof ContentTabPane ) { // Add a node to the proper pane pane.addNode(root, sibling, node, dockPos); + + if (undockedNodes.contains(node)) { + undockedNodes.remove(node); + } } /** @@ -482,6 +505,8 @@ void dock(Node node, DockPos dockPos) { * @param node The node that is to be removed from this dock pane. */ void undock(DockNode node) { + undockedNodes.add(node); + DockNodeEventHandler dockNodeEventHandler = dockNodeEventFilters.get(node); node.removeEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler); dockNodeEventFilters.remove(node); @@ -507,7 +532,7 @@ void undock(DockNode node) { ContentPane contentParent = pane.getContentParent(); contentParent.set((Node) pane, sibling); - ((DockNode)sibling).tabbedProperty().setValue(false); + ((DockNode) sibling).tabbedProperty().setValue(false); } } } @@ -518,11 +543,12 @@ void undock(DockNode node) { public void handle(DockEvent event) { if (event.getEventType() == DockEvent.DOCK_ENTER) { if (!dockIndicatorOverlay.isShowing()) { - Bounds bounds = DockPane.this.getBoundsInParent(); - Bounds localBounds = DockPane.this.getBoundsInLocal(); - Bounds screenBounds = DockPane.this.localToScreen(localBounds); + Bounds bounds = DockPane.this.getBoundsInParent(); + Bounds localBounds = DockPane.this.getBoundsInLocal(); + Bounds screenBounds = DockPane.this.localToScreen(localBounds); - dockIndicatorOverlay.show(DockPane.this, screenBounds.getMinX(), screenBounds.getMinY() - bounds.getMinY()); + dockIndicatorOverlay + .show(DockPane.this, screenBounds.getMinX(), screenBounds.getMinY() - bounds.getMinY()); } } else if (event.getEventType() == DockEvent.DOCK_OVER) { this.receivedEnter = false; @@ -606,10 +632,235 @@ public void handle(DockEvent event) { if ((event.getEventType() == DockEvent.DOCK_EXIT && !this.receivedEnter) || event.getEventType() == DockEvent.DOCK_RELEASED) { - if (dockIndicatorOverlay.isShowing()) - dockIndicatorOverlay.hide(); - if (dockIndicatorPopup.isShowing()) - dockIndicatorPopup.hide(); + 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++) { + 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++; + + holder = new ContentHolder(contentTabPaneName, ContentHolder.Type.TabPane); + contents.put(contentTabPaneName, holder); + } + + 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) { + HashMap + contents = + (HashMap) loadCollection(filePath); + + applyPane(contents, (ContentPane) root); + } + + 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) { + // 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); + node.setFloating(true); + + node.resize(size[0], size[1]); + node.getStage().setX(position[0]); + node.getStage().setY(position[1]); + + dockNodes.remove(title); + } + + // Restore dock location based on the preferences + // Make it sorted + ContentHolder rootHolder = contents.get("0"); + Node newRoot = buildPane(rootHolder, dockNodes); + + this.root = newRoot; + this.getChildren().set(0, this.root); + } + + private Node buildPane(ContentHolder holder, HashMap dockNodes) { + 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 + DockNode n = dockNodes.get(item); + if (n.tabbedProperty().get()) { + n.tabbedProperty().set(false); + } + + splitPane.getItems().add(dockNodes.get(item)); + } else if (item instanceof ContentHolder) { + // Call this function recursively + splitPane.getItems().add(buildPane((ContentHolder) item, dockNodes)); + } + } + + 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 + tabPane.addDockNodeTab(new DockNodeTab(dockNodes.get(item))); + } + } + + pane = tabPane; + } + + return pane; } } diff --git a/src/main/java/org/dockfx/demo/DockFX.java b/src/main/java/org/dockfx/demo/DockFX.java index 41bb757..6f93d25 100644 --- a/src/main/java/org/dockfx/demo/DockFX.java +++ b/src/main/java/org/dockfx/demo/DockFX.java @@ -21,6 +21,7 @@ package org.dockfx.demo; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @@ -28,6 +29,12 @@ 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; @@ -100,17 +107,42 @@ public void start(Stage primaryStage) { tableDock.setPrefSize(300, 100); tableDock.dock(dockPane, DockPos.BOTTOM); - primaryStage.setScene(new Scene(dockPane, 800, 500)); + 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); + + primaryStage.setScene(new Scene(mainBorderPane, 800, 500)); primaryStage.sizeToScene(); primaryStage.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); @@ -156,4 +188,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"; + } + } From cabdf0f79a35acb5e78ef9d3828429a3f4dd5000 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Wed, 24 Aug 2016 18:11:38 +0200 Subject: [PATCH 42/51] Store/restore the selected tab index --- src/main/java/org/dockfx/DockPane.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 858985a..71f001d 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -735,9 +735,12 @@ private ContentHolder checkPane(HashMap contents, Content 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()) { @@ -858,6 +861,7 @@ private Node buildPane(ContentHolder holder, HashMap dockNodes } } + tabPane.getSelectionModel().select((int) holder.getProperties().get("SelectedIndex")); pane = tabPane; } From c212b1ee2a1c8b925349563ac63e4b180b7072ea Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Thu, 25 Aug 2016 17:18:41 +0200 Subject: [PATCH 43/51] Support delayed opening process for some specific applications --- .../java/org/dockfx/DelayOpenHandler.java | 8 ++ src/main/java/org/dockfx/DockPane.java | 78 +++++++++++++++---- 2 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/dockfx/DelayOpenHandler.java 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/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 71f001d..74763b9 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -28,6 +28,7 @@ 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; @@ -763,11 +764,16 @@ private ContentHolder checkPane(HashMap contents, Content } public void loadPreference(String filePath) { + loadPreference(filePath, null); + } + + public void loadPreference(String filePath, DelayOpenHandler delayOpenHandler) + { HashMap contents = (HashMap) loadCollection(filePath); - applyPane(contents, (ContentPane) root); + applyPane(contents, (ContentPane) root, delayOpenHandler); } private void collectDockNodes(HashMap dockNodes, ContentPane pane) { @@ -782,7 +788,7 @@ private void collectDockNodes(HashMap dockNodes, ContentPane p } } - private void applyPane(HashMap contents, ContentPane root) { + private void applyPane(HashMap contents, ContentPane root, DelayOpenHandler delayOpenHandler) { // Collect the current pane information HashMap dockNodes = new HashMap<>(); @@ -810,25 +816,35 @@ private void applyPane(HashMap contents, ContentPane root Double[] size = (Double[]) holder.getProperties().get("Size"); Double[] position = (Double[]) holder.getProperties().get("Position"); DockNode node = dockNodes.get(title); - node.setFloating(true); + 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.resize(size[0], size[1]); - node.getStage().setX(position[0]); - node.getStage().setY(position[1]); + node.getStage().setWidth(size[0]); + node.getStage().setHeight(size[1]); - dockNodes.remove(title); + 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); + Node newRoot = buildPane(rootHolder, dockNodes, delayOpenHandler); this.root = newRoot; this.getChildren().set(0, this.root); } - private Node buildPane(ContentHolder holder, HashMap dockNodes) { + private Node buildPane(ContentHolder holder, HashMap dockNodes, DelayOpenHandler delayOpenHandler) { Node pane = null; if (holder.getType().equals(ContentHolder.Type.SplitPane)) { ContentSplitPane splitPane = new ContentSplitPane(); @@ -838,15 +854,31 @@ private Node buildPane(ContentHolder holder, HashMap dockNodes for (Object item : holder.getChildren()) { if (item instanceof String) { // Use dock node - DockNode n = dockNodes.get(item); - if (n.tabbedProperty().get()) { - n.tabbedProperty().set(false); - } + if(dockNodes.containsKey(item)) { + DockNode n = dockNodes.get(item); + if (n.tabbedProperty().get()) { + n.tabbedProperty().set(false); + } - splitPane.getItems().add(dockNodes.get(item)); + 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)); + splitPane.getItems().add(buildPane((ContentHolder) item, dockNodes, delayOpenHandler)); } } @@ -857,7 +889,21 @@ private Node buildPane(ContentHolder holder, HashMap dockNodes for (Object item : holder.getChildren()) { if (item instanceof String) { // Use dock node - tabPane.addDockNodeTab(new DockNodeTab(dockNodes.get(item))); + 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."); + } } } From 30101af74cf243822bdbb2a88d9af0300c882d2d Mon Sep 17 00:00:00 2001 From: hkmoon Date: Mon, 29 Aug 2016 16:26:06 +0200 Subject: [PATCH 44/51] Fix the bug of not handling undockedNodes properly When it handle floating nodes, it should add the node in undockedNodes list. --- src/main/java/org/dockfx/DockPane.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 74763b9..f033d4d 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -829,6 +829,7 @@ private void applyPane(HashMap contents, ContentPane root node.getStage().setHeight(size[1]); dockNodes.remove(title); + undockedNodes.add(node); } else { System.err.println(item + " is not present."); From cd218e56a721fd9165813ce335e7d2a55911ec1a Mon Sep 17 00:00:00 2001 From: hkmoon Date: Mon, 29 Aug 2016 17:01:47 +0200 Subject: [PATCH 45/51] Revert the fix of handling undockedNodes properly Have to investigate it more. --- src/main/java/org/dockfx/DockPane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index f033d4d..f060ff2 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -654,6 +654,7 @@ public void storePreference(String filePath) { 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); @@ -829,7 +830,6 @@ private void applyPane(HashMap contents, ContentPane root node.getStage().setHeight(size[1]); dockNodes.remove(title); - undockedNodes.add(node); } else { System.err.println(item + " is not present."); From 5386d94f34e506ce125ece788e55e5f80d1ff184 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Tue, 30 Aug 2016 18:15:13 +0200 Subject: [PATCH 46/51] Should have checked the closedProperty for undockedNodes --- src/main/java/org/dockfx/DockNode.java | 2 +- src/main/java/org/dockfx/DockPane.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 64e4d5c..76b081d 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.java @@ -840,12 +840,12 @@ public void undock() { * pane. */ public void close() { + this.closedProperty.set(true); if (isFloating()) { setFloating(false); } else if (isDocked()) { undock(); } - this.closedProperty.set(true); } private DockNodeTab dockNodeTab; diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index f060ff2..d803a68 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -506,7 +506,8 @@ void dock(Node node, DockPos dockPos) { * @param node The node that is to be removed from this dock pane. */ void undock(DockNode node) { - undockedNodes.add(node); + if(!node.closedProperty().get()) + undockedNodes.add(node); DockNodeEventHandler dockNodeEventHandler = dockNodeEventFilters.get(node); node.removeEventFilter(DockEvent.DOCK_OVER, dockNodeEventHandler); From 125686f8f264563b87f4121a56d65dba1980b3c4 Mon Sep 17 00:00:00 2001 From: HongKee Moon Date: Fri, 2 Sep 2016 10:06:59 +0200 Subject: [PATCH 47/51] v0.1.7 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab1ef80..6f8d968 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.dockfx DockFX jar - 0.1.6 + 0.1.7 DockFX https://github.com/ClearControl/DockFX.git From 17f46bd73c59f307e5733ba9ebfa04c1fec86d76 Mon Sep 17 00:00:00 2001 From: Alex Dibrov Date: Mon, 5 Sep 2016 13:19:13 +0200 Subject: [PATCH 48/51] minor --- .gitignore | 3 ++- build.gradle | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index fb52a7f..d8674a4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ dockfxdemo.jar .git/modules /bin target -.gradle \ No newline at end of file +.gradle +/build/ diff --git a/build.gradle b/build.gradle index 48c756b..876d724 100644 --- a/build.gradle +++ b/build.gradle @@ -83,12 +83,19 @@ if (JavaVersion.current().isJava8Compatible()) { */ def getVersionName = { -> - def stdout = new ByteArrayOutputStream() - exec { - commandLine 'git', 'describe', '--tags' - standardOutput = stdout - } - return stdout.toString().trim() + try + { + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'describe', '--tags' + standardOutput = stdout + } + return stdout.toString().trim() + } + catch(Throwable e) + { + println e + } } group = 'org.dockfx' From 92583e923e11ba917625edea179577d0c696d8f2 Mon Sep 17 00:00:00 2001 From: Sam Cooper Date: Wed, 12 Oct 2016 18:21:19 +0100 Subject: [PATCH 49/51] Extend demo to show dock pane can be a sub pane of the application. --- src/main/java/org/dockfx/demo/DockFX.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dockfx/demo/DockFX.java b/src/main/java/org/dockfx/demo/DockFX.java index 6f93d25..028eab9 100644 --- a/src/main/java/org/dockfx/demo/DockFX.java +++ b/src/main/java/org/dockfx/demo/DockFX.java @@ -49,6 +49,7 @@ import javafx.scene.control.TreeView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; import javafx.scene.web.HTMLEditor; import javafx.stage.Stage; @@ -131,7 +132,10 @@ public void handle(ActionEvent event) { 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(); From bdb11594098813561cb14a295ac406871dbbde03 Mon Sep 17 00:00:00 2001 From: Sam Cooper Date: Wed, 12 Oct 2016 18:21:58 +0100 Subject: [PATCH 50/51] Fix overlay not being offset to the correct location when dock pane is not the main pane of the application. --- src/main/java/org/dockfx/DockPane.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index d803a68..18224a5 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -545,12 +545,10 @@ void undock(DockNode node) { public void handle(DockEvent event) { if (event.getEventType() == DockEvent.DOCK_ENTER) { if (!dockIndicatorOverlay.isShowing()) { - Bounds bounds = DockPane.this.getBoundsInParent(); - Bounds localBounds = DockPane.this.getBoundsInLocal(); - Bounds screenBounds = DockPane.this.localToScreen(localBounds); + Point2D originToScreen = root.localToScreen(0, 0); dockIndicatorOverlay - .show(DockPane.this, screenBounds.getMinX(), screenBounds.getMinY() - bounds.getMinY()); + .show(DockPane.this, originToScreen.getX(), originToScreen.getY()); } } else if (event.getEventType() == DockEvent.DOCK_OVER) { this.receivedEnter = false; @@ -573,10 +571,11 @@ public void handle(DockEvent event) { } if (dockPosDrag != null && dockAreaDrag != null) { - Point2D originToScene = dockAreaDrag.localToScene(0, 0); + 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 { From ce405098853bb50da736b6822deac1fa665f5b99 Mon Sep 17 00:00:00 2001 From: Julien Heurtebize Date: Mon, 17 Oct 2016 19:12:45 +0200 Subject: [PATCH 51/51] -Add minimize functionality ->minimize only works when the DockNode is floating ->minimize calls iconify function -Fix a bug causing the docknode to disappear (Linux Mint 64 bits, java 8 Oracle) ->appears when the user drag a docknode outside, then maximize the floating node and maximize again the floating node --- src/main/java/org/dockfx/DockNode.java | 1823 ++++++++++---------- src/main/java/org/dockfx/DockTitleBar.java | 768 +++++---- src/main/java/org/dockfx/demo/DockFX.java | 9 +- src/main/resources/org/dockfx/default.css | 4 + src/main/resources/org/dockfx/minimize.png | Bin 0 -> 17060 bytes 5 files changed, 1353 insertions(+), 1251 deletions(-) create mode 100644 src/main/resources/org/dockfx/minimize.png diff --git a/src/main/java/org/dockfx/DockNode.java b/src/main/java/org/dockfx/DockNode.java index 76b081d..9d225d8 100644 --- a/src/main/java/org/dockfx/DockNode.java +++ b/src/main/java/org/dockfx/DockNode.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 org.dockfx.viewControllers.DockFXViewController; @@ -28,6 +28,8 @@ 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; @@ -53,918 +55,991 @@ * 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; - - /** - * 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"); - - /** - * 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"; - } - }; - - public DockNode(Node contents, String title, Node graphic, DockFXViewController controller) { - initializeDockNode(contents, title, graphic, controller); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * - * 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()); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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; - } - - /** - * 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"); - } - - /** - * 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 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(); - } - } - - /** - * 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; - } - - /** - * ViewController associated with this dock nodes contents, might be null - * - * @return ViewController associated with this dock nodes contents - */ - public final DockFXViewController getViewController() { - return viewController; - } - - /** - * 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"; } - } + }; - DockNode.this.pseudoClassStateChanged(DOCKED_PSEUDO_CLASS, get()); + public final Node getGraphic() { + return graphicProperty.get(); } - @Override - public String getName() { - return "docked"; + public final void setGraphic(Node graphic) { + this.graphicProperty.setValue(graphic); } - }; - public final boolean isDocked() { - return dockedProperty.get(); - } + /** + * 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; + } - public final BooleanProperty maximizedProperty() { - return maximizedProperty; - } + private StringProperty titleProperty = new SimpleStringProperty("Dock") { + @Override + public String getName() { + return "title"; + } + }; - public final boolean isMaximized() { - return maximizedProperty.get(); - } + public final String getTitle() { + return titleProperty.get(); + } - public final boolean isDecorated() { - return stageStyle != StageStyle.TRANSPARENT && stageStyle != StageStyle.UNDECORATED; - } + public final void setTitle(String title) { + this.titleProperty.setValue(title); + } - /** - * Boolean property maintaining whether this node is currently tabbed. - * - * @defaultValue false - */ - public final BooleanProperty tabbedProperty() { - return tabbedProperty; - } + /** + * 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 tabbedProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { + 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(); + } + + 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; + } - if (getChildren() != null) - { - if(get()) - { - getChildren().remove(dockTitleBar); + /** + * 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); } - else - { - getChildren().add(0, dockTitleBar); + 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 "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 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/DockTitleBar.java b/src/main/java/org/dockfx/DockTitleBar.java index bf7bbf5..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; @@ -49,400 +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(); - } - }); - - // create a pane that will stretch to make the buttons right aligned - Pane fillPane = new Pane(); - HBox.setHgrow(fillPane, Priority.ALWAYS); - - 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 ) ); - } - } - } ); - - 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 028eab9..109700c 100644 --- a/src/main/java/org/dockfx/demo/DockFX.java +++ b/src/main/java/org/dockfx/demo/DockFX.java @@ -50,8 +50,10 @@ 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 { @@ -138,8 +140,13 @@ public void handle(ActionEvent event) { 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 diff --git a/src/main/resources/org/dockfx/default.css b/src/main/resources/org/dockfx/default.css index 803e943..96d872d 100644 --- a/src/main/resources/org/dockfx/default.css +++ b/src/main/resources/org/dockfx/default.css @@ -172,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 0000000000000000000000000000000000000000..84dd6c622d5fb24a7e480f1445c47e912581488c GIT binary patch literal 17060 zcmeI4&u`mQ9Kekx##U|Yh{Vat0D+j}=U+)|B^jkzX(dvsHfY^pVlVbfYnIr?cAK;( zG)@yYBqR=S=Eju+66~t@itt?GMV>@w=ov8U&y%2=&XO0NXc7EOyDZ>orP${_s;tbe%@&s?kR6V2SLw zD+eLjJXq`42RCigDP3+Z)JGOL7?2o?qe0(`tWl$s`n5n#hUJo&s>C-NrNtzmxYb@4 zm;8{3M!6!{s8SWHW?3<+RlRyiRFPuHNRu@rshXuDOIOA5r_@{ksUA8#YooP14hR1= zN;`2JSh74E4$DKW?1$U3Vw$FmR9RIe(2$~iFUBLui!NqPaz8B+*`XW6uJ4J7U)=R~ z<3_2J1frkuyaoXc$XK8iQU zWOW!^f%;5GvD=&DCpj_~0vWk;%#tISS(;PXQr7{RwG z6yLn+dX7Jg-m1&fCX>XOcq1(wlV+0Gl7b{;bQHrv@T0tqR12YklT35UN85MY-u_V^ zwT_}#M$J;HMIQytX&pDoN)-pIlA;hpGBpTfSDi`?JGx%! z!8Ee-EI2zB=!4LO0*dorjp= z=2)l|H-c>4FJv*~a#|uc$tOog^8B`{eeh#u^9Uz4uZaG<7AEHJuw)+Jjr9j&5Ojhc${DLz5UWz|@hm+_6W3I>t}NS5d}n5T zQPvL}?acCS?|3Jd+(AG~+sd+YIk{#h?F3}sBN{j8z2HvDDp04bG-ElPGCy*~7<-52 zIq#+)uer@mu3$dPyMNLaI*T&H%_Le*z5uw;sMV8OOAC5%=m*ojsHxQDV`Bwx>9MV( zm+tSTy|QVT3esf-!Dnn*JUnfB?ct`y!_%e{%MU#_ZmQ#GQ^N(T+Xp3w^}aTMMcTo09<-glj-r9cGu?E6~@PQ*)}OWn@N`?8-*u7@X$%7 zPn`-YM(;hIzA%~4)Avn1@*orii@HLW$~dToeV1z;UrqqB{NdiRy4% z6a|aGaj{UMIvf{8!6I;6ER?7Y$3;=F2pks+C91=5Q4}l!$HhX4>Tp~X1&hFOu~4Eq z92Z5wB5+(Rl&B8JMNzN_92W~Es>5+n6f6SA#X^bda9k7xi@