diff --git a/build.gradle b/build.gradle index b43f5e9..35e127c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,66 +1,44 @@ plugins { - id "com.jfrog.bintray" version "1.8.3" - id "com.diffplug.gradle.spotless" version "3.1.0" - id "de.undercouch.download" version "3.4.3" + id 'java-library' + id 'application' + id 'maven-publish' + id 'org.openjfx.javafxplugin' version '0.1.0' } -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'idea' -apply plugin: 'maven' -apply plugin: 'maven-publish' +group = 'org.dockfx' +version = '0.2.0' +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 -project.ext.masterfile = (new File(projectDir,'/gradle/master.gradle')).getAbsolutePath() - -// Attempts to download master gradle file -if(!hasProperty('do_not_update_master_file')) -try { - download { - src 'https://github.com/ClearControl/master/blob/master/master.gradle?raw=true' - dest project.ext.masterfile - overwrite true - onlyIfModified true - tempAndMove true - } -} -catch (Throwable e) -{ + withSourcesJar() + withJavadocJar() } -apply from: project.ext.masterfile - -//*********************************************************************************** -// JAVA CODE BUILDING +javafx { + version = '21.0.2' + modules = ['javafx.controls', 'javafx.fxml', 'javafx.graphics', 'javafx.base', 'javafx.web'] +} -sourceSets -{ - main - { - java - { srcDir 'src/main/java' } - resources - { srcDir 'src/main/resources' } +sourceSets { + main { + java { srcDir 'src/main/java' } + resources { srcDir 'src/main/resources' } } - test - { - java - { srcDir 'src/test/java' } - resources - { srcDir 'src/test/resources' } + test { + java { srcDir 'src/test/java' } + resources { srcDir 'src/test/resources' } } } +tasks.withType(AbstractCopyTask).configureEach { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - - -test -{ +test { testLogging.showStandardStreams = true - testLogging - { events "passed", "skipped", "failed" } + testLogging { events "passed", "skipped", "failed" } exclude '**/demo/**' exclude '**/run/**' @@ -68,69 +46,24 @@ test maxHeapSize = "4G" } -dependencies -{ - - -} - -repositories -{ - // Main repos: +repositories { mavenLocal() mavenCentral() - jcenter() } - -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') - } - } +tasks.withType(Javadoc).configureEach { + options.addStringOption('Xdoclint:none', '-quiet') } - -//*********************************************************************************** -// PUBLISHING - -group = project.ext.groupname -version = project.ext.versionsMap[project.name] - -artifacts - { - archives sourcesJar - archives javadocJar - } - publishing { publications { maven(MavenPublication) { from components.java - artifact sourcesJar { classifier "sources" } } } } - - - - - - - - - - - +application { + mainClass = 'org.dockfx.demo.DockFX' + mainModule = 'org.dockfx' +} diff --git a/gradle.properties b/gradle.properties index 0f4c148..29bb2d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=true -org.gradle.jvmargs=-Xmx4G -Xss256m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4G -Xss256m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 bintray_userorg = clearcontrol bintray_repo = ClearControl diff --git a/gradle/master.gradle b/gradle/master.gradle index 1c0a2a4..926cd31 100644 --- a/gradle/master.gradle +++ b/gradle/master.gradle @@ -2,64 +2,66 @@ // Current release versions for Clear* projects: ext { - groupname = "net.clearcontrol" - versionsMap = [:] - - // the dependency graph is layed out in 'levels' that represent their dependency depth: - // direct dependency is given, dependencies are transitive. - // Dependencies in the same level do not depend on each other. - // When editiong this file, be carefull that GitHub does not always instantly reflect the changes. - - // Level 0 (don't depend on any other lib): - versionsMap['coremem'] = "0.4.6" - versionsMap['dockfx'] = "0.1.12" - versionsMap['cleargl'] = "2.2.1" // Ulrik Guenter (skalarproduktraum) is currently maintaing this project and releases versions - versionsMap['clearaudio'] = "1.0.2" - - // devices adapters: - versionsMap['aptj'] = "0.2.3" - versionsMap['asdkj'] = "1.0.2" - versionsMap['mirao52j'] = "1.0.1" - versionsMap['dmp40j'] = "0.1.0" - versionsMap['nirioj'] = "1.0.2" - versionsMap['andorsdkj'] = "0.1.0" - versionsMap['ecc100j'] = "1.0.1" - - // Level 1: - versionsMap['halcyon'] = "0.3.7" // depends directly on dockfx - - // device adapters: - versionsMap['dcamj'] = "2.1.7" // depends directly on coremem - versionsMap['clearcl'] = "0.6.2" // depends directly on coremem - - // Level 2: - versionsMap['clearvolume'] = "1.4.2" // depends directly on clearcl, clearaudio, cleargl - versionsMap['simbryo'] = "0.9.1" // depends directly on clearcl - - // Level 3: - versionsMap['fastfuse'] = "0.6.5" // depends directly on symbryo - - // Level 4 - versionsMap['clearclij'] = "0.3.10" // depends directly FastFuse and indirectly to ClearCL - - // Level 5: - versionsMap['clearcontrol'] = "0.16.8" // depends directly on halcyon, clearvolume, fastfuse, and _all_ devices adapters - - // Level 6: - versionsMap['clearcontrol-lightsheet'] = "1.27.9" // depends directly on clearcontrol, only - - - // Loads locale file: ~/.gradle/local.gradle - // You can override versions locally for example: - // - // ext/versionsMap['symbrio'] = "1.0.1-LR" - // - - localfilename = project.gradle.gradleUserHomeDir.getAbsolutePath()+'/local.gradle' - if(file(localfilename).exists()){ - apply from: localfilename - } - + groupname = "net.clearcontrol" + versionsMap = [:] + + // the dependency graph is layed out in 'levels' that represent their dependency depth: + // direct dependency is given, dependencies are transitive. + // Dependencies in the same level do not depend on each other. + // When editiong this file, be carefull that GitHub does not always instantly reflect the changes. + + // Level 0 (don't depend on any other lib): + versionsMap['coremem'] = "0.4.8" + versionsMap['dockfx'] = "0.1.12" + versionsMap['cleargl'] = "2.2.1" // Ulrik Guenter (skalarproduktraum) is currently maintaing this project and releases versions + versionsMap['clearaudio'] = "1.0.2" + + // devices adapters: + versionsMap['aptj'] = "0.0.5" + versionsMap['asdkj'] = "1.0.2" + versionsMap['mirao52j'] = "1.0.1" + versionsMap['dmp40j'] = "0.1.0" + versionsMap['nirioj'] = "1.1.0" // breaking version change from 1.0.2 to 1.1.0 + versionsMap['andorsdkj'] = "0.1.0" + versionsMap['ecc100j'] = "1.0.1" + + // Level 1: + versionsMap['halcyon'] = "0.3.7" // depends directly on dockfx + + // device adapters: + versionsMap['dcamj'] = "2.1.7" // depends directly on coremem + versionsMap['clearcl'] = "0.6.3" // depends directly on coremem + + // Level 2: + versionsMap['clearvolume'] = "1.4.2" // depends directly on clearcl, clearaudio, cleargl + versionsMap['simbryo'] = "0.9.1" // depends directly on clearcl + + // Level 3: + versionsMap['fastfuse'] = "0.6.5" // depends directly on symbryo + + // Level 4 + versionsMap['clearclij'] = "0.3.18" // depends directly FastFuse and indirectly to ClearCL + + // Level 5: + versionsMap['clearcontrol'] = "0.17.0" // depends directly on halcyon, clearvolume, fastfuse, and _all_ devices adapters + // breaking version change from 0.16.14 to 0.17.0 + + // Level 6: + versionsMap['clearcontrol-lightsheet'] = "1.30.0" // depends on clearcontrol only; breaking version change from 1.29.0 to 1.30.0 + + // Level 7: + versionsMap['clearcontrol-lightsheet-csbd'] = "0.0.1" // depends directly on clearcontrol-lightsheet + + // Loads locale file: ~/.gradle/local.gradle + // You can override versions locally for example: + // + // ext/versionsMap['symbrio'] = "1.0.1-LR" + // + + localfilename = project.gradle.gradleUserHomeDir.getAbsolutePath()+'/local.gradle' + if(file(localfilename).exists()){ + apply from: localfilename + } } ext.depver = { name -> project.ext.versionsMap[name] } @@ -71,27 +73,27 @@ project.ext.spotlessfile = (new File(projectDir,'/gradle/spotless.gradle')).getA // Download files: try { download { - src 'https://github.com/ClearControl/master/blob/master/installGitHooks?raw=true' - dest project.ext.githooksfile - overwrite false - onlyIfModified true - tempAndMove true + src 'https://github.com/ClearControl/master/blob/master/installGitHooks?raw=true' + dest project.ext.githooksfile + overwrite false + onlyIfModified true + tempAndMove true } - + download { - src 'https://github.com/ClearControl/master/blob/master/bintrayUpload.gradle?raw=true' - dest project.ext.bintrayfile - overwrite false - onlyIfModified true - tempAndMove true + src 'https://github.com/ClearControl/master/blob/master/bintrayUpload.gradle?raw=true' + dest project.ext.bintrayfile + overwrite false + onlyIfModified true + tempAndMove true } - + download { - src 'https://github.com/ClearControl/master/blob/master/spotless.gradle?raw=true' - dest project.ext.spotlessfile - overwrite false - onlyIfModified true - tempAndMove true + src 'https://github.com/ClearControl/master/blob/master/spotless.gradle?raw=true' + dest project.ext.spotlessfile + overwrite false + onlyIfModified true + tempAndMove true } } catch (Throwable e) @@ -111,4 +113,4 @@ apply from: project.ext.spotlessfile - + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e0b3fb8..a595206 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/pom.xml b/pom.xml index 2bd2820..5af1702 100644 --- a/pom.xml +++ b/pom.xml @@ -6,66 +6,83 @@ org.dockfx DockFX jar - 0.1.12 + 0.2.0 DockFX https://github.com/ClearControl/DockFX.git - 1.8 - 1.8 + 21 + 21 + 21.0.2 true UTF-8 java, properties, xml + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-graphics + ${javafx.version} + + + org.openjfx + javafx-base + ${javafx.version} + + + org.openjfx + javafx-web + ${javafx.version} + + + org.apache.maven.plugins - maven-jar-plugin + maven-compiler-plugin + 3.11.0 - - target/classes/META-INF/MANIFEST.MF - - org.dockfx.demo.DockFX - - + 21 + 21 + ${project.build.sourceEncoding} - org.apache.felix - maven-bundle-plugin - 2.5.4 - - - bundle-manifest - process-classes - - manifest - - - + org.openjfx + javafx-maven-plugin + 0.0.8 - - ${project.groupId} - ${project.version} - org.dockfx - org.dockfx.demo - + org.dockfx/org.dockfx.demo.DockFX org.apache.maven.plugins - maven-compiler-plugin - 3.3 + maven-jar-plugin + 3.3.0 - ${project.build.sourceEncoding} + + + org.dockfx.demo.DockFX + + org.apache.maven.plugins maven-resources-plugin - 2.7 + 3.3.1 ${project.build.sourceEncoding} @@ -86,12 +103,12 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 2.8 + 3.4.5 org.apache.maven.plugins maven-checkstyle-plugin - 2.16 + 3.3.0 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9c1fe7b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'DockFX' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..3e34689 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,17 @@ +module org.dockfx { + requires javafx.controls; + requires javafx.fxml; + requires javafx.graphics; + requires javafx.base; + requires javafx.web; + requires java.desktop; + requires java.logging; + + exports org.dockfx; + exports org.dockfx.pane; + exports org.dockfx.pane.skin; + exports org.dockfx.viewControllers; + + opens org.dockfx.demo to javafx.graphics; + opens org.dockfx to javafx.base, javafx.fxml; +} diff --git a/src/main/java/org/dockfx/DockEvent.java b/src/main/java/org/dockfx/DockEvent.java index 9b0a2ed..ba2d56e 100644 --- a/src/main/java/org/dockfx/DockEvent.java +++ b/src/main/java/org/dockfx/DockEvent.java @@ -27,7 +27,7 @@ import javafx.scene.Node; import javafx.scene.input.PickResult; -import com.sun.javafx.scene.input.InputEventUtils; + /** * Base class for DockFX events. Each DockFX event has associated an event @@ -376,9 +376,18 @@ public DockEvent(Object source, this.pickResult = pickResult != null ? pickResult : new PickResult(target, x, y); - final Point3D p = - InputEventUtils.recomputeCoordinates(this.pickResult, - null); + final Point3D p; + Node intersectedNode = this.pickResult.getIntersectedNode(); + if (intersectedNode != null && intersectedNode.getScene() != null) + { + Point3D ip = this.pickResult.getIntersectedPoint(); + p = intersectedNode.localToScene(ip.getX(), ip.getY(), ip.getZ()); + } + else + { + Point3D ip = this.pickResult.getIntersectedPoint(); + p = ip != null ? ip : Point3D.ZERO; + } this.x = p.getX(); this.y = p.getY(); this.z = p.getZ(); diff --git a/src/main/java/org/dockfx/DockPane.java b/src/main/java/org/dockfx/DockPane.java index 1efdb29..cb3054a 100644 --- a/src/main/java/org/dockfx/DockPane.java +++ b/src/main/java/org/dockfx/DockPane.java @@ -57,7 +57,6 @@ import javafx.stage.Stage; import javafx.util.Duration; -import com.sun.javafx.css.StyleManager; import org.dockfx.pane.ContentPane; import org.dockfx.pane.ContentSplitPane; @@ -230,6 +229,11 @@ public DockPane() { super(); + if (!this.getStylesheets().contains(defaultStylesheet)) + { + this.getStylesheets().add(defaultStylesheet); + } + this.addEventHandler(DockEvent.ANY, this); this.addEventFilter(DockEvent.ANY, new EventHandler() { @@ -393,14 +397,20 @@ public final static String getDefaultUserAgentStylesheet() } /** - * Helper function to add the default style sheet of DockFX to the user agent - * style sheets. + * The default stylesheet URL for DockFX. + */ + private static final String defaultStylesheet = + DockPane.class.getResource("default.css").toExternalForm(); + + /** + * Helper function to ensure the default style sheet of DockFX is applied. + * The stylesheet is now automatically added to each DockPane instance via + * the constructor. This method is retained for backward compatibility. */ public final static void initializeDefaultUserAgentStylesheet() { - StyleManager.getInstance() - .addUserAgentStylesheet(DockPane.class.getResource("default.css") - .toExternalForm()); + // Stylesheet is automatically applied in the DockPane constructor. + // This method is kept for API compatibility. } /** diff --git a/src/main/java/org/dockfx/DockTitleBar.java b/src/main/java/org/dockfx/DockTitleBar.java index 77f03c2..49bce1f 100644 --- a/src/main/java/org/dockfx/DockTitleBar.java +++ b/src/main/java/org/dockfx/DockTitleBar.java @@ -21,11 +21,10 @@ package org.dockfx; import java.util.HashMap; +import java.util.List; import java.util.Stack; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.event.EventHandler; @@ -43,7 +42,7 @@ import javafx.stage.Stage; import javafx.stage.Window; -import com.sun.javafx.stage.StageHelper; + /** * Base class for a dock node title bar that provides the mouse dragging @@ -323,8 +322,10 @@ private void pickEventTarget(Point2D location, // RFE for public scene graph traversal API filed but closed: // https://bugs.openjdk.java.net/browse/JDK-8133331 - ObservableList stages = - FXCollections.unmodifiableObservableList(StageHelper.getStages()); + List stages = Window.getWindows().stream() + .filter(w -> w instanceof Stage) + .map(w -> (Stage) w) + .toList(); // fire the dock over event for the active stages for (Stage targetStage : stages) { diff --git a/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java index 67bf455..c89f0d5 100644 --- a/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java +++ b/src/main/java/org/dockfx/pane/skin/ContentTabPaneSkin.java @@ -1,2493 +1,57 @@ - package org.dockfx.pane.skin; -import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -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 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.control.skin.BehaviorSkinBase; -import com.sun.javafx.scene.traversal.Direction; -import com.sun.javafx.scene.traversal.TraversalEngine; -import com.sun.javafx.util.Utils; +import javafx.scene.control.skin.TabPaneSkin; import org.dockfx.pane.DockNodeTab; /** - * 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 + * Custom skin for ContentTabPane that extends the public JavaFX TabPaneSkin. + * + * The original class was a full copy of the internal + * com.sun.javafx.scene.control.skin.TabPaneSkin from Java 8. The only + * DockFX-specific customization was a modified TabMenuItem that used + * DockNodeTab.titleProperty() for the overflow popup menu text, since + * DockNodeTab tabs use custom graphics rather than text. + * + * This rewrite delegates all standard behavior to the public TabPaneSkin + * and binds each DockNodeTab's text property to its title so the overflow + * popup menu displays correctly. + * + * @author HongKee Moon */ -public class ContentTabPaneSkin extends - BehaviorSkinBase +public class ContentTabPaneSkin extends TabPaneSkin { - 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; - } + super(tabPane); - @Override - protected void handleControlPropertyChanged(String property) - { - super.handleControlPropertyChanged(property); - if ("SELECTED_TAB".equals(property)) + for (Tab tab : tabPane.getTabs()) { - isSelectingTab = true; - selectedTab = getSkinnable().getSelectionModel() - .getSelectedItem(); - getSkinnable().requestLayout(); + bindTabText(tab); } - 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) + tabPane.getTabs().addListener((ListChangeListener) c -> { + while (c.next()) { - 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) + if (c.wasAdded()) { - 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()) + for (Tab tab : c.getAddedSubList()) { - tabHeaderArea.setVisible(false); + bindTabText(tab); } - }; - - 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 void bindTabText(Tab tab) { - private Rectangle headerClip; - private StackPane headersRegion; - private StackPane headerBackground; - private TabControlButtons controlButtons; - - private boolean measureClosingTabs = false; - - private double scrollOffset; - - public TabHeaderArea() + if (tab instanceof DockNodeTab) { - 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); + tab.textProperty().bind(((DockNodeTab) tab).titleProperty()); } } }