View Javadoc
1   package de.spiritscorp.datasync.gui;
2   
3   /*-
4    * 		Data Sync
5    *
6    * 		Copyright ©   2022    The Spirit
7    * 		@email                        thespirit@spiritscorp.network
8    *
9    * 		This program is free software; you can redistribute it and/or modify
10   * 		it under the terms of the GNU General Public License as published by
11   * 		the Free Software Foundation; either version 3 of the License, or
12   * 		(at your option) any later version.
13   *
14   * 		This program is distributed in the hope that it will be useful,
15   * 		but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * 		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17   * 		See the GNU General Public License for more details.
18   *
19   * 		You should have received a copy of the GNU General Public License
20   * 		along with this program. If not, see <http://www.gnu.org/licenses/>.
21   */
22  
23  import java.util.NoSuchElementException;
24  import java.util.Optional;
25  import java.util.concurrent.CompletableFuture;
26  import java.util.concurrent.ExecutionException;
27  
28  import javafx.application.Platform;
29  import javafx.scene.control.Alert;
30  import javafx.scene.control.ButtonType;
31  import javafx.scene.control.Label;
32  import javafx.scene.control.TextInputDialog;
33  import javafx.scene.paint.Paint;
34  import javafx.stage.Stage;
35  
36  import org.kordamp.ikonli.javafx.FontIcon;
37  import org.kordamp.ikonli.materialdesign2.MaterialDesignA;
38  import org.kordamp.ikonli.materialdesign2.MaterialDesignK;
39  
40  import de.spiritscorp.datasync.io.Debug;
41  
42  /**
43   * Service responsible for managing UI dialogs and alerts.
44   * Ensures strict thread safety by bridging background thread calls to the JavaFX Application Thread.
45   *
46   * @author Tom Spirit
47   */
48  public class DialogService {
49  
50  	/** The primary application stage acting as the owner window for modal dialogs. */
51  	private final Stage stage;
52  
53  	/**
54  	 * Constructs a new DialogService bound to a specific primary window stage.
55  	 *
56  	 * @param stage The parent stage container, must not be null
57  	 * @throws NoSuchElementException if the provided stage is null
58  	 */
59  	public DialogService( final Stage stage ) {
60  		this.stage = Optional.ofNullable( stage ).orElseThrow();
61  	}
62  
63  	/**
64  	 * Displays a modal confirmation dialog requesting an OK or Cancel decision from the user.
65  	 * Automatically handles cross-thread invocations without throwing structural exceptions.
66  	 *
67  	 * @param title   The descriptive title of the dialog window
68  	 * @param header  The contextual header text of the notification
69  	 * @param content The message body containing instructions or questions
70  	 * @return true if the user confirmed via OK, false if canceled, dismissed, or closed
71  	 */
72  	public boolean promptOkChancel( final String title, final String header, final String content ) {
73  		return promptConfirmation( title, header, content, true );
74  	}
75  
76  	/**
77  	 * Displays a modal confirmation dialog requesting a Yes or No decision from the user. Automatically handles cross-thread invocations without throwing structural exceptions.
78  	 *
79  	 * @param title   The descriptive title of the dialog window
80  	 * @param header  The contextual header text of the notification
81  	 * @param content The message body containing instructions or questions
82  	 * @return true if the user confirmed via YES, false if denied, dismissed, or closed
83  	 */
84  	public boolean promptYesNo( final String title, final String header, final String content ) {
85  		return promptConfirmation( title, header, content, false );
86  	}
87  
88  	/**
89  	 * Dispatches a modal text input dialog to capture a textual response from the user.
90  	 * This method blocks the calling thread and safely orchestrates thread switches if invoked
91  	 * outside the primary JavaFX Application Thread.
92  	 *
93  	 * @param title   The descriptive title of the dialog window
94  	 * @param header  The contextual header text of the notification
95  	 * @param content The descriptive label text guiding the user input
96  	 * @return The sanitized string captured from the input field, or an empty string if dismissed
97  	 */
98  	public String promptTextInput( final String title, final String header, final String content ) {
99  		// Execute immediately if invoked directly on the JavaFX Application Thread
100 		if( Platform.isFxApplicationThread() ) return showTextDialog( title, header, content );
101 
102 		// Bridge execution synchronously if called from a background worker thread
103 		final CompletableFuture<String> userResponse = new CompletableFuture<>();
104 
105 		Platform.runLater( () -> {
106 			try {
107 				userResponse.complete( showTextDialog( title, header, content ) );
108 			}catch( final IllegalStateException exception ) {
109 				userResponse.completeExceptionally( exception );
110 			}
111 		} );
112 
113 		try {
114 			return userResponse.get();
115 		}catch( InterruptedException _ ) {
116 			Thread.currentThread().interrupt();
117 		}catch( final ExecutionException exception ) {
118 			Debug.printDebug( "[Dialog Service Error] Execution Exception -> ", exception.getMessage() );
119 			Debug.printException( getClass(), exception );
120 		}
121 		return "";
122 	}
123 
124 	/**
125 	 * Internal orchestration engine filtering thread contexts before rendering confirmation alerts.
126 	 * Synchronously locks background tasks via futures until human interaction concludes.
127 	 *
128 	 * @param title        The descriptive title of the dialog window
129 	 * @param header       The contextual header text of the notification
130 	 * @param content      The message body containing instructions or questions
131 	 * @param buttonTypeOk If true, initializes OK/CANCEL buttons; if false, initializes YES/NO options.
132 	 * @return true if an affirmative action was captured, false otherwise
133 	 */
134 	private boolean promptConfirmation( final String title, final String header, final String content, final boolean buttonTypeOk ) {
135 		// Execute immediately if invoked directly on the JavaFX Application Thread
136 		if( Platform.isFxApplicationThread() ) {
137 			return buttonTypeOk ? showConfirmationDialog( title, header, content, ButtonType.OK, ButtonType.CANCEL ) : showConfirmationDialog( title, header, content, ButtonType.YES, ButtonType.NO );
138 		}
139 
140 		// Bridge execution synchronously if called from a background worker thread
141 		final CompletableFuture<Boolean> userResponse = new CompletableFuture<>();
142 
143 		Platform.runLater( () -> {
144 			try {
145 				final boolean response = buttonTypeOk ? showConfirmationDialog( title, header, content, ButtonType.OK, ButtonType.CANCEL )
146 						: showConfirmationDialog( title, header, content, ButtonType.YES, ButtonType.NO );
147 				userResponse.complete( response );
148 			}catch( final IllegalStateException exception ) {
149 				userResponse.completeExceptionally( exception );
150 			}
151 		} );
152 
153 		try {
154 			return userResponse.get(); // Halts the background worker here until the user interacts with the UI
155 		}catch( InterruptedException _ ) {
156 			Thread.currentThread().interrupt();
157 		}catch( final ExecutionException exception ) {
158 			Debug.printDebug( "[Dialog Service Error] Execution Exception -> ", exception.getMessage() );
159 			Debug.printException( getClass(), exception );
160 		}
161 		return false;
162 	}
163 
164 	/**
165 	 * Builds and visualizes the native JavaFX Alert window component. This operation must strictly execute inside the boundaries of the FX core thread.
166 	 *
167 	 * @param title    The descriptive title of the dialog window.
168 	 * @param header   The contextual header text of the notification.
169 	 * @param content  The message body containing instructions or questions.
170 	 * @param buttType A variable argument array of button types used to dynamically populate the interface.
171 	 * @throws IllegalStateException if invoked outside the JavaFX Application Thread.
172 	 * @return true if an affirmative action (OK/YES) was captured, false otherwise.
173 	 */
174 	private boolean showConfirmationDialog( final String title, final String header, final String content, final ButtonType... buttType ) {
175 		final FontIcon icon = Gui.createIcon( MaterialDesignA.ALERT );
176 		icon.setIconSize( 50 );
177 		icon.setIconColor( Paint.valueOf( "blue" ) );
178 		icon.getStyleClass().add( "dialog-custom-icon" );
179 		final Label contentLabel = new Label( content );
180 		contentLabel.setPrefWidth( 450 );
181 		contentLabel.setWrapText( true );
182 		final Alert confirmation = new Alert( Alert.AlertType.CONFIRMATION );
183 		confirmation.setTitle( title );
184 		confirmation.setGraphic( icon );
185 		confirmation.setHeaderText( header );
186 		confirmation.getDialogPane().setContent( contentLabel );
187 		confirmation.getButtonTypes().setAll( buttType );
188 		confirmation.initOwner( stage );
189 		final ButtonType result = confirmation.showAndWait().orElse( ButtonType.CANCEL );
190 		return result == ButtonType.OK || result == ButtonType.YES;
191 	}
192 
193 	/**
194 	 * Low-level helper to instantiate and display the native JavaFX TextInputDialog.
195 	 * Combines the custom wrapped text node with the internal input control into a synchronized layout container.
196 	 *
197 	 * @param title   The descriptive title of the dialog window
198 	 * @param header  The contextual header text of the notification
199 	 * @param content The descriptive label text guiding the user input
200 	 * @throws IllegalStateException if invoked outside the JavaFX Application Thread.
201 	 * @return The trimmed user input string, or an empty string if aborted
202 	 */
203 	private String showTextDialog( final String title, final String header, final String content ) {
204 		final FontIcon icon = Gui.createIcon( MaterialDesignK.KEYBOARD_OUTLINE );
205 		icon.setIconSize( 50 );
206 		icon.setIconColor( Paint.valueOf( "blue" ) );
207 		icon.getStyleClass().add( "dialog-custom-icon" );
208 		final TextInputDialog dialog = new TextInputDialog();
209 		dialog.setGraphic( icon );
210 		dialog.setTitle( title );
211 		dialog.setHeaderText( header );
212 		dialog.setContentText( content );
213 		dialog.initOwner( stage );
214 		return dialog.showAndWait().orElse( "" ).trim();
215 	}
216 }