package org.argeo.cms.swt;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
+import java.util.StringTokenizer;
import org.argeo.api.cms.CmsStyle;
import org.argeo.api.cms.CmsTheme;
for (Control child : composite.getChildren())
child.dispose();
}
+
+ /** Clean reserved URL characters for use in HTTP links. */
+ public static String cleanPathForUrl(String path) {
+ StringTokenizer st = new StringTokenizer(path, "/");
+ StringBuilder sb = new StringBuilder();
+ while (st.hasMoreElements()) {
+ sb.append('/');
+ String encoded = URLEncoder.encode(st.nextToken(), StandardCharsets.UTF_8);
+ encoded = encoded.replace("+", "%20");
+ sb.append(encoded);
+
+ }
+ return sb.toString();
+ }
}
--- /dev/null
+package org.argeo.cms.swt.acr;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.spi.ProvidedContent;
+import org.eclipse.swt.widgets.Composite;
+
+/** A composite which can (optionally) manage a content. */
+public class ContentComposite extends Composite {
+ private static final long serialVersionUID = -1447009015451153367L;
+
+ public ContentComposite(Composite parent, int style, Content item) {
+ super(parent, style);
+ setData(item);
+ }
+
+ public Content getContent() {
+ return (Content) getData();
+ }
+
+ @Deprecated
+ public Content getNode() {
+ return getContent();
+ }
+
+ protected ProvidedContent getProvidedContent() {
+ return (ProvidedContent) getContent();
+ }
+
+ public String getSessionLocalId() {
+ return getProvidedContent().getSessionLocalId();
+ }
+
+ protected void itemUpdated() {
+ layout();
+ }
+
+ public void setContent(Content content) {
+ setData(content);
+ itemUpdated();
+ }
+}
--- /dev/null
+package org.argeo.cms.swt.widgets;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.argeo.api.acr.Content;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.acr.ContentComposite;
+import org.argeo.cms.ux.widgets.EditablePart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** A structured UI related to a JCR context. */
+public class SwtSection extends ContentComposite {
+ private static final long serialVersionUID = -5933796173755739207L;
+
+ private final SwtSection parentSection;
+ private Composite sectionHeader;
+ private final Integer relativeDepth;
+
+ public SwtSection(Composite parent, int style, Content node) {
+ this(parent, findSection(parent), style, node);
+ }
+
+ public SwtSection(SwtSection section, int style, Content node) {
+ this(section, section, style, node);
+ }
+
+ protected SwtSection(Composite parent, SwtSection parentSection, int style, Content node) {
+ super(parent, style, node);
+ this.parentSection = parentSection;
+ if (parentSection != null) {
+ relativeDepth = getProvidedContent().getDepth() - parentSection.getProvidedContent().getDepth();
+ } else {
+ relativeDepth = 0;
+ }
+ setLayout(CmsSwtUtils.noSpaceGridLayout());
+ }
+
+ public Map<String, SwtSection> getSubSections() {
+ LinkedHashMap<String, SwtSection> result = new LinkedHashMap<String, SwtSection>();
+ for (Control child : getChildren()) {
+ if (child instanceof Composite) {
+ collectDirectSubSections((Composite) child, result);
+ }
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ private void collectDirectSubSections(Composite composite, LinkedHashMap<String, SwtSection> subSections) {
+ if (composite == sectionHeader || composite instanceof EditablePart)
+ return;
+ if (composite instanceof SwtSection) {
+ SwtSection section = (SwtSection) composite;
+ subSections.put(section.getProvidedContent().getSessionLocalId(), section);
+ return;
+ }
+
+ for (Control child : composite.getChildren())
+ if (child instanceof Composite)
+ collectDirectSubSections((Composite) child, subSections);
+ }
+
+ public Composite createHeader() {
+ return createHeader(this);
+ }
+
+ public Composite createHeader(Composite parent) {
+ if (sectionHeader != null)
+ sectionHeader.dispose();
+
+ sectionHeader = new Composite(parent, SWT.NONE);
+ sectionHeader.setLayoutData(CmsSwtUtils.fillWidth());
+ sectionHeader.setLayout(CmsSwtUtils.noSpaceGridLayout());
+ // sectionHeader.moveAbove(null);
+ // layout();
+ return sectionHeader;
+ }
+
+ public Composite getHeader() {
+ if (sectionHeader != null && sectionHeader.isDisposed())
+ sectionHeader = null;
+ return sectionHeader;
+ }
+
+ // SECTION PARTS
+ public SwtSectionPart getSectionPart(String partId) {
+ for (Control child : getChildren()) {
+ if (child instanceof SwtSectionPart) {
+ SwtSectionPart sectionPart = (SwtSectionPart) child;
+ if (sectionPart.getPartId().equals(partId))
+ return sectionPart;
+ }
+ }
+ return null;
+ }
+
+ public SwtSectionPart nextSectionPart(SwtSectionPart sectionPart) {
+ Control[] children = getChildren();
+ for (int i = 0; i < children.length; i++) {
+ if (sectionPart == children[i]) {
+ for (int j = i + 1; j < children.length; j++) {
+ if (children[i + 1] instanceof SwtSectionPart) {
+ return (SwtSectionPart) children[i + 1];
+ }
+ }
+
+// if (i + 1 < children.length) {
+// Composite next = (Composite) children[i + 1];
+// return (SectionPart) next;
+// } else {
+// // next section
+// }
+ }
+ }
+ return null;
+ }
+
+ public SwtSectionPart previousSectionPart(SwtSectionPart sectionPart) {
+ Control[] children = getChildren();
+ for (int i = 0; i < children.length; i++) {
+ if (sectionPart == children[i])
+ if (i != 0) {
+ Composite previous = (Composite) children[i - 1];
+ return (SwtSectionPart) previous;
+ } else {
+ // previous section
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ if (parentSection == null)
+ return "Main section " + getContent();
+ return "Section " + getContent();
+ }
+
+ public SwtSection getParentSection() {
+ return parentSection;
+ }
+
+ public Integer getRelativeDepth() {
+ return relativeDepth;
+ }
+
+ /** Recursively finds the related section in the parents (can be itself) */
+ public static SwtSection findSection(Control control) {
+ if (control == null)
+ return null;
+ if (control instanceof SwtSection)
+ return (SwtSection) control;
+ else
+ return findSection(control.getParent());
+ }
+}
--- /dev/null
+package org.argeo.cms.swt.widgets;
+
+import org.argeo.cms.ux.widgets.ContentPart;
+import org.argeo.cms.ux.widgets.EditablePart;
+
+/** An editable part dynamically related to a Section */
+public interface SwtSectionPart extends EditablePart, ContentPart {
+ public String getPartId();
+
+ public SwtSection getSection();
+}
--- /dev/null
+package org.argeo.cms.swt.widgets;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.spi.ProvidedContent;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.Selected;
+import org.argeo.cms.swt.acr.SwtUiProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+/** Manages {@link SwtSection} in a tab-like structure. */
+public class SwtTabbedArea extends Composite {
+ private static final long serialVersionUID = 8659669229482033444L;
+
+ private Composite headers;
+ private Composite body;
+
+ private List<SwtSection> sections = new ArrayList<>();
+
+ private ProvidedContent previousNode;
+ private SwtUiProvider previousUiProvider;
+ private SwtUiProvider currentUiProvider;
+
+ private String tabStyle;
+ private String tabSelectedStyle;
+ private String bodyStyle;
+ private Image closeIcon;
+
+ private StackLayout stackLayout;
+
+ private boolean singleTab = false;
+
+ public SwtTabbedArea(Composite parent, int style) {
+ super(parent, SWT.NONE);
+ CmsSwtUtils.style(parent, bodyStyle);
+
+ setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+ // TODO manage tabs at bottom or sides
+ headers = new Composite(this, SWT.NONE);
+ headers.setLayoutData(CmsSwtUtils.fillWidth());
+ body = new Composite(this, SWT.NONE);
+ body.setLayoutData(CmsSwtUtils.fillAll());
+ // body.setLayout(new FormLayout());
+ stackLayout = new StackLayout();
+ body.setLayout(stackLayout);
+ emptyState();
+ }
+
+ protected void refreshTabHeaders() {
+ int tabCount = sections.size() > 0 ? sections.size() : 1;
+ for (Control tab : headers.getChildren())
+ tab.dispose();
+
+ headers.setLayout(CmsSwtUtils.noSpaceGridLayout(new GridLayout(tabCount, true)));
+
+ if (sections.size() == 0) {
+ Composite emptyHeader = new Composite(headers, SWT.NONE);
+ emptyHeader.setLayoutData(CmsSwtUtils.fillAll());
+ emptyHeader.setLayout(new GridLayout());
+ Label lbl = new Label(emptyHeader, SWT.NONE);
+ lbl.setText("");
+ lbl.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false));
+
+ }
+
+ SwtSection currentSection = getCurrentSection();
+ for (SwtSection section : sections) {
+ boolean selected = section == currentSection;
+ Composite sectionHeader = section.createHeader(headers);
+ CmsSwtUtils.style(sectionHeader, selected ? tabSelectedStyle : tabStyle);
+ int headerColumns = singleTab ? 1 : 2;
+ sectionHeader.setLayout(new GridLayout(headerColumns, false));
+ sectionHeader.setLayout(CmsSwtUtils.noSpaceGridLayout(headerColumns));
+ Button title = new Button(sectionHeader, SWT.FLAT);
+ CmsSwtUtils.style(title, selected ? tabSelectedStyle : tabStyle);
+ title.setLayoutData(CmsSwtUtils.fillWidth());
+ title.addSelectionListener((Selected) (e) -> showTab(tabIndex(section.getNode())));
+ Content node = section.getContent();
+
+ // FIXME find a standard way to display titles
+ String titleStr = node.getName().getLocalPart();
+
+ // TODO internationalize
+ title.setText(titleStr);
+ if (!singleTab) {
+ ToolBar toolBar = new ToolBar(sectionHeader, SWT.NONE);
+ ToolItem closeItem = new ToolItem(toolBar, SWT.FLAT);
+ if (closeIcon != null)
+ closeItem.setImage(closeIcon);
+ else
+ closeItem.setText("X");
+ CmsSwtUtils.style(closeItem, selected ? tabSelectedStyle : tabStyle);
+ closeItem.addSelectionListener((Selected) (e) -> closeTab(section));
+ }
+ }
+
+ }
+
+ public void view(SwtUiProvider uiProvider, Content context) {
+ if (body.isDisposed())
+ return;
+ int index = tabIndex(context);
+ if (index >= 0) {
+ showTab(index);
+ previousNode = (ProvidedContent) context;
+ previousUiProvider = uiProvider;
+ return;
+ }
+ SwtSection section = (SwtSection) body.getChildren()[0];
+ previousNode = (ProvidedContent) section.getNode();
+ if (previousNode == null) {// empty state
+ previousNode = (ProvidedContent) context;
+ previousUiProvider = uiProvider;
+ } else {
+ previousUiProvider = currentUiProvider;
+ }
+ currentUiProvider = uiProvider;
+ section.setContent(context);
+ // section.setLayoutData(CmsUiUtils.coverAll());
+ build(section, uiProvider, context);
+ if (sections.size() == 0)
+ sections.add(section);
+ refreshTabHeaders();
+ index = tabIndex(context);
+ showTab(index);
+ layout(true, true);
+ }
+
+ public void open(SwtUiProvider uiProvider, Content context) {
+ if (singleTab)
+ throw new UnsupportedOperationException("Open is not supported in single tab mode.");
+
+ if (previousNode != null
+ && previousNode.getSessionLocalId().equals(((ProvidedContent) context).getSessionLocalId())) {
+ // does nothing
+ return;
+ }
+ if (sections.size() == 0)
+ CmsSwtUtils.clear(body);
+ SwtSection currentSection = getCurrentSection();
+ int currentIndex = sections.indexOf(currentSection);
+ SwtSection previousSection = new SwtSection(body, SWT.NONE, context);
+ build(previousSection, previousUiProvider, previousNode);
+ // previousSection.setLayoutData(CmsUiUtils.coverAll());
+ int newIndex = currentIndex + 1;
+ sections.add(currentIndex, previousSection);
+// sections.add(newIndex, previousSection);
+ showTab(newIndex);
+ refreshTabHeaders();
+ layout(true, true);
+ }
+
+ public void showTab(int index) {
+ SwtSection sectionToShow = sections.get(index);
+ // sectionToShow.moveAbove(null);
+ stackLayout.topControl = sectionToShow;
+ refreshTabHeaders();
+ layout(true, true);
+ }
+
+ protected void build(SwtSection section, SwtUiProvider uiProvider, Content context) {
+ for (Control child : section.getChildren())
+ child.dispose();
+ CmsSwtUtils.style(section, bodyStyle);
+ section.setContent(context);
+ uiProvider.createUiPart(section, context);
+
+ }
+
+ private int tabIndex(Content context) {
+ for (int i = 0; i < sections.size(); i++) {
+ SwtSection section = sections.get(i);
+ if (section.getSessionLocalId().equals(((ProvidedContent) context).getSessionLocalId()))
+ return i;
+ }
+ return -1;
+ }
+
+ public void closeTab(SwtSection section) {
+ int currentIndex = sections.indexOf(section);
+ int nextIndex = currentIndex == 0 ? 0 : currentIndex - 1;
+ sections.remove(section);
+ section.dispose();
+ if (sections.size() == 0) {
+ emptyState();
+ refreshTabHeaders();
+ layout(true, true);
+ return;
+ }
+ refreshTabHeaders();
+ showTab(nextIndex);
+ }
+
+ public void closeAllTabs() {
+ for (SwtSection section : sections) {
+ section.dispose();
+ }
+ sections.clear();
+ emptyState();
+ refreshTabHeaders();
+ layout(true, true);
+ }
+
+ protected void emptyState() {
+ new SwtSection(body, SWT.NONE, null);
+ refreshTabHeaders();
+ }
+
+ public Composite getCurrent() {
+ return getCurrentSection();
+ }
+
+ protected SwtSection getCurrentSection() {
+ return (SwtSection) stackLayout.topControl;
+ }
+
+ public Content getCurrentContext() {
+ SwtSection section = getCurrentSection();
+ if (section != null) {
+ return section.getNode();
+ } else {
+ return null;
+ }
+ }
+
+ public void setTabStyle(String tabStyle) {
+ this.tabStyle = tabStyle;
+ }
+
+ public void setTabSelectedStyle(String tabSelectedStyle) {
+ this.tabSelectedStyle = tabSelectedStyle;
+ }
+
+ public void setBodyStyle(String bodyStyle) {
+ this.bodyStyle = bodyStyle;
+ }
+
+ public void setCloseIcon(Image closeIcon) {
+ this.closeIcon = closeIcon;
+ }
+
+ public void setSingleTab(boolean singleTab) {
+ this.singleTab = singleTab;
+ }
+
+}
@Override
public Content getParent() {
+ if (Jcr.isRoot(getJcrNode())) // root
+ return null;
return new JcrContent(session, provider, jcrWorkspace, Jcr.getParentPath(getJcrNode()));
}
/*
* ADAPTERS
*/
+ @SuppressWarnings("unchecked")
public <A> A adapt(Class<A> clss) {
if (Source.class.isAssignableFrom(clss)) {
// try {
return super.adapt(clss);
}
+ @SuppressWarnings("unchecked")
@Override
public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
if (InputStream.class.isAssignableFrom(clss)) {
return provider;
}
+ @Override
+ public String getSessionLocalId() {
+ try {
+ return getJcrNode().getIdentifier();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get identifier for " + getJcrNode(), e);
+ }
+ }
+
/*
* STATIC UTLITIES
*/
@Override
default Control createUiPart(Composite parent, Content context) {
+ if (context == null)
+ return createUiPart(parent, (Node) null);
if (context instanceof JcrContent) {
Node node = ((JcrContent) context).getJcrNode();
return createUiPart(parent, node);
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.StringTokenizer;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
/** Clean reserved URL characters for use in HTTP links. */
public static String getDataPathForUrl(Node node) {
- return cleanPathForUrl(getDataPath(node));
- }
-
- /** Clean reserved URL characters for use in HTTP links. */
- public static String cleanPathForUrl(String path) {
- StringTokenizer st = new StringTokenizer(path, "/");
- StringBuilder sb = new StringBuilder();
- while (st.hasMoreElements()) {
- sb.append('/');
- String encoded = URLEncoder.encode(st.nextToken(), StandardCharsets.UTF_8);
- encoded = encoded.replace("+", "%20");
- sb.append(encoded);
-
- }
- return sb.toString();
+ return CmsSwtUtils.cleanPathForUrl(getDataPath(node));
}
/** @deprecated Use rowData16px() instead. GridData should not be reused. */
layout();
}
- public Session getSession() {
- return session;
- }
+// public Session getSession() {
+// return session;
+// }
}
ContentProvider getProvider();
+ int getDepth();
+
+ /**
+ * An opaque ID which is guaranteed to uniquely identify this content within the
+ * session return by {@link #getSession()}. Typically used for UI.
+ */
+ String getSessionLocalId();
+
default ProvidedContent getMountPoint(String relativePath) {
throw new UnsupportedOperationException("This content doe not support mount");
}
package org.argeo.cms.ux;
+import org.argeo.api.acr.Content;
import org.argeo.api.acr.ContentRepository;
import org.argeo.api.acr.ContentSession;
import org.argeo.api.cms.CmsView;
return CurrentUser.callAs(cmsView.getCmsSession().getSubject(), () -> contentRepository.get());
}
+ public static String getTitle(Content content) {
+ return content.getName().getLocalPart();
+ }
+
/** singleton */
private CmsUxUtils() {
--- /dev/null
+package org.argeo.cms.ux.widgets;
+
+import org.argeo.api.acr.Content;
+
+/** A part displaying or editing a content. */
+public interface ContentPart {
+ Content getContent();
+
+ @Deprecated
+ Content getNode();
+
+}
--- /dev/null
+package org.argeo.cms.ux.widgets;
+
+/** Manages whether an editable or non editable control is shown. */
+public interface EditablePart {
+ public void startEditing();
+
+ public void stopEditing();
+}
collectAncestors(ancestors, content.getParent());
}
+ @Override
+ public int getDepth() {
+ List<Content> ancestors = new ArrayList<>();
+ collectAncestors(ancestors, this);
+ return ancestors.size();
+ }
+
+ @Override
+ public String getSessionLocalId() {
+ return getPath();
+ }
+
/*
* UTILITIES
*/