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 javafx.geometry.Insets;
24  import javafx.scene.control.Button;
25  import javafx.scene.control.ContextMenu;
26  import javafx.scene.control.Label;
27  import javafx.scene.control.ListCell;
28  import javafx.scene.control.ListView;
29  import javafx.scene.control.MenuButton;
30  import javafx.scene.control.MenuItem;
31  import javafx.scene.control.SeparatorMenuItem;
32  import javafx.scene.control.Tooltip;
33  import javafx.scene.input.ClipboardContent;
34  import javafx.scene.input.Dragboard;
35  import javafx.scene.input.TransferMode;
36  import javafx.scene.layout.Priority;
37  import javafx.scene.layout.VBox;
38  
39  import org.kordamp.ikonli.javafx.FontIcon;
40  import org.kordamp.ikonli.materialdesign2.MaterialDesignC;
41  import org.kordamp.ikonli.materialdesign2.MaterialDesignD;
42  import org.kordamp.ikonli.materialdesign2.MaterialDesignF;
43  import org.kordamp.ikonli.materialdesign2.MaterialDesignH;
44  import org.kordamp.ikonli.materialdesign2.MaterialDesignI;
45  import org.kordamp.ikonli.materialdesign2.MaterialDesignP;
46  import org.kordamp.ikonli.materialdesign2.MaterialDesignS;
47  
48  import de.spiritscorp.datasync.controller.SyncJobContext;
49  import de.spiritscorp.datasync.controller.ViewController;
50  
51  /**
52   * Sidebar Navigation panel hosting the main execution links, managed tasks instances, and dynamic manipulation actions via context menus.
53   *
54   * @author Tom Spirit
55   */
56  final class SidebarView extends VBox {
57  
58  	private final ListView<SyncJobContext> sidebarListView;
59  	private final Gui mainGui;
60  	private final ViewController controller;
61  
62  	private static final String CSS_MENU_ICON = "menu-icon";
63  	private static final String CSS_JOB_ICON = "job-icon";
64  	private static final String CSS_DRAG = "drag-over-target";
65  
66  	private boolean dragAndDropEnabled = true;
67  
68  	/**
69  	 * Constructs the graphical container and links action events directly into specified controllers interfaces.
70  	 *
71  	 * @param mainGui    Application root shell container.
72  	 * @param controller Associated decoupled interaction pipeline boundary coordinator.
73  	 */
74  	SidebarView( final Gui mainGui, final ViewController controller ) {
75  		this.mainGui = mainGui;
76  		this.controller = controller;
77  		this.setSpacing( 16 );
78  		this.setPadding( new Insets( 16, 16, 24, 16 ) );
79  		this.setPrefWidth( 300 );
80  
81  		// Initialize application navigation switcher menu
82  
83  		final MenuButton hamburgerMenu = new MenuButton( "Navigation", Gui.createIcon( MaterialDesignH.HAMBURGER ) );
84  		hamburgerMenu.getGraphic().getStyleClass().addAll( CSS_MENU_ICON );
85  		hamburgerMenu.setMaxWidth( Double.MAX_VALUE );
86  
87  		final MenuItem taskViewItem = new MenuItem( "Aktiver Task-Monitor", Gui.createIcon( MaterialDesignF.FOLDER ) );
88  		taskViewItem.getGraphic().getStyleClass().addAll( CSS_MENU_ICON );
89  		taskViewItem.setOnAction( _ -> controller.handleNavigate( Gui.ViewState.MONITOR ) );
90  
91  		final MenuItem settingsItem = new MenuItem( "Erweiterte Parameter", Gui.createIcon( MaterialDesignS.STORE_SETTINGS ) );
92  		settingsItem.getGraphic().getStyleClass().addAll( CSS_MENU_ICON );
93  		settingsItem.setOnAction( _ -> controller.handleNavigate( Gui.ViewState.SETTINGS ) );
94  
95  		final MenuItem infoItem = new MenuItem( "System-Informationen", Gui.createIcon( MaterialDesignI.INFORMATION ) );
96  		infoItem.getGraphic().getStyleClass().addAll( CSS_MENU_ICON );
97  		infoItem.setOnAction( _ -> controller.handleNavigate( Gui.ViewState.INFO ) );
98  
99  		hamburgerMenu.getItems().addAll( taskViewItem, settingsItem, new SeparatorMenuItem(), infoItem );
100 
101 		// Section header label
102 		final Label sidebarTitleLabel = new Label( "VERWALTETE TASK-INSTANZEN" );
103 		sidebarTitleLabel.getStyleClass().addAll( "sidebar-title-label" );
104 
105 		// Main ListView layout for tasks mapping
106 		sidebarListView = new ListView<>( mainGui.getJobList() );
107 		setupCellFactory();
108 
109 		// Control operation triggers
110 		final Button addJobButton = new Button( "Task hinzufügen", Gui.createIcon( MaterialDesignP.PLUS ) );
111 		addJobButton.setMaxWidth( Double.MAX_VALUE );
112 		addJobButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
113 		addJobButton.getStyleClass().addAll( "add-job-button" );
114 		addJobButton.setOnAction( _ -> controller.handleCreateNewJob() );
115 
116 		final Button exitButton = new Button( "Programm beenden", Gui.createIcon( MaterialDesignP.POWER_STANDBY ) );
117 		exitButton.setMaxWidth( Double.MAX_VALUE );
118 		exitButton.getGraphic().getStyleClass().addAll( Gui.CSS_BUTTON_ICON );
119 		exitButton.getStyleClass().addAll( "exit-button" );
120 		exitButton.setOnAction( _ -> controller.handleApplicationShutdown() );
121 
122 		this.getChildren().addAll( hamburgerMenu, sidebarTitleLabel, sidebarListView, addJobButton, exitButton );
123 		setVgrow( sidebarListView, Priority.ALWAYS );
124 	}
125 
126 	/**
127 	 * Builds and styles custom Cell rendering including interactive management items.
128 	 */
129 	private void setupCellFactory() {
130 		sidebarListView.setCellFactory( _ -> {
131 			@SuppressWarnings( { "PMD.CommentRequired" } )
132 			final ListCell<SyncJobContext> cell = new ListCell<>() {
133 				private final FontIcon itemIcon = Gui.createIcon( MaterialDesignF.FOLDER );
134 				private final Tooltip toolTip = new Tooltip();
135 
136 				@Override
137 				protected void updateItem( final SyncJobContext item, final boolean empty ) {
138 					super.updateItem( item, empty );
139 					textProperty().unbind();
140 					toolTip.textProperty().unbind();
141 					if( empty || item == null ) {
142 						setText( "" );
143 						setGraphic( null );
144 						setTooltip( null );
145 					}else {
146 						textProperty().bind( item.jobNameProperty() );
147 						itemIcon.getStyleClass().addAll( CSS_JOB_ICON );
148 						setGraphic( itemIcon );
149 						toolTip.textProperty().bind( item.jobNameProperty() );
150 						setTooltip( toolTip );
151 					}
152 				}
153 			};
154 
155 			final ContextMenu contextMenu = new ContextMenu();
156 
157 			final MenuItem renameItem = new MenuItem( "Task umbenennen", Gui.createIcon( MaterialDesignS.SWAP_HORIZONTAL ) );
158 			renameItem.setOnAction( _ -> controller.handleRenameJob( cell.getItem() ) );
159 
160 			final MenuItem duplicateItem = new MenuItem( "Task duplizieren", Gui.createIcon( MaterialDesignC.CONTENT_DUPLICATE ) );
161 			duplicateItem.setOnAction( _ -> controller.handleDuplicateJob( cell.getItem() ) );
162 
163 			final MenuItem deleteItem = new MenuItem( "Task löschen", Gui.createIcon( MaterialDesignD.DELETE ) );
164 			deleteItem.setOnAction( _ -> controller.handleDeleteJob( cell.getItem() ) );
165 
166 			contextMenu.getItems().addAll( renameItem, duplicateItem, new SeparatorMenuItem(), deleteItem );
167 
168 			cell.setOnContextMenuRequested( event -> {
169 				// Guard clause: Block popup triggers on empty rows or missing models
170 				if( cell.isEmpty() || cell.getItem() == null ) {
171 					event.consume();
172 					return;
173 				}
174 
175 				// Explicitly verify if the mouse coordinates hit the bounded layout area of the graphic/text
176 				// This prevents triggers on empty spaces inside extended high-width list cells
177 				contextMenu.show( cell, event.getScreenX(), event.getScreenY() );
178 				event.consume();
179 			} );
180 
181 			// ==========================================
182 			// DRAG & DROP IMPLEMENTATION START
183 			// ==========================================
184 			cell.setOnDragDetected( event -> {
185 				// Guard clause: Block gesture if disabled or triggered on an empty row
186 				if( cell.isEmpty() || !dragAndDropEnabled ) { return; }
187 
188 				// Initiate move transfer mode and store the source index inside the dragboard
189 				final Dragboard db = cell.startDragAndDrop( TransferMode.MOVE );
190 				final ClipboardContent content = new ClipboardContent();
191 				content.putString( String.valueOf( cell.getIndex() ) );
192 				db.setContent( content );
193 
194 				event.consume();
195 			} );
196 
197 			cell.setOnDragOver( event -> {
198 				// Only accept the move if functionality is enabled and data type matches
199 				if( dragAndDropEnabled && event.getDragboard().hasString() ) {
200 					event.acceptTransferModes( TransferMode.MOVE );
201 				}
202 				event.consume();
203 			} );
204 
205 			cell.setOnDragEntered( event -> {
206 				// Apply visual feedback style when dragging an item over a valid cell
207 				if( dragAndDropEnabled && !cell.isEmpty() && event.getDragboard().hasString() ) {
208 					cell.getStyleClass().add( CSS_DRAG );
209 				}
210 				event.consume();
211 			} );
212 
213 			cell.setOnDragExited( event -> {
214 				// Clear visual feedback style immediately when leaving the cell area
215 				cell.getStyleClass().remove( CSS_DRAG );
216 				event.consume();
217 			} );
218 
219 			cell.setOnDragDropped( event -> {
220 				// Ensure style cleanup before processing data logic
221 				cell.getStyleClass().remove( CSS_DRAG );
222 
223 				if( !dragAndDropEnabled ) { return; }
224 
225 				final Dragboard db = event.getDragboard();
226 				if( db.hasString() ) {
227 					final int draggedIdx = Integer.parseInt( db.getString() );
228 
229 					// Fall back to last list index if item dropped onto an empty trailing cell
230 					int thisIdx = cell.isEmpty() ? sidebarListView.getItems().size() - 1 : cell.getIndex();
231 					if( thisIdx < 0 ) thisIdx = 0;
232 					if( draggedIdx < thisIdx ) thisIdx -= 1;
233 					if( draggedIdx != thisIdx && draggedIdx >= 0 ) {
234 						controller.handleDragJob( thisIdx, draggedIdx );
235 						// Immediately restore focus and selection onto the moved element
236 						sidebarListView.getSelectionModel().select( thisIdx );
237 						event.setDropCompleted( true );
238 					}
239 				}
240 				event.consume();
241 			} );
242 			// ==========================================
243 			// DRAG & DROP IMPLEMENTATION END
244 			// ==========================================
245 
246 			return cell;
247 		} );
248 
249 		sidebarListView.getSelectionModel().selectedItemProperty().addListener( ( _, oldVal, newVal ) -> {
250 			if( newVal != null && !newVal.equals( oldVal ) ) mainGui.setCurrentActiveJob( newVal );
251 		} );
252 	}
253 
254 	/**
255 	 * @return The underlying task selection view platform.
256 	 */
257 	ListView<SyncJobContext> getSidebarListView() { return sidebarListView; }
258 }