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.Arrays;
24  import java.util.List;
25  
26  import javafx.animation.KeyFrame;
27  import javafx.animation.Timeline;
28  import javafx.beans.binding.Bindings;
29  import javafx.geometry.Insets;
30  import javafx.geometry.Pos;
31  import javafx.scene.Node;
32  import javafx.scene.control.Button;
33  import javafx.scene.control.Label;
34  import javafx.scene.control.ProgressBar;
35  import javafx.scene.control.ProgressIndicator;
36  import javafx.scene.control.ScrollPane;
37  import javafx.scene.control.TableColumn;
38  import javafx.scene.control.TableView;
39  import javafx.scene.control.TextArea;
40  import javafx.scene.control.Tooltip;
41  import javafx.scene.control.cell.CheckBoxTableCell;
42  import javafx.scene.layout.HBox;
43  import javafx.scene.layout.Priority;
44  import javafx.scene.layout.StackPane;
45  import javafx.scene.layout.VBox;
46  import javafx.util.Duration;
47  
48  import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
49  import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
50  import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
51  
52  import de.spiritscorp.datasync.ScanType;
53  import de.spiritscorp.datasync.controller.SyncJobContext;
54  import de.spiritscorp.datasync.controller.ViewController;
55  import de.spiritscorp.datasync.io.Preference;
56  
57  /**
58   * Display workspace panel hosting the interactive operational consoles,
59   * data lists, detailed execution metadata bars and the dynamic target settings configurations grid.
60   *
61   * @author Tom Spirit
62   */
63  final class WorkspaceView extends VBox {
64  
65  	/** The central user interface anchor context managing stage overlays. */
66  	private final Gui mainGui;
67  	/** The action routing controller handling state transitions and business logic. */
68  	private final ViewController controller;
69  	/** The configuration layout coordinator assembling parameter option controls. */
70  	private final SettingsGrid settingsGrid;
71  	/** The visual heading label indicating the active workspace scope. */
72  	private final Label wrkspcHeaderLabel;
73  	/** The contextual metadata label displaying active task descriptions. */
74  	private final Label contextInfoLabel;
75  	/** The top layout container holding execution toolbar action elements. */
76  	private final HBox controlToolbar;
77  	/** The primary content viewport switching between execution outputs. */
78  	private final StackPane centerViewport;
79  
80  	/** The scrollable container framing the text-based console output. */
81  	private final ScrollPane consoleViewNode;
82  	/** The textual log stream output region rendering live terminal logs. */
83  	private final TextArea consoleTextArea;
84  	/** The structural layout pane wrapping the duplicate file assessment grid. */
85  	private final VBox duplicateViewNode;
86  	/** The tabular data viewer presenting matching duplicate path records. */
87  	private TableView<SyncJobContext.FileRow> duplicateTable;
88  
89  	/** The execution trigger button initiating chosen synchronization flows. */
90  	private final Button actionButton;
91  	/** The termination trigger button requesting active task cancellations. */
92  	private final Button cancelButton;
93  	/** The destructive cleanup button executing selected file pruning routines. */
94  	private Button deleteButton;
95  	/** The quantitative progress bar tracking transaction completion ratios. */
96  	private final ProgressBar progressBar;
97  	/** The status message label summarizing system operations in real-time. */
98  	private final Label statusLabel;
99  
100 	/**
101 	 * Prepares layouts and maps operations targets onto implementation controller.
102 	 *
103 	 * @param mainGui    Configuration context core coordinator link.
104 	 * @param controller Strategy abstraction dealing with interface state management mutations.
105 	 */
106 	WorkspaceView( final Gui mainGui, final ViewController controller ) {
107 		this(
108 				mainGui,
109 				controller,
110 				new SettingsGrid( controller, mainGui.getWindowStage(), new ContextPathRenderer() ) );
111 	}
112 
113 	/**
114 	 * For TESTING
115 	 * <br>
116 	 * Prepares layouts and maps operations targets onto implementation controller.
117 	 *
118 	 * @param mainGui      Configuration context core coordinator link.
119 	 * @param controller   Strategy abstraction dealing with interface state management mutations.
120 	 * @param settingsGrid The layout factory engine responsible for parameter control rendering.
121 	 */
122 	WorkspaceView( final Gui mainGui, final ViewController controller, final SettingsGrid settingsGrid ) {
123 		super();
124 		this.mainGui = mainGui;
125 		this.controller = controller;
126 		this.settingsGrid = settingsGrid;
127 		this.setPadding( new Insets( 24 ) );
128 		this.setSpacing( 12 );
129 
130 		wrkspcHeaderLabel = new Label( "Kein Task aktiv" );
131 		wrkspcHeaderLabel.getStyleClass().addAll( "workspace-header-label" );
132 
133 		// Subtitle dynamic information bar containing directories context mapping
134 		contextInfoLabel = new Label( "" );
135 		contextInfoLabel.getStyleClass().addAll( "context-info-label" );
136 
137 		controlToolbar = new HBox( 12 );
138 		controlToolbar.setAlignment( Pos.CENTER_LEFT );
139 
140 		actionButton = new Button( "Ausführen", Gui.createIcon( MaterialDesignP.PLAY ) );
141 		actionButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
142 		actionButton.getStyleClass().addAll( "action-button" );
143 		actionButton.setTooltip( new Tooltip( "Starte Job" ) );
144 		cancelButton = new Button( "Abbrechen", Gui.createIcon( MaterialDesignS.STOP ) );
145 		cancelButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
146 		cancelButton.getStyleClass().addAll( "cancel-button" );
147 		cancelButton.setTooltip( new Tooltip( "Stoppe Job" ) );
148 
149 		controlToolbar.getChildren().addAll( actionButton, cancelButton );
150 
151 		consoleTextArea = new TextArea();
152 		consoleTextArea.setEditable( false );
153 		consoleTextArea.getStyleClass().addAll( "console-text-area" );
154 		consoleViewNode = new ScrollPane( consoleTextArea );
155 		consoleViewNode.setFitToWidth( true );
156 		consoleViewNode.setFitToHeight( true );
157 
158 		duplicateViewNode = assembleDuplicateTableView();
159 		centerViewport = new StackPane( consoleViewNode );
160 
161 		final HBox statusFooter = new HBox( 12 );
162 		statusFooter.setAlignment( Pos.CENTER_LEFT );
163 		progressBar = new ProgressBar( 0 );
164 		progressBar.setTooltip( new Tooltip( "Aktueller Fortschritt" ) );
165 		statusLabel = new Label( "Bereit" );
166 		statusLabel.setTooltip( new Tooltip( "Aktueller Status" ) );
167 		statusFooter.getChildren().addAll( progressBar, statusLabel );
168 
169 		this.getChildren().addAll( wrkspcHeaderLabel, contextInfoLabel, controlToolbar, centerViewport, statusFooter );
170 		setVgrow( centerViewport, Priority.ALWAYS );
171 	}
172 
173 	/**
174 	 * Builds standard layout configuration frame for processing double files arrays.
175 	 */
176 	private VBox assembleDuplicateTableView() {
177 		duplicateTable = new TableView<>();
178 		duplicateTable.setEditable( true );
179 		duplicateTable.setFixedCellSize( 24.0 );
180 
181 		final TableColumn<SyncJobContext.FileRow, Boolean> selCol = new TableColumn<>( "Auswahl" );
182 		selCol.setCellValueFactory( d -> d.getValue().selectedProperty() );
183 		selCol.setCellFactory( CheckBoxTableCell.forTableColumn( selCol ) );
184 		selCol.setPrefWidth( 100 );
185 
186 		final TableColumn<SyncJobContext.FileRow, String> nameCol = new TableColumn<>( "Dateiname" );
187 		nameCol.setCellValueFactory( d -> d.getValue().fileNameProperty() );
188 		nameCol.setPrefWidth( 250 );
189 
190 		final TableColumn<SyncJobContext.FileRow, String> sizeCol = new TableColumn<>( "Größe" );
191 		sizeCol.setCellValueFactory( d -> d.getValue().sizeProperty() );
192 		sizeCol.setPrefWidth( 250 );
193 
194 		final TableColumn<SyncJobContext.FileRow, String> pathCol = new TableColumn<>( "Pfad" );
195 		pathCol.setCellValueFactory( d -> d.getValue().pathProperty() );
196 		pathCol.setPrefWidth( 600 );
197 
198 		final TableColumn<SyncJobContext.FileRow, String> hashCol = new TableColumn<>( "Hash" );
199 		hashCol.setCellValueFactory( d -> d.getValue().hashProperty() );
200 		hashCol.setPrefWidth( 250 );
201 
202 		duplicateTable.getColumns().addAll( List.of( selCol, nameCol, sizeCol, hashCol, pathCol ) );
203 		deleteButton = new Button( "Duplikate löschen", Gui.createIcon( MaterialDesignD.DELETE ) );
204 		deleteButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
205 		deleteButton.getStyleClass().addAll( "delete-button" );
206 		deleteButton.setTooltip( new Tooltip( "Ausgewählte Dateien werden gelöscht" ) );
207 
208 		final VBox frame = new VBox( 8, duplicateTable, deleteButton );
209 		setVgrow( duplicateTable, Priority.ALWAYS );
210 		return frame;
211 	}
212 
213 	/**
214 	 * Redraws visible frame items based on routing navigation instructions and current job payload state.
215 	 *
216 	 * @param state The target navigation ViewState.
217 	 * @param job   The selected target sync context model instance.
218 	 */
219 	void refreshView( final Gui.ViewState state, final SyncJobContext job ) {
220 
221 		centerViewport.getChildren().clear();
222 		contextInfoLabel.setTooltip( null );
223 
224 		if( state == Gui.ViewState.INFO ) {
225 			wrkspcHeaderLabel.setText( "About" );
226 			contextInfoLabel.setText( "Backup Software" );
227 			controlToolbar.setVisible( false );
228 			return;
229 		}else if( job == null ) { return; }
230 
231 		final String finalTip;
232 		if( state == Gui.ViewState.MONITOR ) {
233 			wrkspcHeaderLabel.setText( "Task-Monitor: " + job.getJobName() );
234 			controlToolbar.setVisible( true );
235 
236 			// Build informative context metadata bar metrics string
237 			final Preference pref = job.getPreference();
238 			final String src = pref.getSourcePaths() != null ? Arrays.toString( pref.getSourcePaths().toArray() ) : "Keine Quelle";
239 			final String dest = pref.getDestPaths() != null && !pref.getDestPaths().isEmpty() ? pref.getDestPaths().toString() : "Kein Ziel";
240 			final String srcTip = src.replace( '[', ' ' ).replace( ']', ' ' ).replace( ',', '\n' );
241 
242 			if( ScanType.DUBLICATE_SCAN == job.getSelectedMode() ) {
243 				finalTip = String.format( """
244 						Quelle:
245 						%s
246 						""", srcTip );
247 				contextInfoLabel.setText( String.format( "Modus: %s  |  Verzeichnisse: %s", job.getSelectedMode().getDescription(), src ) );
248 				centerViewport.getChildren().add( duplicateViewNode );
249 			}else {
250 				finalTip = String.format( """
251 						Quelle:
252 						%s
253 						Ziel:
254 						%s
255 						""", srcTip, dest.replace( '[', ' ' ).replace( ']', ' ' ) );
256 				contextInfoLabel.setText( String.format( "Modus: %s  |  Quelle: %s  |  Ziel: %s", job.getSelectedMode().getDescription(), src, dest ) );
257 				centerViewport.getChildren().add( consoleViewNode );
258 			}
259 			contextInfoLabel.setTooltip( new Tooltip( finalTip ) );
260 		}else if( state == Gui.ViewState.SETTINGS ) {
261 			wrkspcHeaderLabel.setText( "Einstellungen für: " + job.getJobName() );
262 			contextInfoLabel.setText( "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen." );
263 			controlToolbar.setVisible( false );
264 			displayCustomViewNode( settingsGrid.buildSettingsGridTab( this, job, mainGui.getAvailableThemes() ) );
265 		}
266 	}
267 
268 	/**
269 	 * Swaps out current content layouts for custom visual configurations nodes.
270 	 *
271 	 * @param content Visual layout UI node element.
272 	 */
273 	void displayCustomViewNode( final Node content ) {
274 		centerViewport.getChildren().clear();
275 		final ScrollPane scroll = new ScrollPane( content );
276 		scroll.setFitToWidth( true );
277 		scroll.setPadding( new Insets( 12 ) );
278 		centerViewport.getChildren().add( scroll );
279 	}
280 
281 	/**
282 	 * Rebinds background parameters changes metrics values directly onto visual output listeners text nodes.
283 	 *
284 	 * @param job Selected pipeline source.
285 	 */
286 	void bindJob( final SyncJobContext job ) {
287 		statusLabel.textProperty().unbind();
288 		consoleTextArea.textProperty().unbind();
289 		statusLabel.textProperty().bind( job.statusMessageProperty() );
290 		consoleTextArea.textProperty().bind( job.logOutputProperty() );
291 		duplicateTable.setItems( job.getDuplicateFiles() );
292 
293 		progressBar.progressProperty().unbind();
294 		progressBar.progressProperty().bind( Bindings.when( job.runningProperty() ).then( ProgressIndicator.INDETERMINATE_PROGRESS ).otherwise( 0.0 ) );
295 
296 		cancelButton.disableProperty().unbind();
297 		actionButton.disableProperty().unbind();
298 		cancelButton.disableProperty().bind( job.runningProperty().not() );
299 		actionButton.disableProperty().bind( job.runningProperty() );
300 
301 		cancelButton.setOnAction( _ -> controller.handleStopTask( job ) );
302 		actionButton.setOnAction( _ -> controller.handleExecuteTask( job ) );
303 		deleteButton.setOnAction( _ -> controller.deleteSelectedDuplicates( job ) );
304 	}
305 
306 	/**
307 	 * Displays a temporary status message within the context info banner.
308 	 * Automatically reverts to the default baseline description after a set duration.
309 	 * Includes programmatic safety fallbacks if the active theme lacks CSS class declarations.
310 	 *
311 	 * @param message         The localized text string to display.
312 	 * @param cssNotifyStatus The status for the notification..
313 	 * @param durationSec     The visibility duration in seconds before auto-reverting.
314 	 */
315 	void displayTemporaryStatus( final String message, final NotifyStatus cssNotifyStatus, final int durationSec ) {
316 		final String origContextText = contextInfoLabel.getText();
317 		final String originalStyle = contextInfoLabel.getStyle();
318 		// 1. Clean up any existing status style classes to prevent collision states
319 		contextInfoLabel.getStyleClass().removeAll( "status-success", "status-error", "status-warning" );
320 
321 		// 2. Programmatic structural fallback: Set a default color via inline styles
322 		// This acts as a safety net if the active CSS theme completely lacks the targeted class definition.
323 		// CHECKSTYLE:OFF its ok without a default
324 		switch( cssNotifyStatus ) {
325 			case SUCCESS -> {
326 				contextInfoLabel.setStyle( "-fx-text-fill: #22aa22; -fx-font-weight: bold;" );
327 				contextInfoLabel.setText( "✔ " + message );
328 			}
329 			case ERROR -> {
330 				contextInfoLabel.setStyle( "-fx-text-fill: #ff3333; -fx-font-weight: bold;" );
331 				contextInfoLabel.setText( "❌ " + message );
332 			}
333 			case WARNING -> {
334 				contextInfoLabel.setStyle( "-fx-text-fill: #ffaa00; -fx-font-weight: bold;" );
335 				contextInfoLabel.setText( "⚠ " + message );
336 			}
337 		}
338 		// CHECKSTYLE:ON
339 
340 		// 3. Inject the theme's class rule.
341 		// If the theme defines this class, the stylesheet will cleanly override our inline fallback style.
342 		contextInfoLabel.getStyleClass().addFirst( cssNotifyStatus.getCssClass() );
343 
344 		// Initialize asynchronous fade-out/revert timer
345 		final Timeline fallbackTimeline = new Timeline( new KeyFrame(
346 				Duration.seconds( durationSec ),
347 				_ -> {
348 					contextInfoLabel.setText( origContextText );
349 					contextInfoLabel.getStyleClass().remove( cssNotifyStatus.getCssClass() );
350 					contextInfoLabel.setStyle( originalStyle );
351 				} ) );
352 
353 		fallbackTimeline.setCycleCount( 1 );
354 		fallbackTimeline.play();
355 	}
356 }