View Javadoc
1   package org.argeo.eclipse.ui.fs;
2   
3   import java.io.IOException;
4   import java.nio.file.DirectoryStream;
5   import java.nio.file.Files;
6   import java.nio.file.Path;
7   import java.util.LinkedHashMap;
8   
9   import org.apache.commons.logging.Log;
10  import org.apache.commons.logging.LogFactory;
11  import org.argeo.eclipse.ui.EclipseUiUtils;
12  import org.eclipse.jface.viewers.ISelectionChangedListener;
13  import org.eclipse.jface.viewers.IStructuredSelection;
14  import org.eclipse.jface.viewers.SelectionChangedEvent;
15  import org.eclipse.jface.viewers.StructuredSelection;
16  import org.eclipse.swt.SWT;
17  import org.eclipse.swt.custom.SashForm;
18  import org.eclipse.swt.custom.ScrolledComposite;
19  import org.eclipse.swt.events.ControlAdapter;
20  import org.eclipse.swt.events.ControlEvent;
21  import org.eclipse.swt.events.KeyEvent;
22  import org.eclipse.swt.events.KeyListener;
23  import org.eclipse.swt.events.ModifyEvent;
24  import org.eclipse.swt.events.ModifyListener;
25  import org.eclipse.swt.graphics.Rectangle;
26  import org.eclipse.swt.layout.GridData;
27  import org.eclipse.swt.layout.GridLayout;
28  import org.eclipse.swt.widgets.Composite;
29  import org.eclipse.swt.widgets.Control;
30  import org.eclipse.swt.widgets.Label;
31  import org.eclipse.swt.widgets.Table;
32  import org.eclipse.swt.widgets.Text;
33  
34  /** Simple UI provider that populates a composite parent given a NIO path */
35  public class AdvancedFsBrowser {
36  	private final static Log log = LogFactory.getLog(AdvancedFsBrowser.class);
37  
38  	// Some local constants to experiment. should be cleaned
39  	// private final static int THUMBNAIL_WIDTH = 400;
40  	// private Point imageWidth = new Point(250, 0);
41  	private final static int COLUMN_WIDTH = 160;
42  
43  	private Path initialPath;
44  	private Path currEdited;
45  	// Filter
46  	private Composite displayBoxCmp;
47  	private Text parentPathTxt;
48  	private Text filterTxt;
49  	// Browser columns
50  	private ScrolledComposite scrolledCmp;
51  	// Keep a cache of the opened directories
52  	private LinkedHashMap<Path, FilterEntitiesVirtualTable> browserCols = new LinkedHashMap<>();
53  	private Composite scrolledCmpBody;
54  
55  	public Control createUi(Composite parent, Path basePath) {
56  		if (basePath == null)
57  			throw new IllegalArgumentException("Context cannot be null");
58  		parent.setLayout(new GridLayout());
59  
60  		// top filter
61  		Composite filterCmp = new Composite(parent, SWT.NO_FOCUS);
62  		filterCmp.setLayoutData(EclipseUiUtils.fillWidth());
63  		addFilterPanel(filterCmp);
64  
65  		// Bottom part a sash with browser on the left
66  		SashForm form = new SashForm(parent, SWT.HORIZONTAL);
67  		// form.setLayout(new FillLayout());
68  		form.setLayoutData(EclipseUiUtils.fillAll());
69  		Composite leftCmp = new Composite(form, SWT.NO_FOCUS);
70  		displayBoxCmp = new Composite(form, SWT.NONE);
71  		form.setWeights(new int[] { 3, 1 });
72  
73  		createBrowserPart(leftCmp, basePath);
74  		// leftCmp.addControlListener(new ControlAdapter() {
75  		// @Override
76  		// public void controlResized(ControlEvent e) {
77  		// Rectangle r = leftCmp.getClientArea();
78  		// log.warn("Browser resized: " + r.toString());
79  		// scrolledCmp.setMinSize(browserCols.size() * (COLUMN_WIDTH + 2),
80  		// SWT.DEFAULT);
81  		// // scrolledCmp.setMinSize(scrolledCmpBody.computeSize(SWT.DEFAULT,
82  		// // r.height));
83  		// }
84  		// });
85  
86  		populateCurrEditedDisplay(displayBoxCmp, basePath);
87  
88  		// INIT
89  		setEdited(basePath);
90  		initialPath = basePath;
91  		// form.layout(true, true);
92  		return parent;
93  	}
94  
95  	private void createBrowserPart(Composite parent, Path context) {
96  		parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
97  
98  		// scrolled composite
99  		scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS);
100 		scrolledCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
101 		scrolledCmp.setExpandVertical(true);
102 		scrolledCmp.setExpandHorizontal(true);
103 		scrolledCmp.setShowFocusedControl(true);
104 
105 		scrolledCmpBody = new Composite(scrolledCmp, SWT.NO_FOCUS);
106 		scrolledCmp.setContent(scrolledCmpBody);
107 		scrolledCmpBody.addControlListener(new ControlAdapter() {
108 			private static final long serialVersionUID = 183238447102854553L;
109 
110 			@Override
111 			public void controlResized(ControlEvent e) {
112 				Rectangle r = scrolledCmp.getClientArea();
113 				scrolledCmp.setMinSize(scrolledCmpBody.computeSize(SWT.DEFAULT, r.height));
114 			}
115 		});
116 		initExplorer(scrolledCmpBody, context);
117 		scrolledCmpBody.layout(true, true);
118 		scrolledCmp.layout();
119 
120 	}
121 
122 	private Control initExplorer(Composite parent, Path context) {
123 		parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
124 		return createBrowserColumn(parent, context);
125 	}
126 
127 	private Control createBrowserColumn(Composite parent, Path context) {
128 		// TODO style is not correctly managed.
129 		FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context);
130 		// CmsUtils.style(table, ArgeoOrgStyle.browserColumn.style());
131 		table.filterList("*");
132 		table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
133 		browserCols.put(context, table);
134 		parent.layout(true, true);
135 		return table;
136 	}
137 
138 	public void addFilterPanel(Composite parent) {
139 		parent.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false)));
140 
141 		parentPathTxt = new Text(parent, SWT.NO_FOCUS);
142 		parentPathTxt.setEditable(false);
143 
144 		filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
145 		filterTxt.setMessage("Filter current list");
146 		filterTxt.setLayoutData(EclipseUiUtils.fillWidth());
147 		filterTxt.addModifyListener(new ModifyListener() {
148 			private static final long serialVersionUID = 1L;
149 
150 			public void modifyText(ModifyEvent event) {
151 				modifyFilter(false);
152 			}
153 		});
154 		filterTxt.addKeyListener(new KeyListener() {
155 			private static final long serialVersionUID = 2533535233583035527L;
156 
157 			@Override
158 			public void keyReleased(KeyEvent e) {
159 			}
160 
161 			@Override
162 			public void keyPressed(KeyEvent e) {
163 				boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
164 				// boolean altPressed = (e.stateMask & SWT.ALT) != 0;
165 				FilterEntitiesVirtualTable currTable = null;
166 				if (currEdited != null) {
167 					FilterEntitiesVirtualTable table = browserCols.get(currEdited);
168 					if (table != null && !table.isDisposed())
169 						currTable = table;
170 				}
171 
172 				if (e.keyCode == SWT.ARROW_DOWN)
173 					currTable.setFocus();
174 				else if (e.keyCode == SWT.BS) {
175 					if (filterTxt.getText().equals("")
176 							&& !(currEdited.getNameCount() == 1 || currEdited.equals(initialPath))) {
177 						Path oldEdited = currEdited;
178 						Path parentPath = currEdited.getParent();
179 						setEdited(parentPath);
180 						if (browserCols.containsKey(parentPath))
181 							browserCols.get(parentPath).setSelected(oldEdited);
182 						filterTxt.setFocus();
183 						e.doit = false;
184 					}
185 				} else if (e.keyCode == SWT.TAB && !shiftPressed) {
186 					Path uniqueChild = getOnlyChild(currEdited, filterTxt.getText());
187 					if (uniqueChild != null) {
188 						// Highlight the unique chosen child
189 						currTable.setSelected(uniqueChild);
190 						setEdited(uniqueChild);
191 					}
192 					filterTxt.setFocus();
193 					e.doit = false;
194 				}
195 			}
196 		});
197 	}
198 
199 	private Path getOnlyChild(Path parent, String filter) {
200 		try (DirectoryStream<Path> stream = Files.newDirectoryStream(currEdited, filter + "*")) {
201 			Path uniqueChild = null;
202 			boolean moreThanOne = false;
203 			loop: for (Path entry : stream) {
204 				if (uniqueChild == null) {
205 					uniqueChild = entry;
206 				} else {
207 					moreThanOne = true;
208 					break loop;
209 				}
210 			}
211 			if (!moreThanOne)
212 				return uniqueChild;
213 			return null;
214 		} catch (IOException ioe) {
215 			throw new FsUiException(
216 					"Unable to determine unique child existence and get it under " + parent + " with filter " + filter,
217 					ioe);
218 		}
219 	}
220 
221 	private void setEdited(Path path) {
222 		currEdited = path;
223 		EclipseUiUtils.clear(displayBoxCmp);
224 		populateCurrEditedDisplay(displayBoxCmp, currEdited);
225 		refreshFilters(path);
226 		refreshBrowser(path);
227 	}
228 
229 	private void refreshFilters(Path path) {
230 		parentPathTxt.setText(path.toUri().toString());
231 		filterTxt.setText("");
232 		filterTxt.getParent().layout();
233 	}
234 
235 	private void refreshBrowser(Path currPath) {
236 		Path currParPath = currPath.getParent();
237 		Object[][] colMatrix = new Object[browserCols.size()][2];
238 
239 		int i = 0, currPathIndex = -1, lastLeftOpenedIndex = -1;
240 		for (Path path : browserCols.keySet()) {
241 			colMatrix[i][0] = path;
242 			colMatrix[i][1] = browserCols.get(path);
243 			if (currPathIndex >= 0 && lastLeftOpenedIndex < 0 && currParPath != null) {
244 				boolean leaveOpened = path.startsWith(currPath);
245 				if (!leaveOpened)
246 					lastLeftOpenedIndex = i;
247 			}
248 			if (currParPath.equals(path))
249 				currPathIndex = i;
250 			i++;
251 		}
252 
253 		if (currPathIndex >= 0 && lastLeftOpenedIndex >= 0) {
254 			// dispose and remove useless cols
255 			for (int l = i - 1; l >= lastLeftOpenedIndex; l--) {
256 				((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose();
257 				browserCols.remove(colMatrix[l][0]);
258 			}
259 		}
260 
261 		if (browserCols.containsKey(currPath)) {
262 			FilterEntitiesVirtualTable currCol = browserCols.get(currPath);
263 			if (currCol.isDisposed()) {
264 				// Does it still happen ?
265 				log.warn(currPath + " browser column was disposed and still listed");
266 				browserCols.remove(currPath);
267 			}
268 		}
269 
270 		if (!browserCols.containsKey(currPath) && Files.isDirectory(currPath))
271 			createBrowserColumn(scrolledCmpBody, currPath);
272 
273 		scrolledCmpBody.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false)));
274 		scrolledCmpBody.layout(true, true);
275 		// also resize the scrolled composite
276 		scrolledCmp.layout();
277 	}
278 
279 	private void modifyFilter(boolean fromOutside) {
280 		if (!fromOutside)
281 			if (currEdited != null) {
282 				String filter = filterTxt.getText() + "*";
283 				FilterEntitiesVirtualTable table = browserCols.get(currEdited);
284 				if (table != null && !table.isDisposed())
285 					table.filterList(filter);
286 			}
287 	}
288 
289 	/**
290 	 * Recreates the content of the box that displays information about the current
291 	 * selected node.
292 	 */
293 	private void populateCurrEditedDisplay(Composite parent, Path context) {
294 		parent.setLayout(new GridLayout());
295 
296 		// if (isImg(context)) {
297 		// EditableImage image = new Img(parent, RIGHT, context, imageWidth);
298 		// image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false,
299 		// 2, 1));
300 		// }
301 
302 		try {
303 			Label contextL = new Label(parent, SWT.NONE);
304 			contextL.setText(context.getFileName().toString());
305 			contextL.setFont(EclipseUiUtils.getBoldFont(parent));
306 			addProperty(parent, "Last modified", Files.getLastModifiedTime(context).toString());
307 			addProperty(parent, "Owner", Files.getOwner(context).getName());
308 			if (Files.isDirectory(context)) {
309 				addProperty(parent, "Type", "Folder");
310 			} else {
311 				String mimeType = Files.probeContentType(context);
312 				if (EclipseUiUtils.isEmpty(mimeType))
313 					mimeType = "<i>Unknown</i>";
314 				addProperty(parent, "Type", mimeType);
315 				addProperty(parent, "Size", FsUiUtils.humanReadableByteCount(Files.size(context), false));
316 			}
317 			parent.layout(true, true);
318 		} catch (IOException e) {
319 			throw new FsUiException("Cannot display details for " + context, e);
320 		}
321 	}
322 
323 	private void addProperty(Composite parent, String propName, String value) {
324 		Label contextL = new Label(parent, SWT.NONE);
325 		contextL.setText(propName + ": " + value);
326 	}
327 
328 	/**
329 	 * Almost canonical implementation of a table that displays the content of a
330 	 * directory
331 	 */
332 	private class FilterEntitiesVirtualTable extends Composite {
333 		private static final long serialVersionUID = 2223410043691844875L;
334 
335 		// Context
336 		private Path context;
337 		private Path currSelected = null;
338 
339 		// UI Objects
340 		private FsTableViewer viewer;
341 
342 		@Override
343 		public boolean setFocus() {
344 			if (viewer.getTable().isDisposed())
345 				return false;
346 			if (currSelected != null)
347 				viewer.setSelection(new StructuredSelection(currSelected), true);
348 			else if (viewer.getSelection().isEmpty()) {
349 				Object first = viewer.getElementAt(0);
350 				if (first != null)
351 					viewer.setSelection(new StructuredSelection(first), true);
352 			}
353 			return viewer.getTable().setFocus();
354 		}
355 
356 		/**
357 		 * Enable highlighting the correct element in the table when externally browsing
358 		 * (typically via the command-line-like Text field)
359 		 */
360 		void setSelected(Path selected) {
361 			// to prevent change selection event to be thrown
362 			currSelected = selected;
363 			viewer.setSelection(new StructuredSelection(currSelected), true);
364 		}
365 
366 		void filterList(String filter) {
367 			viewer.setInput(context, filter);
368 		}
369 
370 		public FilterEntitiesVirtualTable(Composite parent, int style, Path context) {
371 			super(parent, SWT.NO_FOCUS);
372 			this.context = context;
373 			createTableViewer(this);
374 		}
375 
376 		private void createTableViewer(final Composite parent) {
377 			parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
378 
379 			// We must limit the size of the table otherwise the full list is
380 			// loaded before the layout happens
381 			// Composite listCmp = new Composite(parent, SWT.NO_FOCUS);
382 			// GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true);
383 			// gd.widthHint = COLUMN_WIDTH;
384 			// listCmp.setLayoutData(gd);
385 			// listCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
386 			// viewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.MULTI |
387 			// SWT.V_SCROLL);
388 			// Table table = viewer.getTable();
389 			// table.setLayoutData(EclipseUiUtils.fillAll());
390 
391 			viewer = new FsTableViewer(parent, SWT.MULTI);
392 			Table table = viewer.configureDefaultSingleColumnTable(COLUMN_WIDTH);
393 
394 			viewer.addSelectionChangedListener(new ISelectionChangedListener() {
395 
396 				@Override
397 				public void selectionChanged(SelectionChangedEvent event) {
398 					IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
399 					if (selection.isEmpty())
400 						return;
401 					Object obj = selection.getFirstElement();
402 					Path newSelected;
403 					if (obj instanceof Path)
404 						newSelected = (Path) obj;
405 					else if (obj instanceof ParentDir)
406 						newSelected = ((ParentDir) obj).getPath();
407 					else
408 						return;
409 					if (newSelected.equals(currSelected))
410 						return;
411 					currSelected = newSelected;
412 					setEdited(newSelected);
413 
414 				}
415 			});
416 
417 			table.addKeyListener(new KeyListener() {
418 				private static final long serialVersionUID = -8083424284436715709L;
419 
420 				@Override
421 				public void keyReleased(KeyEvent e) {
422 				}
423 
424 				@Override
425 				public void keyPressed(KeyEvent e) {
426 					IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
427 					Path selected = null;
428 					if (!selection.isEmpty())
429 						selected = ((Path) selection.getFirstElement());
430 					if (e.keyCode == SWT.ARROW_RIGHT) {
431 						if (!Files.isDirectory(selected))
432 							return;
433 						if (selected != null) {
434 							setEdited(selected);
435 							browserCols.get(selected).setFocus();
436 						}
437 					} else if (e.keyCode == SWT.ARROW_LEFT) {
438 						if (context.equals(initialPath))
439 							return;
440 						Path parent = context.getParent();
441 						if (parent == null)
442 							return;
443 
444 						setEdited(parent);
445 						browserCols.get(parent).setFocus();
446 					}
447 				}
448 			});
449 		}
450 	}
451 }