View Javadoc
1   package org.argeo.cms.e4.maintenance;
2   
3   import static org.eclipse.swt.SWT.RIGHT;
4   
5   import java.text.DateFormat;
6   import java.text.SimpleDateFormat;
7   import java.util.LinkedHashMap;
8   
9   import javax.jcr.ItemNotFoundException;
10  import javax.jcr.Node;
11  import javax.jcr.NodeIterator;
12  import javax.jcr.Property;
13  import javax.jcr.PropertyIterator;
14  import javax.jcr.PropertyType;
15  import javax.jcr.RepositoryException;
16  import javax.jcr.Value;
17  
18  import org.argeo.cms.CmsException;
19  import org.argeo.cms.ui.CmsUiProvider;
20  import org.argeo.cms.ui.util.CmsLink;
21  import org.argeo.cms.ui.util.CmsUiUtils;
22  import org.argeo.cms.ui.widgets.EditableImage;
23  import org.argeo.cms.ui.widgets.Img;
24  import org.argeo.jcr.JcrUtils;
25  import org.eclipse.jface.viewers.ColumnLabelProvider;
26  import org.eclipse.jface.viewers.ILazyContentProvider;
27  import org.eclipse.jface.viewers.ISelectionChangedListener;
28  import org.eclipse.jface.viewers.IStructuredSelection;
29  import org.eclipse.jface.viewers.SelectionChangedEvent;
30  import org.eclipse.jface.viewers.StructuredSelection;
31  import org.eclipse.jface.viewers.TableViewer;
32  import org.eclipse.jface.viewers.TableViewerColumn;
33  import org.eclipse.jface.viewers.Viewer;
34  import org.eclipse.swt.SWT;
35  import org.eclipse.swt.custom.ScrolledComposite;
36  import org.eclipse.swt.events.ControlAdapter;
37  import org.eclipse.swt.events.ControlEvent;
38  import org.eclipse.swt.events.KeyEvent;
39  import org.eclipse.swt.events.KeyListener;
40  import org.eclipse.swt.events.ModifyEvent;
41  import org.eclipse.swt.events.ModifyListener;
42  import org.eclipse.swt.graphics.Point;
43  import org.eclipse.swt.graphics.Rectangle;
44  import org.eclipse.swt.layout.GridData;
45  import org.eclipse.swt.layout.GridLayout;
46  import org.eclipse.swt.widgets.Composite;
47  import org.eclipse.swt.widgets.Control;
48  import org.eclipse.swt.widgets.Label;
49  import org.eclipse.swt.widgets.Table;
50  import org.eclipse.swt.widgets.TableColumn;
51  import org.eclipse.swt.widgets.Text;
52  
53  public class Browse implements CmsUiProvider {
54  
55  	// Some local constants to experiment. should be cleaned
56  	private final static String BROWSE_PREFIX = "browse#";
57  	private final static int THUMBNAIL_WIDTH = 400;
58  	private final static int COLUMN_WIDTH = 160;
59  	private DateFormat timeFormatter = new SimpleDateFormat("dd-MM-yyyy', 'HH:mm");
60  
61  	// keep a cache of the opened nodes
62  	// Key is the path
63  	private LinkedHashMap<String, FilterEntitiesVirtualTable> browserCols = new LinkedHashMap<String, Browse.FilterEntitiesVirtualTable>();
64  	private Composite nodeDisplayParent;
65  	private Composite colViewer;
66  	private ScrolledComposite scrolledCmp;
67  	private Text parentPathTxt;
68  	private Text filterTxt;
69  	private Node currEdited;
70  
71  	private String initialPath;
72  
73  	@Override
74  	public Control createUi(Composite parent, Node context) throws RepositoryException {
75  		if (context == null)
76  			// return null;
77  			throw new CmsException("Context cannot be null");
78  		GridLayout layout = CmsUiUtils.noSpaceGridLayout();
79  		layout.numColumns = 2;
80  		parent.setLayout(layout);
81  
82  		// Left
83  		Composite leftCmp = new Composite(parent, SWT.NO_FOCUS);
84  		leftCmp.setLayoutData(CmsUiUtils.fillAll());
85  		createBrowserPart(leftCmp, context);
86  
87  		// Right
88  		nodeDisplayParent = new Composite(parent, SWT.NO_FOCUS | SWT.BORDER);
89  		GridData gd = new GridData(SWT.RIGHT, SWT.FILL, false, true);
90  		gd.widthHint = THUMBNAIL_WIDTH;
91  		nodeDisplayParent.setLayoutData(gd);
92  		createNodeView(nodeDisplayParent, context);
93  
94  		// INIT
95  		setEdited(context);
96  		initialPath = context.getPath();
97  
98  		// Workaround we don't yet manage the delete to display parent of the
99  		// initial context node
100 
101 		return null;
102 	}
103 
104 	private void createBrowserPart(Composite parent, Node context) throws RepositoryException {
105 		GridLayout layout = CmsUiUtils.noSpaceGridLayout();
106 		parent.setLayout(layout);
107 		Composite filterCmp = new Composite(parent, SWT.NO_FOCUS);
108 		filterCmp.setLayoutData(CmsUiUtils.fillWidth());
109 
110 		// top filter
111 		addFilterPanel(filterCmp);
112 
113 		// scrolled composite
114 		scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS);
115 		scrolledCmp.setLayoutData(CmsUiUtils.fillAll());
116 		scrolledCmp.setExpandVertical(true);
117 		scrolledCmp.setExpandHorizontal(true);
118 		scrolledCmp.setShowFocusedControl(true);
119 
120 		colViewer = new Composite(scrolledCmp, SWT.NO_FOCUS);
121 		scrolledCmp.setContent(colViewer);
122 		scrolledCmp.addControlListener(new ControlAdapter() {
123 			private static final long serialVersionUID = 6589392045145698201L;
124 
125 			@Override
126 			public void controlResized(ControlEvent e) {
127 				Rectangle r = scrolledCmp.getClientArea();
128 				scrolledCmp.setMinSize(colViewer.computeSize(SWT.DEFAULT, r.height));
129 			}
130 		});
131 		initExplorer(colViewer, context);
132 	}
133 
134 	private Control initExplorer(Composite parent, Node context) throws RepositoryException {
135 		parent.setLayout(CmsUiUtils.noSpaceGridLayout());
136 		createBrowserColumn(parent, context);
137 		return null;
138 	}
139 
140 	private Control createBrowserColumn(Composite parent, Node context) throws RepositoryException {
141 		// TODO style is not correctly managed.
142 		FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context);
143 		// CmsUiUtils.style(table, ArgeoOrgStyle.browserColumn.style());
144 		table.filterList("*");
145 		table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
146 		browserCols.put(context.getPath(), table);
147 		return null;
148 	}
149 
150 	public void addFilterPanel(Composite parent) {
151 
152 		parent.setLayout(CmsUiUtils.noSpaceGridLayout(new GridLayout(2, false)));
153 
154 		// Text Area for the filter
155 		parentPathTxt = new Text(parent, SWT.NO_FOCUS);
156 		parentPathTxt.setEditable(false);
157 		filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
158 		filterTxt.setMessage("Filter current list");
159 		filterTxt.setLayoutData(CmsUiUtils.fillWidth());
160 		filterTxt.addModifyListener(new ModifyListener() {
161 			private static final long serialVersionUID = 7709303319740056286L;
162 
163 			public void modifyText(ModifyEvent event) {
164 				modifyFilter(false);
165 			}
166 		});
167 
168 		filterTxt.addKeyListener(new KeyListener() {
169 			private static final long serialVersionUID = -4523394262771183968L;
170 
171 			@Override
172 			public void keyReleased(KeyEvent e) {
173 			}
174 
175 			@Override
176 			public void keyPressed(KeyEvent e) {
177 				boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
178 				// boolean altPressed = (e.stateMask & SWT.ALT) != 0;
179 				FilterEntitiesVirtualTable currTable = null;
180 				if (currEdited != null) {
181 					FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited));
182 					if (table != null && !table.isDisposed())
183 						currTable = table;
184 				}
185 
186 				try {
187 					if (e.keyCode == SWT.ARROW_DOWN)
188 						currTable.setFocus();
189 					else if (e.keyCode == SWT.BS) {
190 						if (filterTxt.getText().equals("")
191 								&& !(getPath(currEdited).equals("/") || getPath(currEdited).equals(initialPath))) {
192 							setEdited(currEdited.getParent());
193 							e.doit = false;
194 							filterTxt.setFocus();
195 						}
196 					} else if (e.keyCode == SWT.TAB && !shiftPressed) {
197 						if (currEdited.getNodes(filterTxt.getText() + "*").getSize() == 1) {
198 							setEdited(currEdited.getNodes(filterTxt.getText() + "*").nextNode());
199 						}
200 						filterTxt.setFocus();
201 						e.doit = false;
202 					}
203 				} catch (RepositoryException e1) {
204 					throw new CmsException("Unexpected error in key management for " + currEdited + "with filter "
205 							+ filterTxt.getText(), e1);
206 				}
207 
208 			}
209 		});
210 	}
211 
212 	private void setEdited(Node node) {
213 		try {
214 			currEdited = node;
215 			CmsUiUtils.clear(nodeDisplayParent);
216 			createNodeView(nodeDisplayParent, currEdited);
217 			nodeDisplayParent.layout();
218 			refreshFilters(node);
219 			refreshBrowser(node);
220 		} catch (RepositoryException re) {
221 			throw new CmsException("Unable to update browser for " + node, re);
222 		}
223 	}
224 
225 	private void refreshFilters(Node node) throws RepositoryException {
226 		String currNodePath = node.getPath();
227 		parentPathTxt.setText(currNodePath);
228 		filterTxt.setText("");
229 		filterTxt.getParent().layout();
230 	}
231 
232 	private void refreshBrowser(Node node) throws RepositoryException {
233 
234 		// Retrieve
235 		String currNodePath = node.getPath();
236 		String currParPath = "";
237 		if (!"/".equals(currNodePath))
238 			currParPath = JcrUtils.parentPath(currNodePath);
239 		if ("".equals(currParPath))
240 			currParPath = "/";
241 
242 		Object[][] colMatrix = new Object[browserCols.size()][2];
243 
244 		int i = 0, j = -1, k = -1;
245 		for (String path : browserCols.keySet()) {
246 			colMatrix[i][0] = path;
247 			colMatrix[i][1] = browserCols.get(path);
248 			if (j >= 0 && k < 0 && !currNodePath.equals("/")) {
249 				boolean leaveOpened = path.startsWith(currNodePath);
250 
251 				// workaround for same name siblings
252 				// fix me weird side effect when we go left or click on anb
253 				// already selected, unfocused node
254 				if (leaveOpened && (path.lastIndexOf("/") == 0 && currNodePath.lastIndexOf("/") == 0
255 						|| JcrUtils.parentPath(path).equals(JcrUtils.parentPath(currNodePath))))
256 					leaveOpened = JcrUtils.lastPathElement(path).equals(JcrUtils.lastPathElement(currNodePath));
257 
258 				if (!leaveOpened)
259 					k = i;
260 			}
261 			if (currParPath.equals(path))
262 				j = i;
263 			i++;
264 		}
265 
266 		if (j >= 0 && k >= 0)
267 			// remove useless cols
268 			for (int l = i - 1; l >= k; l--) {
269 				browserCols.remove(colMatrix[l][0]);
270 				((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose();
271 			}
272 
273 		// Remove disposed columns
274 		// TODO investigate and fix the mechanism that leave them there after
275 		// disposal
276 		if (browserCols.containsKey(currNodePath)) {
277 			FilterEntitiesVirtualTable currCol = browserCols.get(currNodePath);
278 			if (currCol.isDisposed())
279 				browserCols.remove(currNodePath);
280 		}
281 
282 		if (!browserCols.containsKey(currNodePath))
283 			createBrowserColumn(colViewer, node);
284 
285 		colViewer.setLayout(CmsUiUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false)));
286 		// colViewer.pack();
287 		colViewer.layout();
288 		// also resize the scrolled composite
289 		scrolledCmp.layout();
290 		scrolledCmp.getShowFocusedControl();
291 		// colViewer.getParent().layout();
292 		// if (JcrUtils.parentPath(currNodePath).equals(currBrowserKey)) {
293 		// } else {
294 		// }
295 	}
296 
297 	private void modifyFilter(boolean fromOutside) {
298 		if (!fromOutside)
299 			if (currEdited != null) {
300 				String filter = filterTxt.getText() + "*";
301 				FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited));
302 				if (table != null && !table.isDisposed())
303 					table.filterList(filter);
304 			}
305 
306 	}
307 
308 	private String getPath(Node node) {
309 		try {
310 			return node.getPath();
311 		} catch (RepositoryException e) {
312 			throw new CmsException("Unable to get path for node " + node, e);
313 		}
314 	}
315 
316 	private Point imageWidth = new Point(250, 0);
317 
318 	/**
319 	 * Recreates the content of the box that displays information about the current
320 	 * selected node.
321 	 */
322 	private Control createNodeView(Composite parent, Node context) throws RepositoryException {
323 
324 		parent.setLayout(new GridLayout(2, false));
325 
326 		if (isImg(context)) {
327 			EditableImage image = new Img(parent, RIGHT, context, imageWidth);
328 			image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 2, 1));
329 		}
330 
331 		// Name and primary type
332 		Label contextL = new Label(parent, SWT.NONE);
333 		CmsUiUtils.markup(contextL);
334 		contextL.setText("<b>" + context.getName() + "</b>");
335 		new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType().getName());
336 
337 		// Children
338 		for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) {
339 			Node child = nIt.nextNode();
340 			new CmsLink(child.getName(), BROWSE_PREFIX + child.getPath()).createUi(parent, context);
341 			new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType().getName());
342 		}
343 
344 		// Properties
345 		for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) {
346 			Property property = pIt.nextProperty();
347 			Label label = new Label(parent, SWT.NONE);
348 			label.setText(property.getName());
349 			label.setToolTipText(JcrUtils.getPropertyDefinitionAsString(property));
350 			new Label(parent, SWT.NONE).setText(getPropAsString(property));
351 		}
352 
353 		return null;
354 	}
355 
356 	private boolean isImg(Node node) throws RepositoryException {
357 		// TODO support images
358 		return false;
359 //		return node.hasNode(JCR_CONTENT) && node.isNodeType(CmsTypes.CMS_IMAGE);
360 	}
361 
362 	private String getPropAsString(Property property) throws RepositoryException {
363 		String result = "";
364 		if (property.isMultiple()) {
365 			result = getMultiAsString(property, ", ");
366 		} else {
367 			Value value = property.getValue();
368 			if (value.getType() == PropertyType.BINARY)
369 				result = "<binary>";
370 			else if (value.getType() == PropertyType.DATE)
371 				result = timeFormatter.format(value.getDate().getTime());
372 			else
373 				result = value.getString();
374 		}
375 		return result;
376 	}
377 
378 	private String getMultiAsString(Property property, String separator) throws RepositoryException {
379 		if (separator == null)
380 			separator = "; ";
381 		Value[] values = property.getValues();
382 		StringBuilder builder = new StringBuilder();
383 		for (Value val : values) {
384 			String currStr = val.getString();
385 			if (!"".equals(currStr.trim()))
386 				builder.append(currStr).append(separator);
387 		}
388 		if (builder.lastIndexOf(separator) >= 0)
389 			return builder.substring(0, builder.length() - separator.length());
390 		else
391 			return builder.toString();
392 	}
393 
394 	/** Almost canonical implementation of a table that display entities */
395 	private class FilterEntitiesVirtualTable extends Composite {
396 		private static final long serialVersionUID = 8798147431706283824L;
397 
398 		// Context
399 		private Node context;
400 
401 		// UI Objects
402 		private TableViewer entityViewer;
403 
404 		// enable management of multiple columns
405 		Node getNode() {
406 			return context;
407 		}
408 
409 		@Override
410 		public boolean setFocus() {
411 			if (entityViewer.getTable().isDisposed())
412 				return false;
413 			if (entityViewer.getSelection().isEmpty()) {
414 				Object first = entityViewer.getElementAt(0);
415 				if (first != null) {
416 					entityViewer.setSelection(new StructuredSelection(first), true);
417 				}
418 			}
419 			return entityViewer.getTable().setFocus();
420 		}
421 
422 		void filterList(String filter) {
423 			try {
424 				NodeIterator nit = context.getNodes(filter);
425 				refreshFilteredList(nit);
426 			} catch (RepositoryException e) {
427 				throw new CmsException("Unable to filter " + getNode() + " children with filter " + filter, e);
428 			}
429 
430 		}
431 
432 		public FilterEntitiesVirtualTable(Composite parent, int style, Node context) {
433 			super(parent, SWT.NO_FOCUS);
434 			this.context = context;
435 			populate();
436 		}
437 
438 		protected void populate() {
439 			Composite parent = this;
440 			GridLayout layout = CmsUiUtils.noSpaceGridLayout();
441 
442 			this.setLayout(layout);
443 			createTableViewer(parent);
444 		}
445 
446 		private void createTableViewer(final Composite parent) {
447 			// the list
448 			// We must limit the size of the table otherwise the full list is
449 			// loaded
450 			// before the layout happens
451 			Composite listCmp = new Composite(parent, SWT.NO_FOCUS);
452 			GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true);
453 			gd.widthHint = COLUMN_WIDTH;
454 			listCmp.setLayoutData(gd);
455 			listCmp.setLayout(CmsUiUtils.noSpaceGridLayout());
456 
457 			entityViewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.SINGLE);
458 			Table table = entityViewer.getTable();
459 
460 			table.setLayoutData(CmsUiUtils.fillAll());
461 			table.setLinesVisible(true);
462 			table.setHeaderVisible(false);
463 			CmsUiUtils.markup(table);
464 
465 			CmsUiUtils.style(table, MaintenanceStyles.BROWSER_COLUMN);
466 
467 			// first column
468 			TableViewerColumn column = new TableViewerColumn(entityViewer, SWT.NONE);
469 			TableColumn tcol = column.getColumn();
470 			tcol.setWidth(COLUMN_WIDTH);
471 			tcol.setResizable(true);
472 			column.setLabelProvider(new SimpleNameLP());
473 
474 			entityViewer.setContentProvider(new MyLazyCP(entityViewer));
475 			entityViewer.addSelectionChangedListener(new ISelectionChangedListener() {
476 
477 				@Override
478 				public void selectionChanged(SelectionChangedEvent event) {
479 					IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection();
480 					if (selection.isEmpty())
481 						return;
482 					else
483 						setEdited((Node) selection.getFirstElement());
484 
485 				}
486 			});
487 
488 			table.addKeyListener(new KeyListener() {
489 				private static final long serialVersionUID = -330694313896036230L;
490 
491 				@Override
492 				public void keyReleased(KeyEvent e) {
493 				}
494 
495 				@Override
496 				public void keyPressed(KeyEvent e) {
497 
498 					IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection();
499 					Node selected = null;
500 					if (!selection.isEmpty())
501 						selected = ((Node) selection.getFirstElement());
502 					try {
503 						if (e.keyCode == SWT.ARROW_RIGHT) {
504 							if (selected != null) {
505 								setEdited(selected);
506 								browserCols.get(selected.getPath()).setFocus();
507 							}
508 						} else if (e.keyCode == SWT.ARROW_LEFT) {
509 							try {
510 								selected = getNode().getParent();
511 								String newPath = selected.getPath(); // getNode().getParent()
512 								setEdited(selected);
513 								if (browserCols.containsKey(newPath))
514 									browserCols.get(newPath).setFocus();
515 							} catch (ItemNotFoundException ie) {
516 								// root silent
517 							}
518 						}
519 					} catch (RepositoryException ie) {
520 						throw new CmsException("Error while managing arrow " + "events in the browser for " + selected,
521 								ie);
522 					}
523 				}
524 			});
525 		}
526 
527 		private class MyLazyCP implements ILazyContentProvider {
528 			private static final long serialVersionUID = 1L;
529 			private TableViewer viewer;
530 			private Object[] elements;
531 
532 			public MyLazyCP(TableViewer viewer) {
533 				this.viewer = viewer;
534 			}
535 
536 			public void dispose() {
537 			}
538 
539 			public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
540 				// IMPORTANT: don't forget this: an exception will be thrown if
541 				// a selected object is not part of the results anymore.
542 				viewer.setSelection(null);
543 				this.elements = (Object[]) newInput;
544 			}
545 
546 			public void updateElement(int index) {
547 				viewer.replace(elements[index], index);
548 			}
549 		}
550 
551 		protected void refreshFilteredList(NodeIterator children) {
552 			Object[] rows = JcrUtils.nodeIteratorToList(children).toArray();
553 			entityViewer.setInput(rows);
554 			entityViewer.setItemCount(rows.length);
555 			entityViewer.refresh();
556 		}
557 
558 		public class SimpleNameLP extends ColumnLabelProvider {
559 			private static final long serialVersionUID = 2465059387875338553L;
560 
561 			@Override
562 			public String getText(Object element) {
563 				if (element instanceof Node) {
564 					Node curr = ((Node) element);
565 					try {
566 						return curr.getName();
567 					} catch (RepositoryException e) {
568 						throw new CmsException("Unable to get name for" + curr);
569 					}
570 				}
571 				return super.getText(element);
572 			}
573 		}
574 	}
575 }