Merge remote-tracking branch 'origin/master' into v2.x
authorMathieu Baudier <mbaudier@argeo.org>
Sun, 7 Feb 2021 12:12:31 +0000 (13:12 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Sun, 7 Feb 2021 12:12:31 +0000 (13:12 +0100)
core/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultEditionLayer.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultHeader.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/SuiteApp.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/SuiteLayer.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/SuiteUi.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/SuiteUiUtils.java
core/org.argeo.suite.ui/src/org/argeo/suite/ui/widgets/TabbedArea.java [new file with mode: 0644]

index 7b8bb3eb360c7eb4f8d56fb25e8626e96a7998ce..9c2ad475e6e5c219622c1cd38f73609f48ccf7da 100644 (file)
@@ -7,16 +7,19 @@ import java.util.Map;
 import javax.jcr.Node;
 import javax.jcr.RepositoryException;
 
+import org.argeo.cms.Localized;
 import org.argeo.cms.ui.CmsTheme;
 import org.argeo.cms.ui.CmsUiProvider;
 import org.argeo.cms.ui.util.CmsUiUtils;
-import org.argeo.cms.ui.widgets.TabbedArea;
+import org.argeo.suite.ui.widgets.TabbedArea;
 import org.argeo.util.LangUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.SashForm;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
 
 /** An app layer based on an entry area and an editor area. */
 public class DefaultEditionLayer implements SuiteLayer {
@@ -24,6 +27,7 @@ public class DefaultEditionLayer implements SuiteLayer {
        private CmsUiProvider workArea;
        private List<String> weights = new ArrayList<>();
        private boolean startMaximized = false;
+       private Localized title = null;
 
        @Override
        public Control createUi(Composite parent, Node context) throws RepositoryException {
@@ -64,12 +68,43 @@ public class DefaultEditionLayer implements SuiteLayer {
                tabbedArea.open(uiProvider, context);
        }
 
-       public void init(Map<String, Object> properties) {
+       @Override
+       public Localized getTitle() {
+               return title;
+       }
+
+       public void init(BundleContext bundleContext, Map<String, Object> properties) {
                weights = LangUtils.toStringList(properties.get(Property.weights.name()));
                startMaximized = properties.containsKey(Property.startMaximized.name())
                                && "true".equals(properties.get(Property.startMaximized.name()));
+
+               String titleStr = (String) properties.get(SuiteLayer.Property.title.name());
+               if (titleStr != null) {
+                       if (titleStr.startsWith("%")) {
+                               title = new Localized() {
+
+                                       @Override
+                                       public String name() {
+                                               return titleStr;
+                                       }
+
+                                       @Override
+                                       public ClassLoader getL10nClassLoader() {
+                                               return bundleContext != null
+                                                               ? bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader()
+                                                               : getClass().getClassLoader();
+                                       }
+                               };
+                       } else {
+                               title = new Localized.Untranslated(titleStr);
+                       }
+               }
        }
 
+       public void destroy() {
+               
+       }
+       
        public void setEntryArea(CmsUiProvider entryArea) {
                this.entryArea = entryArea;
        }
index 692a23badf2c4c1f3c5d2446420a5404ddb676d5..080bac2cfa121b92817a22a95aecabfb700c6641 100644 (file)
@@ -7,7 +7,7 @@ import java.util.TreeMap;
 import javax.jcr.Node;
 import javax.jcr.RepositoryException;
 
-import org.argeo.cms.LocaleUtils;
+import org.argeo.cms.Localized;
 import org.argeo.cms.auth.CurrentUser;
 import org.argeo.cms.ui.CmsTheme;
 import org.argeo.cms.ui.CmsUiProvider;
@@ -23,14 +23,18 @@ 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.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
 import org.osgi.service.cm.ConfigurationException;
 import org.osgi.service.cm.ManagedService;
 
-/** HEader of a standard Argeo Suite applicaiton. */
+/** HEader of a standard Argeo Suite application. */
 public class DefaultHeader implements CmsUiProvider, ManagedService {
        public final static String TITLE_PROPERTY = "argeo.suite.ui.header.title";
        private Map<String, String> properties;
 
+       private Localized title = null;
+
        @Override
        public Control createUi(Composite parent, Node context) throws RepositoryException {
                CmsView cmsView = CmsView.getCmsView(parent);
@@ -44,9 +48,11 @@ public class DefaultHeader implements CmsUiProvider, ManagedService {
                lead.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, true, false));
                lead.setLayout(new GridLayout());
                Label lbl = new Label(lead, SWT.NONE);
-               String title = properties.get(TITLE_PROPERTY);
-               lbl.setText(LocaleUtils.isLocaleKey(title) ? LocaleUtils.local(title, getClass().getClassLoader()).toString()
-                               : title);
+//             String title = properties.get(TITLE_PROPERTY);
+//             // TODO expose the localized
+//             lbl.setText(LocaleUtils.isLocaleKey(title) ? LocaleUtils.local(title, getClass().getClassLoader()).toString()
+//                             : title);
+               lbl.setText(title.lead());
                CmsUiUtils.style(lbl, SuiteStyle.headerTitle);
                lbl.setLayoutData(CmsUiUtils.fillWidth());
 
@@ -85,8 +91,33 @@ public class DefaultHeader implements CmsUiProvider, ManagedService {
                return lbl;
        }
 
-       public void init(Map<String, String> properties) {
+       public void init(BundleContext bundleContext, Map<String, String> properties) {
                this.properties = new TreeMap<>(properties);
+               String titleStr = (String) properties.get(TITLE_PROPERTY);
+               if (titleStr != null) {
+                       if (titleStr.startsWith("%")) {
+                               title = new Localized() {
+
+                                       @Override
+                                       public String name() {
+                                               return titleStr;
+                                       }
+
+                                       @Override
+                                       public ClassLoader getL10nClassLoader() {
+                                               return bundleContext != null
+                                                               ? bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader()
+                                                               : getClass().getClassLoader();
+                                       }
+                               };
+                       } else {
+                               title = new Localized.Untranslated(titleStr);
+                       }
+               }
+       }
+
+       public void destroy() {
+
        }
 
        @Override
@@ -95,4 +126,8 @@ public class DefaultHeader implements CmsUiProvider, ManagedService {
                        this.properties.putAll(LangUtils.dictToStringMap(properties));
        }
 
+       public Localized getTitle() {
+               return title;
+       }
+
 }
index 9a5248e7db8d85d2cc1f2fd1dd22806ea633f846..10d386cb54959176f8462228abd79c7f320c3f9d 100644 (file)
@@ -87,15 +87,16 @@ public class DefaultLeadPane implements CmsUiProvider {
                                }
                                RankedObject<SuiteLayer> layerObj = layers.get(layerId);
 
-                               // TODO deal with i10n
-                               String titleStr = (String) layerObj.getProperties().get(SuiteLayer.Property.title.name());
                                Localized title = null;
-                               if (titleStr != null) {
-                                       if (titleStr.startsWith("%")) {
-                                               // LocaleUtils.local(titleStr, getClass().getClassLoader());
-                                               title = () -> titleStr;
-                                       } else {
-                                               title = new Localized.Untranslated(titleStr);
+                               if (!adminLayers.contains(layerId)) {
+                                       String titleStr = (String) layerObj.getProperties().get(SuiteLayer.Property.title.name());
+                                       if (titleStr != null) {
+                                               if (titleStr.startsWith("%")) {
+                                                       // LocaleUtils.local(titleStr, getClass().getClassLoader());
+                                                       title = () -> titleStr;
+                                               } else {
+                                                       title = new Localized.Untranslated(titleStr);
+                                               }
                                        }
                                }
 
@@ -114,41 +115,9 @@ public class DefaultLeadPane implements CmsUiProvider {
                                        first = b;
                        }
                }
-
-//             if (isAdmin && adminLayers != null)
-//                     for (String layerId : adminLayers) {
-//                             if (layers.containsKey(layerId)) {
-//                                     RankedObject<SuiteLayer> layerObj = layers.get(layerId);
-//
-//                                     String titleStr = (String) layerObj.getProperties().get(SuiteLayer.Property.title.name());
-//                                     Localized title = null;
-//                                     if (titleStr != null)
-//                                             title = new Localized.Untranslated(titleStr);
-//
-//                                     String iconName = (String) layerObj.getProperties().get(SuiteLayer.Property.icon.name());
-//                                     SuiteIcon icon = null;
-//                                     if (iconName != null)
-//                                             icon = SuiteIcon.valueOf(iconName);
-//
-//                                     Button b = SuiteUiUtils.createLayerButton(parent, layerId, title, icon);
-//                                     if (first == null)
-//                                             first = b;
-//                             }
-//                     }
-
-//             Button dashboardB = createButton(parent, SuiteMsg.dashboard.name(), SuiteMsg.dashboard, SuiteIcon.dashboard);
-               if (!cmsView.isAnonymous()) {
-//                     createButton(parent, SuiteMsg.documents.name(), SuiteMsg.documents, SuiteIcon.documents);
-//                     createButton(parent, SuiteMsg.people.name(), SuiteMsg.people, SuiteIcon.people);
-//                     createButton(parent, SuiteMsg.locations.name(), SuiteMsg.locations, SuiteIcon.location);
-               }
                return first;
        }
 
-       protected void processLayer(String layerDef) {
-
-       }
-
        public void init(Map<String, Object> properties) {
                String[] defaultLayers = (String[]) properties.get(Property.defaultLayers.toString());
                if (defaultLayers == null)
@@ -193,7 +162,7 @@ public class DefaultLeadPane implements CmsUiProvider {
                if (msg != null) {
                        Label lbl = new Label(parent, SWT.CENTER);
                        CmsUiUtils.style(lbl, SuiteStyle.leadPane);
-                       //CmsUiUtils.markup(lbl);
+                       // CmsUiUtils.markup(lbl);
                        ClassLoader l10nClassLoader = getClass().getClassLoader();
                        String txt = LocaleUtils.lead(msg, l10nClassLoader);
 //                     String txt = msg.lead();
index dc8060b3ad6d2bed0f91cf4defcb9eb7422e0f3a..e8bfa5f0ba48f922e8ad072f0be5134400257e83 100644 (file)
@@ -24,13 +24,13 @@ import org.apache.commons.logging.LogFactory;
 import org.argeo.api.NodeUtils;
 import org.argeo.cms.CmsUserManager;
 import org.argeo.cms.LocaleUtils;
+import org.argeo.cms.Localized;
 import org.argeo.cms.auth.CmsSession;
 import org.argeo.cms.ui.AbstractCmsApp;
 import org.argeo.cms.ui.CmsTheme;
 import org.argeo.cms.ui.CmsUiProvider;
 import org.argeo.cms.ui.CmsView;
 import org.argeo.cms.ui.dialogs.CmsFeedback;
-import org.argeo.cms.ui.util.CmsEvent;
 import org.argeo.cms.ui.util.CmsUiUtils;
 import org.argeo.eclipse.ui.specific.UiContext;
 import org.argeo.entity.EntityConstants;
@@ -54,6 +54,7 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
        public final static String PUBLIC_BASE_PATH_PROPERTY = "publicBasePath";
        public final static String DEFAULT_UI_NAME_PROPERTY = "defaultUiName";
        public final static String DEFAULT_THEME_ID_PROPERTY = "defaultThemeId";
+       private final static String LOGIN = "login";
 
        private String publicBasePath = null;
 
@@ -61,8 +62,8 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
        private String headerPid;
        private String leadPanePid;
        private String loginScreenPid;
-//     private String DASHBOARD_PID = pidPrefix + "dashboard";
-//     private String RECENT_ITEMS_PID = pidPrefix + "recentItems";
+
+       private String defaultLayerPid = "argeo.suite.ui.dashboardLayer";
 
        private String defaultUiName = "app";
        private String defaultThemeId = "org.argeo.suite.theme.default";
@@ -77,8 +78,6 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
        // TODO make more optimal or via CmsSession/CmsView
        private Map<String, SuiteUi> managedUis = new HashMap<>();
 
-//     private CmsUiProvider headerPart = null;
-
        public void init(Map<String, Object> properties) {
                if (log.isDebugEnabled())
                        log.info("Argeo Suite App started");
@@ -151,12 +150,20 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                        Node context = null;
                        SuiteUi ui = (SuiteUi) parent;
                        CmsView cmsView = CmsView.getCmsView(parent);
+                       CmsUiProvider headerUiProvider = findUiProvider(headerPid);
+                       Localized appTitle = null;
+                       if (headerUiProvider instanceof DefaultHeader) {
+                               appTitle = ((DefaultHeader) headerUiProvider).getTitle();
+                       }
+                       ui.setTitle(appTitle);
+
                        if (cmsView.isAnonymous() && publicBasePath == null) {// internal app, must login
                                ui.logout();
-                               refreshPart(findUiProvider(headerPid), ui.getHeader(), context);
+                               refreshPart(headerUiProvider, ui.getHeader(), context);
                                ui.refreshBelowHeader(false);
                                refreshPart(findUiProvider(loginScreenPid), ui.getBelowHeader(), context);
                                ui.layout(true, true);
+                               setState(ui, LOGIN);
                        } else {
                                CmsSession cmsSession = cmsView.getCmsSession();
                                if (ui.getUserDir() == null) {
@@ -179,7 +186,7 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                                if (context == null)
                                        context = ui.getUserDir();
 
-                               refreshPart(findUiProvider(headerPid), ui.getHeader(), context);
+                               refreshPart(headerUiProvider, ui.getHeader(), context);
                                ui.refreshBelowHeader(true);
                                for (String key : layersByPid.keySet()) {
                                        SuiteLayer layer = layersByPid.get(key).get();
@@ -187,7 +194,7 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                                }
                                refreshPart(findUiProvider(leadPanePid), ui.getLeadPane(), context);
                                ui.layout(true, true);
-                               setState(parent, state);
+                               setState(parent, state != null ? state : defaultLayerPid);
                        }
                } catch (Exception e) {
                        CmsFeedback.show("Unexpected exception", e);
@@ -214,6 +221,12 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                return uiProvidersByPid.get(pid).get();
        }
 
+       private SuiteLayer findLayer(String pid) {
+               if (!layersByPid.containsKey(pid))
+                       throw new IllegalArgumentException("No UI provider registered as " + pid);
+               return layersByPid.get(pid).get();
+       }
+
        private <T> T findByType(Map<String, RankedObject<T>> byType, Node context) {
                if (context == null)
                        throw new IllegalArgumentException("A node should be provided");
@@ -269,11 +282,18 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
 
        @Override
        public void setState(Composite parent, String state) {
-               if (state == null || state.equals("~"))
+               if (state == null)
                        return;
-               if (!state.startsWith("/") && !state.equals("~")) {
+               if (!state.startsWith("/")) {
                        if (parent instanceof SuiteUi) {
                                SuiteUi ui = (SuiteUi) parent;
+                               if (LOGIN.equals(state) || state.equals("~")) {
+                                       String appTitle = "";
+                                       if (ui.getTitle() != null)
+                                               appTitle = ui.getTitle().lead();
+                                       ui.getCmsView().stateChanged(state, appTitle);
+                                       return;
+                               }
                                String currentLayerId = ui.getCurrentLayerId();
                                if (state.equals(currentLayerId))
                                        return; // does nothing
@@ -343,9 +363,13 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                if (ui == null)
                        return;
                try {
+                       String appTitle = "";
+                       if (ui.getTitle() != null)
+                               appTitle = ui.getTitle().lead() + " - ";
+
 //                     String currentLayerId = ui.getCurrentLayerId();
 //                     SuiteLayer currentLayer = currentLayerId != null ? layersByPid.get(currentLayerId).get() : null;
-                       if (isTopic(event, SuiteEvent.refreshPart)) {
+                       if (SuiteUiUtils.isTopic(event, SuiteEvent.refreshPart)) {
                                Node node = getNode(ui, event);
                                if (node == null)
                                        return;
@@ -353,8 +377,8 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                                SuiteLayer layer = findByType(layersByType, node);
                                ui.switchToLayer(layer, node);
                                ui.getCmsView().runAs(() -> layer.view(uiProvider, ui.getCurrentWorkArea(), node));
-                               ui.getCmsView().stateChanged(nodeToState(node), Jcr.getTitle(node));
-                       } else if (isTopic(event, SuiteEvent.openNewPart)) {
+                               ui.getCmsView().stateChanged(nodeToState(node), appTitle + Jcr.getTitle(node));
+                       } else if (SuiteUiUtils.isTopic(event, SuiteEvent.openNewPart)) {
                                Node node = getNode(ui, event);
                                if (node == null)
                                        return;
@@ -362,13 +386,18 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                                SuiteLayer layer = findByType(layersByType, node);
                                ui.switchToLayer(layer, node);
                                ui.getCmsView().runAs(() -> layer.open(uiProvider, ui.getCurrentWorkArea(), node));
-                               ui.getCmsView().stateChanged(nodeToState(node), Jcr.getTitle(node));
-                       } else if (isTopic(event, SuiteEvent.switchLayer)) {
+                               ui.getCmsView().stateChanged(nodeToState(node), appTitle + Jcr.getTitle(node));
+                       } else if (SuiteUiUtils.isTopic(event, SuiteEvent.switchLayer)) {
                                String layerId = get(event, SuiteEvent.LAYER);
                                if (layerId != null) {
 //                                     ui.switchToLayer(layerId, ui.getUserDir());
+                                       SuiteLayer suiteLayer = findLayer(layerId);
+                                       Localized layerTitle = suiteLayer.getTitle();
                                        ui.getCmsView().runAs(() -> ui.switchToLayer(layerId, ui.getUserDir()));
-                                       ui.getCmsView().navigateTo(layerId);
+                                       String title = null;
+                                       if (layerTitle != null)
+                                               title = layerTitle.lead();
+                                       ui.getCmsView().stateChanged(layerId, appTitle + title);
                                } else {
                                        Node node = getNode(ui, event);
                                        if (node != null) {
@@ -426,11 +455,7 @@ public class SuiteApp extends AbstractCmsApp implements EventHandler {
                return managedUis.get(get(event, CMS_VIEW_UID_PROPERTY));
        }
 
-       private static boolean isTopic(Event event, CmsEvent cmsEvent) {
-               return event.getTopic().equals(cmsEvent.topic());
-       }
-
-       private static String get(Event event, String key) {
+       public static String get(Event event, String key) {
                Object value = event.getProperty(key);
                if (value == null)
                        return null;
index 8af761171fb7a540957719123ddfc4f13b13ef5f..994ea8459c06e313f04dcef783c815b85d05feed 100644 (file)
@@ -2,6 +2,7 @@ package org.argeo.suite.ui;
 
 import javax.jcr.Node;
 
+import org.argeo.cms.Localized;
 import org.argeo.cms.ui.CmsUiProvider;
 import org.eclipse.swt.widgets.Composite;
 
@@ -16,4 +17,8 @@ public interface SuiteLayer extends CmsUiProvider {
        default void open(CmsUiProvider uiProvider, Composite workArea, Node context) {
                view(uiProvider, workArea, context);
        }
+
+       default Localized getTitle() {
+               return null;
+       }
 }
index b245762cc7d8f27220d7ee6bfef5f5eff82246d8..3c4474ff66edc12e0e3beee9f33323c7ed024f3c 100644 (file)
@@ -11,6 +11,7 @@ import javax.jcr.Session;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.api.NodeConstants;
+import org.argeo.cms.Localized;
 import org.argeo.cms.ui.CmsView;
 import org.argeo.cms.ui.util.CmsUiUtils;
 import org.argeo.jcr.Jcr;
@@ -22,6 +23,8 @@ import org.eclipse.swt.widgets.Composite;
 class SuiteUi extends Composite {
        private static final long serialVersionUID = 6207018859086689108L;
        private final static Log log = LogFactory.getLog(SuiteUi.class);
+
+       private Localized title;
        private Composite header;
        private Composite belowHeader;
        private Composite leadPane;
@@ -223,4 +226,12 @@ class SuiteUi extends Composite {
                return cmsView;
        }
 
+       public Localized getTitle() {
+               return title;
+       }
+
+       public void setTitle(Localized title) {
+               this.title = title;
+       }
+
 }
index 3c47b96a6053a187abb123cbdc95af5bfc1c4305..79edb6416e38ffd7644037b9bbae3b68fce58ea2 100644 (file)
@@ -12,6 +12,7 @@ import org.argeo.cms.auth.CurrentUser;
 import org.argeo.cms.ui.CmsEditable;
 import org.argeo.cms.ui.CmsView;
 import org.argeo.cms.ui.dialogs.LightweightDialog;
+import org.argeo.cms.ui.util.CmsEvent;
 import org.argeo.cms.ui.util.CmsUiUtils;
 import org.argeo.eclipse.ui.EclipseUiUtils;
 import org.argeo.entity.EntityNames;
@@ -32,6 +33,7 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Text;
+import org.osgi.service.event.Event;
 
 /** UI utilities related to the APAF project. */
 public class SuiteUiUtils {
@@ -318,6 +320,10 @@ public class SuiteUiUtils {
                return coworker;
        }
 
+       public static boolean isTopic(Event event, CmsEvent cmsEvent) {
+               return event.getTopic().equals(cmsEvent.topic());
+       }
+
 //     public static String createAndConfigureEntity(Shell shell, Session referenceSession, String mainMixin,
 //                     String... additionnalProps) {
 //
diff --git a/core/org.argeo.suite.ui/src/org/argeo/suite/ui/widgets/TabbedArea.java b/core/org.argeo.suite.ui/src/org/argeo/suite/ui/widgets/TabbedArea.java
new file mode 100644 (file)
index 0000000..5192f84
--- /dev/null
@@ -0,0 +1,244 @@
+package org.argeo.suite.ui.widgets;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.ui.util.CmsUiUtils;
+import org.argeo.cms.ui.viewers.Section;
+import org.argeo.eclipse.ui.Selected;
+import org.argeo.jcr.Jcr;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FormLayout;
+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 Section} in a tab-like structure. */
+public class TabbedArea extends Composite {
+       private static final long serialVersionUID = 8659669229482033444L;
+
+       private Composite headers;
+       private Composite body;
+
+       private List<Section> sections = new ArrayList<>();
+
+       private Node previousNode;
+       private CmsUiProvider previousUiProvider;
+       private CmsUiProvider currentUiProvider;
+
+       private String tabStyle;
+       private String tabSelectedStyle;
+       private String bodyStyle;
+       private Image closeIcon;
+
+       public TabbedArea(Composite parent, int style) {
+               super(parent, style);
+               CmsUiUtils.style(parent, bodyStyle);
+
+               setLayout(CmsUiUtils.noSpaceGridLayout());
+
+               // TODO manage tabs at bottom or sides
+               headers = new Composite(this, SWT.NONE);
+               headers.setLayoutData(CmsUiUtils.fillWidth());
+               // CmsUiUtils.style(headers, bodyStyle);
+               body = new Composite(this, SWT.NONE);
+               body.setLayoutData(CmsUiUtils.fillAll());
+               body.setLayout(new FormLayout());
+               emptyState();
+       }
+
+       protected void refreshTabHeaders() {
+               // TODO deal with initialisation better
+//             CmsUiUtils.style(body, bodyStyle);
+
+//             int tabCount = sections.size() > 0 ?(sections.size()>1?sections.size()+1:1)  : 1;
+               int tabCount = sections.size() > 0 ? sections.size() : 1;
+               for (Control tab : headers.getChildren())
+                       tab.dispose();
+
+//             GridLayout headersGridLayout = new GridLayout(tabCount, true);
+//             headersGridLayout.marginHeight=0;
+//             headers.setLayout(headersGridLayout);
+               headers.setLayout(CmsUiUtils.noSpaceGridLayout(new GridLayout(tabCount, true)));
+
+               if (sections.size() == 0) {
+                       Composite emptyHeader = new Composite(headers, SWT.NONE);
+                       emptyHeader.setLayoutData(CmsUiUtils.fillAll());
+                       emptyHeader.setLayout(new GridLayout());
+                       Label lbl = new Label(emptyHeader, SWT.NONE);
+                       lbl.setText("");
+                       lbl.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false));
+
+               }
+
+               Section currentSection = getCurrentSection();
+               for (Section section : sections) {
+                       boolean selected = section == currentSection;
+                       Composite sectionHeader = section.createHeader(headers);
+                       CmsUiUtils.style(sectionHeader, selected ? tabSelectedStyle : tabStyle);
+                       int headerColumns = 2;
+                       sectionHeader.setLayout(new GridLayout(headerColumns, false));
+                       sectionHeader.setLayout(CmsUiUtils.noSpaceGridLayout(headerColumns));
+                       Button title = new Button(sectionHeader, SWT.FLAT);
+                       CmsUiUtils.style(title, selected ? tabSelectedStyle : tabStyle);
+                       title.setLayoutData(CmsUiUtils.fillWidth());
+                       title.addSelectionListener((Selected) (e) -> showTab(tabIndex(section.getNode())));
+                       Node node = section.getNode();
+                       title.setText(Jcr.getTitle(node));
+                       ToolBar toolBar = new ToolBar(sectionHeader, SWT.NONE);
+//                     CmsUiUtils.style(toolBar, selected ? tabSelectedStyle : tabStyle);
+                       ToolItem closeItem = new ToolItem(toolBar, SWT.FLAT);
+                       if (closeIcon != null)
+                               closeItem.setImage(closeIcon);
+                       else
+                               closeItem.setText("X");
+                       CmsUiUtils.style(closeItem, selected ? tabSelectedStyle : tabStyle);
+                       closeItem.addSelectionListener((Selected) (e) -> closeTab(section));
+               }
+
+//             if(sections.size()>1)
+//             {
+//                     ToolBar toolBar = new ToolBar(headers, SWT.NONE);
+//                     CmsUiUtils.style(toolBar, tabStyle);
+//                     ToolItem closeAllItem = new ToolItem(toolBar, SWT.FLAT);
+//                     closeAllItem.setText("X");
+//             }
+       }
+
+       public void view(CmsUiProvider uiProvider, Node context) {
+               if (body.isDisposed())
+                       return;
+               int index = tabIndex(context);
+               if (index >= 0) {
+                       showTab(index);
+                       previousNode = context;
+                       previousUiProvider = uiProvider;
+                       return;
+               }
+               Section section = (Section) body.getChildren()[0];
+               previousNode = section.getNode();
+               if (previousNode == null) {// empty state
+                       previousNode = context;
+                       previousUiProvider = uiProvider;
+               } else {
+                       previousUiProvider = currentUiProvider;
+               }
+               currentUiProvider = uiProvider;
+               section.setNode(context);
+               section.setLayoutData(CmsUiUtils.coverAll());
+               build(section, uiProvider, context);
+               if (sections.size() == 0)
+                       sections.add(section);
+               refreshTabHeaders();
+               layout(true, true);
+       }
+
+       public void open(CmsUiProvider uiProvider, Node context) {
+//             try {
+//                     if (openingTimer > 0)
+//                             Thread.sleep(openingTimer);
+//             } catch (InterruptedException e) {
+//                     // silent
+//             }
+
+               // int index = tabIndex(context);
+               if (previousNode != null && Jcr.getIdentifier(previousNode).equals(Jcr.getIdentifier(context))) {
+                       // does nothing
+                       return;
+               }
+               if (sections.size() == 0)
+                       CmsUiUtils.clear(body);
+               Section currentSection = getCurrentSection();
+               int currentIndex = sections.indexOf(currentSection);
+               Section previousSection = new Section(body, SWT.NONE, context);
+               build(previousSection, previousUiProvider, previousNode);
+               previousSection.setLayoutData(CmsUiUtils.coverAll());
+//             sections.remove(currentSection);
+               sections.add(currentIndex + 1, previousSection);
+//             sections.add(currentSection);
+//             nextCurrentSection.moveAbove(null);
+//             if (previousNode != null) {
+//                     view(previousUiProvider, previousNode);
+//             }
+               refreshTabHeaders();
+               layout(true, true);
+       }
+       
+       public void showTab(int index) {
+               Section sectionToShow = sections.get(index);
+               sectionToShow.moveAbove(null);
+               refreshTabHeaders();
+               layout(true, true);
+       }
+
+       protected void build(Section section, CmsUiProvider uiProvider, Node context) {
+               for (Control child : section.getChildren())
+                       child.dispose();
+               CmsUiUtils.style(section, bodyStyle);
+               section.setNode(context);
+               uiProvider.createUiPart(section, context);
+
+       }
+
+       private int tabIndex(Node node) {
+               for (int i = 0; i < sections.size(); i++) {
+                       Section section = sections.get(i);
+                       if (Jcr.getIdentifier(section.getNode()).equals(Jcr.getIdentifier(node)))
+                               return i;
+               }
+               return -1;
+       }
+
+       public void closeTab(Section 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);
+       }
+
+       protected void emptyState() {
+               new Section(body, SWT.NONE, null);
+               refreshTabHeaders();
+       }
+
+       public Composite getCurrent() {
+               return getCurrentSection();
+       }
+
+       protected Section getCurrentSection() {
+               return (Section) body.getChildren()[0];
+       }
+
+       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;
+       }
+}