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.io.File;
24  import java.nio.file.Path;
25  import java.nio.file.Paths;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.List;
29  
30  import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
31  import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
32  import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
33  
34  import de.spiritscorp.datasync.BgTime;
35  import de.spiritscorp.datasync.Main;
36  import de.spiritscorp.datasync.ScanType;
37  import de.spiritscorp.datasync.controller.SyncJobContext;
38  import de.spiritscorp.datasync.controller.ViewController;
39  import de.spiritscorp.datasync.io.Preference;
40  import de.spiritscorp.datasync.io.PreferenceManager;
41  import de.spiritscorp.datasync.theme.AppTheme;
42  import javafx.animation.KeyFrame;
43  import javafx.animation.Timeline;
44  import javafx.collections.FXCollections;
45  import javafx.collections.ObservableList;
46  import javafx.geometry.Insets;
47  import javafx.geometry.Pos;
48  import javafx.scene.Node;
49  import javafx.scene.control.Button;
50  import javafx.scene.control.CheckBox;
51  import javafx.scene.control.ComboBox;
52  import javafx.scene.control.Label;
53  import javafx.scene.control.ListCell;
54  import javafx.scene.control.ListView;
55  import javafx.scene.control.ProgressBar;
56  import javafx.scene.control.ScrollPane;
57  import javafx.scene.control.Separator;
58  import javafx.scene.control.TableColumn;
59  import javafx.scene.control.TableView;
60  import javafx.scene.control.TextArea;
61  import javafx.scene.control.TextField;
62  import javafx.scene.control.Tooltip;
63  import javafx.scene.control.cell.CheckBoxTableCell;
64  import javafx.scene.layout.GridPane;
65  import javafx.scene.layout.HBox;
66  import javafx.scene.layout.Priority;
67  import javafx.scene.layout.StackPane;
68  import javafx.scene.layout.VBox;
69  import javafx.stage.DirectoryChooser;
70  import javafx.util.Duration;
71  
72  /**
73   * Display workspace panel hosting the interactive operational consoles,
74   * data lists, detailed execution metadata bars and the dynamic target settings configurations grid.
75   * * @author Tom Spirit
76   */
77  public final class WorkspaceView extends VBox {
78  
79  	private final Gui mainGui;
80  	private final ViewController controller;
81  	private final Label workspaceHeaderLabel;
82  	private final Label contextInfoLabel;
83  	private final HBox controlToolbar;
84  	private final StackPane centerViewport;
85  
86  	private final ScrollPane consoleViewNode;
87  	private final TextArea consoleTextArea;
88  	private final VBox duplicateViewNode;
89  	private TableView<SyncJobContext.FileRow> duplicateTable;
90  
91  	private final Button actionButton;
92  	private final Button cancelButton;
93  	private Button deleteButton;
94  	private final ProgressBar progressBar;
95  	private final Label statusLabel;
96  
97  	public enum NotifyStatus {
98  		SUCESS( "status-success"),
99  		ERROR( "status-error"),
100 		WARNING( "status-warning");
101 
102 		private final String cssClass;
103 
104 		NotifyStatus( String cssClass ) {
105 			this.cssClass = cssClass;
106 		}
107 
108 		String getCssClass() { return cssClass; }
109 	}
110 
111 	/**
112 	 * Prepares layouts and maps operations targets onto implementation controller.
113 	 *
114 	 * @param mainGui    Configuration context core coordinator link.
115 	 * @param controller Strategy abstraction dealing with interface state management mutations.
116 	 */
117 	public WorkspaceView( Gui mainGui, ViewController controller ) {
118 		this.mainGui = mainGui;
119 		this.controller = controller;
120 		this.setPadding( new Insets( 24 ) );
121 		this.setSpacing( 12 );
122 
123 		workspaceHeaderLabel = new Label( "Kein Task aktiv" );
124 		workspaceHeaderLabel.setStyle( "-fx-font-size: 22px; -fx-font-weight: bold;" );
125 
126 		// Subtitle dynamic information bar containing directories context mapping
127 		contextInfoLabel = new Label( "" );
128 		contextInfoLabel.setStyle( "-fx-font-size: 13px; -fx-font-style: italic; -fx-padding: 0 0 8px 0;" );
129 
130 		controlToolbar = new HBox( 12 );
131 		controlToolbar.setAlignment( Pos.CENTER_LEFT );
132 
133 		actionButton = new Button( "Ausführen", Gui.createIcon( MaterialDesignP.PLAY ) );
134 		actionButton.setStyle( "-fx-background-color: #2ecc71; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 20px;" );
135 		actionButton.setTooltip( new Tooltip( "Starte Job" ) );
136 		cancelButton = new Button( "Abbrechen", Gui.createIcon( MaterialDesignS.STOP ) );
137 		cancelButton.setStyle( "-fx-background-color: #95a5a6; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 16px;" );
138 		cancelButton.setTooltip( new Tooltip( "Stoppe Job" ) );
139 
140 		controlToolbar.getChildren().addAll( actionButton, cancelButton );
141 
142 		consoleTextArea = new TextArea();
143 		consoleTextArea.setEditable( false );
144 		consoleTextArea.setStyle( " -fx-font-family: 'Consolas', monospace; -fx-font-size: 12px;" );
145 		consoleViewNode = new ScrollPane( consoleTextArea );
146 		consoleViewNode.setFitToWidth( true );
147 		consoleViewNode.setFitToHeight( true );
148 
149 		duplicateViewNode = assembleDuplicateTableView();
150 		centerViewport = new StackPane( consoleViewNode );
151 
152 		final HBox statusFooter = new HBox( 12 );
153 		statusFooter.setAlignment( Pos.CENTER_LEFT );
154 		progressBar = new ProgressBar( 0 );
155 		progressBar.setTooltip( new Tooltip( "Aktueller Fortschritt" ) );
156 		statusLabel = new Label( "Bereit" );
157 		statusLabel.setTooltip( new Tooltip( "Aktueller Status" ) );
158 		statusFooter.getChildren().addAll( progressBar, statusLabel );
159 
160 		this.getChildren().addAll( workspaceHeaderLabel, contextInfoLabel, controlToolbar, centerViewport, statusFooter );
161 		setVgrow( centerViewport, Priority.ALWAYS );
162 	}
163 
164 	/**
165 	 * Builds standard layout configuration frame for processing double files arrays.
166 	 */
167 	private VBox assembleDuplicateTableView() {
168 		duplicateTable = new TableView<>();
169 		duplicateTable.setEditable( true );
170 		duplicateTable.setFixedCellSize( 24.0 );
171 
172 		final TableColumn<SyncJobContext.FileRow, Boolean> selCol = new TableColumn<>( "Auswahl" );
173 		selCol.setCellValueFactory( d -> d.getValue().selectedProperty() );
174 		selCol.setCellFactory( CheckBoxTableCell.forTableColumn( selCol ) );
175 		selCol.setPrefWidth( 50 );
176 
177 		final TableColumn<SyncJobContext.FileRow, String> nameCol = new TableColumn<>( "Dateiname" );
178 		nameCol.setCellValueFactory( d -> d.getValue().fileNameProperty() );
179 		nameCol.setPrefWidth( 250 );
180 
181 		final TableColumn<SyncJobContext.FileRow, String> sizeCol = new TableColumn<>( "Größe" );
182 		sizeCol.setCellValueFactory( d -> d.getValue().sizeProperty() );
183 		sizeCol.setPrefWidth( 250 );
184 
185 		final TableColumn<SyncJobContext.FileRow, String> pathCol = new TableColumn<>( "Pfad" );
186 		pathCol.setCellValueFactory( d -> d.getValue().pathProperty() );
187 		pathCol.setPrefWidth( 600 );
188 
189 		final TableColumn<SyncJobContext.FileRow, String> hashCol = new TableColumn<>( "Hash" );
190 		hashCol.setCellValueFactory( d -> d.getValue().hashProperty() );
191 		hashCol.setPrefWidth( 250 );
192 
193 		duplicateTable.getColumns().addAll( List.of( selCol, nameCol, sizeCol, hashCol, pathCol ) );
194 		deleteButton = new Button( "Duplikate löschen", Gui.createIcon( MaterialDesignD.DELETE ) );
195 		deleteButton.setStyle( "-fx-background-color: #e74c3c; -fx-text-fill: white;" );
196 		deleteButton.setTooltip( new Tooltip( "Ausgewählte Dateien werden gelöscht" ) );
197 
198 		final VBox frame = new VBox( 8, duplicateTable, deleteButton );
199 		setVgrow( duplicateTable, Priority.ALWAYS );
200 		return frame;
201 	}
202 
203 	/**
204 	 * Redraws visible frame items based on routing navigation instructions and current job payload state.
205 	 * * @param state The target navigation ViewState.
206 	 *
207 	 * @param job The selected target sync context model instance.
208 	 */
209 	public void refreshView( final Gui.ViewState state, final SyncJobContext job ) {
210 		if( job == null ) return;
211 
212 		centerViewport.getChildren().clear();
213 
214 		if( state == Gui.ViewState.MONITOR ) {
215 			workspaceHeaderLabel.setText( "Task-Monitor: " + job.getJobName() );
216 			controlToolbar.setVisible( true );
217 
218 			// Build informative context metadata bar metrics string
219 			final Preference p = job.getPreference();
220 			final String src = p.getSourcePath() != null ? Arrays.toString( p.getSourcePath().toArray() ) : "Keine Quelle";
221 			final String dest = p.getDestPath() != null && !p.getDestPath().isEmpty() ? p.getDestPath().toString() : "Kein Ziel";
222 
223 			if( ScanType.DUBLICATE_SCAN.equals( job.getSelectedMode() ) ) {
224 				contextInfoLabel.setText( String.format( "Modus: %s  |  Ziel: %s", job.getSelectedMode().getDescription(), src ) );
225 				centerViewport.getChildren().add( duplicateViewNode );
226 			}else {
227 				contextInfoLabel.setText( String.format( "Modus: %s  |  Quelle: %s  |  Ziel: %s", job.getSelectedMode().getDescription(), src, dest ) );
228 				centerViewport.getChildren().add( consoleViewNode );
229 			}
230 		}else if( state == Gui.ViewState.SETTINGS ) {
231 			workspaceHeaderLabel.setText( "Einstellungen für: " + job.getJobName() );
232 			contextInfoLabel.setText( "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen." );
233 			controlToolbar.setVisible( false );
234 			displayCustomViewNode( buildSettingsGridTab( job ) );
235 		}
236 	}
237 
238 	/**
239 	 * Swaps out current content layouts for custom visual configurations nodes.
240 	 * * @param content Visual layout UI node element.
241 	 */
242 	public void displayCustomViewNode( Node content ) {
243 		centerViewport.getChildren().clear();
244 		final ScrollPane scroll = new ScrollPane( content );
245 		scroll.setFitToWidth( true );
246 		scroll.setPadding( new Insets( 12 ) );
247 		centerViewport.getChildren().add( scroll );
248 	}
249 
250 	/**
251 	 * Rebinds background parameters changes metrics values directly onto visual output listeners text nodes.
252 	 * * @param job Selected pipeline source.
253 	 */
254 	public void bindJob( SyncJobContext job ) {
255 		statusLabel.textProperty().unbind();
256 		consoleTextArea.textProperty().unbind();
257 		statusLabel.textProperty().bind( job.statusMessageProperty() );
258 		consoleTextArea.textProperty().bind( job.logOutputProperty() );
259 		duplicateTable.setItems( job.getDuplicateFiles() );
260 
261 		cancelButton.disableProperty().unbind();
262 		actionButton.disableProperty().unbind();
263 		cancelButton.disableProperty().bind( job.runningProperty().not() );
264 		actionButton.disableProperty().bind( job.runningProperty() );
265 
266 		cancelButton.setOnAction( _ -> controller.handleStopTask( job ) );
267 		actionButton.setOnAction( _ -> controller.handleExecuteTask( job ) );
268 		deleteButton.setOnAction( _ -> controller.deleteSelectedDuplicates( job ) );
269 	}
270 
271 	/**
272 	 * Displays a temporary status message within the context info banner.
273 	 * Automatically reverts to the default baseline description after a set duration.
274 	 * Includes programmatic safety fallbacks if the active theme lacks CSS class declarations.
275 	 *
276 	 * @param message         The localized text string to display.
277 	 * @param cssNotifyStatus The status for the notification..
278 	 * @param durationSec     The visibility duration in seconds before auto-reverting.
279 	 */
280 	void displayTemporaryStatus( String message, NotifyStatus cssNotifyStatus, int durationSec ) {
281 		final String originalContextText = "Konfiguration der task-spezifischen Ablaufparameter, Dateiattribute und Verzeichnisstrukturen.";
282 		final String originalStyle = contextInfoLabel.getStyle();
283 		// 1. Clean up any existing status style classes to prevent collision states
284 		contextInfoLabel.getStyleClass().removeAll( "status-success", "status-error", "status-warning" );
285 
286 		// 2. Programmatic structural fallback: Set a default color via inline styles
287 		// This acts as a safety net if the active CSS theme completely lacks the targeted class definition.
288 		// CHECKSTYLE:OFF
289 		switch( cssNotifyStatus ) {
290 			case SUCESS -> {
291 				contextInfoLabel.setStyle( "-fx-text-fill: #22aa22  !important; -fx-font-weight: bold;" );
292 				contextInfoLabel.setText( "✔ " + message );
293 			}
294 			case ERROR -> {
295 				contextInfoLabel.setStyle( "-fx-text-fill: #ff3333  !important; -fx-font-weight: bold;" );
296 				contextInfoLabel.setText( "❌ " + message );
297 			}
298 			case WARNING -> {
299 				contextInfoLabel.setStyle( "-fx-text-fill: #ffaa00  !important; -fx-font-weight: bold;" );
300 				contextInfoLabel.setText( "⚠ " + message );
301 			}
302 		}
303 		// CHECKSTYLE:ON
304 
305 		// 3. Inject the theme's class rule.
306 		// If the theme defines this class, the stylesheet will cleanly override our inline fallback style.
307 		contextInfoLabel.getStyleClass().add( cssNotifyStatus.getCssClass() );
308 
309 		// Initialize asynchronous fade-out/revert timer
310 		final Timeline fallbackTimeline = new Timeline( new KeyFrame(
311 				Duration.seconds( durationSec ),
312 				_ -> {
313 					contextInfoLabel.setText( originalContextText );
314 					contextInfoLabel.getStyleClass().remove( cssNotifyStatus.getCssClass() );
315 					contextInfoLabel.setStyle( originalStyle );
316 				} ) );
317 
318 		fallbackTimeline.setCycleCount( 1 );
319 		fallbackTimeline.play();
320 	}
321 
322 	/**
323 	 * Assembles all parameter configurations fields structured nicely within grid metrics elements.
324 	 */
325 	private Node buildSettingsGridTab( SyncJobContext job ) {
326 		final Preference pref = job.getPreference();
327 		final GridPane grid = new GridPane();
328 		grid.setHgap( 24 );
329 		grid.setVgap( 16 );
330 		grid.setPadding( new Insets( 10 ) );
331 
332 		// --- Section 1: Execution Mode Selection ---
333 		final Label modeTitle = new Label( "Ausführungsmodus:" );
334 		modeTitle.setStyle( "-fx-font-weight: bold;" );
335 		final ComboBox<String> taskModeComboBox = new ComboBox<>();
336 		taskModeComboBox.setTooltip( new Tooltip( "Listet die möglichen Betriebsmodis auf" ) );
337 		taskModeComboBox.getItems().addAll( ScanType.getAllDescriptions() );
338 		job.selectedModeProperty().set( pref.getScanMode() != null ? pref.getScanMode().getDescription() : ScanType.FLAT_SCAN.getDescription() );
339 		taskModeComboBox.valueProperty().bindBidirectional( job.selectedModeProperty() );
340 		taskModeComboBox.setPrefWidth( 260 );
341 		grid.add( modeTitle, 0, 0 );
342 		grid.add( taskModeComboBox, 1, 0 );
343 
344 		// --- Section 2: Context Paths Configurations Box ---
345 		final VBox dynamicPathsContainer = new VBox( 12 );
346 		grid.add( dynamicPathsContainer, 0, 1, 2, 1 );
347 
348 		final PathContext pathCtx = new PathContext();
349 		if( pref.getSourcePath() != null ) {
350 			pathCtx.sources.addAll( pref.getSourcePath() );
351 		}
352 		if( pref.getDestPath() != null ) {
353 			pathCtx.destinations.addAll( pref.getDestPath() );
354 		}
355 
356 		taskModeComboBox.valueProperty().addListener( ( obs, o, n ) -> renderContextPaths( ScanType.get( n ), dynamicPathsContainer, pref, pathCtx ) );
357 		renderContextPaths( job.getSelectedMode(), dynamicPathsContainer, pref, pathCtx );
358 
359 		// --- Section 3: Task Parameter Flags Options ---
360 		final Label paramsTitle = new Label( "Erweiterte Ablaufparameter" );
361 		paramsTitle.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
362 
363 		final CheckBox subDirCheck = new CheckBox( "Unterordner einbeziehen (SubDir)" );
364 		subDirCheck.setSelected( pref.isSubDir() );
365 		final String subDirText = """
366 				Aktiviert: Kopiert nur die nackten Dateien und Unterordner DIREKT in das Zielverzeichnis
367 				(ideal, um mehrere Quellen in einem einzigen Zielordner zusammenzuführen).
368 				Deaktiviert: Erstellt für jeden Quellpfad einen eigenen Hauptordner im Zielverzeichnis, um die Quellen sauber voneinander zu trennen.
369 				""";
370 		subDirCheck.setTooltip( new Tooltip( subDirText ) );
371 		final CheckBox trashbinCheck = new CheckBox( "Papierkorb verwenden (Trashbin)" );
372 		trashbinCheck.setSelected( pref.isTrashbin() );
373 		trashbinCheck.setTooltip( new Tooltip( "Verschiebt modifizierte/gelöschte Dateien temporär in Sicherungsstrukturen" ) );
374 		final CheckBox autoDelCheck = new CheckBox( "Automatisches Löschen erlauben (AutoDel)" );
375 		autoDelCheck.setSelected( pref.isAutoDel() );
376 		autoDelCheck.setTooltip( new Tooltip( "Erlaubt dem System, verwaiste Dateien im Zielordner restlos zu bereinigen" ) );
377 		final CheckBox autoSyncCheck = new CheckBox( "Automatisches kopieren erlauben (AutoSync)" );
378 		autoSyncCheck.setSelected( pref.isAutoSync() );
379 		autoSyncCheck.setTooltip( new Tooltip( "Erlaubt dem System, nach einem Scan alle nötigen Dateien zu kopieren." ) );
380 		final CheckBox logOnCheck = new CheckBox( "Protokollierung aktivieren (LogOn)" );
381 		logOnCheck.setSelected( pref.isLogOn() );
382 		logOnCheck.setTooltip( new Tooltip( "Schreibt detaillierte Transaktionsprotokolle in das System-Logverzeichnis" ) );
383 		final CheckBox bgSyncCheck = new CheckBox( "Hintergrund-Synchronisation aktiv" );
384 		bgSyncCheck.setSelected( pref.isBgSync() );
385 		bgSyncCheck.setTooltip( new Tooltip( "Setzt den Autostart und aktiviert die Hintergrundsyncronisierung im nachfolgenden Intervall" ) );
386 
387 		final Label bgTimeLabel = new Label( "Hintergrund Scan-Intervall:" );
388 		final ComboBox<String> bgTimeComboBox = new ComboBox<>();
389 		bgTimeComboBox.setTooltip( new Tooltip( "Listet die möglichen Job Intervalle auf" ) );
390 		bgTimeComboBox.getItems().addAll( BgTime.getNames() );
391 		bgTimeComboBox.getSelectionModel().select( pref.getBgTime() != null ? pref.getBgTime().getName() : BgTime.MIN_30.getName() );
392 		bgTimeComboBox.disableProperty().bind( bgSyncCheck.selectedProperty().not() );
393 
394 		final VBox optionsBox = new VBox( 12, paramsTitle, subDirCheck, trashbinCheck, autoDelCheck, autoSyncCheck, logOnCheck, bgSyncCheck, new HBox( 8, bgTimeLabel, bgTimeComboBox ) );
395 		grid.add( optionsBox, 0, 2, 2, 1 );
396 
397 		// --- Section 4: Global Parameters Stack ---
398 		final Label globalTitle = new Label( "Globale System-Konfiguration" );
399 		globalTitle.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
400 
401 		final CheckBox globalAutostartCheck = new CheckBox( "DataSync beim Systemstart minimiert laden (Autostart OS)" );
402 		globalAutostartCheck.setSelected( PreferenceManager.getInstance().isGlobalAutoStart() ); // Bind status fallback trace
403 
404 		// NEW: Theme Changer Layout Elements Configuration
405 		final Label themeLabel = new Label( "Visuelles Anwendungs-Theme:" );
406 		final ComboBox<AppTheme> themeComboBox = new ComboBox<>( mainGui.getAvailableThemes() );
407 		themeComboBox.setTooltip( new Tooltip( "Listet alle möglichen Themes auf" ) );
408 		// Custom cell rendering to display the specific Strategy names cleanly
409 		themeComboBox.setCellFactory( _ -> new ListCell<>() {
410 			@Override
411 			protected void updateItem( AppTheme item, boolean empty ) {
412 				super.updateItem( item, empty );
413 				setText( empty || item == null ? "" : item.getName() );
414 			}
415 		} );
416 		themeComboBox.setButtonCell( new ListCell<>() {
417 			@Override
418 			protected void updateItem( AppTheme item, boolean empty ) {
419 				super.updateItem( item, empty );
420 				setText( empty || item == null ? "" : item.getName() );
421 			}
422 		} );
423 		themeComboBox.getSelectionModel().select( mainGui.getCurrentTheme() );
424 
425 		final VBox globalBox = new VBox( 10, globalTitle, globalAutostartCheck, new HBox( 8, themeLabel, themeComboBox ) );
426 		grid.add( globalBox, 0, 3, 2, 1 );
427 
428 		// --- Commit Action Triggers ---
429 		final Button saveButton = new Button( "Einstellungen speichern", Gui.createIcon( MaterialDesignD.DISC ) );
430 		saveButton.setStyle( "-fx-background-color: #2ecc71; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10px 24px;" );
431 		saveButton.setTooltip( new Tooltip( "Übernimmt alle geänderten Zustandsparameter permanent in die JSON-Konfigurationsdatei" ) );
432 		saveButton.setOnAction( _ -> {
433 			PreferenceManager.getInstance().setGlobalAutoStart( globalAutostartCheck.isSelected() );
434 			PreferenceManager.getInstance().setTheme( themeComboBox.getValue() );
435 			final Preference jobPref = job.getPreference();
436 			final ScanType scanType = ScanType.get( taskModeComboBox.getValue() );
437 			jobPref.setSubDir( subDirCheck.isSelected() );
438 			jobPref.setTrashbin( trashbinCheck.isSelected() );
439 			jobPref.setAutoDel( autoDelCheck.isSelected() );
440 			jobPref.setLogOn( logOnCheck.isSelected() );
441 			jobPref.setBgSync( bgSyncCheck.isSelected() );
442 			jobPref.setBgTime( BgTime.get( bgTimeComboBox.getValue() ) );
443 			jobPref.setAutoSync( autoSyncCheck.isSelected() );
444 			jobPref.setScanMode( scanType );
445 
446 			jobPref.setSourcePath( new ArrayList<>( pathCtx.sources ) );
447 			jobPref.setDestPath( new ArrayList<>( pathCtx.destinations ) );
448 
449 			if( scanType == ScanType.DUBLICATE_SCAN && !pathCtx.sources.isEmpty() ) {
450 				jobPref.setStartSourcePath( pathCtx.sources.get( 0 ) );
451 				jobPref.setStartDestPath( Paths.get( "" ) );
452 			}else if( !pathCtx.sources.isEmpty() && !pathCtx.destinations.isEmpty() ) {
453 				jobPref.setStartSourcePath( pathCtx.sources.get( 0 ) );
454 				jobPref.setStartDestPath( pathCtx.destinations.get( 0 ) );
455 			}else {
456 				displayTemporaryStatus( "Fehlender Pfad! Einstellungen nicht gespeichert!", NotifyStatus.WARNING, Main.INFO_DELAY );
457 				return;
458 			}
459 			controller.handleSaveSettings( job.getPreference(), themeComboBox.getValue() );
460 		} );
461 
462 		final HBox buttonRow = new HBox( saveButton );
463 		buttonRow.setAlignment( Pos.CENTER_RIGHT );
464 		grid.add( buttonRow, 1, 4 );
465 
466 		return grid;
467 	}
468 
469 	/**
470 	 * Morph layouts rendering dynamically mapped on target selected Action mode definitions.
471 	 */
472 	private void renderContextPaths( ScanType type, VBox container, Preference pref, PathContext pathCtx ) {
473 		container.getChildren().clear();
474 		final GridPane pathsGrid = new GridPane();
475 		pathsGrid.setHgap( 12 );
476 		pathsGrid.setVgap( 10 );
477 
478 		final Label title = new Label( "Verzeichnis-Konfiguration (" + type.getDescription() + ")" );
479 		title.setStyle( "-fx-font-size: 14px; -fx-font-weight: bold;" );
480 		container.getChildren().add( title );
481 
482 		if( ScanType.SYNCHRONIZE.equals( type ) || ScanType.DUBLICATE_SCAN.equals( type ) ) {
483 			final Path initialSrc = ( pref.getSourcePath() != null && !pref.getSourcePath().isEmpty() ) ? pref.getSourcePath().get( 0 ) : null;
484 			final TextField srcField = new TextField( initialSrc.toString() );
485 			srcField.setPrefWidth( 400 );
486 			final Button srcBtn = new Button( "Durchsuchen..." );
487 			srcBtn.setOnAction( _ -> {
488 				final File f = chooseDirectory( initialSrc.toFile(), "Quellverzeichnis für " + pref.getScanMode().getDescription() );
489 				if( f != null ) {
490 					srcField.setText( f.getAbsolutePath() );
491 					pathCtx.sources.clear();
492 					pathCtx.sources.add( Paths.get( f.getAbsolutePath() ) );
493 				}
494 			} );
495 			pathsGrid.add( new Label( ScanType.DUBLICATE_SCAN.equals( type ) ? "Scanverzeichnis:" : "Quellverzeichnis:" ), 0, 0 );
496 			pathsGrid.add( new HBox( 8, srcField, srcBtn ), 1, 0 );
497 		}
498 
499 		if( ScanType.SYNCHRONIZE.equals( type ) ) {
500 			final Path initialDest = ( pref.getDestPath() != null && !pref.getDestPath().isEmpty() ) ? pref.getDestPath().get( 0 ) : null;
501 			final TextField destField = new TextField( initialDest.toString() );
502 			destField.setPrefWidth( 400 );
503 			final Button destBtn = new Button( "Durchsuchen..." );
504 			destBtn.setOnAction( _ -> {
505 				final File f = chooseDirectory( initialDest.toFile(), "Zielverzeichnis für " + pref.getScanMode().getDescription() );
506 				if( f != null ) {
507 					destField.setText( f.getAbsolutePath() );
508 					pathCtx.destinations.clear();
509 					pathCtx.destinations.add( Paths.get( f.getAbsolutePath() ) );
510 				}
511 			} );
512 			pathsGrid.add( new Label( "Zielverzeichnis:" ), 0, 1 );
513 			pathsGrid.add( new HBox( 8, destField, destBtn ), 1, 1 );
514 		}
515 
516 		if( ScanType.FLAT_SCAN.equals( type ) || ScanType.DEEP_SCAN.equals( type ) ) {
517 			final VBox multiSrcBox = new VBox( 6 );
518 			final Label multiLabel = new Label( "Quellverzeichnisse (Multi-Source Pathing):" );
519 			multiLabel.setStyle( "-fx-font-weight: bold;" );
520 
521 			final ObservableList<String> backupPaths = FXCollections.observableArrayList();
522 			for( final Path p : pathCtx.sources ) {
523 				backupPaths.add( p.toString() );
524 			}
525 
526 			final ListView<String> pathsListView = new ListView<>( backupPaths );
527 			pathsListView.setPrefHeight( 100 );
528 			final Button add = new Button( "Verzeichnis hinzufügen", Gui.createIcon( MaterialDesignP.PLUS ) );
529 			add.setOnAction( _ -> {
530 				final File f = chooseDirectory( pathCtx.sources.getLast().toFile(), "Quellverzeichnis für " + pref.getScanMode().getDescription() );
531 				if( f != null && !backupPaths.contains( f.getAbsolutePath() ) ) {
532 					backupPaths.add( f.getAbsolutePath() );
533 					pathCtx.sources.add( Paths.get( f.getAbsolutePath() ) );
534 				}
535 			} );
536 			final Button rem = new Button( "Entfernen", Gui.createIcon( MaterialDesignD.DELETE ) );
537 			rem.setOnAction( _ -> {
538 				final String sel = pathsListView.getSelectionModel().getSelectedItem();
539 				if( sel != null ) {
540 					backupPaths.remove( sel );
541 					pathCtx.sources.remove( Paths.get( sel ) );
542 				}
543 			} );
544 
545 			multiSrcBox.getChildren().addAll( multiLabel, pathsListView, new HBox( 8, add, rem ) );
546 			pathsGrid.add( multiSrcBox, 0, 0, 2, 1 );
547 
548 			final GridPane destGrid = new GridPane();
549 			destGrid.setHgap( 12 );
550 			final String initialDest = ( pref.getDestPath() != null && !pref.getDestPath().isEmpty() ) ? pref.getDestPath().get( 0 ).toString() : "";
551 			final TextField destField = new TextField( initialDest );
552 			destField.setPrefWidth( 400 );
553 			final Button destBtn = new Button( "Durchsuchen..." );
554 			destBtn.setOnAction( _ -> {
555 				final File f = chooseDirectory( pathCtx.destinations.getFirst().toFile(), "Ziielverzeichnis für " + pref.getScanMode().getDescription() );
556 				if( f != null ) {
557 					destField.setText( f.getAbsolutePath() );
558 					pathCtx.destinations.clear();
559 					pathCtx.destinations.add( Paths.get( f.getAbsolutePath() ) );
560 				}
561 			} );
562 			destGrid.add( new Label( "Zielverzeichnis:" ), 0, 0 );
563 			destGrid.add( new HBox( 8, destField, destBtn ), 1, 0 );
564 			pathsGrid.add( new VBox( 10, new Separator(), destGrid ), 0, 1, 2, 1 );
565 		}
566 
567 		container.getChildren().add( pathsGrid );
568 	}
569 
570 	private File chooseDirectory( File initialDir, String title ) {
571 		final DirectoryChooser chooser = new DirectoryChooser();
572 		if( initialDir != null && initialDir.exists() ) chooser.setInitialDirectory( initialDir );
573 		chooser.setTitle( title );
574 		return chooser.showDialog( mainGui.getWindowStage() );
575 	}
576 
577 	/**
578 	 * Simple structural model container mirroring active paths modifications
579 	 * decoupled from underlying live Node hierarchies.
580 	 */
581 	private static final class PathContext {
582 		private final ArrayList<Path> sources = new ArrayList<>();
583 		private final ArrayList<Path> destinations = new ArrayList<>();
584 	}
585 }