UIForAnyWindow.java

  1. package swingtree;

  2. import org.jspecify.annotations.Nullable;
  3. import sprouts.Val;
  4. import swingtree.input.Keyboard;

  5. import javax.swing.*;
  6. import java.awt.Component;
  7. import java.awt.Window;
  8. import java.awt.event.*;
  9. import java.util.Optional;
  10. import java.util.function.Consumer;

  11. /**
  12.  *  A SwingTree builder node for configuring any kind of {@link Window} type.
  13.  *  Take a look at the {@link UIForJDialog} and {@link UIForJFrame} classes,
  14.  *  which are specialized subtypes of this class.
  15.  *
  16.  * @param <I> The type of the builder itself.
  17.  * @param <W> The type of the window which is being configured by this builder.
  18.  */
  19. public abstract class UIForAnyWindow<I extends UIForAnyWindow<I,W>, W extends Window> extends UIForAnything<I,W,Component>
  20. {
  21.     private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UIForAnyWindow.class);

  22.     /**
  23.      *  Adds a title to the window. <br>
  24.      *  Note that the way this is displayed depends on the window type and the
  25.      *  operating system.
  26.      *
  27.      * @param title The title to be shown in the top bar of the window.
  28.      * @return This builder.
  29.      */
  30.     public final I withTitle( String title ) {
  31.         return _with( thisWindow -> {
  32.                     _setTitleOf( thisWindow, title );
  33.                })
  34.                ._this();
  35.     }

  36.     /**
  37.      *  Binds a text property to the window determining the title displayed in the top bar of the window. <br>
  38.      *  Note that the way this is displayed depends on the window type and the
  39.      *  operating system.
  40.      *
  41.      * @param title The title property whose text will be shown in the top bar of the window.
  42.      * @return This builder.
  43.      */
  44.     public final I withTitle( Val<String> title ) {
  45.         NullUtil.nullArgCheck(title, "title", Val.class);
  46.         NullUtil.nullPropertyCheck(title, "title");
  47.         return _withOnShow( title, (thisWindow,v) -> {
  48.                    _setTitleOf(thisWindow, v);
  49.                })
  50.                ._with( thisWindow -> {
  51.                    _setTitleOf( thisWindow, title.orElseThrowUnchecked() );
  52.                })
  53.                ._this();
  54.     }

  55.     /**
  56.      *  Sets the {@link UI.OnWindowClose} operation for the window. <br>
  57.      *  This translates to {@link JFrame#setDefaultCloseOperation(int)} or
  58.      *  {@link JDialog#setDefaultCloseOperation(int)} depending on the window type.
  59.      *  The following operations are supported:
  60.      *  <ul>
  61.      *      <li>{@link UI.OnWindowClose#DO_NOTHING} - Do nothing when the window is closed.</li>
  62.      *      <li>{@link UI.OnWindowClose#HIDE} - Hide the window when it is closed.</li>
  63.      *      <li>{@link UI.OnWindowClose#DISPOSE} - Dispose the window when it is closed.</li>
  64.      *  </ul>
  65.      * @param onClose The operation to be executed when the window is closed.
  66.      * @return This declarative builder instance to enable method chaining.
  67.      */
  68.     public final I withOnCloseOperation(UI.OnWindowClose onClose ) {
  69.         NullUtil.nullArgCheck(onClose, "onClose", UI.OnWindowClose.class);
  70.         return _with( thisWindow -> {
  71.                     if ( thisWindow instanceof JFrame )
  72.                         ((JFrame)thisWindow).setDefaultCloseOperation(onClose.forSwing());
  73.                     else if ( thisWindow instanceof JDialog )
  74.                         ((JDialog)thisWindow).setDefaultCloseOperation(onClose.forSwing());
  75.                     else
  76.                         log.warn("Cannot set close operation on window of type: {}", thisWindow.getClass().getName());
  77.                })
  78.                ._this();
  79.     }

  80.     /**
  81.      *  Makes the window visible in the center of the screen.
  82.      */
  83.     public abstract void show();

  84.     protected abstract Optional<JRootPane> _getRootPaneOf(W thisWindow);

  85.     protected abstract void _setTitleOf( W thisWindow, String title );

  86.     private void _onKeyStroke( int code, Consumer<ActionEvent> action, W thisWindow ) {
  87.         _getRootPaneOf(thisWindow).ifPresent(rootPane -> {
  88.             KeyStroke k = KeyStroke.getKeyStroke(code, 0);
  89.             int w = JComponent.WHEN_IN_FOCUSED_WINDOW;
  90.             rootPane.registerKeyboardAction(action::accept, k, w);
  91.         });
  92.     }

  93.     /**
  94.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link KeyListener} to the component,
  95.      * to receive key events triggered when the wrapped component receives a particular
  96.      * keyboard input matching the provided {@link swingtree.input.Keyboard.Key}.
  97.      * <br><br>
  98.      * @param key The {@link swingtree.input.Keyboard.Key} which should be matched to the key event.
  99.      * @param onKeyPressed The {@link sprouts.Action} which will be executed once the wrapped component received the targeted key press.
  100.      * @return This very instance, which enables builder-style method chaining.
  101.      */
  102.     public final I onPressed( Keyboard.Key key, sprouts.Action<WindowDelegate<W, ActionEvent>> onKeyPressed ) {
  103.         NullUtil.nullArgCheck(key, "key", Keyboard.Key.class);
  104.         NullUtil.nullArgCheck(onKeyPressed, "onKeyPressed", sprouts.Action.class);
  105.         return _with( thisWindow -> {
  106.                     _onKeyStroke( key.code, e -> {
  107.                         try {
  108.                             onKeyPressed.accept(_createDelegate(thisWindow, null));
  109.                         } catch (Exception ex) {
  110.                             log.error("Error occurred while processing key press event.", ex);
  111.                         }
  112.                     }, thisWindow );
  113.                })
  114.                ._this();
  115.     }

  116.     /**
  117.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.FocusListener}
  118.      * to the component, to receive those focus events where the wrapped component gains input focus.
  119.      *
  120.      * @param onFocus The {@link sprouts.Action} which should be executed once the input focus was gained on the wrapped component.
  121.      * @return This very instance, which enables builder-style method chaining.
  122.      */
  123.     public final I onFocusGain( sprouts.Action<WindowDelegate<W, FocusEvent>> onFocus ) {
  124.         NullUtil.nullArgCheck(onFocus, "onFocus", sprouts.Action.class);
  125.         return _with( thisWindow -> {
  126.                     thisWindow.addFocusListener(new FocusAdapter() {
  127.                         @Override public void focusGained(FocusEvent e) {
  128.                             _runInApp(()->{
  129.                                 try {
  130.                                     onFocus.accept(_createDelegate(thisWindow, e));
  131.                                 } catch (Exception ex) {
  132.                                     log.error("Error occurred while processing focus gain event.", ex);
  133.                                 }
  134.                             });
  135.                         }
  136.                     });
  137.                })
  138.                ._this();
  139.     }

  140.     /**
  141.      * Adds the supplied {@link sprouts.Action} wrapped in a focus listener
  142.      * to receive those focus events where the wrapped component loses input focus.
  143.      *
  144.      * @param onFocus The {@link sprouts.Action} which should be executed once the input focus was lost on the wrapped component.
  145.      * @return This very instance, which enables builder-style method chaining.
  146.      */
  147.     public final I onFocusLoss( sprouts.Action<WindowDelegate<W, FocusEvent>> onFocus ) {
  148.         NullUtil.nullArgCheck(onFocus, "onFocus", Action.class);
  149.         return _with( thisWindow -> {
  150.                     thisWindow.addFocusListener(new FocusAdapter() {
  151.                         @Override public void focusLost(FocusEvent e) {
  152.                             _runInApp(()->{
  153.                                 try {
  154.                                     onFocus.accept(_createDelegate(thisWindow, e));
  155.                                 } catch (Exception ex) {
  156.                                     log.error("Error occurred while processing focus loss event.", ex);
  157.                                 }
  158.                             });
  159.                         }
  160.                     });
  161.                })
  162.                ._this();
  163.     }

  164.     /**
  165.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  166.      * to the component, to receive {@link WindowListener#windowClosing(WindowEvent)} events
  167.      * which are invoked when a window is in the process of being closed.
  168.      * The close operation can be overridden at this point (see {@link JFrame#DO_NOTHING_ON_CLOSE}). <br>
  169.      * Note that this kind of event is typically triggered when the user clicks
  170.      * the close button in the top bar of the window.
  171.      *
  172.      * @param onClose The {@link sprouts.Action} which should be invoked when the wrapped component is in the process of being closed.
  173.      * @return This very instance, which enables builder-style method chaining.
  174.      */
  175.     public final I onClose( sprouts.Action<WindowDelegate<W, WindowEvent>> onClose ) {
  176.         NullUtil.nullArgCheck(onClose, "onClose", Action.class);
  177.         return _with( thisWindow -> {
  178.                     thisWindow.addWindowListener(new WindowAdapter() {
  179.                         @Override public void windowClosing( WindowEvent e ) {
  180.                             _runInApp(()->{
  181.                                 try {
  182.                                     onClose.accept(_createDelegate(thisWindow, e));
  183.                                 } catch (Exception ex) {
  184.                                     log.error("Error occurred while processing window closing event.", ex);
  185.                                 }
  186.                             });
  187.                         }
  188.                     });
  189.                })
  190.                ._this();
  191.     }

  192.     /**
  193.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  194.      * to the component, to receive {@link WindowListener#windowClosed(WindowEvent)} events
  195.      * which are invoked when a window has been closed. <br>
  196.      * Note that this kind of event is typically triggered when the user clicks
  197.      * the close button in the top bar of the window.
  198.      *
  199.      * @param onClose The {@link sprouts.Action} which should be invoked when the wrapped component has been closed.
  200.      * @return This very instance, which enables builder-style method chaining.
  201.      */
  202.     public final I onClosed( sprouts.Action<WindowDelegate<W, WindowEvent>> onClose ) {
  203.         NullUtil.nullArgCheck(onClose, "onClose", Action.class);
  204.         return _with( thisWindow -> {
  205.                     thisWindow.addWindowListener(new WindowAdapter() {
  206.                         @Override public void windowClosed( WindowEvent e ) {
  207.                             _runInApp(()->{
  208.                                 try {
  209.                                     onClose.accept(_createDelegate(thisWindow, e));
  210.                                 } catch (Exception ex) {
  211.                                     log.error("Error occurred while processing window closed event.", ex);
  212.                                 }
  213.                             });
  214.                         }
  215.                     });
  216.                })
  217.                ._this();
  218.     }

  219.     /**
  220.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  221.      * to the component, to receive {@link WindowListener#windowOpened(WindowEvent)} events
  222.      * which are invoked when a window has been opened. <br>
  223.      * Note that this kind of event is typically triggered when the user clicks
  224.      * the close button in the top bar of the window.
  225.      *
  226.      * @param onOpen The {@link sprouts.Action} which should be invoked when the wrapped component has been opened.
  227.      * @return This very instance, which enables builder-style method chaining.
  228.      */
  229.     public final I onOpened( sprouts.Action<WindowDelegate<W, WindowEvent>> onOpen ) {
  230.         NullUtil.nullArgCheck(onOpen, "onOpen", Action.class);
  231.         return _with( thisWindow -> {
  232.                     thisWindow.addWindowListener(new WindowAdapter() {
  233.                         @Override public void windowOpened( WindowEvent e ) {
  234.                             _runInApp(()->{
  235.                                 try {
  236.                                     onOpen.accept(_createDelegate(thisWindow, e));
  237.                                 } catch (Exception ex) {
  238.                                     log.error("Error occurred while processing window opened event.", ex);
  239.                                 }
  240.                             });
  241.                         }
  242.                     });
  243.                })
  244.                ._this();
  245.     }

  246.     /**
  247.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  248.      * to the component, to receive {@link WindowListener#windowIconified(WindowEvent)} events
  249.      * which are invoked when a window is changed from a normal to a minimized state.
  250.      * For many platforms, a minimized window is displayed as the icon
  251.      * specified in the window's iconImage property.
  252.      * <br>
  253.      * Minification is usually triggered when the user clicks the minimize button
  254.      * in the top bar of the window. But this depends on the operating system.
  255.      *
  256.      * @param onIconify The {@link sprouts.Action} which should be invoked when the wrapped component has been iconified.
  257.      * @return This very instance, which enables builder-style method chaining.
  258.      */
  259.     public final I onIconified( sprouts.Action<WindowDelegate<W, WindowEvent>> onIconify ) {
  260.         NullUtil.nullArgCheck(onIconify, "onIconify", Action.class);
  261.         return _with( thisWindow -> {
  262.                     thisWindow.addWindowListener(new WindowAdapter() {
  263.                         @Override public void windowIconified( WindowEvent e ) {
  264.                             _runInApp(()->{
  265.                                 try {
  266.                                     onIconify.accept(_createDelegate(thisWindow, e));
  267.                                 } catch (Exception ex) {
  268.                                     log.error("Error occurred while processing window iconified event.", ex);
  269.                                 }
  270.                             });
  271.                         }
  272.                     });
  273.                })
  274.                ._this();
  275.     }

  276.     /**
  277.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  278.      * to the component, to receive {@link WindowListener#windowDeiconified(WindowEvent)} events
  279.      * which are invoked when a window is changed from a minimized
  280.      * to a normal state, usually by the user restoring it from the task bar.
  281.      *
  282.      * @param onDeiconify The {@link sprouts.Action} which should be invoked when the wrapped component has been deiconified.
  283.      * @return This very instance, which enables builder-style method chaining.
  284.      */
  285.     public final I onDeiconified( sprouts.Action<WindowDelegate<W, WindowEvent>> onDeiconify ) {
  286.         NullUtil.nullArgCheck(onDeiconify, "onDeiconify", Action.class);
  287.         return _with( thisWindow -> {
  288.                     thisWindow.addWindowListener(new WindowAdapter() {
  289.                         @Override public void windowDeiconified( WindowEvent e ) {
  290.                             _runInApp(()->{
  291.                                 try {
  292.                                     onDeiconify.accept(_createDelegate(thisWindow, e));
  293.                                 } catch (Exception ex) {
  294.                                     log.error("Error occurred while processing window deiconified event.", ex);
  295.                                 }
  296.                             });
  297.                         }
  298.                     });
  299.                })
  300.                ._this();
  301.     }

  302.     /**
  303.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  304.      * to the component, to receive {@link WindowListener#windowActivated(WindowEvent)} events
  305.      * which are invoked when the Window is set to be the active Window.
  306.      * Only a Frame or a Dialog can be the active Window.
  307.      * The native windowing system may denote the active Window or
  308.      * its children with special decorations, such as a highlighted title bar.
  309.      * The active Window is always either the focused Window,
  310.      * or the first Frame or Dialog that is an owner of the focused Window.
  311.      * So this kind of event is usually triggered when the user makes the window active
  312.      * by clicking it.
  313.      *
  314.      * @param onActivate The {@link sprouts.Action} which should be invoked when the wrapped component has been activated.
  315.      * @return This very instance, which enables builder-style method chaining.
  316.      */
  317.     public final I onActivated( sprouts.Action<WindowDelegate<W, WindowEvent>> onActivate ) {
  318.         NullUtil.nullArgCheck(onActivate, "onActivate", Action.class);
  319.         return _with( thisWindow -> {
  320.                     thisWindow.addWindowListener(new WindowAdapter() {
  321.                         @Override public void windowActivated( WindowEvent e ) {
  322.                             _runInApp(()->{
  323.                                 try {
  324.                                     onActivate.accept(_createDelegate(thisWindow, e));
  325.                                 } catch (Exception ex) {
  326.                                     log.error("Error occurred while processing window activated event.", ex);
  327.                                 }
  328.                             });
  329.                         }
  330.                     });
  331.                })
  332.                ._this();
  333.     }

  334.     /**
  335.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  336.      * to the component, to receive {@link WindowListener#windowDeactivated(WindowEvent)} events
  337.      * which are invoked when a Window is no longer the active Window. Only a Frame or a
  338.      * Dialog can be the active Window. The native windowing system may denote
  339.      * the active Window or its children with special decorations, such as a
  340.      * highlighted title bar. The active Window is always either the focused
  341.      * Window, or the first Frame or Dialog that is an owner of the focused
  342.      * Window.
  343.      * This kind of event typically occurs when the user clicks another window
  344.      * in the task bar of the operating system.
  345.      *
  346.      * @param onDeactivate The {@link sprouts.Action} which should be invoked when the wrapped component has been deactivated.
  347.      * @return This very instance, which enables builder-style method chaining.
  348.      */
  349.     public final I onDeactivated( sprouts.Action<WindowDelegate<W, WindowEvent>> onDeactivate ) {
  350.         NullUtil.nullArgCheck(onDeactivate, "onDeactivate", Action.class);
  351.         return _with( thisWindow -> {
  352.                     thisWindow.addWindowListener(new WindowAdapter() {
  353.                         @Override public void windowDeactivated( WindowEvent e ) {
  354.                             _runInApp(()->{
  355.                                 try {
  356.                                     onDeactivate.accept(_createDelegate(thisWindow, e));
  357.                                 } catch (Exception ex) {
  358.                                     log.error("Error occurred while processing window deactivated event.", ex);
  359.                                 }
  360.                             });
  361.                         }
  362.                     });
  363.                })
  364.                ._this();
  365.     }

  366.     /**
  367.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  368.      * to the component, to receive {@link WindowStateListener#windowStateChanged(WindowEvent)} events
  369.      * which are invoked when a window has been changed. <br>
  370.      * Note that this kind of event is typically invoked when the window is
  371.      * iconified, minimized, maximized or restored.
  372.      *
  373.      * @param onStateChanged The {@link sprouts.Action} which should be invoked when the wrapped component has been changed.
  374.      * @return This very instance, which enables builder-style method chaining.
  375.      */
  376.     public final I onStateChanged( sprouts.Action<WindowDelegate<W, WindowEvent>> onStateChanged ) {
  377.         NullUtil.nullArgCheck(onStateChanged, "onStateChanged", Action.class);
  378.         return _with( thisWindow -> {
  379.                     thisWindow.addWindowListener(new WindowAdapter() {
  380.                         @Override public void windowStateChanged( WindowEvent e ) {
  381.                             _runInApp(()->{
  382.                                 try {
  383.                                     onStateChanged.accept(_createDelegate(thisWindow, e));
  384.                                 } catch (Exception ex) {
  385.                                     log.error("Error occurred while processing window state changed event.", ex);
  386.                                 }
  387.                             });
  388.                         }
  389.                     });
  390.                })
  391.                ._this();
  392.     }

  393.     /**
  394.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  395.      * to the component, to receive {@link WindowFocusListener#windowGainedFocus(WindowEvent)} events
  396.      * which are invoked when the window is set to be gaining input focus, which means
  397.      * that the Window, or one of its subcomponents, will receive keyboard
  398.      * events.
  399.      * This event is typically triggered when the user clicks the window.
  400.      *
  401.      * @param onFocusGained The {@link sprouts.Action} which should be invoked when the wrapped component has gained input focus.
  402.      * @return This very instance, which enables builder-style method chaining.
  403.      */
  404.     public final I onInputFocusGained( sprouts.Action<WindowDelegate<W, WindowEvent>> onFocusGained ) {
  405.         NullUtil.nullArgCheck(onFocusGained, "onFocusGained", Action.class);
  406.         return _with( thisWindow -> {
  407.                     thisWindow.addWindowFocusListener(new WindowFocusListener() {
  408.                         @Override public void windowGainedFocus( WindowEvent e ) {
  409.                             _runInApp(()->{
  410.                                 try {
  411.                                     onFocusGained.accept(_createDelegate(thisWindow, e));
  412.                                 } catch (Exception ex) {
  413.                                     log.error("Error occurred while processing focus gain event.", ex);
  414.                                 }
  415.                             });
  416.                         }
  417.                         @Override public void windowLostFocus( WindowEvent e ) {}
  418.                     });
  419.                })
  420.                ._this();
  421.     }

  422.     /**
  423.      * Adds the supplied {@link sprouts.Action} wrapped in a {@link java.awt.event.WindowListener}
  424.      * to the component, to receive {@link WindowFocusListener#windowLostFocus(WindowEvent)} events
  425.      * which are invoked when the window is set to be losing input focus, which means
  426.      * that input focus is being transferred to another Window or no Window at all and
  427.      * that keyboard events will no longer be delivered to the Window or any of
  428.      * its subcomponents.
  429.      *
  430.      * @param onFocusLost The {@link sprouts.Action} which should be invoked when the wrapped component has lost input focus.
  431.      * @return This very instance, which enables builder-style method chaining.
  432.      */
  433.     public final I onInputFocusLost( sprouts.Action<WindowDelegate<W, WindowEvent>> onFocusLost ) {
  434.         NullUtil.nullArgCheck(onFocusLost, "onFocusLost", Action.class);
  435.         return _with( thisWindow -> {
  436.                     thisWindow.addWindowFocusListener(new WindowFocusListener() {
  437.                         @Override public void windowGainedFocus( WindowEvent e ) {}
  438.                         @Override public void windowLostFocus( WindowEvent e ) {
  439.                             _runInApp(()->{
  440.                                 try {
  441.                                     onFocusLost.accept(_createDelegate(thisWindow, e));
  442.                                 } catch (Exception ex) {
  443.                                     log.error("Error occurred while processing focus loss event.", ex);
  444.                                 }
  445.                             });
  446.                         }
  447.                     });
  448.                })
  449.                ._this();
  450.     }

  451.     private <E> WindowDelegate<W, E> _createDelegate( W window, @Nullable E event ) {
  452.         return new WindowDelegate<W, E>() {
  453.             @Override public W get() { return window; }
  454.             @Override public @Nullable E getEvent() { return event; }
  455.         };
  456.     }

  457. }