View Javadoc
1   /*
2    * Tethys: GUI Utilities
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.tethys.javafx.base;
18  
19  import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
20  import javafx.geometry.Bounds;
21  import javafx.geometry.Dimension2D;
22  import javafx.geometry.Insets;
23  import javafx.geometry.Point2D;
24  import javafx.geometry.Pos;
25  import javafx.geometry.Rectangle2D;
26  import javafx.geometry.Side;
27  import javafx.scene.Node;
28  import javafx.scene.Scene;
29  import javafx.scene.control.Label;
30  import javafx.scene.image.Image;
31  import javafx.scene.image.ImageView;
32  import javafx.scene.layout.HBox;
33  import javafx.scene.layout.Pane;
34  import javafx.scene.layout.Priority;
35  import javafx.scene.layout.StackPane;
36  import javafx.scene.paint.Color;
37  import javafx.stage.Screen;
38  import javafx.stage.Window;
39  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIconId;
40  
41  import java.util.List;
42  
43  /**
44   * Simple UI Utilities for javaFX.
45   */
46  public final class TethysUIFXUtils {
47      /**
48       * Base StyleSheet Class.
49       */
50      public static final String CSS_STYLE_BASE = "-jtethys";
51  
52      /**
53       * The titled style.
54       */
55      private static final String STYLE_TITLED = CSS_STYLE_BASE + "-titled";
56  
57      /**
58       * The title style.
59       */
60      private static final String STYLE_TITLE = STYLE_TITLED + "-title";
61  
62      /**
63       * The border style.
64       */
65      private static final String STYLE_BORDER = STYLE_TITLED + "-border";
66  
67      /**
68       * The content style.
69       */
70      private static final String STYLE_CONTENT = STYLE_TITLED + "-content";
71  
72      /**
73       * RGB header.
74       */
75      private static final String RGB_HDR = "#";
76  
77      /**
78       * private constructor.
79       */
80      private TethysUIFXUtils() {
81      }
82  
83      /**
84       * format a colour as a hexadecimal string.
85       *
86       * @param pValue the long value
87       * @return the string
88       */
89      public static String colorToHexString(final Color pValue) {
90          /* Return the string */
91          final StringBuilder myBuilder = new StringBuilder();
92          myBuilder.append(RGB_HDR);
93          appendColorPart(myBuilder, pValue.getRed());
94          appendColorPart(myBuilder, pValue.getGreen());
95          appendColorPart(myBuilder, pValue.getBlue());
96          return myBuilder.toString();
97      }
98  
99      /**
100      * format a colour part.
101      *
102      * @param pBuilder the string builder
103      * @param pValue   the value
104      */
105     private static void appendColorPart(final StringBuilder pBuilder,
106                                         final double pValue) {
107         /* Convert to integer */
108         final int myMax = OceanusDataConverter.BYTE_MASK + 1;
109         int myValue = (int) (pValue * myMax);
110 
111         /* Handle boundary issue */
112         if (myValue == myMax) {
113             myValue--;
114         }
115 
116         /* Format the high nibble */
117         int myDigit = myValue >>> OceanusDataConverter.NYBBLE_SHIFT;
118         char myChar = Character.forDigit(myDigit, OceanusDataConverter.HEX_RADIX);
119         pBuilder.append(myChar);
120 
121         /* Access the low nibble */
122         myDigit = myValue
123                 & OceanusDataConverter.NYBBLE_MASK;
124         myChar = Character.forDigit(myDigit, OceanusDataConverter.HEX_RADIX);
125         pBuilder.append(myChar);
126     }
127 
128     /**
129      * Create titled/padded border around pane.
130      *
131      * @param pTitle   the title
132      * @param pPadding the padding
133      * @param pNode    the node
134      * @return the titled pane
135      */
136     static Pane getBorderedPane(final String pTitle,
137                                 final Integer pPadding,
138                                 final Node pNode) {
139         /* Access the Node */
140         final Pane myPane;
141         if (!(pNode instanceof Pane p)) {
142             /* Create an HBox for the content */
143             final HBox myBox = new HBox();
144             myBox.getChildren().add(pNode);
145             myPane = myBox;
146 
147             /* Set the HBox to fill the pane */
148             HBox.setHgrow(pNode, Priority.ALWAYS);
149         } else {
150             myPane = p;
151         }
152 
153         /* Return the pane if we have no title or padding */
154         if (pTitle == null
155                 && pPadding == null) {
156             return myPane;
157         }
158 
159         /* Create the stack pane */
160         final StackPane myPanel = new StackPane();
161 
162         /* If we have a title */
163         if (pTitle != null) {
164             final Label myTitle = new Label(pTitle);
165             StackPane.setAlignment(myTitle, Pos.TOP_LEFT);
166             StackPane.setAlignment(myPane, Pos.CENTER);
167             myPanel.getChildren().add(myTitle);
168 
169             /* Set the styles */
170             myPane.getStyleClass().add(STYLE_CONTENT);
171             myTitle.getStyleClass().add(STYLE_TITLE);
172             myPanel.getStyleClass().add(STYLE_BORDER);
173         }
174 
175         /* Set padding if required */
176         if (pPadding != null) {
177             myPanel.setPadding(new Insets(pPadding, pPadding, pPadding, pPadding));
178         }
179 
180         /* Add the node */
181         myPanel.getChildren().add(myPane);
182 
183         /* Return the panel */
184         return myPanel;
185     }
186 
187     /**
188      * Obtain display point for dialog.
189      *
190      * @param pAnchor   the anchor node
191      * @param pLocation the preferred location relative to node
192      * @param pSize     the size of the dialog
193      * @return the (adjusted) rectangle
194      */
195     public static Point2D obtainDisplayPoint(final Node pAnchor,
196                                              final Point2D pLocation,
197                                              final Dimension2D pSize) {
198         /* First of all determine the display screen for the anchor node */
199         final Screen myScreen = getScreenForNode(pAnchor);
200 
201         /* Next obtain the fully qualified location */
202         final Point2D myLocation = getLocationForNode(pAnchor, pLocation);
203 
204         /* determine the display rectangle */
205         Rectangle2D myArea = new Rectangle2D(myLocation.getX(), myLocation.getY(),
206                 pSize.getWidth(), pSize.getHeight());
207         myArea = adjustDisplayLocation(myArea, myScreen);
208 
209         /* Return the location */
210         return new Point2D(myArea.getMinX(), myArea.getMinY());
211     }
212 
213     /**
214      * Obtain display point for dialog.
215      *
216      * @param pAnchor the anchor node
217      * @param pSide   the preferred side to display on
218      * @param pSize   the size of the dialog
219      * @return the (adjusted) rectangle
220      */
221     public static Point2D obtainDisplayPoint(final Node pAnchor,
222                                              final Side pSide,
223                                              final Dimension2D pSize) {
224         /* First of all determine the display screen for the anchor node */
225         final Screen myScreen = getScreenForNode(pAnchor);
226 
227         /* Next obtain the fully qualified location */
228         final Point2D myLocation = getOriginForNode(pAnchor);
229 
230         /* determine the display rectangle */
231         Rectangle2D myArea = new Rectangle2D(myLocation.getX(), myLocation.getY(),
232                 pSize.getWidth(), pSize.getHeight());
233         myArea = adjustDisplayLocation(myArea, pAnchor, pSide, myScreen);
234 
235         /* Return the location */
236         return new Point2D(myArea.getMinX(), myArea.getMinY());
237     }
238 
239     /**
240      * Obtain the screen that best contains the anchor node.
241      *
242      * @param pAnchor the anchor node.
243      * @return the relevant screen
244      */
245     private static Screen getScreenForNode(final Node pAnchor) {
246         /* Access the list of screens */
247         final List<Screen> myScreens = Screen.getScreens();
248 
249         /* Obtain full-qualified origin of node */
250         final Point2D myOrigin = getOriginForNode(pAnchor);
251 
252         /* Build fully-qualified bounds */
253         final Bounds myLocalBounds = pAnchor.getBoundsInLocal();
254         final Rectangle2D myBounds = new Rectangle2D(myOrigin.getX(),
255                 myOrigin.getY(),
256                 myLocalBounds.getWidth(),
257                 myLocalBounds.getHeight());
258 
259         /* Set values */
260         double myBest = 0;
261         Screen myBestScreen = null;
262 
263         /* Look for a screen that contains the point */
264         for (final Screen myScreen : myScreens) {
265             final Rectangle2D myScreenBounds = myScreen.getBounds();
266 
267             /* Calculate intersection and record best */
268             final double myIntersection = getIntersection(myBounds, myScreenBounds);
269             if (myIntersection > myBest) {
270                 myBest = myIntersection;
271                 myBestScreen = myScreen;
272             }
273         }
274 
275         /* If none found then default to primary */
276         return myBestScreen == null
277                 ? Screen.getPrimary()
278                 : myBestScreen;
279     }
280 
281     /**
282      * Obtain the fully-qualified node origin.
283      *
284      * @param pNode the node.
285      * @return the origin
286      */
287     private static Point2D getOriginForNode(final Node pNode) {
288         /* Access scene and window details */
289         final Scene myScene = pNode.getScene();
290         final Window myWindow = myScene == null
291                 ? null
292                 : myScene.getWindow();
293         final boolean bVisible = myScene != null && myWindow != null;
294 
295         /* Determine base of scene */
296         final double mySceneX = bVisible
297                 ? myWindow.getX() + myScene.getX()
298                 : 0;
299         final double mySceneY = bVisible
300                 ? myWindow.getY() + myScene.getY()
301                 : 0;
302 
303         /* Determine node bounds in scene */
304         final Bounds myLocalBounds = pNode.getBoundsInLocal();
305         final Bounds myNodeBounds = pNode.localToScene(myLocalBounds);
306 
307         /* Build fully-qualified location */
308         return new Point2D(myNodeBounds.getMinX() + mySceneX,
309                 myNodeBounds.getMinY() + mySceneY);
310     }
311 
312     /**
313      * Obtain the fully-qualified node location.
314      *
315      * @param pAnchor   the node.
316      * @param pLocation the location relative to the n
317      * @return the origin
318      */
319     private static Point2D getLocationForNode(final Node pAnchor,
320                                               final Point2D pLocation) {
321         /* Access node origin */
322         final Point2D myOrigin = getOriginForNode(pAnchor);
323 
324         /* Calculate fully-qualified location */
325         return new Point2D(myOrigin.getX() + pLocation.getX(),
326                 myOrigin.getY() + pLocation.getY());
327     }
328 
329     /**
330      * Calculate the intersection of bounds and screen.
331      *
332      * @param pBounds the bounds.
333      * @param pScreen the screen
334      * @return the intersection
335      */
336     private static double getIntersection(final Rectangle2D pBounds,
337                                           final Rectangle2D pScreen) {
338 
339         /* Calculate intersection coordinates */
340         final double myMinX = Math.max(pBounds.getMinX(), pScreen.getMinX());
341         final double myMaxX = Math.min(pBounds.getMaxX(), pScreen.getMaxX());
342         final double myMinY = Math.max(pBounds.getMinY(), pScreen.getMinY());
343         final double myMaxY = Math.min(pBounds.getMaxY(), pScreen.getMaxY());
344 
345         /* Calculate intersection lengths */
346         final double myX = Math.max(myMaxX - myMinX, 0);
347         final double myY = Math.max(myMaxY - myMinY, 0);
348 
349         /* Calculate intersection */
350         return myX * myY;
351     }
352 
353     /**
354      * Adjust display location to fit on screen.
355      *
356      * @param pSource the proposed location
357      * @param pScreen the screen
358      * @return the (adjusted) location
359      */
360     private static Rectangle2D adjustDisplayLocation(final Rectangle2D pSource,
361                                                      final Screen pScreen) {
362         /* Access Screen bounds */
363         final Rectangle2D myScreenBounds = pScreen.getBounds();
364         double myAdjustX = 0;
365         double myAdjustY = 0;
366 
367         /* Adjust for too far right */
368         if (pSource.getMaxX() > myScreenBounds.getMaxX()) {
369             myAdjustX = myScreenBounds.getMaxX() - pSource.getMaxX();
370         }
371 
372         /* Adjust for too far down */
373         if (pSource.getMaxY() > myScreenBounds.getMaxY()) {
374             myAdjustY = myScreenBounds.getMaxY() - pSource.getMaxY();
375         }
376 
377         /* Adjust for too far left */
378         if (pSource.getMinX() + myAdjustX < myScreenBounds.getMinX()) {
379             myAdjustX = myScreenBounds.getMinX() - pSource.getMinX();
380         }
381 
382         /* Adjust for too far down */
383         if (pSource.getMinY() + myAdjustY < myScreenBounds.getMinY()) {
384             myAdjustY = myScreenBounds.getMinY() - pSource.getMinY();
385         }
386 
387         /* Calculate new rectangle */
388         return (Double.doubleToRawLongBits(myAdjustX) != 0)
389                 || (Double.doubleToRawLongBits(myAdjustY) != 0)
390                 ? new Rectangle2D(pSource.getMinX() + myAdjustX,
391                 pSource.getMinY() + myAdjustY,
392                 pSource.getWidth(),
393                 pSource.getHeight())
394                 : pSource;
395     }
396 
397     /**
398      * Adjust display location to fit at side of node.
399      *
400      * @param pSource the proposed location
401      * @param pAnchor the anchor node
402      * @param pSide   the preferred side to display on
403      * @param pScreen the screen
404      * @return the (adjusted) location
405      */
406     private static Rectangle2D adjustDisplayLocation(final Rectangle2D pSource,
407                                                      final Node pAnchor,
408                                                      final Side pSide,
409                                                      final Screen pScreen) {
410         /* Access Screen bounds */
411         final Rectangle2D myScreenBounds = pScreen.getBounds();
412         final Bounds myBounds = pAnchor.getBoundsInLocal();
413         double myAdjustX = 0;
414         double myAdjustY = 0;
415 
416         /* Determine initial adjustment */
417         switch (pSide) {
418             case RIGHT:
419                 myAdjustX = myBounds.getWidth();
420                 if (pSource.getMaxX() + myAdjustX > myScreenBounds.getMaxX()) {
421                     myAdjustX = -pSource.getWidth();
422                 }
423                 break;
424             case LEFT:
425                 myAdjustX = -pSource.getWidth();
426                 if (pSource.getMinX() + myAdjustX < myScreenBounds.getMinX()) {
427                     myAdjustX = myBounds.getWidth();
428                 }
429                 break;
430             case BOTTOM:
431                 myAdjustY = myBounds.getHeight();
432                 if (pSource.getMaxY() + myAdjustY > myScreenBounds.getMaxY()) {
433                     myAdjustY = -pSource.getHeight();
434                 }
435                 break;
436             case TOP:
437                 myAdjustY = -pSource.getHeight();
438                 if (pSource.getMinY() + myAdjustY < myScreenBounds.getMinY()) {
439                     myAdjustY = myBounds.getHeight();
440                 }
441                 break;
442             default:
443                 break;
444         }
445 
446         /* Calculate new rectangle */
447         final Rectangle2D myArea = (Double.doubleToRawLongBits(myAdjustX) != 0)
448                 || (Double.doubleToRawLongBits(myAdjustY) != 0)
449                 ? new Rectangle2D(pSource.getMinX() + myAdjustX,
450                 pSource.getMinY() + myAdjustY,
451                 pSource.getWidth(),
452                 pSource.getHeight())
453                 : pSource;
454         return adjustDisplayLocation(myArea, pScreen);
455     }
456 
457     /**
458      * Obtain the raw icon.
459      *
460      * @param pId the icon Id
461      * @return the icon
462      */
463     public static TethysUIFXIcon getIcon(final TethysUIIconId pId) {
464         final Image myImage = new Image(pId.getInputStream());
465         final ImageView myView = new ImageView(myImage);
466         return new TethysUIFXIcon(myView);
467     }
468 
469     /**
470      * Obtain raw icons.
471      *
472      * @param pIds the icon Id
473      * @return the icon
474      */
475     public static Image[] getIcons(final TethysUIIconId[] pIds) {
476         final Image[] myIcons = new Image[pIds.length];
477         for (int i = 0; i < pIds.length; i++) {
478             myIcons[i] = getIcon(pIds[i]).getImage();
479         }
480         return myIcons;
481     }
482 
483     /**
484      * Obtain the reSized icon.
485      *
486      * @param pId   the icon Id
487      * @param pSize the new size for the icon
488      * @return the icon
489      */
490     public static TethysUIFXIcon getIconAtSize(final TethysUIIconId pId,
491                                                final int pSize) {
492         final ImageView myNewImage = getIcon(pId).getImageView();
493         myNewImage.setFitWidth(pSize);
494         myNewImage.setPreserveRatio(true);
495         myNewImage.setSmooth(true);
496         myNewImage.setCache(true);
497         return new TethysUIFXIcon(myNewImage);
498     }
499 }