/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* For further information about Alkacon Software, please see the
* company website: http://www.alkacon.com
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.gwt.client.ui.tree;
import org.opencms.gwt.client.dnd.CmsDNDHandler.Orientation;
import org.opencms.gwt.client.dnd.I_CmsDraggable;
import org.opencms.gwt.client.dnd.I_CmsDropTarget;
import org.opencms.gwt.client.ui.CmsList;
import org.opencms.gwt.client.ui.CmsListItem;
import org.opencms.gwt.client.ui.CmsToggleButton;
import org.opencms.gwt.client.ui.I_CmsListItem;
import org.opencms.gwt.client.ui.css.I_CmsImageBundle;
import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.I_CmsListTreeCss;
import org.opencms.gwt.client.ui.input.CmsCheckBox;
import org.opencms.gwt.client.util.CmsDomUtil;
import org.opencms.gwt.client.util.CmsStyleVariable;
import com.google.gwt.animation.client.Animation;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
/**
* List tree item implementation.<p>
*
* Implemented as:
* <pre>
* <li class='listTreeItem listTreeItem*state*'>
* <span class='listTreeItemImage'></span>
* <div class='listTreeItemContent'>...*content*</div>
* <ul class='listTreeItemChildren'>
* *children*
* </ul>
* </li>
* </pre>
*
* Where state can be <code>opened</code>, <code>closed</code> or <code>leaf</code>.<p>
*
* @since 8.0.0
*/
public class CmsTreeItem extends CmsListItem {
/** The duration of the animations. */
public static final int ANIMATION_DURATION = 200;
/** The CSS bundle used for this widget. */
private static final I_CmsListTreeCss CSS = I_CmsLayoutBundle.INSTANCE.listTreeCss();
/** The width of the opener. */
private static final int OPENER_WIDTH = 16;
/** The children list. */
protected CmsList<CmsTreeItem> m_children;
/** The element showing the open/close icon. */
protected CmsToggleButton m_opener;
/** Flag to indicate if drag'n drop is enabled. 3-states: if <code>null</code> the tree decides. */
private Boolean m_dropEnabled;
/** The style variable controlling this tree item's leaf/non-leaf state. */
private CmsStyleVariable m_leafStyleVar;
/** Flag to indicate if open or closed. */
private boolean m_open;
/** The item parent. */
private CmsTreeItem m_parentItem;
/** The style variable controlling this tree item's open/closed state. */
private CmsStyleVariable m_styleVar;
/** The tree reference. */
private CmsTree<CmsTreeItem> m_tree;
/**
* Creates a new list tree item containing a main widget and a check box.<p>
*
* @param showOpeners if true, show open/close icons
* @param checkbox the check box
* @param mainWidget the main widget
*/
public CmsTreeItem(boolean showOpeners, CmsCheckBox checkbox, Widget mainWidget) {
this(showOpeners);
addMainWidget(mainWidget);
addCheckBox(checkbox);
initContent();
if (!showOpeners) {
hideOpeners();
}
}
/**
* Creates a new list tree item containing a main widget.<p>
*
* @param showOpeners if true, show open/close icons
* @param mainWidget the main widget
*/
public CmsTreeItem(boolean showOpeners, Widget mainWidget) {
this(showOpeners);
addMainWidget(mainWidget);
initContent();
if (!showOpeners) {
hideOpeners();
}
}
/**
* Creates a new tree item with a 24px wide icon.<p>
*
* @param showOpeners
* @param mainWidget
* @param icon
*/
public CmsTreeItem(boolean showOpeners, Widget mainWidget, String icon) {
this(showOpeners);
addMainWidget(mainWidget);
Label label = new Label();
label.addStyleName(icon);
addDecoration(label, 28, true);
initContent();
if (!showOpeners) {
hideOpeners();
}
}
/**
* Default constructor.<p>
*
* @param showOpeners if true, the opener icons should be shown
*/
protected CmsTreeItem(boolean showOpeners) {
super();
m_styleVar = new CmsStyleVariable(this);
m_leafStyleVar = new CmsStyleVariable(this);
m_opener = createOpener();
addDecoration(m_opener, showOpeners ? OPENER_WIDTH : 0, true);
m_children = new CmsList<CmsTreeItem>();
m_children.setStyleName(CSS.listTreeItemChildren());
m_panel.add(m_children);
onChangeChildren();
m_open = true;
setOpen(false);
}
/**
* Returns the last opened item of a tree fragment.<p>
*
* @param item the tree item
* @param stopLevel the level to stop at, set -1 to go to the very last opened item
* @param requiresDropEnabled <code>true</code> if it is required the returned element to be drop enabled
*
* @return the last visible item of a tree fragment
*/
protected static CmsTreeItem getLastOpenedItem(CmsTreeItem item, int stopLevel, boolean requiresDropEnabled) {
if (stopLevel != -1) {
// stop level is set
int currentLevel = getPathLevel(item.getPath());
if (currentLevel > stopLevel) {
// we are past the stop level, prevent further checks
stopLevel = -1;
} else if (currentLevel == stopLevel) {
// matches stop level
return item;
}
}
if (item.getChildCount() > 0) {
int childIndex = item.getChildCount() - 1;
CmsTreeItem child = item.getChild(childIndex);
if (requiresDropEnabled) {
while (!child.isDropEnabled()) {
childIndex--;
if (childIndex < 0) {
return item;
}
child = item.getChild(childIndex);
}
}
if (child.isOpen()) {
return CmsTreeItem.getLastOpenedItem(child, stopLevel, requiresDropEnabled);
}
}
return item;
}
/**
* Method determining the path level by counting the number of '/'.<p>
* Example: '/xxx/xxx/' has a path-level of 2.<p>
*
* @param path the path to test
*
* @return the path level
*/
protected static native int getPathLevel(String path)/*-{
return path.match(/\//g).length - 1;
}-*/;
/**
* Unsupported operation.<p>
*
* @see org.opencms.gwt.client.ui.CmsListItem#add(com.google.gwt.user.client.ui.Widget)
*/
@Override
public void add(Widget w) {
throw new UnsupportedOperationException();
}
/**
* Adds a child list item.<p>
*
* @param item the child to add
*
* @see org.opencms.gwt.client.ui.CmsList#addItem(org.opencms.gwt.client.ui.I_CmsListItem)
*/
public void addChild(CmsTreeItem item) {
m_children.addItem(item);
adopt(item);
}
/**
* @see com.google.gwt.user.client.ui.HasWidgets#clear()
*/
public void clear() {
clearChildren();
}
/**
* Removes all children.<p>
*
* @see org.opencms.gwt.client.ui.CmsList#clearList()
*/
public void clearChildren() {
for (int i = getChildCount(); i > 0; i--) {
removeChild(i - 1);
}
}
/**
* Closes all empty child entries.<p>
*/
public void closeAllEmptyChildren() {
for (Widget child : m_children) {
if (child instanceof CmsTreeItem) {
CmsTreeItem item = (CmsTreeItem)child;
if (item.isOpen()) {
if (item.getChildCount() == 0) {
item.setOpen(false);
} else {
item.closeAllEmptyChildren();
}
}
}
}
}
/**
* Returns the child tree item at the given position.<p>
*
* @param index the position
*
* @return the tree item
*
* @see org.opencms.gwt.client.ui.CmsList#getItem(int)
*/
public CmsTreeItem getChild(int index) {
return m_children.getItem(index);
}
/**
* Returns the tree item with the given id.<p>
*
* @param itemId the id of the item to retrieve
*
* @return the tree item
*
* @see org.opencms.gwt.client.ui.CmsList#getItem(String)
*/
public CmsTreeItem getChild(String itemId) {
CmsTreeItem result = m_children.getItem(itemId);
return result;
}
/**
* Helper method which gets the number of children.<p>
*
* @return the number of children
*
* @see org.opencms.gwt.client.ui.CmsList#getWidgetCount()
*/
public int getChildCount() {
return m_children.getWidgetCount();
}
/**
* Returns the children of this list item.<p>
*
* @return the children list
*/
public CmsList<? extends I_CmsListItem> getChildren() {
return m_children;
}
/**
* @see org.opencms.gwt.client.dnd.I_CmsDraggable#getDragHelper(I_CmsDropTarget)
*/
@Override
public Element getDragHelper(I_CmsDropTarget target) {
// disable animation to get a drag helper without any visible children
boolean isAnimated = getTree().isAnimationEnabled();
getTree().setAnimationEnabled(false);
setOpen(false);
getTree().setAnimationEnabled(isAnimated);
return super.getDragHelper(target);
}
/**
* Returns the given item position.<p>
*
* @param item the item to get the position for
*
* @return the item position
*/
public int getItemPosition(CmsTreeItem item) {
return m_children.getWidgetIndex(item);
}
/**
* Returns the parent item.<p>
*
* @return the parent item
*/
public CmsTreeItem getParentItem() {
return m_parentItem;
}
/**
* @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDragCancel()
*/
@Override
public void onDragCancel() {
CmsTreeItem parent = getParentItem();
if (parent != null) {
parent.insertChild(this, parent.getItemPosition(this));
}
super.onDragCancel();
}
/**
* @see org.opencms.gwt.client.ui.CmsListItem#getParentTarget()
*/
@Override
public I_CmsDropTarget getParentTarget() {
return getTree();
}
/**
* Returns the path of IDs for the this item.<p>
*
* @return a path of IDs separated by slash
*/
public String getPath() {
StringBuffer path = new StringBuffer("/");
CmsTreeItem current = this;
while (current != null) {
path.insert(0, current.getId()).insert(0, "/");
current = current.getParentItem();
}
String result = path.toString();
if (result.startsWith("//")) {
// This happens if the root item has an empty id.
// In that case, we cut off the first slash.
result = result.substring(1);
}
return result;
}
/**
* Gets the tree to which this tree item belongs, or null if it does not belong to a tree.<p>
*
* @return a tree or <code>null</code>
*/
public CmsTree<CmsTreeItem> getTree() {
return m_tree;
}
/**
* Hides the open/close icons for this tree item and its descendants.<p>
*/
public void hideOpeners() {
addStyleName(CSS.listTreeItemNoOpeners());
}
/**
* Inserts the given item at the given position.<p>
*
* @param item the item to insert
* @param position the position
*
* @see org.opencms.gwt.client.ui.CmsList#insertItem(org.opencms.gwt.client.ui.I_CmsListItem, int)
*/
public void insertChild(CmsTreeItem item, int position) {
m_children.insert(item, position);
adopt(item);
}
/**
* Checks if dropping is enabled.<p>
*
* @return <code>true</code> if dropping is enabled
*/
public boolean isDropEnabled() {
if (m_dropEnabled != null) {
return m_dropEnabled.booleanValue();
}
CmsTree<?> tree = getTree();
if (tree == null) {
return false;
}
return tree.isDropEnabled();
}
/**
* Checks if the item is open or closed.<p>
*
* @return <code>true</code> if open
*/
public boolean isOpen() {
return m_open;
}
/**
* Removes an item from the list.<p>
*
* @param item the item to remove
*
* @return the removed item
*
* @see org.opencms.gwt.client.ui.CmsList#removeItem(org.opencms.gwt.client.ui.I_CmsListItem)
*/
public CmsTreeItem removeChild(final CmsTreeItem item) {
item.setParentItem(null);
item.setTree(null);
if ((m_tree != null) && m_tree.isAnimationEnabled()) {
// could be null if already detached
// animate
(new Animation() {
/**
* @see com.google.gwt.animation.client.Animation#onComplete()
*/
@Override
protected void onComplete() {
super.onComplete();
m_children.removeItem(item);
onChangeChildren();
}
/**
* @see com.google.gwt.animation.client.Animation#onUpdate(double)
*/
@Override
protected void onUpdate(double progress) {
item.getElement().getStyle().setOpacity(1 - progress);
}
}).run(ANIMATION_DURATION);
} else {
m_children.removeItem(item);
onChangeChildren();
}
return item;
}
/**
* Removes the item identified by the given index from the list.<p>
*
* @param index the index of the item to remove
*
* @return the removed item
*
* @see org.opencms.gwt.client.ui.CmsList#remove(int)
*/
public CmsTreeItem removeChild(int index) {
return removeChild(m_children.getItem(index));
}
/**
* Removes an item from the list.<p>
*
* @param itemId the id of the item to remove
*
* @return the removed item
*
* @see org.opencms.gwt.client.ui.CmsList#removeItem(String)
*/
public CmsTreeItem removeChild(String itemId) {
return removeChild(m_children.getItem(itemId));
}
/**
* Positions the drag and drop placeholder as a sibling or descendant of this element.<p>
*
* @param x the cursor client x position
* @param y the cursor client y position
* @param placeholder the placeholder
* @param orientation the drag and drop orientation
*
* @return the placeholder index
*/
public int repositionPlaceholder(int x, int y, Element placeholder, Orientation orientation) {
I_CmsDraggable draggable = null;
if (getTree().getDnDHandler() != null) {
draggable = getTree().getDnDHandler().getDraggable();
// if (draggable == this) {
// // can't drop item on itself, keeping previous position
// return getTree().getPlaceholderIndex();
// }
}
Element itemElement = getListItemWidget().getElement();
// check if the mouse pointer is within the height of the element
int top = CmsDomUtil.getRelativeY(y, itemElement);
int height = itemElement.getOffsetHeight();
int index;
String parentPath;
boolean isParentDndEnabled;
CmsTreeItem parentItem = getParentItem();
if (parentItem == null) {
index = getTree().getItemPosition(this);
parentPath = "/";
isParentDndEnabled = getTree().isRootDropEnabled();
} else {
index = parentItem.getItemPosition(this);
parentPath = getParentItem().getPath();
isParentDndEnabled = getParentItem().isDropEnabled();
}
if (top < height) {
// the mouse pointer is within the widget
int diff = x - getListItemWidget().getAbsoluteLeft();
if ((draggable != this) && isDropEnabled() && (diff > 0) && (diff < 32)) {
// over icon
getTree().setOpenTimer(this);
m_children.getElement().insertBefore(placeholder, m_children.getElement().getFirstChild());
getTree().setPlaceholderPath(getPath());
return 0;
}
getTree().cancelOpenTimer();
// In this case try to drop on the parent
if (!isParentDndEnabled) {
// we are not allowed to drop here
// keeping old position
return getTree().getPlaceholderIndex();
}
int originalPathLevel = -1;
if (draggable instanceof CmsTreeItem) {
originalPathLevel = getPathLevel(((CmsTreeItem)draggable).getPath()) - 1;
}
if (shouldInsertIntoSiblingList(originalPathLevel, parentItem, index)) {
@SuppressWarnings("null")
CmsTreeItem previousSibling = parentItem.getChild(index - 1);
if (previousSibling.isOpen()) {
// insert as last into the last opened of the siblings tree fragment
return CmsTreeItem.getLastOpenedItem(previousSibling, originalPathLevel, true).insertPlaceholderAsLastChild(
placeholder);
}
}
// insert place holder at the parent before the current item
getElement().getParentElement().insertBefore(placeholder, getElement());
getTree().setPlaceholderPath(parentPath);
return index;
} else if ((draggable != this) && isOpen()) {
getTree().cancelOpenTimer();
// the mouse pointer is on children
for (int childIndex = 0; childIndex < getChildCount(); childIndex++) {
CmsTreeItem child = getChild(childIndex);
Element childElement = child.getElement();
boolean over = false;
switch (orientation) {
case HORIZONTAL:
over = CmsDomUtil.checkPositionInside(childElement, x, -1);
break;
case VERTICAL:
over = CmsDomUtil.checkPositionInside(childElement, -1, y);
break;
case ALL:
default:
over = CmsDomUtil.checkPositionInside(childElement, x, y);
}
if (over) {
return child.repositionPlaceholder(x, y, placeholder, orientation);
}
}
}
getTree().cancelOpenTimer();
// keeping old position
return getTree().getPlaceholderIndex();
}
/**
* Enables/disables dropping.<p>
*
* @param enabled <code>true</code> to enable, or <code>false</code> to disable
*/
public void setDropEnabled(boolean enabled) {
if ((m_dropEnabled != null) && (m_dropEnabled.booleanValue() == enabled)) {
return;
}
m_dropEnabled = Boolean.valueOf(enabled);
}
/**
* Sets the tree item style to leaf, hiding the list opener.<p>
*
* @param isLeaf <code>true</code> to set to leaf style
*/
public void setLeafStyle(boolean isLeaf) {
if (isLeaf) {
m_leafStyleVar.setValue(CSS.listTreeItemLeaf());
} else {
m_leafStyleVar.setValue(CSS.listTreeItemInternal());
}
}
/**
* Opens or closes this tree item (i.e. shows or hides its descendants).<p>
*
* @param open if <code>true</code>, open the tree item, else close it
*/
public void setOpen(boolean open) {
if (m_open == open) {
return;
}
m_open = open;
executeOpen();
// if ((m_tree != null) && m_tree.isAnimationEnabled()) {
// Command openCallback = new Command() {
//
// /**
// * @see com.google.gwt.user.client.Command#execute()
// */
// public void execute() {
//
// executeOpen();
// }
// };
// if (m_open) {
// CmsSlideAnimation.slideIn(m_children.getElement(), openCallback, ANIMATION_DURATION);
// } else {
// CmsSlideAnimation.slideOut(m_children.getElement(), openCallback, ANIMATION_DURATION);
// }
// } else {
// executeOpen();
// }
}
/**
* Sets the parent item.<p>
*
* @param parentItem the parent item to set
*/
public void setParentItem(CmsTreeItem parentItem) {
m_parentItem = parentItem;
}
/**
* Sets the tree to which this tree item belongs.<p>
*
* This is automatically called when this tree item or one of its ancestors is inserted into a tree.<p>
*
* @param tree the tree into which the item has been inserted
*/
public void setTree(CmsTree<CmsTreeItem> tree) {
m_tree = tree;
for (Widget widget : m_children) {
if (widget instanceof CmsTreeItem) {
((CmsTreeItem)widget).setTree(tree);
}
}
}
/**
* Shows the open/close icons for this tree item and its descendants.<p>
*/
public void showOpeners() {
removeStyleName(CSS.listTreeItemNoOpeners());
}
/**
* Adopts the given item.<p>
*
* @param item the item to adopt
*/
protected void adopt(final CmsTreeItem item) {
item.setParentItem(this);
item.setTree(m_tree);
onChangeChildren();
if ((m_tree != null) && m_tree.isAnimationEnabled()) {
// could be null if not yet attached
item.getElement().getStyle().setOpacity(0);
// animate
(new Animation() {
/**
* @see com.google.gwt.animation.client.Animation#onUpdate(double)
*/
@Override
protected void onUpdate(double progress) {
item.getElement().getStyle().setOpacity(progress);
}
}).run(ANIMATION_DURATION);
}
}
/**
* Creates the button for opening/closing this item.<p>
*
* @return a button
*/
protected CmsToggleButton createOpener() {
final CmsToggleButton opener = new CmsToggleButton();
opener.setStyleName(CSS.listTreeItemOpener());
opener.setUpFace("", CSS.plus());
opener.setDownFace("", CSS.minus());
opener.addClickHandler(new ClickHandler() {
/**
* @see com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event.dom.client.ClickEvent)
*/
public void onClick(ClickEvent e) {
setOpen(opener.isDown());
e.stopPropagation();
e.preventDefault();
}
});
return opener;
}
/**
* Executes the open call.<p>
*/
protected void executeOpen() {
m_styleVar.setValue(m_open ? CSS.listTreeItemOpen() : CSS.listTreeItemClosed());
m_children.getElement().getStyle().clearDisplay();
if (m_opener.isDown() != m_open) {
m_opener.setDown(m_open);
}
if (m_open) {
fireOpen();
}
}
/**
* Fires the open event on the tree.<p>
*/
protected void fireOpen() {
if (m_tree != null) {
m_tree.fireOpen(this);
}
}
/**
* The '-' image.<p>
*
* @return the minus image
*/
protected Image getMinusImage() {
return new Image(I_CmsImageBundle.INSTANCE.minusImage());
}
/**
* The '+' image.<p>
*
* @return the plus image
*/
protected Image getPlusImage() {
return new Image(I_CmsImageBundle.INSTANCE.plusImage());
}
/**
* Inserts the placeholder element as last child of the children list.
* Setting it's path as the current placeholder path and returning the new index.<p>
*
* @param placeholder the placeholder element
*
* @return the new index
*/
protected int insertPlaceholderAsLastChild(Element placeholder) {
m_children.getElement().appendChild(placeholder);
getTree().setPlaceholderPath(getPath());
return getChildCount();
}
/**
* Helper method which is called when the list of children changes.<p>
*/
protected void onChangeChildren() {
int count = getChildCount();
setLeafStyle(count == 0);
}
/**
* Determines if the draggable should be inserted into the previous siblings children list.<p>
*
* @param originalPathLevel the original path level
* @param parent the parent item
* @param index the current index
*
* @return <code>true</code> if the item should be inserted into the previous siblings children list
*/
private boolean shouldInsertIntoSiblingList(int originalPathLevel, CmsTreeItem parent, int index) {
if ((index <= 0) || (parent == null)) {
return false;
}
return originalPathLevel != getPathLevel(parent.getPath());
}
}