From d8987e4a03d92ac91fb54a5fb350da85e0a4f58f Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Thu, 8 Aug 2019 20:45:28 +0200
Subject: [PATCH] OO-4168: allow to stack tools in the toolbar automatically

---
 .../segmentedview/SegmentViewRenderer.java    |   1 +
 .../stack/BreadcrumbBarRenderer.java          |  82 +++++++
 .../stack/BreadcrumbedStackedPanel.java       | 142 ++++++++++-
 .../BreadcrumbedStackedPanelRenderer.java     |  31 +--
 .../gui/components/stack/ToolBarRenderer.java | 135 ++++++++++
 .../components/stack/TooledStackedPanel.java  | 231 ++++++++++++------
 .../stack/TooledStackedPanelRenderer.java     | 123 +---------
 .../org/olat/core/gui/render/Renderer.java    |  19 +-
 8 files changed, 525 insertions(+), 239 deletions(-)
 create mode 100644 src/main/java/org/olat/core/gui/components/stack/BreadcrumbBarRenderer.java
 create mode 100644 src/main/java/org/olat/core/gui/components/stack/ToolBarRenderer.java

diff --git a/src/main/java/org/olat/core/gui/components/segmentedview/SegmentViewRenderer.java b/src/main/java/org/olat/core/gui/components/segmentedview/SegmentViewRenderer.java
index 432c24caac4..881e76c8ac4 100644
--- a/src/main/java/org/olat/core/gui/components/segmentedview/SegmentViewRenderer.java
+++ b/src/main/java/org/olat/core/gui/components/segmentedview/SegmentViewRenderer.java
@@ -42,6 +42,7 @@ public class SegmentViewRenderer extends DefaultComponentRenderer {
 			ComponentRenderer subRenderer = segment.getHTMLRendererSingleton();
 			Translator subTranslator = segment.getTranslator();
 			subRenderer.render(renderer, sb, segment, ubu, subTranslator, renderResult, args);
+			segment.setDirty(false);
 		}
 		sb.append("</div>");
 	}
diff --git a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbBarRenderer.java b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbBarRenderer.java
new file mode 100644
index 00000000000..92128b1268b
--- /dev/null
+++ b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbBarRenderer.java
@@ -0,0 +1,82 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.gui.components.stack;
+
+import java.util.List;
+
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.DefaultComponentRenderer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.stack.BreadcrumbedStackedPanel.BreadcrumbBar;
+import org.olat.core.gui.render.RenderResult;
+import org.olat.core.gui.render.Renderer;
+import org.olat.core.gui.render.StringOutput;
+import org.olat.core.gui.render.URLBuilder;
+import org.olat.core.gui.translator.Translator;
+
+/**
+ * 
+ * Initial date: 7 août 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BreadcrumbBarRenderer extends DefaultComponentRenderer {
+	
+	@Override
+	public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
+			RenderResult renderResult, String[] args) {
+		
+		BreadcrumbBar bar = (BreadcrumbBar)source;
+		BreadcrumbedStackedPanel panel = bar.getPanel();
+		List<Link> breadCrumbs = panel.getBreadCrumbs();
+
+		if (breadCrumbs.size() > panel.getInvisibleCrumb()) {
+			sb.append("<div id='o_c").append(source.getDispatchID()).append("' class='o_breadcrumb'>")
+			  .append("<ol class='breadcrumb'>");
+	
+			Link backLink = panel.getBackLink();
+			int numOfCrumbs = breadCrumbs.size();
+			if(backLink.isVisible() && numOfCrumbs > panel.getInvisibleCrumb()) {
+				sb.append("<li class='o_breadcrumb_back'>");
+				backLink.getHTMLRendererSingleton().render(renderer, sb, backLink, ubu, translator, renderResult, args);
+				backLink.setDirty(false);
+				sb.append("</li>");
+				
+				for(Link crumb:breadCrumbs) {
+					sb.append("<li>");
+					renderer.render(crumb, sb, args);
+					sb.append("</li>");
+				}
+			}
+			
+			Link closeLink = panel.getCloseLink();
+			if (closeLink.isVisible()) {
+				sb.append("<li class='o_breadcrumb_close'>");
+				closeLink.getHTMLRendererSingleton().render(renderer, sb, closeLink, ubu, translator, renderResult, args);
+				closeLink.setDirty(false);
+				sb.append("</li>");				
+			}	
+			sb.append("</ol>");
+		} else {
+			sb.append("<div id='o_c").append(source.getDispatchID()).append("'>");
+		}
+		sb.append("</div>");
+	}
+}
diff --git a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java
index b9b91209e9b..d17b5fd662e 100644
--- a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java
+++ b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanel.java
@@ -24,13 +24,16 @@ import java.util.List;
 
 import org.apache.logging.log4j.Logger;
 import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.AbstractComponent;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.ComponentCollection;
 import org.olat.core.gui.components.ComponentEventListener;
 import org.olat.core.gui.components.ComponentRenderer;
 import org.olat.core.gui.components.link.Link;
 import org.olat.core.gui.components.link.LinkFactory;
 import org.olat.core.gui.components.panel.Panel;
 import org.olat.core.gui.components.panel.StackedPanel;
+import org.olat.core.gui.components.stack.TooledStackedPanel.Align;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
@@ -49,14 +52,16 @@ import org.olat.core.util.Util;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, BreadcrumbPanel, ComponentEventListener {
+public class BreadcrumbedStackedPanel extends Panel implements BreadcrumbPanel, ComponentEventListener {
 	private static final Logger log = Tracing.createLoggerFor(BreadcrumbedStackedPanel.class);
+	private static final ComponentRenderer BAR_RENDERER = new BreadcrumbBarRenderer();
 	private static final ComponentRenderer RENDERER = new BreadcrumbedStackedPanelRenderer();
 	
 	protected final List<Link> stack = new ArrayList<>(3);
 	
 	protected final Link backLink;
 	protected final Link closeLink;
+	protected final BreadcrumbBar breadcrumbBar;
 	
 	private int invisibleCrumb = 1;
 	private String cssClass;
@@ -75,6 +80,9 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 		
 		this.cssClass = cssClass;
 		
+		String barId = getDispatchID().concat("_bbar");
+		breadcrumbBar = new BreadcrumbBar(barId);
+		
 		// Add back link before the bread crumbs, when pressed delegates click to current bread-crumb - 1
 		backLink = LinkFactory.createCustomLink("back", "back", "\u00A0", Link.NONTRANSLATED + Link.LINK_CUSTOM_CSS, null, this);
 		backLink.setIconLeftCSS("o_icon o_icon_back");
@@ -174,19 +182,27 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 	public List<Link> getBreadCrumbs() {
 		return stack;
 	}
+	
+	public BreadcrumbBar getBreadcrumbBar() {
+		return breadcrumbBar;
+	}
+	
+	@Override
+	public Component getComponent(String name) {
+		if(breadcrumbBar.getComponentName().equals(name)) {
+			return breadcrumbBar;
+		}
+		return super.getComponent(name);
+	}
 
 	@Override
 	public Iterable<Component> getComponents() {
 		List<Component> cmps = new ArrayList<>(3 + stack.size());
-		cmps.add(backLink);
-		cmps.add(closeLink);
+		cmps.add(breadcrumbBar);
 		Component content = getContent();
 		if(content != null && content != this) {
 			cmps.add(getContent());
 		}
-		for(Link crumb:stack) {
-			cmps.add(crumb);
-		}
 		return cmps;
 	}
 
@@ -408,7 +424,7 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 	
 	@Override
 	public void rootController(String displayName, Controller controller) {
-		if(stack.size() > 0) {
+		if(!stack.isEmpty()) {
 			for(int i=stack.size(); i-->0; ) {
 				Link link = stack.remove(i);
 				BreadCrumb crumb = (BreadCrumb)link.getUserObject();
@@ -493,7 +509,7 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 	@Override
 	public void changeDisplayname(String diplayName) {
 		stack.get(stack.size() - 1).setCustomDisplayText(diplayName);
-		setDirty(true);
+		breadcrumbBar.setDirty(true);
 	}
 
 	@Override
@@ -584,9 +600,72 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 		closeLink.setVisible(showClose);								
 	}
 	
+	public class BreadcrumbBar extends AbstractComponent implements ComponentCollection {
+		
+		public BreadcrumbBar(String id) {
+			super(id, null, null);
+			setDomReplacementWrapperRequired(false);
+		}
+		
+		public BreadcrumbedStackedPanel getPanel() {
+			return BreadcrumbedStackedPanel.this;
+		}
+		
+		@Override
+		public Translator getTranslator() {
+			return BreadcrumbedStackedPanel.this.getTranslator();
+		}
+
+		@Override
+		protected void doDispatchRequest(UserRequest ureq) {
+			String cmd = ureq.getParameter(VelocityContainer.COMMAND_ID);
+			if(cmd != null) {
+				if(backLink.getCommand().equals(cmd)) {
+					dispatchEvent(ureq, backLink, null);
+				} else if(closeLink.getCommand().equals(cmd)) {
+					dispatchEvent(ureq, closeLink, null);
+				}
+			}
+		}
+
+		@Override
+		public ComponentRenderer getHTMLRendererSingleton() {
+			return BAR_RENDERER;
+		}
+
+		@Override
+		public Component getComponent(String name) {
+			if(backLink.getComponentName().equals(name)) {
+				return backLink;
+			}
+			if(closeLink.getComponentName().equals(name)) {
+				return closeLink;
+			}
+			for(Link crumb:stack) {
+				if(crumb != null && crumb.getComponentName().equals(name)) {
+					return crumb;
+				}
+			}
+			return null;
+		}
+
+		@Override
+		public Iterable<Component> getComponents() {
+			List<Component> cmps = new ArrayList<>(3 + stack.size());
+			cmps.add(backLink);
+			cmps.add(closeLink);
+			for(Link crumb:stack) {
+				cmps.add(crumb);
+			}
+			return cmps;
+		}
+	}
+
 	public static class BreadCrumb {
+		
 		private final Object uobject;
 		private final Controller controller;
+		private final List<Tool> tools = new ArrayList<>(5);
 		
 		public BreadCrumb(Controller controller, Object uobject) {
 			this.uobject = uobject;
@@ -596,15 +675,58 @@ public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, Bre
 		public Object getUserObject() {
 			return uobject;
 		}
-
+	
 		public Controller getController() {
 			return controller;
 		}
-
+		
+		public List<Tool> getTools() {
+			return tools;
+		}
+		
+		public void addTool(Tool tool) {
+			tools.add(tool);
+		}
+		
+		public void removeTool(Tool tool) {
+			tools.remove(tool);
+		}
+	
 		public void dispose() {
 			if(controller != null) {
 				controller.dispose();
 			}
 		}
 	}
+	
+	public static class Tool {
+
+		private final  Align align;
+		private final boolean inherit;
+		private final Component component;
+		private String toolCss;
+		
+		public Tool(Component component, Align align, boolean inherit, String toolCss) {
+			this.align = align;
+			this.inherit = inherit;
+			this.component = component;
+			this.toolCss = toolCss;
+		}
+		
+		public boolean isInherit() {
+			return inherit;
+		}
+
+		public Align getAlign() {
+			return align;
+		}
+
+		public Component getComponent() {
+			return component;
+		}
+		
+		public String getToolCss() {
+			return toolCss;
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanelRenderer.java b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanelRenderer.java
index 5eef3c988ce..b00e50a8a01 100644
--- a/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanelRenderer.java
+++ b/src/main/java/org/olat/core/gui/components/stack/BreadcrumbedStackedPanelRenderer.java
@@ -19,11 +19,8 @@
  */
 package org.olat.core.gui.components.stack;
 
-import java.util.List;
-
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.DefaultComponentRenderer;
-import org.olat.core.gui.components.link.Link;
 import org.olat.core.gui.render.RenderResult;
 import org.olat.core.gui.render.Renderer;
 import org.olat.core.gui.render.StringOutput;
@@ -42,39 +39,13 @@ public class BreadcrumbedStackedPanelRenderer extends DefaultComponentRenderer {
 	public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
 			RenderResult renderResult, String[] args) {
 		BreadcrumbedStackedPanel panel = (BreadcrumbedStackedPanel) source;
-		List<Link> breadCrumbs = panel.getBreadCrumbs();
 
 		// panel div
 		String mainCssClass = panel.getCssClass();
 		sb.append("<div id='o_c").append(source.getDispatchID()).append("' class='")
 				.append(mainCssClass, mainCssClass != null).append("'>");
 
-		if (breadCrumbs.size() > panel.getInvisibleCrumb()) {
-			sb.append("<div class='o_breadcrumb'><ol class='breadcrumb'>");
-
-			Link backLink = panel.getBackLink();
-			int numOfCrumbs = breadCrumbs.size();
-			if(backLink.isVisible() && numOfCrumbs > panel.getInvisibleCrumb()) {
-				sb.append("<li class='o_breadcrumb_back'>");
-				backLink.getHTMLRendererSingleton().render(renderer, sb, backLink, ubu, translator, renderResult, args);
-				sb.append("</li>");
-				
-				for(Link crumb:breadCrumbs) {
-					sb.append("<li>");
-					renderer.render(crumb, sb, args);
-					sb.append("</li>");
-				}
-			}
-			
-			Link closeLink = panel.getCloseLink();
-			if (closeLink.isVisible()) {
-				sb.append("<li class='o_breadcrumb_close'>");
-				closeLink.getHTMLRendererSingleton().render(renderer, sb, closeLink, ubu, translator, renderResult, args);
-				sb.append("</li>");				
-			}	
-
-			sb.append("</ol></div>");
-		}
+		renderer.render(panel.getBreadcrumbBar(), sb, args);
 		
 		Component toRender = panel.getContent();
 		if(toRender != null) {
diff --git a/src/main/java/org/olat/core/gui/components/stack/ToolBarRenderer.java b/src/main/java/org/olat/core/gui/components/stack/ToolBarRenderer.java
new file mode 100644
index 00000000000..b873a97c35e
--- /dev/null
+++ b/src/main/java/org/olat/core/gui/components/stack/ToolBarRenderer.java
@@ -0,0 +1,135 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.gui.components.stack;
+
+import java.util.List;
+
+import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.DefaultComponentRenderer;
+import org.olat.core.gui.components.dropdown.Dropdown;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.stack.BreadcrumbedStackedPanel.Tool;
+import org.olat.core.gui.components.stack.TooledStackedPanel.Align;
+import org.olat.core.gui.components.stack.TooledStackedPanel.ToolBar;
+import org.olat.core.gui.components.stack.TooledStackedPanel.ToolsSlot;
+import org.olat.core.gui.render.RenderResult;
+import org.olat.core.gui.render.Renderer;
+import org.olat.core.gui.render.StringOutput;
+import org.olat.core.gui.render.URLBuilder;
+import org.olat.core.gui.translator.Translator;
+
+/**
+ * 
+ * Initial date: 7 août 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ToolBarRenderer extends DefaultComponentRenderer {
+
+	@Override
+	public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
+			RenderResult renderResult, String[] args) {
+		
+		ToolBar toolBar = (ToolBar)source;
+		TooledStackedPanel panel = toolBar.getPanel();
+		List<Tool> leftTools = panel.getTools(Align.left);
+		List<Tool> rightEdgeTools = panel.getTools(Align.rightEdge);
+		List<Tool> rightTools = panel.getTools(Align.right);
+		List<Tool> segmentsTools = panel.getTools(Align.segment);
+		List<Tool> centerTools = panel.getTools(Align.center);
+		
+		if(panel.isToolbarEnabled() || (panel.isToolbarAutoEnabled()
+				&& (!leftTools.isEmpty() || !rightTools.isEmpty() || !centerTools.isEmpty() || !segmentsTools.isEmpty()))) {
+			sb.append("<div id='o_c").append(source.getDispatchID()).append("' class='o_tools_container'><div class='container-fluid'>");
+
+			renderTools(leftTools, renderer, toolBar.getSlot(Align.left), sb, translator, args);
+			renderTools(rightEdgeTools, renderer, toolBar.getSlot(Align.rightEdge), sb, translator, args);
+			renderTools(rightTools, renderer, toolBar.getSlot(Align.right), sb, translator, args);
+			renderTools(centerTools, renderer, toolBar.getSlot(Align.center), sb, translator, args);
+
+			sb.append("</div>"); // container-fluid,
+			
+			if(!segmentsTools.isEmpty()) {
+				boolean segmentAlone = leftTools.isEmpty() && rightTools.isEmpty() && centerTools.isEmpty();
+				sb.append("<ul class='o_tools o_tools_segments list-inline")
+				  .append(" o_tools_segments_alone", segmentAlone).append("'>");
+				
+				Tool segmentTool = segmentsTools.get(segmentsTools.size() - 1);
+				renderTool(segmentTool, renderer, sb, args);
+				
+				sb.append("</ul>");
+			}
+			sb.append("</div>"); 
+		} else {
+			sb.append("<div id='o_c").append(source.getDispatchID()).append("'></div>");
+		}
+	}
+	
+	private void renderTools(List<Tool> tools, Renderer renderer, ToolsSlot slot, StringOutput sb, Translator translator, String[] args) {
+		if(!tools.isEmpty()) {
+			Align align = slot.getSlot();
+			sb.append("<ul class='o_tools ").append(align.cssClass()).append(" list-inline'>");
+			
+			int limit = slot.getLimitOfTools();
+			for(int i=0; i<tools.size() && i<limit-1; i++) {
+				renderTool(tools.get(i), renderer, sb, args);
+			}
+			if(tools.size() > limit) {
+				List<Tool> droppedTools = tools.subList(limit - 1, tools.size());
+				renderDropDown(droppedTools, renderer, slot, sb, translator, args); 
+			}
+			sb.append("</ul>");
+		}
+	}
+	
+	private void renderDropDown(List<Tool> tools, Renderer renderer, ToolsSlot slot, StringOutput sb, Translator translator, String[] args) {
+		String label = translator == null ? slot.getToolDropdownI18nKey() : translator.translate(slot.getToolDropdownI18nKey());// paranoia
+		
+		sb.append("<li class='o_tool_dropdown dropdown'>")
+		  .append("<a href='#' class='dropdown-toggle' data-toggle='dropdown'>")
+		  .append("<span class='o_inner_wrapper'><i class='o_icon o_icon_tools'>\u00A0</i></span> <i class='o_icon o_icon_caret'> </i> <span class='o_label'>")
+		  .append(label).append("</span></a>")
+		  .append("<ul class='dropdown-menu").append(" dropdown-menu-right", slot.getSlot() == Align.right || slot.getSlot() == Align.rightEdge).append("' role='menu'>");
+		for(Tool tool:tools) {
+			sb.append("<li>");
+			renderer.render(tool.getComponent(), sb, args);
+			sb.append("</li>");
+		}
+		sb.append("</ul></li>");
+	}
+	
+	private void renderTool(Tool tool, Renderer renderer, StringOutput sb, String[] args) {
+		Component cmp = tool.getComponent();
+		String cssClass = tool.getToolCss();
+		if (cssClass == null) {
+			// use defaults
+			if(cmp instanceof Dropdown) {
+				cssClass = "o_tool_dropdown dropdown";
+			} else if(cmp instanceof Link && !cmp.isEnabled()) {
+				cssClass = "o_text";
+			} else {
+				cssClass = "o_tool";
+			}				
+		}
+		sb.append("<li class='").append(cssClass).append("'>");
+		renderer.render(cmp, sb, args);
+		sb.append("</li>");
+	}
+}
diff --git a/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanel.java b/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanel.java
index d99c9aced8e..ed506d5f0f3 100644
--- a/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanel.java
+++ b/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanel.java
@@ -20,16 +20,20 @@
 package org.olat.core.gui.components.stack;
 
 import java.util.ArrayList;
+import java.util.EnumMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Collectors;
 
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.AbstractComponent;
 import org.olat.core.gui.components.Component;
+import org.olat.core.gui.components.ComponentCollection;
 import org.olat.core.gui.components.ComponentEventListener;
 import org.olat.core.gui.components.ComponentRenderer;
-import org.olat.core.gui.components.link.Link;
-import org.olat.core.gui.components.panel.StackedPanel;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.translator.Translator;
+import org.olat.core.util.StringHelper;
 
 /**
  * 
@@ -39,9 +43,10 @@ import org.olat.core.gui.translator.Translator;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class TooledStackedPanel extends BreadcrumbedStackedPanel implements StackedPanel, BreadcrumbPanel, ComponentEventListener {
+public class TooledStackedPanel extends BreadcrumbedStackedPanel {
 	
 	private static final ComponentRenderer RENDERER = new TooledStackedPanelRenderer();
+	private static final ComponentRenderer TOOLS_RENDERER = new ToolBarRenderer();
 	private boolean toolbarEnabled = true;
 	private boolean toolbarAutoEnabled = false;
 	private boolean breadcrumbEnabled = true;
@@ -49,6 +54,8 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	private String message;
 	private String messageCssClass;
 	private Component messageCmp;
+	private final ToolBar toolBar;
+	private final EnumMap<Align,ToolsSlot> toolsSlots;
 	
 	public TooledStackedPanel(String name, Translator translator, ComponentEventListener listener) {
 		this(name, translator, listener, null);
@@ -57,22 +64,26 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	public TooledStackedPanel(String name, Translator translator, ComponentEventListener listener, String cssClass) {
 		super(name, translator, listener, cssClass);
 		setDomReplacementWrapperRequired(false); // renders own div in Renderer
+		toolsSlots = new EnumMap<>(Align.class);
+		for(Align val:Align.values()) {
+			toolsSlots.put(val, new ToolsSlot(val));
+		}
+		toolBar = new ToolBar(getDispatchID().concat("_tbar"));
+	}
+	
+	public ToolBar getToolBar() {
+		return toolBar;
 	}
 
 	@Override
 	public Iterable<Component> getComponents() {
 		List<Component> cmps = new ArrayList<>();
-		cmps.add(getBackLink());
+		cmps.add(getBreadcrumbBar());
 		cmps.add(getContent());
+		cmps.add(toolBar);
 		if(messageCmp != null) {
 			cmps.add(messageCmp);
 		}
-		for(Link crumb:stack) {
-			cmps.add(crumb);
-		}
-		for(Tool tool:getTools()) {
-			cmps.add(tool.getComponent());
-		}
 		return cmps;
 	}
 
@@ -83,7 +94,7 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	
 	@Override
 	protected BreadCrumb createCrumb(Controller controller, Object uobject) {
-		return new TooledBreadCrumb(controller, uobject);
+		return new BreadCrumb(controller, uobject);
 	}
 
 	/**
@@ -122,7 +133,7 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	public void removeTool(Component toolComponent) {
 		if(toolComponent == null) return;
 		
-		TooledBreadCrumb breadCrumb = getCurrentCrumb();
+		BreadCrumb breadCrumb = getCurrentCrumb();
 		if(breadCrumb != null) {
 			removeTool(toolComponent, breadCrumb);
 		}
@@ -131,8 +142,8 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	public void removeTool(Component toolComponent, Controller controller) {
 		for(int i=0; i<stack.size(); i++) {
 			Object uo = stack.get(i).getUserObject();
-			if(uo instanceof TooledBreadCrumb) {
-				TooledBreadCrumb crumb = (TooledBreadCrumb)uo;
+			if(uo instanceof BreadCrumb) {
+				BreadCrumb crumb = (BreadCrumb)uo;
 				if (controller.equals(crumb.getController())) {
 					removeTool(toolComponent, crumb);
 				}
@@ -140,21 +151,21 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 		}
 	}
 	
-	private void removeTool(Component toolComponent, TooledBreadCrumb breadCrumb) {
+	private void removeTool(Component toolComponent, BreadCrumb breadCrumb) {
 		for(Iterator<Tool> it=breadCrumb.getTools().iterator(); it.hasNext(); ) {
 			if(toolComponent == it.next().getComponent()) {
 				it.remove();
-				setDirty(true);
+				toolBar.setDirty(true);
 			}
 		}
 	}
 	
 	public void removeAllTools() {
-		TooledBreadCrumb breadCrumb = getCurrentCrumb();
+		BreadCrumb breadCrumb = getCurrentCrumb();
 		if(breadCrumb != null) {
 			breadCrumb.getTools().clear();
 		}
-		setDirty(true);
+		toolBar.setDirty(true);
 	}
 
 	/**
@@ -168,21 +179,22 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	public void addTool(Component toolComponent, Align align, boolean inherit, String css, Controller controller) {
 		if(toolComponent == null) return;
 		
+		align = align == null ? Align.center : align;
 		Tool tool = new Tool(toolComponent, align, inherit, css);
-		TooledBreadCrumb breadCrumb = controller == null
+		BreadCrumb breadCrumb = controller == null
 				? getCurrentCrumb()
 				: getBreadCrumb(controller);
 		if(breadCrumb != null) {
 			breadCrumb.addTool(tool);
 		}
-		setDirty(true);
+		toolBar.setDirty(true);
 	}
-	
-	private TooledBreadCrumb getBreadCrumb(Controller controller) {
+
+	private BreadCrumb getBreadCrumb(Controller controller) {
 		for(int i=0; i<stack.size(); i++) {
 			Object uo = stack.get(i).getUserObject();
-			if(uo instanceof TooledBreadCrumb) {
-				TooledBreadCrumb crumb = (TooledBreadCrumb)uo;
+			if(uo instanceof BreadCrumb) {
+				BreadCrumb crumb = (BreadCrumb)uo;
 				if (controller.equals(crumb.getController())) {
 					return crumb;
 				}
@@ -197,8 +209,8 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 		int lastStep = stack.size() - 1;
 		for(int i=0; i<lastStep; i++) {
 			Object uo = stack.get(i).getUserObject();
-			if(uo instanceof TooledBreadCrumb) {
-				TooledBreadCrumb crumb = (TooledBreadCrumb)uo;
+			if(uo instanceof BreadCrumb) {
+				BreadCrumb crumb = (BreadCrumb)uo;
 				List<Tool> tools = crumb.getTools();
 				for(Tool tool:tools) {
 					if(tool.isInherit()) {
@@ -208,18 +220,25 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 			}
 		}
 		
-		TooledBreadCrumb breadCrumb = getCurrentCrumb();
+		BreadCrumb breadCrumb = getCurrentCrumb();
 		if(breadCrumb != null) {
 			currentTools.addAll(breadCrumb.getTools());
 		}
 		return currentTools;
 	}
 	
-	private TooledBreadCrumb getCurrentCrumb() {
+	public List<Tool> getTools(Align alignement) {
+		List<Tool> tools = getTools();
+		return tools.stream()
+			.filter(tool -> alignement.equals(tool.getAlign()) && tool.getComponent().isVisible())
+			.collect(Collectors.toList());
+	}
+	
+	private BreadCrumb getCurrentCrumb() {
 		if(stack.isEmpty()) {
 			return null;
 		}
-		return (TooledBreadCrumb)stack.get(stack.size() - 1).getUserObject();
+		return (BreadCrumb)stack.get(stack.size() - 1).getUserObject();
 	}
 	
 	@Override
@@ -229,7 +248,7 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 
 	@Override
 	public void pushController(String displayName, String iconLeftCss, Controller controller) {
-		TooledBreadCrumb currentCrumb = getCurrentCrumb();
+		BreadCrumb currentCrumb = getCurrentCrumb();
 		if(currentCrumb == null || currentCrumb.getController() != controller) {
 			super.pushController(displayName, iconLeftCss, controller);
 			if(controller instanceof TooledController) {
@@ -279,6 +298,28 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 	public void setBreadcrumbEnabled(boolean breadcrumbEnabled) {
 		this.breadcrumbEnabled = breadcrumbEnabled;
 	}
+	
+	public void setToolsLimit(Align slot, int maxNumberOfTools, String dropdownI18nKey) {
+		setToolsLimit(slot, maxNumberOfTools, dropdownI18nKey, null);
+	}
+
+	/**
+	 * 
+	 * @param slot The slot to limit
+	 * @param maxNumberOfTools The maximum of tools visible in the toolbar
+	 * @param dropdownI18nKey The i18n key to label the dropdown
+	 * @param dropdownIconCss The CSS class to decorate the dropdown
+	 */
+	public void setToolsLimit(Align slot, int maxNumberOfTools, String dropdownI18nKey, String dropdownIconCss) {
+		ToolsSlot config = toolsSlots.get(slot);
+		config.setLimitOfTools(maxNumberOfTools);
+		config.setToolDropdownI18nKey(dropdownI18nKey);
+		if(StringHelper.containsNonWhitespace(dropdownIconCss)) {
+			config.setToolDropdownIconCss(dropdownIconCss);
+		} else {
+			config.setToolDropdownIconCss("o_icon o_icon_menuhandel");
+		}
+	}
 
 	public String getMessage() {
 		return message;
@@ -306,63 +347,113 @@ public class TooledStackedPanel extends BreadcrumbedStackedPanel implements Stac
 			setDirty(true);
 		}
 	}
-
-	public static class Tool {
-		private final  Align align;
-		private final boolean inherit;
-		private final Component component;
-		private String toolCss;
+	
+	public enum Align {
+		left("o_tools_left"),
+		center("o_tools_center"),
+		right("o_tools_right"),
+		rightEdge("o_tools_right_edge"),
+		segment("o_tools_segments");
+		
+		private final String cssClass;
 		
-		public Tool(Component component, Align align, boolean inherit, String toolCss) {
-			this.align = align;
-			this.inherit = inherit;
-			this.component = component;
-			this.toolCss = toolCss;
+		private Align(String cssClass) {
+			this.cssClass = cssClass;
 		}
 		
-		public boolean isInherit() {
-			return inherit;
+		public String cssClass() {
+			return cssClass;
+		}
+	}
+	
+	public class ToolsSlot {
+		
+		private final Align slot;
+		private int limitOfTools = 32;
+		private String toolDropdownI18nKey;
+		private String toolDropdownIconCss;
+		
+		public ToolsSlot(Align slot) {
+			this.slot = slot;
 		}
 
-		public Align getAlign() {
-			return align;
+		public int getLimitOfTools() {
+			return limitOfTools;
 		}
 
-		public Component getComponent() {
-			return component;
+		public void setLimitOfTools(int limitOfTools) {
+			this.limitOfTools = limitOfTools;
 		}
-		
-		public String getToolCss() {
-			return toolCss;
+
+		public Align getSlot() {
+			return slot;
+		}
+
+		public String getToolDropdownI18nKey() {
+			return toolDropdownI18nKey;
+		}
+
+		public void setToolDropdownI18nKey(String i18nKey) {
+			toolDropdownI18nKey = i18nKey;
+		}
+
+		public String getToolDropdownIconCss() {
+			return toolDropdownIconCss;
+		}
+
+		public void setToolDropdownIconCss(String toolDropdownIconCss) {
+			this.toolDropdownIconCss = toolDropdownIconCss;
 		}
-		
 	}
 	
-	public static class TooledBreadCrumb extends BreadCrumb {
-		private final List<Tool> tools = new ArrayList<>(5);
-
-		public TooledBreadCrumb(Controller controller, Object uobject) {
-			super(controller, uobject);
+	public class ToolBar extends AbstractComponent implements ComponentCollection {
+		
+		public ToolBar(String id) {
+			super(id, null, null);
+			setDomReplacementWrapperRequired(false);
 		}
 		
-		public List<Tool> getTools() {
-			return tools;
+		public TooledStackedPanel getPanel() {
+			return TooledStackedPanel.this;
 		}
 		
-		public void addTool(Tool tool) {
-			tools.add(tool);
+		public ToolsSlot getSlot(Align align) {
+			return toolsSlots.get(align);
 		}
 		
-		public void removeTool(Tool tool) {
-			tools.remove(tool);
+		@Override
+		public Translator getTranslator() {
+			return TooledStackedPanel.this.getTranslator();
+		}
+		
+		@Override
+		public Component getComponent(String name) {
+			for(Tool tool:getTools()) {
+				Component cmp = tool.getComponent();
+				if(cmp != null && cmp.getComponentName().equals(name)) {
+					return cmp;
+				}
+			}
+			return null;
+		}
+
+		@Override
+		public Iterable<Component> getComponents() {
+			List<Component> cmps = new ArrayList<>();
+			for(Tool tool:getTools()) {
+				cmps.add(tool.getComponent());
+			}
+			return cmps;
+		}
+
+		@Override
+		protected void doDispatchRequest(UserRequest ureq) {
+			//
+		}
+
+		@Override
+		public ComponentRenderer getHTMLRendererSingleton() {
+			return TOOLS_RENDERER;
 		}
 	}
-	
-	public enum Align {
-		left,
-		right,
-		rightEdge,
-		segment
-	}
-	
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanelRenderer.java b/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanelRenderer.java
index f359b3b5097..79d5d0bcbcb 100644
--- a/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanelRenderer.java
+++ b/src/main/java/org/olat/core/gui/components/stack/TooledStackedPanelRenderer.java
@@ -19,16 +19,13 @@
  */
 package org.olat.core.gui.components.stack;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.DefaultComponentRenderer;
-import org.olat.core.gui.components.dropdown.Dropdown;
 import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.components.stack.BreadcrumbedStackedPanel.Tool;
 import org.olat.core.gui.components.stack.TooledStackedPanel.Align;
-import org.olat.core.gui.components.stack.TooledStackedPanel.Tool;
 import org.olat.core.gui.render.RenderResult;
 import org.olat.core.gui.render.Renderer;
 import org.olat.core.gui.render.StringOutput;
@@ -57,86 +54,17 @@ public class TooledStackedPanelRenderer extends DefaultComponentRenderer {
 		
 		if((panel.isBreadcrumbEnabled() && breadCrumbs.size() > panel.getInvisibleCrumb()) || (!tools.isEmpty() && panel.isToolbarEnabled())) {
 			sb.append("<div id='o_main_toolbar' class='o_toolbar");
-			if ((panel.isToolbarAutoEnabled() || panel.isToolbarEnabled() ) && !getTools(tools, Align.segment).isEmpty()) {
+			if ((panel.isToolbarAutoEnabled() || panel.isToolbarEnabled() ) && !panel.getTools(Align.segment).isEmpty()) {
 				sb.append(" o_toolbar_with_segments");
 			}
 			sb.append("'>");
 
-			if(panel.isBreadcrumbEnabled() && breadCrumbs.size() > panel.getInvisibleCrumb()) {
-				sb.append("<div class='o_breadcrumb'><ol class='breadcrumb'>");
-				Link backLink = panel.getBackLink();
-				int numOfCrumbs = breadCrumbs.size();
-				if(backLink.isVisible() && numOfCrumbs > panel.getInvisibleCrumb()) {
-					sb.append("<li class='o_breadcrumb_back'>");
-					backLink.getHTMLRendererSingleton().render(renderer, sb, backLink, ubu, translator, renderResult, args);
-					sb.append("</li>");
-					
-					for(Link crumb:breadCrumbs) {
-						sb.append("<li").append(" class='active'", breadCrumbs.indexOf(crumb) == numOfCrumbs-1).append(">");
-						renderer.render(crumb, sb, args);
-						sb.append("</li>");
-					}
-				}
-
-				Link closeLink = panel.getCloseLink();
-				if (closeLink.isVisible()) {
-					sb.append("<li class='o_breadcrumb_close'>");
-					closeLink.getHTMLRendererSingleton().render(renderer, sb, closeLink, ubu, translator, renderResult, args);
-					sb.append("</li>");				
-				}	
-
-				sb.append("</ol></div>"); // o_breadcrumb
+			if(panel.isBreadcrumbEnabled() && breadCrumbs.size() > panel.getInvisibleCrumb()) {	
+				renderer.render(panel.getBreadcrumbBar(), sb, args);
 			}
 			
 			if (panel.isToolbarAutoEnabled() || panel.isToolbarEnabled()) {
-				List<Tool> leftTools = getTools(tools, Align.left);
-				List<Tool> rightEdgeTools = getTools(tools, Align.rightEdge);
-				List<Tool> rightTools = getTools(tools, Align.right);
-				List<Tool> segmentsTools = getTools(tools, Align.segment);
-				List<Tool> notAlignedTools = getTools(tools, null);
-				
-				if(panel.isToolbarEnabled() || (panel.isToolbarAutoEnabled()
-						&& (!leftTools.isEmpty() || !rightTools.isEmpty() || !notAlignedTools.isEmpty() || !segmentsTools.isEmpty()))) {
-					sb.append("<div class='o_tools_container'><div class='container-fluid'>");
-					
-					if(!leftTools.isEmpty()) {
-						sb.append("<ul class='o_tools o_tools_left list-inline'>");
-						renderTools(leftTools, renderer, sb, args);
-						sb.append("</ul>");
-					}
-					
-					if(!rightEdgeTools.isEmpty()) {
-						sb.append("<ul class='o_tools o_tools_right_edge list-inline'>");
-						renderTools(rightEdgeTools, renderer, sb, args);
-						sb.append("</ul>");
-					}
-
-					if(!rightTools.isEmpty()) {
-						sb.append("<ul class='o_tools o_tools_right list-inline'>");
-						renderTools(rightTools, renderer, sb, args);
-						sb.append("</ul>");
-					}
-
-					if(!notAlignedTools.isEmpty()) {
-						sb.append("<ul class='o_tools o_tools_center list-inline'>");
-						renderTools(notAlignedTools, renderer, sb, args);
-						sb.append("</ul>");
-					}
-					sb.append("</div>"); // container-fluid,
-					
-					if(!segmentsTools.isEmpty()) {
-						boolean segmentAlone = leftTools.isEmpty() && rightTools.isEmpty() && notAlignedTools.isEmpty();
-						sb.append("<ul class='o_tools o_tools_segments list-inline")
-						  .append(" o_tools_segments_alone", segmentAlone).append("'>");
-						
-						Tool segmentTool = segmentsTools.get(segmentsTools.size() - 1);
-						List<Tool> lastSegmentTool = Collections.singletonList(segmentTool);
-						renderTools(lastSegmentTool, renderer, sb, args);
-						
-						sb.append("</ul>");
-					}
-					sb.append("</div>"); 
-				}
+				renderer.render(panel.getToolBar(), sb, args);
 			}
 			sb.append("</div>"); // o_toolbar
 		}
@@ -152,6 +80,7 @@ public class TooledStackedPanelRenderer extends DefaultComponentRenderer {
 			Component messageCmp = panel.getMessageComponent();
 			URLBuilder cubu = ubu.createCopyFor(messageCmp);
 			messageCmp.getHTMLRendererSingleton().render(renderer, sb, messageCmp, cubu, translator, renderResult, args);
+			messageCmp.setDirty(false);
 		}
 		
 		Component toRender = panel.getContent();
@@ -161,44 +90,4 @@ public class TooledStackedPanelRenderer extends DefaultComponentRenderer {
 
 		sb.append("</div>"); // end of panel div
 	}
-	
-	private List<Tool> getTools(List<Tool> tools, Align alignement) {
-		List<Tool> alignedTools = new ArrayList<>(tools.size());
-		if(alignement == null) {
-			for(Tool tool:tools) {
-				if(tool.getAlign() == null && tool.getComponent().isVisible()) {
-					alignedTools.add(tool);
-				}
-			}
-		} else {
-			for(Tool tool:tools) {
-				if(alignement.equals(tool.getAlign()) && tool.getComponent().isVisible()) {
-					alignedTools.add(tool);
-				}
-			}
-		}
-		return alignedTools;
-	}
-	
-	private void renderTools(List<Tool> tools, Renderer renderer, StringOutput sb, String[] args) {
-		int numOfTools = tools.size();
-		for(int i=0; i<numOfTools; i++) {
-			Tool tool = tools.get(i);
-			Component cmp = tool.getComponent();
-			String cssClass = tool.getToolCss();
-			if (cssClass == null) {
-				// use defaults
-				if(cmp instanceof Dropdown) {
-					cssClass = "o_tool_dropdown dropdown";
-				} else if(cmp instanceof Link && !cmp.isEnabled()) {
-					cssClass = "o_text";
-				} else {
-					cssClass = "o_tool";
-				}				
-			}
-			sb.append("<li class='").append(cssClass).append("'>");
-			renderer.render(cmp, sb, args);
-			sb.append("</li>");
-		}
-	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/gui/render/Renderer.java b/src/main/java/org/olat/core/gui/render/Renderer.java
index 96db777b724..e4ac409e2e0 100644
--- a/src/main/java/org/olat/core/gui/render/Renderer.java
+++ b/src/main/java/org/olat/core/gui/render/Renderer.java
@@ -34,7 +34,6 @@ import org.olat.core.gui.components.ComponentRenderer;
 import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.render.intercept.InterceptHandlerInstance;
 import org.olat.core.gui.translator.Translator;
-import org.olat.core.helpers.Settings;
 import org.olat.core.logging.AssertException;
 import org.olat.core.util.WebappHelper;
 
@@ -79,13 +78,13 @@ public class Renderer {
 	 * use renderStaticURI
 	 * 
 	 * @param target
-	 * @param URI e.g. myspecialdispatcher/somestuff
+	 * @param uri e.g. myspecialdispatcher/somestuff
 	 */
-	public static void renderNormalURI(StringOutput target, String URI) {
+	public static void renderNormalURI(StringOutput target, String uri) {
 		String root = WebappHelper.getServletContextPath();
 		target.append(root); // e.g /olat
 		target.append("/");
-		target.append(URI);
+		target.append(uri);
 	}
 
 	/**
@@ -96,11 +95,11 @@ public class Renderer {
 	 * renders a uri which is mounted to the webapp/static/ directory of your webapplication.
 	 * 
 	 * @param target
-	 * @param URI e.g. img/specialimagenotpossiblewithcss.jpg
+	 * @param uri e.g. img/specialimagenotpossiblewithcss.jpg
 	 */
-	public static void renderStaticURI(StringOutput target, String URI) {
+	public static void renderStaticURI(StringOutput target, String uri) {
 		// forward to static dispatcher that knows how to deliver the static files!
-		StaticMediaDispatcher.renderStaticURI(target, URI);
+		StaticMediaDispatcher.renderStaticURI(target, uri);
 	}
 
 	/**
@@ -172,8 +171,7 @@ public class Renderer {
 	 * @return
 	 */
 	public Component findComponent(String componentName) {
-		Component source = renderContainer.getComponent(componentName);
-		return source;
+		return renderContainer.getComponent(componentName);
 	}
 
 	/**
@@ -214,9 +212,6 @@ public class Renderer {
 				} else {
 					sb.append("<div id='o_c").append(source.getDispatchID());
 				}
-				if(Settings.isDebuging()) {
-					//sb.append("' title='").append(source.getComponentName());
-				}
 				sb.append("'>");
 			}			
 			
-- 
GitLab