/**
* <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.ArrayList;
import java.util.List;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
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.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.VetoableCloseController;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
/**
*
* Initial date: 25.03.2014<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class BreadcrumbedStackedPanel extends Panel implements StackedPanel, BreadcrumbPanel, ComponentEventListener {
private static final OLog log = Tracing.createLoggerFor(BreadcrumbedStackedPanel.class);
private static final ComponentRenderer RENDERER = new BreadcrumbedStackedPanelRenderer();
protected final List<Link> stack = new ArrayList<>(3);
protected final Link backLink;
protected final Link closeLink;
private int invisibleCrumb = 1;
private String cssClass;
private boolean showCloseLink = false;
private boolean showCloseLinkForRootCrumb = false;
private boolean neverDisposeRootController = false;
public BreadcrumbedStackedPanel(String name, Translator translator, ComponentEventListener listener) {
this(name, translator, listener, null);
}
public BreadcrumbedStackedPanel(String name, Translator translator, ComponentEventListener listener, String cssClass) {
super(name);
setTranslator(Util.createPackageTranslator(BreadcrumbedStackedPanel.class, translator.getLocale(), translator));
addListener(listener);
this.cssClass = cssClass;
// Add back link before the bread crumbs, when pressed delegates click to current bread-crumb - 1
backLink = LinkFactory.createCustomLink("back", "back", null, Link.NONTRANSLATED + Link.LINK_CUSTOM_CSS, null, this);
backLink.setIconLeftCSS("o_icon o_icon_back");
backLink.setTitle(translator.translate("back"));
backLink.setAccessKey("b"); // allow navigation using keyboard
// Add back link before the bread crumbs, when pressed delegates click to current bread-crumb - 1
closeLink = LinkFactory.createCustomLink("close", "close", null, Link.NONTRANSLATED + Link.LINK_CUSTOM_CSS, null, this);
closeLink.setIconLeftCSS("o_icon o_icon_close_tool");
closeLink.setCustomDisplayText(translator.translate("close"));
closeLink.setAccessKey("x"); // allow navigation using keyboard
this.setDomReplacementWrapperRequired(false);
}
/**
* Get a string with all css classes to be applied to this DOM element
* @return
*/
public String getCssClass() {
return cssClass;
}
/**
* Set and overwrite any existing cssClasses. Use addCssClass to just add a
* class
*
* @param cssClass
*/
public void setCssClass(String cssClass) {
this.cssClass = cssClass;
}
/**
* Add css class to this DOM element. Does not overwrite other classes
* @param cssClassToAdd
*/
public void addCssClass(String cssClassToAdd) {
if (this.cssClass == null) {
setCssClass(cssClassToAdd);
} else if (cssClassToAdd != null && !this.cssClass.contains(cssClassToAdd)) {
setCssClass(this.cssClass + " " + cssClassToAdd);
}
}
/**
* Remove the css class from this DOM element, but keep all the other
* classes
*
* @param cssClassToRemove
*/
public void removeCssClass(String cssClassToRemove) {
if (this.cssClass != null && cssClassToRemove != null) {
setCssClass(this.cssClass.replace(cssClassToRemove, ""));
}
}
public int getInvisibleCrumb() {
return invisibleCrumb;
}
public void setInvisibleCrumb(int invisibleCrumb) {
this.invisibleCrumb = invisibleCrumb;
}
public Link getBackLink() {
return backLink;
}
public Link getCloseLink() {
return closeLink;
}
public boolean isShowCloseLink() {
return showCloseLink;
}
public boolean isShowCloseLinkForRootCrumb() {
return showCloseLinkForRootCrumb;
}
public void setShowCloseLink(boolean showCloseLinkForCrumbs, boolean showCloseLinkForRootCrumb) {
this.showCloseLink = showCloseLinkForCrumbs;
this.showCloseLinkForRootCrumb = showCloseLinkForRootCrumb;
}
public boolean isNeverDisposeRootController() {
return neverDisposeRootController;
}
public void setNeverDisposeRootController(boolean neverDisposeRootController) {
this.neverDisposeRootController = neverDisposeRootController;
}
public List<Link> getBreadCrumbs() {
return stack;
}
@Override
public Iterable<Component> getComponents() {
List<Component> cmps = new ArrayList<>(3 + stack.size());
cmps.add(backLink);
cmps.add(closeLink);
Component content = getContent();
if(content != null && content != this) {
cmps.add(getContent());
}
for(Link crumb:stack) {
cmps.add(crumb);
}
return cmps;
}
@Override
public ComponentRenderer getHTMLRendererSingleton() {
return RENDERER;
}
@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 void dispatchEvent(UserRequest ureq, Component source, Event event) {
boolean closeEvent = source.equals(closeLink);
boolean backEvent = source.equals(backLink);
if (backEvent || closeEvent) {
if (stack.size() > 1) {
// back means to one level down, change source to the stack item one below current
source = stack.get(stack.size()-2);
// now continue as if user manually pressed a stack item in the list
} else {
// notify listeners that back or link beyond breadcrumb has been called
fireEvent(ureq, Event.CLOSE_EVENT);
}
}
if(stack.contains(source)) {
Controller controllerToPop = getControllerToPop(source);
//part of a hack for QTI editor
if(controllerToPop instanceof VetoableCloseController
&& !((VetoableCloseController)controllerToPop).requestForClose(ureq)) {
// not my problem anymore, I have done what I can
fireEvent(ureq, new VetoPopEvent());
return;
}
BreadCrumb popedCrumb = popController(source);
if(popedCrumb != null) {
Controller last = getLastController();
if(last != null) {
addToHistory(ureq, last);
}
if(popedCrumb.getController() != null) {
fireEvent(ureq, new PopEvent(popedCrumb.getController(), popedCrumb.getUserObject(), closeEvent));
} else if(popedCrumb.getUserObject() != null) {
fireEvent(ureq, new PopEvent(popedCrumb.getUserObject(), closeEvent));
}
} else if(stack.indexOf(source) == 0) {
fireEvent(ureq, new RootEvent());
}
}
}
private void addToHistory(UserRequest ureq, Controller controller) {
WindowControl wControl = controller.getWindowControlForDebug();
BusinessControlFactory.getInstance().addToHistory(ureq, wControl);
}
public int size() {
return stack == null ? 0 : stack.size();
}
@Override
public Controller getRootController() {
Controller controller = null;
if(stack.size() > 0) {
Link lastPath = stack.get(0);
BreadCrumb crumb = (BreadCrumb)lastPath.getUserObject();
controller = crumb.getController();
}
return controller;
}
public Controller getLastController() {
Controller controller = null;
if(stack.size() > 0) {
Link lastPath = stack.get(stack.size() - 1);
BreadCrumb crumb = (BreadCrumb)lastPath.getUserObject();
controller = crumb.getController();
}
return controller;
}
@Override
public void popContent() {
if(stack.size() > 1) {
Link link = stack.remove(stack.size() - 1);
BreadCrumb crumb = (BreadCrumb)link.getUserObject();
crumb.dispose();
}
}
@Override
public boolean popUpToController(Controller controller) {
int index = getIndex(controller);
if(index > 0 && index < stack.size() - 1) {
BreadCrumb popedCrumb = null;
for(int i=stack.size(); i-->(index+1); ) {
Link link = stack.remove(i);
popedCrumb = (BreadCrumb)link.getUserObject();
popedCrumb.dispose();
}
setContent(index);
updateCloseLinkTitle();
return true;
}
return false;
}
@Override
public void popController(Controller controller) {
int index = getIndex(controller);
if(index > 0 && index < stack.size()) {
BreadCrumb popedCrumb = null;
for(int i=stack.size(); i--> index; ) {
Link link = stack.remove(i);
popedCrumb = (BreadCrumb)link.getUserObject();
popedCrumb.dispose();
}
setContent(index - 1);
updateCloseLinkTitle();
}
}
@Override
public void pushContent(Component newContent) {
setContent(newContent);
}
private int getIndex(Controller controller) {
int index = -1;
for(int i=0; i<stack.size(); i++) {
BreadCrumb crumb = (BreadCrumb)stack.get(i).getUserObject();
if(crumb.getController() == controller) {
index = i;
}
}
return index;
}
private Controller getControllerToPop(Component source) {
int index = stack.indexOf(source);
if(index < (stack.size() - 1)) {
BreadCrumb popedCrumb = null;
for(int i=stack.size(); i-->(index+1); ) {
Link link = stack.get(i);
popedCrumb = (BreadCrumb)link.getUserObject();
}
return popedCrumb.getController();
}
return null;
}
private BreadCrumb popController(Component source) {
int index = stack.indexOf(source);
if(index < (stack.size() - 1)) {
BreadCrumb popedCrumb = null;
for(int i=stack.size(); i-->(index+1); ) {
Link link = stack.remove(i);
popedCrumb = (BreadCrumb)link.getUserObject();
popedCrumb.dispose();
}
setContent(index);
updateCloseLinkTitle();
return popedCrumb;
}
return null;
}
@Override
public void rootController(String displayName, Controller controller) {
if(stack.size() > 0) {
for(int i=stack.size(); i-->0; ) {
Link link = stack.remove(i);
BreadCrumb crumb = (BreadCrumb)link.getUserObject();
if(neverDisposeRootController && i == 0) {
continue;
}
crumb.dispose();
}
}
pushController(displayName, controller);
}
@Override
public void popUpToRootController(UserRequest ureq) {
if(stack.size() > 1) {
for(int i=stack.size(); i-->1; ) {
Link link = stack.remove(i);
BreadCrumb crumb = (BreadCrumb)link.getUserObject();
crumb.dispose();
}
//set the root controller
Link rootLink = stack.get(0);
BreadCrumb rootCrumb = (BreadCrumb)rootLink.getUserObject();
setContent(rootCrumb.getController());
updateCloseLinkTitle();
fireEvent(ureq, new PopEvent(rootCrumb.getController(), false));
}
}
@Override
public void pushController(String displayName, Controller controller) {
pushController(displayName, null, controller, null);
}
@Override
public void pushController(String displayName, String iconLeftCss, Controller controller) {
pushController(displayName, iconLeftCss, controller, null);
}
@Override
public void pushController(String displayName, String iconLeftCss, Object uobject) {
pushController(displayName, iconLeftCss, null, uobject);
}
/**
* Push the controller in the stack. If the breadcrumb has no controller, the method
* prevent the last breadcrumb to be the same has the new one and be same, it's mean
* the same uobject.
*
* @param displayName
* @param iconLeftCss
* @param controller
* @param uobject
*/
public void pushController(String displayName, String iconLeftCss, Controller controller, Object uobject) {
//deduplicate last crumb
if(uobject != null && controller == null && stack.size() > 0) {
Link lastLink = stack.get(stack.size() - 1);
BreadCrumb lastCrumb = (BreadCrumb)lastLink.getUserObject();
if(lastCrumb.getController() == null && lastCrumb.getUserObject() != null && lastCrumb.getUserObject().equals(uobject)) {
stack.remove(lastLink);
}
}
Link link = LinkFactory.createLink("crumb_" + stack.size(), (Translator)null, this);
link.setCustomDisplayText(StringHelper.escapeHtml(displayName));
if(StringHelper.containsNonWhitespace(iconLeftCss)) {
link.setIconLeftCSS(iconLeftCss);
}
link.setDomReplacementWrapperRequired(false);
link.setUserObject(createCrumb(controller, uobject));
stack.add(link);
if(controller != null) {
setContent(controller);
}
updateCloseLinkTitle();
}
public void changeDisplayname(String diplayName) {
stack.get(stack.size() - 1).setCustomDisplayText(diplayName);
setDirty(true);
}
@Override
public void changeDisplayname(String displayName, String iconLeftCss, Controller ctrl) {
for(int i=stack.size(); i-->1; ) {
Link link = stack.get(i);
BreadCrumb crumb = (BreadCrumb)link.getUserObject();
if(crumb.getController() == ctrl) {
link.setCustomDisplayText(StringHelper.escapeHtml(displayName));
if(StringHelper.containsNonWhitespace(iconLeftCss)) {
link.setIconLeftCSS(iconLeftCss);
} else {
link.setIconLeftCSS(null);
}
}
}
}
protected BreadCrumb createCrumb(Controller controller, Object uobject) {
return new BreadCrumb(controller, uobject);
}
private void setContent(int crumbIndex) {
Link currentLink = stack.get(crumbIndex);
BreadCrumb crumb = (BreadCrumb)currentLink.getUserObject();
if(crumb.getController() == null) {
if(crumbIndex - 1 >= 0) {
Link parentLink = stack.get(crumbIndex - 1);
BreadCrumb parentCrumb = (BreadCrumb)parentLink.getUserObject();
setContent(parentCrumb.getController());
}
} else {
setContent(crumb.getController());
}
}
private void setContent(Controller ctrl) {
Component cmp = ctrl.getInitialComponent();
if(cmp == this) {
log.error("Set itself as content is forbidden");
throw new AssertException("Set itself as content is forbidden");
}
setContent(cmp);
}
@Override
public void setContent(Component newContent) {
// 1: remove any stack css from current active stack
Component currentComponent = getContent();
if (currentComponent != null) {
if (currentComponent instanceof StackedPanel) {
StackedPanel currentPanel = (StackedPanel) currentComponent;
String currentStackCss = currentPanel.getCssClass();
removeCssClass(currentStackCss);
}
}
// 2: update stack with new component on standard Panel
super.setContent(newContent);
// 3: add new stack css
if (newContent != null) {
if (newContent instanceof StackedPanel) {
StackedPanel newPanel = (StackedPanel) newContent;
String newStackCss = newPanel.getCssClass();
addCssClass(newStackCss);
}
}
}
/**
* Update the close link title to match the name of the last visible item
*/
private void updateCloseLinkTitle() {
String closeText;
boolean showClose;
if(stack.size() < 2) {
// special case: root crumb
Link link = stack.get(0);
closeText = getTranslator().translate("doclose", new String[] { link.getCustomDisplayText() });
showClose = isShowCloseLinkForRootCrumb();
backLink.setTitle(closeText);
} else {
Link link = stack.get(stack.size()-1);
closeText = getTranslator().translate("doclose", new String[] { link.getCustomDisplayText() });
showClose = isShowCloseLink();
backLink.setTitle(getTranslator().translate("back"));
}
closeLink.setCustomDisplayText(closeText);
closeLink.setTitle(closeText);
closeLink.setVisible(showClose);
}
public static class BreadCrumb {
private final Object uobject;
private final Controller controller;
public BreadCrumb(Controller controller, Object uobject) {
this.uobject = uobject;
this.controller = controller;
}
public Object getUserObject() {
return uobject;
}
public Controller getController() {
return controller;
}
public void dispose() {
if(controller != null) {
controller.dispose();
}
}
}
}