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.List;
28  
29  import javafx.collections.FXCollections;
30  import javafx.collections.ObservableList;
31  import javafx.scene.control.Button;
32  import javafx.scene.control.Label;
33  import javafx.scene.control.ListView;
34  import javafx.scene.control.Separator;
35  import javafx.scene.control.TextField;
36  import javafx.scene.layout.GridPane;
37  import javafx.scene.layout.HBox;
38  import javafx.scene.layout.VBox;
39  import javafx.stage.DirectoryChooser;
40  import javafx.stage.Stage;
41  
42  import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
43  import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
44  
45  import de.spiritscorp.datasync.ScanType;
46  import de.spiritscorp.datasync.io.Preference;
47  import de.spiritscorp.datasync.io.PreferenceManager;
48  
49  /**
50   * Component renderer responsible for dynamically constructing and mutating layout nodes
51   * within the user interface based on the active structural {@link ScanType}.
52   * <p>
53   * This class orchestrates single-source inputs, multi-source lists, and destination selection
54   * panes, ensuring that the visual representation consistently matches the capabilities of the
55   * selected execution profile.
56   * </p>
57   *
58   * @author Tom Spirit
59   * @since 1.0.0
60   */
61  class ContextPathRenderer {
62  
63  	/**
64  	 * The horizontal gap spacing in pixels used uniformly across structural grid layouts.
65  	 */
66  	private static final int H_GAP = 12;
67  	/**
68  	 * The vertical gap spacing in pixels used uniformly across structural box and grid configurations.
69  	 */
70  	private static final int V_GAP = 10;
71  	/**
72  	 * The preferred layout width in pixels allocated for descriptive structural labels.
73  	 */
74  	private static final int LABEL_WIDTH = 170;
75  	/**
76  	 * The preferred layout width in pixels allocated for path display text input fields.
77  	 */
78  	private static final int TEXT_FIELD_WIDTH = 400;
79  	/**
80  	 * The preferred layout width in pixels allocated for the multi-source directory selection list view.
81  	 */
82  	private static final int PATH_LIST_WIDTH = 600;
83  	/**
84  	 * The preferred layout height in pixels allocated for the multi-source directory selection list view.
85  	 */
86  	private static final int PATH_LIST_HEIGHT = 140;
87  	/**
88  	 * The system-wide fallback path context extracted from the global preference manager initialization state.
89  	 */
90  	private static final Path DEFAULT_PATH = PreferenceManager.getInstance().getRootPath();
91  
92  	/**
93  	 * Dynamically morphs and populates the layout components inside a target container
94  	 * mapped to the contextual rules of the active {@link ScanType}.
95  	 *
96  	 * @param container    The target UI container to be cleared and rebuilt with the generated nodes
97  	 * @param pref         The operational data model context carrying active synchronization properties
98  	 * @param primaryStage The application-level window stage acting as the parent for modal dialogue screens
99  	 */
100 	void renderContextPaths( final VBox container, final Preference pref, final Stage primaryStage ) {
101 		final ScanType type = pref.getScanMode();
102 		// Clear previous components to prepare for dynamic layout generation
103 		container.getChildren().clear();
104 
105 		final GridPane pathsGrid = new GridPane();
106 		pathsGrid.setHgap( H_GAP );
107 		pathsGrid.setVgap( V_GAP );
108 
109 		// Set up descriptive section title header
110 		final Label dirTitleLabel = new Label( "Verzeichnis-Konfiguration (" + type.getDescription() + ")" );
111 		dirTitleLabel.getStyleClass().addAll( "dir-title-label" );
112 		container.getChildren().add( dirTitleLabel );
113 
114 		// Evaluate source component requirements
115 		if( ScanType.SYNCHRONIZE == type ) {
116 			pathsGrid.add( getSourceBox( pref, primaryStage ), 0, 0, 2, 1 );
117 		}else if( ScanType.FLAT_SCAN == type || ScanType.DEEP_SCAN == type || ScanType.DUBLICATE_SCAN == type ) {
118 			pathsGrid.add( getSourceList( type, pref, primaryStage ), 0, 0, 2, 1 );
119 		}
120 
121 		// Evaluate destination component requirements
122 		if( ScanType.FLAT_SCAN == type || ScanType.DEEP_SCAN == type || ScanType.SYNCHRONIZE == type ) {
123 			pathsGrid.add( getDestBox( pref, primaryStage ), 0, 1, 2, 1 );
124 		}
125 
126 		container.getChildren().add( pathsGrid );
127 	}
128 
129 	/**
130 	 * Constructs a single-source path input panel complete with an interactive file browser trigger.
131 	 *
132 	 * @param pref         The underlying preference data model containing current source configuration
133 	 * @param primaryStage The parent stage context required to display the file selection overlay modal
134 	 * @return A configured {@link VBox} layout element encapsulating the single-source configuration nodes
135 	 */
136 	private VBox getSourceBox( final Preference pref, final Stage primaryStage ) {
137 		final GridPane destGrid = new GridPane();
138 		destGrid.setHgap( H_GAP );
139 		final String initialString = getInitialPath( pref.getSourcePaths() ).toString();
140 		final TextField srcTextField = new TextField( initialString );
141 		srcTextField.setPrefWidth( TEXT_FIELD_WIDTH );
142 		final Button srcBtn = new Button( "Durchsuchen..." );
143 		srcBtn.setOnAction( _ -> {
144 			final File initialSrc = getInitialPath( pref.getSourcePaths() );
145 			final File dirPath = chooseDirectory( initialSrc, "Arbeitsverzeichnis A für " + pref.getScanMode().getDescription(), primaryStage );
146 			if( dirPath != null ) {
147 				srcTextField.setText( dirPath.getAbsolutePath() );
148 				pref.setSourcePaths( new ArrayList<>( List.of( dirPath.toPath() ) ) );
149 			}
150 		} );
151 		final Label label = new Label( "Arbeitsverzeichnis A:" );
152 		label.setPrefWidth( LABEL_WIDTH );
153 		destGrid.add( label, 0, 0 );
154 		destGrid.add( new HBox( 8, srcTextField, srcBtn ), 1, 0 );
155 		return new VBox( V_GAP, destGrid );
156 	}
157 
158 	/**
159 	 * Builds a multi-source selection control allowing multiple distinct execution directories
160 	 * to be systematically managed and registered.
161 	 *
162 	 * @param type         The specific profile category to tailor UI labeling rules
163 	 * @param pref         The underlying data model containing cumulative configuration state
164 	 * @param primaryStage The structural window layout owner used to bind standard chooser instances
165 	 * @return A structurally complete {@link VBox} controlling multi-directory source path management
166 	 */
167 	private VBox getSourceList( final ScanType type, final Preference pref, final Stage primaryStage ) {
168 		// Contextual labeling mapping specific scan constraints
169 		final String label = ScanType.DUBLICATE_SCAN == type ? "Scanverzeichnisse:" : "Quellverzeichnisse (Multi-Source Pathing):";
170 		final String chooserTitle = String.format( ScanType.DUBLICATE_SCAN == type ? "Scanverzeichnis für %s" : "Quellverzeichnis für %s", pref.getScanMode().getDescription() );
171 
172 		final VBox multiSrcBox = new VBox( 6 );
173 		final Label multiSrcLabel = new Label( label );
174 		multiSrcLabel.getStyleClass().addAll( "multi-src-label" );
175 
176 		// Populate view wrapper backed by existing preferences
177 		final ObservableList<String> actualSourcePaths = FXCollections.observableArrayList();
178 		for( final Path path : pref.getSourcePaths() ) {
179 			actualSourcePaths.add( path.toString() );
180 		}
181 
182 		final ListView<String> pathsListView = new ListView<>( actualSourcePaths );
183 		pathsListView.getStyleClass().addAll( "multi-src-list" );
184 		pathsListView.setPrefHeight( PATH_LIST_HEIGHT );
185 		pathsListView.setPrefWidth( PATH_LIST_WIDTH );
186 
187 		// Setup add action handler
188 		final Button add = new Button( "Verzeichnis hinzufügen", Gui.createIcon( MaterialDesignP.PLUS ) );
189 		add.setOnAction( _ -> {
190 			final File dirPath = chooseDirectory( getInitialPath( pref.getSourcePaths() ), chooserTitle, primaryStage );
191 			if( dirPath != null && !actualSourcePaths.contains( dirPath.getAbsolutePath() ) ) {
192 				actualSourcePaths.add( dirPath.getAbsolutePath() );
193 				pref.setSourcePath( dirPath.toPath() );
194 			}
195 		} );
196 
197 		// Setup remove action handler
198 		final Button remove = new Button( "Entfernen", Gui.createIcon( MaterialDesignD.DELETE ) );
199 		remove.setOnAction( _ -> {
200 			final String selectedPath = pathsListView.getSelectionModel().getSelectedItem();
201 			if( selectedPath != null ) {
202 				actualSourcePaths.remove( selectedPath );
203 				pref.removeSourcePath( Paths.get( selectedPath ) );
204 			}
205 		} );
206 
207 		multiSrcBox.getChildren().addAll( multiSrcLabel, pathsListView, new HBox( 8, add, remove ) );
208 		return multiSrcBox;
209 	}
210 
211 	/**
212 	 * Constructs a single-destination path input panel configured with targeting properties.
213 	 *
214 	 * @param pref         The core configurations instance carrying structural file target references
215 	 * @param primaryStage The parent stage context tracking native file directory browser overlays
216 	 * @return A configured {@link VBox} layout component ready for application visualization
217 	 */
218 	private VBox getDestBox( final Preference pref, final Stage primaryStage ) {
219 		final String directoryName = pref.getScanMode() == ScanType.SYNCHRONIZE ? "Arbeitsverzeichnis B" : "Zielverzeichnis";
220 		final GridPane destGrid = new GridPane();
221 		destGrid.setHgap( H_GAP );
222 		final String initialString = getInitialPath( pref.getDestPaths() ).toString();
223 		final TextField destField = new TextField( initialString );
224 		destField.setPrefWidth( TEXT_FIELD_WIDTH );
225 		final Button destBtn = new Button( "Durchsuchen..." );
226 		destBtn.setOnAction( _ -> {
227 			final File initialDest = getInitialPath( pref.getDestPaths() );
228 			final File dirPath = chooseDirectory( initialDest, directoryName + " für " + pref.getScanMode().getDescription(), primaryStage );
229 			if( dirPath != null ) {
230 				destField.setText( dirPath.getAbsolutePath() );
231 				pref.setDestPaths( new ArrayList<>( List.of( dirPath.toPath() ) ) );
232 			}
233 		} );
234 		final Label label = new Label( directoryName + ":" );
235 		label.setPrefWidth( LABEL_WIDTH );
236 		destGrid.add( label, 0, 0 );
237 		destGrid.add( new HBox( 8, destField, destBtn ), 1, 0 );
238 		return new VBox( V_GAP, new Separator(), destGrid );
239 	}
240 
241 	/**
242 	 * Creates and presents a platform-native directory modal browser window.
243 	 *
244 	 * @param initialDir   The file system path representation establishing the initial viewport directory
245 	 * @param title        The descriptive display header initialized across the file system modal dialog frame
246 	 * @param primaryStage The system UI layout thread window mapping modal ownership constraints
247 	 * @return The selected {@link File} context, or {@code null} if processing was terminated by the user
248 	 */
249 	private File chooseDirectory( final File initialDir, final String title, final Stage primaryStage ) {
250 		final DirectoryChooser chooser = new DirectoryChooser();
251 		if( initialDir != null && initialDir.exists() ) chooser.setInitialDirectory( initialDir );
252 		chooser.setTitle( title );
253 		return chooser.showDialog( primaryStage );
254 	}
255 
256 	/**
257 	 * Extends lookup heuristics to safely retrieve a reliable initial navigation directory context.
258 	 * <p>
259 	 * This strategy evaluates existing records to pick the last interacted directory path location
260 	 * to support persistent scrolling memory across subsequent browser invocations.
261 	 * </p>
262 	 *
263 	 * @param paths The list of recorded historical directory path variables available for extraction
264 	 * @return A valid {@link File} instance referencing either the last list entry or the fallback default root
265 	 */
266 	private File getInitialPath( final List<Path> paths ) {
267 		return ( paths != null && !paths.isEmpty() ) ? paths.get( paths.size() - 1 ).toFile() : DEFAULT_PATH.toFile();
268 	}
269 }