package org.argeo.cms.e4.maintenance; import static org.eclipse.swt.SWT.RIGHT; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.LinkedHashMap; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import org.argeo.api.cms.Cms2DSize; import org.argeo.cms.CmsException; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.cms.ui.CmsUiProvider; import org.argeo.cms.ui.util.CmsLink; import org.argeo.cms.ui.widgets.EditableImage; import org.argeo.cms.ui.widgets.Img; import org.argeo.jcr.JcrUtils; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ILazyContentProvider; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; public class Browse implements CmsUiProvider { // Some local constants to experiment. should be cleaned private final static String BROWSE_PREFIX = "browse#"; private final static int THUMBNAIL_WIDTH = 400; private final static int COLUMN_WIDTH = 160; private DateFormat timeFormatter = new SimpleDateFormat("dd-MM-yyyy', 'HH:mm"); // keep a cache of the opened nodes // Key is the path private LinkedHashMap browserCols = new LinkedHashMap(); private Composite nodeDisplayParent; private Composite colViewer; private ScrolledComposite scrolledCmp; private Text parentPathTxt; private Text filterTxt; private Node currEdited; private String initialPath; @Override public Control createUi(Composite parent, Node context) throws RepositoryException { if (context == null) // return null; throw new CmsException("Context cannot be null"); GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); layout.numColumns = 2; parent.setLayout(layout); // Left Composite leftCmp = new Composite(parent, SWT.NO_FOCUS); leftCmp.setLayoutData(CmsSwtUtils.fillAll()); createBrowserPart(leftCmp, context); // Right nodeDisplayParent = new Composite(parent, SWT.NO_FOCUS | SWT.BORDER); GridData gd = new GridData(SWT.RIGHT, SWT.FILL, false, true); gd.widthHint = THUMBNAIL_WIDTH; nodeDisplayParent.setLayoutData(gd); createNodeView(nodeDisplayParent, context); // INIT setEdited(context); initialPath = context.getPath(); // Workaround we don't yet manage the delete to display parent of the // initial context node return null; } private void createBrowserPart(Composite parent, Node context) throws RepositoryException { GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); parent.setLayout(layout); Composite filterCmp = new Composite(parent, SWT.NO_FOCUS); filterCmp.setLayoutData(CmsSwtUtils.fillWidth()); // top filter addFilterPanel(filterCmp); // scrolled composite scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS); scrolledCmp.setLayoutData(CmsSwtUtils.fillAll()); scrolledCmp.setExpandVertical(true); scrolledCmp.setExpandHorizontal(true); scrolledCmp.setShowFocusedControl(true); colViewer = new Composite(scrolledCmp, SWT.NO_FOCUS); scrolledCmp.setContent(colViewer); scrolledCmp.addControlListener(new ControlAdapter() { private static final long serialVersionUID = 6589392045145698201L; @Override public void controlResized(ControlEvent e) { Rectangle r = scrolledCmp.getClientArea(); scrolledCmp.setMinSize(colViewer.computeSize(SWT.DEFAULT, r.height)); } }); initExplorer(colViewer, context); } private Control initExplorer(Composite parent, Node context) throws RepositoryException { parent.setLayout(CmsSwtUtils.noSpaceGridLayout()); createBrowserColumn(parent, context); return null; } private Control createBrowserColumn(Composite parent, Node context) throws RepositoryException { // TODO style is not correctly managed. FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context); // CmsUiUtils.style(table, ArgeoOrgStyle.browserColumn.style()); table.filterList("*"); table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true)); browserCols.put(context.getPath(), table); return null; } public void addFilterPanel(Composite parent) { parent.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(2, false))); // Text Area for the filter parentPathTxt = new Text(parent, SWT.NO_FOCUS); parentPathTxt.setEditable(false); filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL); filterTxt.setMessage("Filter current list"); filterTxt.setLayoutData(CmsSwtUtils.fillWidth()); filterTxt.addModifyListener(new ModifyListener() { private static final long serialVersionUID = 7709303319740056286L; public void modifyText(ModifyEvent event) { modifyFilter(false); } }); filterTxt.addKeyListener(new KeyListener() { private static final long serialVersionUID = -4523394262771183968L; @Override public void keyReleased(KeyEvent e) { } @Override public void keyPressed(KeyEvent e) { boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0; // boolean altPressed = (e.stateMask & SWT.ALT) != 0; FilterEntitiesVirtualTable currTable = null; if (currEdited != null) { FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited)); if (table != null && !table.isDisposed()) currTable = table; } try { if (e.keyCode == SWT.ARROW_DOWN) currTable.setFocus(); else if (e.keyCode == SWT.BS) { if (filterTxt.getText().equals("") && !(getPath(currEdited).equals("/") || getPath(currEdited).equals(initialPath))) { setEdited(currEdited.getParent()); e.doit = false; filterTxt.setFocus(); } } else if (e.keyCode == SWT.TAB && !shiftPressed) { if (currEdited.getNodes(filterTxt.getText() + "*").getSize() == 1) { setEdited(currEdited.getNodes(filterTxt.getText() + "*").nextNode()); } filterTxt.setFocus(); e.doit = false; } } catch (RepositoryException e1) { throw new CmsException("Unexpected error in key management for " + currEdited + "with filter " + filterTxt.getText(), e1); } } }); } private void setEdited(Node node) { try { currEdited = node; CmsSwtUtils.clear(nodeDisplayParent); createNodeView(nodeDisplayParent, currEdited); nodeDisplayParent.layout(); refreshFilters(node); refreshBrowser(node); } catch (RepositoryException re) { throw new CmsException("Unable to update browser for " + node, re); } } private void refreshFilters(Node node) throws RepositoryException { String currNodePath = node.getPath(); parentPathTxt.setText(currNodePath); filterTxt.setText(""); filterTxt.getParent().layout(); } private void refreshBrowser(Node node) throws RepositoryException { // Retrieve String currNodePath = node.getPath(); String currParPath = ""; if (!"/".equals(currNodePath)) currParPath = JcrUtils.parentPath(currNodePath); if ("".equals(currParPath)) currParPath = "/"; Object[][] colMatrix = new Object[browserCols.size()][2]; int i = 0, j = -1, k = -1; for (String path : browserCols.keySet()) { colMatrix[i][0] = path; colMatrix[i][1] = browserCols.get(path); if (j >= 0 && k < 0 && !currNodePath.equals("/")) { boolean leaveOpened = path.startsWith(currNodePath); // workaround for same name siblings // fix me weird side effect when we go left or click on anb // already selected, unfocused node if (leaveOpened && (path.lastIndexOf("/") == 0 && currNodePath.lastIndexOf("/") == 0 || JcrUtils.parentPath(path).equals(JcrUtils.parentPath(currNodePath)))) leaveOpened = JcrUtils.lastPathElement(path).equals(JcrUtils.lastPathElement(currNodePath)); if (!leaveOpened) k = i; } if (currParPath.equals(path)) j = i; i++; } if (j >= 0 && k >= 0) // remove useless cols for (int l = i - 1; l >= k; l--) { browserCols.remove(colMatrix[l][0]); ((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose(); } // Remove disposed columns // TODO investigate and fix the mechanism that leave them there after // disposal if (browserCols.containsKey(currNodePath)) { FilterEntitiesVirtualTable currCol = browserCols.get(currNodePath); if (currCol.isDisposed()) browserCols.remove(currNodePath); } if (!browserCols.containsKey(currNodePath)) createBrowserColumn(colViewer, node); colViewer.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false))); // colViewer.pack(); colViewer.layout(); // also resize the scrolled composite scrolledCmp.layout(); scrolledCmp.getShowFocusedControl(); // colViewer.getParent().layout(); // if (JcrUtils.parentPath(currNodePath).equals(currBrowserKey)) { // } else { // } } private void modifyFilter(boolean fromOutside) { if (!fromOutside) if (currEdited != null) { String filter = filterTxt.getText() + "*"; FilterEntitiesVirtualTable table = browserCols.get(getPath(currEdited)); if (table != null && !table.isDisposed()) table.filterList(filter); } } private String getPath(Node node) { try { return node.getPath(); } catch (RepositoryException e) { throw new CmsException("Unable to get path for node " + node, e); } } private Cms2DSize imageWidth = new Cms2DSize(250, 0); /** * Recreates the content of the box that displays information about the current * selected node. */ private Control createNodeView(Composite parent, Node context) throws RepositoryException { parent.setLayout(new GridLayout(2, false)); if (isImg(context)) { EditableImage image = new Img(parent, RIGHT, context, imageWidth); image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 2, 1)); } // Name and primary type Label contextL = new Label(parent, SWT.NONE); CmsSwtUtils.markup(contextL); contextL.setText("" + context.getName() + ""); new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType().getName()); // Children for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) { Node child = nIt.nextNode(); new CmsLink(child.getName(), BROWSE_PREFIX + child.getPath()).createUi(parent, context); new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType().getName()); } // Properties for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) { Property property = pIt.nextProperty(); Label label = new Label(parent, SWT.NONE); label.setText(property.getName()); label.setToolTipText(JcrUtils.getPropertyDefinitionAsString(property)); new Label(parent, SWT.NONE).setText(getPropAsString(property)); } return null; } private boolean isImg(Node node) throws RepositoryException { // TODO support images return false; // return node.hasNode(JCR_CONTENT) && node.isNodeType(CmsTypes.CMS_IMAGE); } private String getPropAsString(Property property) throws RepositoryException { String result = ""; if (property.isMultiple()) { result = getMultiAsString(property, ", "); } else { Value value = property.getValue(); if (value.getType() == PropertyType.BINARY) result = ""; else if (value.getType() == PropertyType.DATE) result = timeFormatter.format(value.getDate().getTime()); else result = value.getString(); } return result; } private String getMultiAsString(Property property, String separator) throws RepositoryException { if (separator == null) separator = "; "; Value[] values = property.getValues(); StringBuilder builder = new StringBuilder(); for (Value val : values) { String currStr = val.getString(); if (!"".equals(currStr.trim())) builder.append(currStr).append(separator); } if (builder.lastIndexOf(separator) >= 0) return builder.substring(0, builder.length() - separator.length()); else return builder.toString(); } /** Almost canonical implementation of a table that display entities */ private class FilterEntitiesVirtualTable extends Composite { private static final long serialVersionUID = 8798147431706283824L; // Context private Node context; // UI Objects private TableViewer entityViewer; // enable management of multiple columns Node getNode() { return context; } @Override public boolean setFocus() { if (entityViewer.getTable().isDisposed()) return false; if (entityViewer.getSelection().isEmpty()) { Object first = entityViewer.getElementAt(0); if (first != null) { entityViewer.setSelection(new StructuredSelection(first), true); } } return entityViewer.getTable().setFocus(); } void filterList(String filter) { try { NodeIterator nit = context.getNodes(filter); refreshFilteredList(nit); } catch (RepositoryException e) { throw new CmsException("Unable to filter " + getNode() + " children with filter " + filter, e); } } public FilterEntitiesVirtualTable(Composite parent, int style, Node context) { super(parent, SWT.NO_FOCUS); this.context = context; populate(); } protected void populate() { Composite parent = this; GridLayout layout = CmsSwtUtils.noSpaceGridLayout(); this.setLayout(layout); createTableViewer(parent); } private void createTableViewer(final Composite parent) { // the list // We must limit the size of the table otherwise the full list is // loaded // before the layout happens Composite listCmp = new Composite(parent, SWT.NO_FOCUS); GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true); gd.widthHint = COLUMN_WIDTH; listCmp.setLayoutData(gd); listCmp.setLayout(CmsSwtUtils.noSpaceGridLayout()); entityViewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.SINGLE); Table table = entityViewer.getTable(); table.setLayoutData(CmsSwtUtils.fillAll()); table.setLinesVisible(true); table.setHeaderVisible(false); CmsSwtUtils.markup(table); CmsSwtUtils.style(table, MaintenanceStyles.BROWSER_COLUMN); // first column TableViewerColumn column = new TableViewerColumn(entityViewer, SWT.NONE); TableColumn tcol = column.getColumn(); tcol.setWidth(COLUMN_WIDTH); tcol.setResizable(true); column.setLabelProvider(new SimpleNameLP()); entityViewer.setContentProvider(new MyLazyCP(entityViewer)); entityViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection(); if (selection.isEmpty()) return; else setEdited((Node) selection.getFirstElement()); } }); table.addKeyListener(new KeyListener() { private static final long serialVersionUID = -330694313896036230L; @Override public void keyReleased(KeyEvent e) { } @Override public void keyPressed(KeyEvent e) { IStructuredSelection selection = (IStructuredSelection) entityViewer.getSelection(); Node selected = null; if (!selection.isEmpty()) selected = ((Node) selection.getFirstElement()); try { if (e.keyCode == SWT.ARROW_RIGHT) { if (selected != null) { setEdited(selected); browserCols.get(selected.getPath()).setFocus(); } } else if (e.keyCode == SWT.ARROW_LEFT) { try { selected = getNode().getParent(); String newPath = selected.getPath(); // getNode().getParent() setEdited(selected); if (browserCols.containsKey(newPath)) browserCols.get(newPath).setFocus(); } catch (ItemNotFoundException ie) { // root silent } } } catch (RepositoryException ie) { throw new CmsException("Error while managing arrow " + "events in the browser for " + selected, ie); } } }); } private class MyLazyCP implements ILazyContentProvider { private static final long serialVersionUID = 1L; private TableViewer viewer; private Object[] elements; public MyLazyCP(TableViewer viewer) { this.viewer = viewer; } public void dispose() { } public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // IMPORTANT: don't forget this: an exception will be thrown if // a selected object is not part of the results anymore. viewer.setSelection(null); this.elements = (Object[]) newInput; } public void updateElement(int index) { viewer.replace(elements[index], index); } } protected void refreshFilteredList(NodeIterator children) { Object[] rows = JcrUtils.nodeIteratorToList(children).toArray(); entityViewer.setInput(rows); entityViewer.setItemCount(rows.length); entityViewer.refresh(); } public class SimpleNameLP extends ColumnLabelProvider { private static final long serialVersionUID = 2465059387875338553L; @Override public String getText(Object element) { if (element instanceof Node) { Node curr = ((Node) element); try { return curr.getName(); } catch (RepositoryException e) { throw new CmsException("Unable to get name for" + curr); } } return super.getText(element); } } } }