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.swing.base;
18  
19  import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
20  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
21  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
22  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIconId;
23  
24  import javax.swing.BorderFactory;
25  import javax.swing.ImageIcon;
26  import javax.swing.JComponent;
27  import javax.swing.JPanel;
28  import javax.swing.SwingConstants;
29  import javax.swing.border.Border;
30  import java.awt.BorderLayout;
31  import java.awt.Color;
32  import java.awt.Component;
33  import java.awt.Dimension;
34  import java.awt.Font;
35  import java.awt.FontMetrics;
36  import java.awt.GraphicsConfiguration;
37  import java.awt.GraphicsDevice;
38  import java.awt.GraphicsEnvironment;
39  import java.awt.Image;
40  import java.awt.Point;
41  import java.awt.Rectangle;
42  import java.io.IOException;
43  
44  /**
45   * Simple UI Utilities for Swing.
46   */
47  public final class TethysUISwingUtils {
48      /**
49       * Logger.
50       */
51      private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(TethysUISwingUtils.class);
52  
53      /**
54       * Height adjustment for field.
55       */
56      private static final int PADDING_HEIGHT = 4;
57  
58      /**
59       * private constructor.
60       */
61      private TethysUISwingUtils() {
62      }
63  
64      /**
65       * Restrict field.
66       *
67       * @param pComponent the component to restrict
68       * @param pWidth     field width in characters
69       */
70      public static void restrictField(final JComponent pComponent,
71                                       final int pWidth) {
72          /* Calculate the character width */
73          final Font myFont = pComponent.getFont();
74          final FontMetrics myMetrics = pComponent.getFontMetrics(myFont);
75          final int myCharWidth = myMetrics.stringWidth("w");
76          final int myCharHeight = myMetrics.getHeight() + PADDING_HEIGHT;
77  
78          /* Allocate Dimensions */
79          final Dimension myPrefDims = new Dimension(pWidth * myCharWidth, myCharHeight);
80          final Dimension myMaxDims = new Dimension(Integer.MAX_VALUE, myCharHeight);
81          final Dimension myMinDims = new Dimension(1, myCharHeight);
82  
83          /* Restrict the field */
84          pComponent.setPreferredSize(myPrefDims);
85          pComponent.setMaximumSize(myMaxDims);
86          pComponent.setMinimumSize(myMinDims);
87      }
88  
89      /**
90       * create wrapper pane.
91       *
92       * @param pTitle   the title
93       * @param pPadding the padding
94       * @param pNode    the node
95       * @return the new pane
96       */
97      public static JComponent addPanelBorder(final String pTitle,
98                                              final Integer pPadding,
99                                              final JComponent pNode) {
100         if (pPadding == null
101                 && pTitle == null) {
102             return pNode;
103         } else {
104             final JComponent myNode = new JPanel(new BorderLayout());
105             myNode.add(pNode, BorderLayout.CENTER);
106             setPanelBorder(pTitle, pPadding, myNode);
107             return myNode;
108         }
109     }
110 
111     /**
112      * Apply titled and padded borders around panel.
113      *
114      * @param pTitle   the title
115      * @param pPadding the padding
116      * @param pNode    the node
117      */
118     protected static void setPanelBorder(final String pTitle,
119                                          final Integer pPadding,
120                                          final JComponent pNode) {
121         /* Access contents */
122         final boolean hasTitle = pTitle != null;
123         final boolean hasPadding = pPadding != null;
124 
125         /* Create borders */
126         final Border myPaddedBorder = hasPadding
127                 ? BorderFactory.createEmptyBorder(pPadding, pPadding, pPadding, pPadding)
128                 : null;
129         final Border myTitleBorder = hasTitle
130                 ? BorderFactory.createTitledBorder(pTitle)
131                 : null;
132 
133         /* Create relevant border */
134         if (hasPadding) {
135             pNode.setBorder(hasTitle ? BorderFactory.createCompoundBorder(myPaddedBorder, myTitleBorder) : myPaddedBorder);
136         } else {
137             pNode.setBorder(hasTitle ? myTitleBorder : BorderFactory.createEmptyBorder());
138         }
139     }
140 
141     /**
142      * Obtain display point for dialog.
143      *
144      * @param pAnchor   the anchor node
145      * @param pLocation the preferred location relative to node
146      * @param pSize     the size of the dialog
147      * @return the (adjusted) rectangle
148      */
149     public static Point obtainDisplayPoint(final Component pAnchor,
150                                            final Point pLocation,
151                                            final Dimension pSize) {
152         /* First of all determine the display screen for the anchor component */
153         final GraphicsDevice myScreen = getScreenForComponent(pAnchor);
154 
155         /* Next obtain the fully qualified location */
156         final Point myLocation = getLocationForComponent(pAnchor, pLocation);
157 
158         /* determine the display rectangle */
159         Rectangle myArea = new Rectangle(myLocation.x, myLocation.y,
160                 pSize.width, pSize.height);
161         myArea = adjustDisplayLocation(myArea, myScreen);
162 
163         /* Return the location */
164         return new Point(myArea.x, myArea.y);
165     }
166 
167     /**
168      * Obtain display point for dialog.
169      *
170      * @param pAnchor the anchor node
171      * @param pSide   the preferred side to display on
172      * @param pSize   the size of the dialog
173      * @return the (adjusted) rectangle
174      */
175     public static Point obtainDisplayPoint(final Component pAnchor,
176                                            final int pSide,
177                                            final Dimension pSize) {
178         /* First of all determine the display screen for the anchor node */
179         final GraphicsDevice myScreen = getScreenForComponent(pAnchor);
180 
181         /* Next obtain the fully qualified location */
182         final Point myLocation = pAnchor.getLocationOnScreen();
183 
184         /* determine the display rectangle */
185         Rectangle myArea = new Rectangle(myLocation.x, myLocation.y,
186                 pSize.width, pSize.height);
187         myArea = adjustDisplayLocation(myArea, pAnchor.getBounds(), pSide, myScreen);
188 
189         /* Return the location */
190         return new Point(myArea.x, myArea.y);
191     }
192 
193     /**
194      * Obtain display point for dialog.
195      *
196      * @param pAnchor the anchor node
197      * @param pSide   the preferred side to display on
198      * @param pSize   the size of the dialog
199      * @return the (adjusted) rectangle
200      */
201     public static Point obtainDisplayPoint(final Rectangle pAnchor,
202                                            final int pSide,
203                                            final Dimension pSize) {
204         /* First of all determine the display screen for the anchor node */
205         final GraphicsDevice myScreen = getScreenForRectangle(pAnchor);
206 
207         /* determine the display rectangle */
208         Rectangle myArea = new Rectangle(pAnchor.x, pAnchor.y,
209                 pSize.width, pSize.height);
210         myArea = adjustDisplayLocation(myArea, pAnchor.getBounds(), pSide, myScreen);
211 
212         /* Return the location */
213         return new Point(myArea.x, myArea.y);
214     }
215 
216     /**
217      * Obtain the screen that best contains the anchor node.
218      *
219      * @param pAnchor the anchor node.
220      * @return the relevant screen
221      */
222     private static GraphicsDevice getScreenForComponent(final Component pAnchor) {
223         /* Obtain full-qualified origin of node */
224         final Point myOrigin = pAnchor.getLocationOnScreen();
225 
226         /* Build fully-qualified bounds */
227         final Rectangle myLocalBounds = pAnchor.getBounds();
228         final Rectangle myBounds = new Rectangle(myOrigin.x,
229                 myOrigin.y,
230                 myLocalBounds.width,
231                 myLocalBounds.height);
232 
233         /* Look for rectangle */
234         return getScreenForRectangle(myBounds);
235     }
236 
237     /**
238      * Obtain the screen that best contains the rectangle.
239      *
240      * @param pAnchor the anchor node.
241      * @return the relevant screen
242      */
243     private static GraphicsDevice getScreenForRectangle(final Rectangle pAnchor) {
244         /* Access the list of screens */
245         final GraphicsEnvironment myEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
246         final GraphicsDevice[] myDevices = myEnv.getScreenDevices();
247 
248         /* Set values */
249         double myBest = 0;
250         GraphicsDevice myBestDevice = null;
251 
252         /* Look for a device that contains the point */
253         for (final GraphicsDevice myDevice : myDevices) {
254             /* Only deal with screens */
255             if (myDevice.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
256                 /* Access configuration */
257                 final GraphicsConfiguration myConfig = myDevice.getDefaultConfiguration();
258                 final Rectangle myDevBounds = myConfig.getBounds();
259 
260                 /* Calculate intersection and record best */
261                 final double myIntersection = getIntersection(pAnchor, myDevBounds);
262                 if (myIntersection > myBest) {
263                     myBest = myIntersection;
264                     myBestDevice = myDevice;
265                 }
266             }
267         }
268 
269         /* If none found then default to primary */
270         return myBestDevice == null
271                 ? myEnv.getDefaultScreenDevice()
272                 : myBestDevice;
273     }
274 
275     /**
276      * Calculate the intersection of bounds and screen.
277      *
278      * @param pBounds the bounds.
279      * @param pScreen the screen
280      * @return the intersection
281      */
282     private static double getIntersection(final Rectangle pBounds,
283                                           final Rectangle pScreen) {
284 
285         /* Calculate intersection coordinates */
286         final double myMinX = Math.max(pBounds.getMinX(), pScreen.getMinX());
287         final double myMaxX = Math.min(pBounds.getMaxX(), pScreen.getMaxX());
288         final double myMinY = Math.max(pBounds.getMinY(), pScreen.getMinY());
289         final double myMaxY = Math.min(pBounds.getMaxY(), pScreen.getMaxY());
290 
291         /* Calculate intersection lengths */
292         final double myX = Math.max(myMaxX - myMinX, 0);
293         final double myY = Math.max(myMaxY - myMinY, 0);
294 
295         /* Calculate intersection */
296         return myX * myY;
297     }
298 
299     /**
300      * Obtain the fully-qualified node location.
301      *
302      * @param pAnchor   the node.
303      * @param pLocation the location relative to the n
304      * @return the origin
305      */
306     private static Point getLocationForComponent(final Component pAnchor,
307                                                  final Point pLocation) {
308         /* Access node origin */
309         final Point myOrigin = pAnchor.getLocationOnScreen();
310 
311         /* Calculate fully-qualified location */
312         return new Point(myOrigin.x + pLocation.x,
313                 myOrigin.y + pLocation.y);
314     }
315 
316     /**
317      * Adjust display location to fit on screen.
318      *
319      * @param pSource the proposed location
320      * @param pScreen the screen
321      * @return the (adjusted) location
322      */
323     private static Rectangle adjustDisplayLocation(final Rectangle pSource,
324                                                    final GraphicsDevice pScreen) {
325         /* Access Screen bounds */
326         final Rectangle myScreenBounds = pScreen.getDefaultConfiguration().getBounds();
327         double myAdjustX = 0;
328         double myAdjustY = 0;
329 
330         /* Adjust for too far right */
331         if (pSource.getMaxX() > myScreenBounds.getMaxX()) {
332             myAdjustX = myScreenBounds.getMaxX() - pSource.getMaxX();
333         }
334 
335         /* Adjust for too far down */
336         if (pSource.getMaxY() > myScreenBounds.getMaxY()) {
337             myAdjustY = myScreenBounds.getMaxY() - pSource.getMaxY();
338         }
339 
340         /* Adjust for too far left */
341         if (pSource.getMinX() + myAdjustX < myScreenBounds.getMinX()) {
342             myAdjustX = myScreenBounds.getMinX() - pSource.getMinX();
343         }
344 
345         /* Adjust for too far down */
346         if (pSource.getMinY() + myAdjustY < myScreenBounds.getMinY()) {
347             myAdjustY = myScreenBounds.getMinY() - pSource.getMinY();
348         }
349 
350         /* Calculate new rectangle */
351         return (Double.doubleToRawLongBits(myAdjustX) != 0)
352                 || (Double.doubleToRawLongBits(myAdjustY) != 0)
353                 ? new Rectangle((int) (pSource.getX() + myAdjustX),
354                 (int) (pSource.getY() + myAdjustY),
355                 pSource.width,
356                 pSource.height)
357                 : pSource;
358     }
359 
360     /**
361      * Adjust display location to fit at side of node.
362      *
363      * @param pSource the proposed location
364      * @param pAnchor the anchor node
365      * @param pSide   the preferred side to display on
366      * @param pScreen the screen
367      * @return the (adjusted) location
368      */
369     private static Rectangle adjustDisplayLocation(final Rectangle pSource,
370                                                    final Rectangle pAnchor,
371                                                    final int pSide,
372                                                    final GraphicsDevice pScreen) {
373         /* Access Screen bounds */
374         final Rectangle myScreenBounds = pScreen.getDefaultConfiguration().getBounds();
375         double myAdjustX = 0;
376         double myAdjustY = 0;
377 
378         /* Determine initial adjustment */
379         switch (pSide) {
380             case SwingConstants.RIGHT:
381                 myAdjustX = pAnchor.getWidth();
382                 if (pSource.getMaxX() + myAdjustX > myScreenBounds.getMaxX()) {
383                     myAdjustX = -pSource.getWidth();
384                 }
385                 break;
386             case SwingConstants.LEFT:
387                 myAdjustX = -pSource.getWidth();
388                 if (pSource.getMinX() + myAdjustX < myScreenBounds.getMinX()) {
389                     myAdjustX = pAnchor.getWidth();
390                 }
391                 break;
392             case SwingConstants.BOTTOM:
393                 myAdjustY = pAnchor.getHeight();
394                 if (pSource.getMaxY() + myAdjustY > myScreenBounds.getMaxY()) {
395                     myAdjustY = -pSource.getHeight();
396                 }
397                 break;
398             case SwingConstants.TOP:
399                 myAdjustY = -pSource.getHeight();
400                 if (pSource.getMinY() + myAdjustY < myScreenBounds.getMinY()) {
401                     myAdjustY = pAnchor.getHeight();
402                 }
403                 break;
404             default:
405                 break;
406         }
407 
408         /* Calculate new rectangle */
409         final Rectangle myArea = (Double.doubleToRawLongBits(myAdjustX) != 0)
410                 || (Double.doubleToRawLongBits(myAdjustY) != 0)
411                 ? new Rectangle((int) (pSource.getMinX() + myAdjustX),
412                 (int) (pSource.getMinY() + myAdjustY),
413                 pSource.width,
414                 pSource.height)
415                 : pSource;
416         return adjustDisplayLocation(myArea, pScreen);
417     }
418 
419     /**
420      * format a colour as a hexadecimal string.
421      *
422      * @param pValue the long value
423      * @return the string
424      */
425     public static String colorToHexString(final Color pValue) {
426         /* Access the RGB value */
427         int myValue = pValue.getRGB();
428         myValue &= OceanusDataConverter.COLOR_MASK;
429 
430         /* Allocate the string builder */
431         final StringBuilder myBuilder = new StringBuilder();
432 
433         /* While we have digits to format */
434         while (myValue > 0) {
435             /* Access the digit and move to next one */
436             final int myDigit = myValue & OceanusDataConverter.NYBBLE_MASK;
437             final char myChar = Character.forDigit(myDigit, OceanusDataConverter.HEX_RADIX);
438             myBuilder.insert(0, myChar);
439             myValue >>>= OceanusDataConverter.NYBBLE_SHIFT;
440         }
441 
442         /* Add zeros to front if less than 6 digits */
443         while (myBuilder.length() < OceanusDataConverter.RGB_LENGTH) {
444             myBuilder.insert(0, '0');
445         }
446 
447         /* Insert a # sign */
448         myBuilder.insert(0, '#');
449 
450         /* Return the string */
451         return myBuilder.toString();
452     }
453 
454     /**
455      * Obtain the raw icon.
456      *
457      * @param pId the icon Id
458      * @return the icon
459      */
460     public static TethysUISwingIcon getIcon(final TethysUIIconId pId) {
461         try {
462             final ImageIcon myIcon = new ImageIcon(pId.loadResourceToBytes());
463             return new TethysUISwingIcon(myIcon);
464         } catch (IOException e) {
465             LOGGER.error("Failed to load Icon " + pId.getSourceName(), e);
466             return null;
467         }
468     }
469 
470     /**
471      * Obtain raw icons.
472      *
473      * @param pIds the icon Id
474      * @return the icon
475      */
476     public static Image[] getIcons(final TethysUIIconId[] pIds) {
477         final Image[] myIcons = new Image[pIds.length];
478         for (int i = 0; i < pIds.length; i++) {
479             final TethysUISwingIcon myIcon = getIcon(pIds[i]);
480             myIcons[i] = myIcon == null
481                     ? null
482                     : myIcon.getImage();
483         }
484         return myIcons;
485     }
486 
487     /**
488      * Obtain the reSized icon.
489      *
490      * @param pId   the icon Id
491      * @param pSize the new size for the icon
492      * @return the icon
493      */
494     public static TethysUISwingIcon getIconAtSize(final TethysUIIconId pId,
495                                                   final int pSize) {
496         final TethysUISwingIcon mySource = getIcon(pId);
497         if (mySource == null) {
498             return null;
499         }
500         final Image myImage = mySource.getImage();
501         final Image myNewImage = myImage.getScaledInstance(pSize,
502                 pSize,
503                 Image.SCALE_DEFAULT);
504         final ImageIcon myIcon = new ImageIcon(myNewImage);
505         return new TethysUISwingIcon(myIcon);
506     }
507 }
508