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 }