Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package dev.ikm.komet.kview.controls;

import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.layout.Region;
import javafx.scene.text.TextFlow;

/**
* A responsive extension of JavaFX's TextFlow component that properly handles
* text wrapping and layout recalculation based on parent container dimensions.
* <p>
* This component improves upon the standard TextFlow by:
* <ul>
* <li>Calculating preferred height based on parent width for proper text wrapping</li>
* <li>Deferring layout requests to the next JavaFX pulse cycle</li>
* <li>Ensuring text properly wraps and reflows when container dimensions change</li>
* </ul>
* <p>
* Use this component when you need text that automatically adjusts to its container
* while maintaining proper text wrapping behavior.
*
* @see TextFlow
* @see Region
*/
public class ResponsiveTextFlow extends TextFlow {

/**
* Constructs a new responsive text flow component with deferred layout behavior.
*/
public ResponsiveTextFlow() {
super();
}

/**
* Requests a layout pass on this component with deferred scheduling.
* <p>
* This implementation improves upon the standard TextFlow layout behavior by
* deferring layout requests to the next pulse via Platform.runLater().
* <p>
* The deferred processing allows text wrapping calculations to complete before
* final layout positioning occurs, ensuring that the component's size properly
* accounts for text that needs to wrap based on the parent container's dimensions.
*/
@Override
public void requestLayout() {
Platform.runLater(super::requestLayout);
}

/**
* Computes the preferred height of this text flow component based on the
* given width.
* <p>
* This implementation uses the parent container's width (accounting for insets)
* rather than the provided width parameter, which allows for accurate text wrapping
* and height calculations even before the component is fully laid out.
*
* @param width The width available for layout (not used directly in this implementation)
* @return The preferred height for this component based on parent width
*/
@Override
protected double computePrefHeight(double width) {
return super.computePrefHeight(getParentWidth());
}

/**
* Determines the effective width of the parent container, accounting for
* insets when applicable.
* <p>
* This method handles different parent types:
* <ul>
* <li>For Region parents, uses width minus insets</li>
* <li>For other parent types, uses the layout bounds width</li>
* </ul>
* <p>
* If no parent exists or the parent width is not positive, returns
* {@link Region#USE_COMPUTED_SIZE} to indicate that a computed size should be used.
*
* @return The effective width of the parent container, or {@link Region#USE_COMPUTED_SIZE}
* if the parent width cannot be determined
*/
private double getParentWidth() {
final Parent parent = getParent();
double parentWidth;
if (parent != null) {
if (parent instanceof Region region) {
parentWidth = region.getWidth();
Insets insets = region.getInsets();
if (insets != null) {
parentWidth -= insets.getLeft() + insets.getRight();
}
} else {
parentWidth = parent.getLayoutBounds().getWidth();
}

return parentWidth > 0 ? parentWidth : Region.USE_COMPUTED_SIZE;
}
return Region.USE_COMPUTED_SIZE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
Expand All @@ -33,6 +35,7 @@
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
Expand Down Expand Up @@ -108,6 +111,9 @@ public class KLWorkspaceSkin extends SkinBase<KLWorkspace> {
*/
private static final String CLAMP_WINDOW_POSITION_LISTENER = "clampWindowPositionListener";

private static final String WINDOW_SCENE_SYNC_LISTENER = "windowSceneSyncListener";
private static final String WINDOW_HEIGHT_SYNC_LISTENER = "windowHeightSyncListener";

/**
* The pane that holds all {@link ChapterKlWindow} nodes within the workspace.
* This pane is scrollable through a {@link ScrollPane}.
Expand Down Expand Up @@ -596,6 +602,9 @@ private void addWindow(ChapterKlWindow<Pane> window) {
// Apply a maximum height constraint
windowPanel.setMaxHeight(MAX_WINDOW_HEIGHT);

// Synchronize the window panel's preferred height with its actual height
initializeWindowPrefHeight(windowPanel);

final KLDropRegion dropRegion = desktopPane.getDropRegion();
final KLWorkspace workspace = getSkinnable();

Expand Down Expand Up @@ -627,7 +636,7 @@ private void addWindow(ChapterKlWindow<Pane> window) {
if (canPlace(dropX, dropY, dropW, dropH, desktopWidth, desktopHeight)) {
windowPanel.setTranslateX(dropX);
windowPanel.setTranslateY(dropY);
windowPanel.setPrefSize(dropW, dropH);
windowPanel.setPrefWidth(dropW);
desktopPane.getChildren().add(windowPanel);

// Auto-scroll the workspace to reveal the newly dropped window
Expand Down Expand Up @@ -665,7 +674,7 @@ private void addWindow(ChapterKlWindow<Pane> window) {

windowPanel.setTranslateX(newX);
windowPanel.setTranslateY(newY);
windowPanel.setPrefSize(windowWidth, windowHeight);
windowPanel.setPrefWidth(windowWidth);
desktopPane.getChildren().add(windowPanel);

// Auto-scroll the workspace to reveal the newly dropped window
Expand Down Expand Up @@ -695,7 +704,7 @@ private void addWindow(ChapterKlWindow<Pane> window) {
if (canPlace(savedX, savedY, windowWidth, windowHeight, desktopWidth, desktopHeight)) {
windowPanel.setTranslateX(savedX);
windowPanel.setTranslateY(savedY);
windowPanel.setPrefSize(windowWidth, windowHeight);
windowPanel.setPrefWidth(windowWidth);
desktopPane.getChildren().add(windowPanel);
desktopPane.layout();
// No auto-scrolling for returning windows
Expand All @@ -713,7 +722,7 @@ private void addWindow(ChapterKlWindow<Pane> window) {
if (placement != null) {
windowPanel.setTranslateX(placement.getX());
windowPanel.setTranslateY(placement.getY());
windowPanel.setPrefSize(windowWidth, windowHeight);
windowPanel.setPrefWidth(windowWidth);
desktopPane.getChildren().add(windowPanel);

if (firstTime) {
Expand Down Expand Up @@ -1299,6 +1308,59 @@ private void clampWindowPosition(Pane pane) {
}
}

/**
* Initializes listeners that synchronize the window panel's preferred height with its actual height
* after rendering. This ensures the window maintains a consistent size that reflects its content.
*
* @param pane the window pane to set up preferred height synchronization for
*/
private void initializeWindowPrefHeight(Pane pane) {
// Create the window scene sync listener
InvalidationListener windowSceneSyncListener = sceneObservable -> {
Scene scene = pane.getScene();
if (scene != null) {
// Synchronize the window panel's preferred height with its actual height after rendering.
Platform.runLater(() -> {
pane.layout();

// Create the window height sync listener
InvalidationListener windowHeightSyncListener = heightObservable -> {
final double height = pane.getHeight();
if (height > 0) {
pane.setPrefHeight(height);

if (pane.getProperties().containsKey(WINDOW_HEIGHT_SYNC_LISTENER)) {
final InvalidationListener heightListener =
(InvalidationListener) pane.getProperties().get(WINDOW_HEIGHT_SYNC_LISTENER);
pane.heightProperty().removeListener(heightListener);
pane.getProperties().remove(WINDOW_HEIGHT_SYNC_LISTENER);
}
}
};

// Store a reference for later cleanup
pane.getProperties().put(WINDOW_HEIGHT_SYNC_LISTENER, windowHeightSyncListener);

// Add the listener to the window panel
pane.heightProperty().addListener(windowHeightSyncListener);
});
}

if (pane.getProperties().containsKey(WINDOW_SCENE_SYNC_LISTENER)) {
final InvalidationListener sceneListener =
(InvalidationListener) pane.getProperties().get(WINDOW_SCENE_SYNC_LISTENER);
pane.sceneProperty().removeListener(sceneListener);
pane.getProperties().remove(WINDOW_SCENE_SYNC_LISTENER);
}
};

// Store a reference for later cleanup
pane.getProperties().put(WINDOW_SCENE_SYNC_LISTENER, windowSceneSyncListener);

// Add the listener to the window panel
pane.sceneProperty().addListener(windowSceneSyncListener);
}

// =========================================================================
// DISPOSAL / CLEANUP
// =========================================================================
Expand Down
Loading