/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.web.resources.criterion; import static org.libreplan.web.I18nHelper._; import java.util.HashSet; import java.util.Set; import org.apache.commons.lang3.Validate; import org.libreplan.business.common.exceptions.ValidationException; import org.libreplan.business.common.exceptions.ValidationException.InvalidValue; import org.libreplan.business.costcategories.entities.CostCategory; import org.libreplan.web.common.IMessagesForUser; import org.libreplan.web.common.Level; import org.libreplan.web.common.MessagesForUser; import org.libreplan.web.common.Util; import org.libreplan.web.common.components.Autocomplete; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.DropEvent; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Button; import org.zkoss.zul.Checkbox; import org.zkoss.zul.Comboitem; import org.zkoss.zul.Textbox; import org.zkoss.zul.TreeModel; import org.zkoss.zul.Treecell; import org.zkoss.zul.Treeitem; import org.zkoss.zul.TreeitemRenderer; import org.zkoss.zul.Treerow; import org.zkoss.zul.Tree; public class CriterionTreeController extends GenericForwardComposer { private Tree tree; private CriterionTreeitemRenderer renderer = new CriterionTreeitemRenderer(); private TreeViewStateSnapshot snapshotOfOpenedNodes; private Component messagesContainer; private IMessagesForUser messagesForUser; private final ICriterionsModel criterionsModel; private Textbox criterionName; private boolean codeEditionDisabled; public CriterionTreeitemRenderer getRenderer() { return renderer; } public CriterionTreeController(ICriterionsModel _criterionsModel) { Validate.notNull(_criterionsModel); this.criterionsModel = _criterionsModel; } @Override public void doAfterCompose(Component comp) throws Exception { super.doAfterCompose(comp); messagesForUser = new MessagesForUser(messagesContainer); comp.setAttribute("criterionTreeController", this, true); criterionName = (Textbox) self.getFellowIfAny("criterionName"); clearCriterionName(); } private void clearCriterionName() { criterionName.setValue(""); } public TreeModel getCriterionTreeModel() { if (getModel() == null) { return null; } return getModel().asTree(); } private ICriterionTreeModel getModel() { return criterionsModel.getCriterionTreeModel(); } public class CriterionTreeitemRenderer implements TreeitemRenderer { public CriterionTreeitemRenderer() { } @Override public void render(Treeitem item, Object data, int i) { final CriterionDTO criterionForThisRow = (CriterionDTO) data; item.setValue(data); if (snapshotOfOpenedNodes != null) { snapshotOfOpenedNodes.openIfRequired(item); } Treecell cellForName = new Treecell(); Textbox textboxName= new Textbox(); textboxName.setWidth("400px"); cellForName.appendChild(Util.bind( textboxName, () -> { return criterionForThisRow.getName(); }, value -> { criterionForThisRow.setName(value); })); String message = _("cannot be empty"); textboxName.setConstraint("no empty:"+message); Treecell cellForActive = new Treecell(); cellForActive.setStyle("center"); Checkbox checkboxActive = new Checkbox(); cellForActive.appendChild(Util.bind( checkboxActive, () -> { return criterionForThisRow.isActive(); }, value -> { criterionForThisRow.setActive(value); })); checkboxActive.addEventListener(Events.ON_CHECK, event -> { getModel().updateEnabledCriterions(criterionForThisRow.isActive(),criterionForThisRow); reloadTree(); }); Treerow tr; /* * Since only one treerow is allowed, if treerow is not null, append treecells to it. * If treerow is null, contruct a new treerow and attach it to item. */ if (item.getTreerow() == null) { tr = new Treerow(); tr.setParent(item); } else { tr = item.getTreerow(); tr.getChildren().clear(); } // Attach treecells to treerow tr.setDraggable("true"); tr.setDroppable("true"); // Treecell with the cost category of the Criterion Treecell cellForCostCategory = new Treecell(); criterionForThisRow.getCriterion().getCostCategory(); cellForCostCategory.appendChild(appendAutocompleteType(item)); // Treecell with the code of the Criterion Treecell cellForCode = new Treecell(); cellForCode.setStyle("center"); Textbox codeLabel = new Textbox(); codeLabel.setDisabled(codeEditionDisabled); cellForCode.appendChild(Util.bind( codeLabel, () -> { return criterionForThisRow.getCriterion().getCode(); }, value -> { criterionForThisRow.getCriterion().setCode(value); })); cellForName.setParent(tr); cellForCostCategory.setParent(tr); cellForCode.setParent(tr); cellForActive.setParent(tr); Treecell tcOperations = new Treecell(); Button upButton = new Button("", "/common/img/ico_bajar1.png"); upButton.setHoverImage("/common/img/ico_bajar.png"); upButton.setParent(tcOperations); upButton.setSclass("icono"); upButton.addEventListener(Events.ON_CLICK, event -> { getModel().down(criterionForThisRow); reloadTree(); }); Button downButton = new Button("", "/common/img/ico_subir1.png"); downButton.setHoverImage("/common/img/ico_subir.png"); downButton.setParent(tcOperations); downButton.setSclass("icono"); downButton.addEventListener(Events.ON_CLICK, event -> { getModel().up(criterionForThisRow); reloadTree(); }); Button indentButton = createButtonIndent(); indentButton.setParent(tcOperations); if (getModel().getCriterionType().allowHierarchy()) { indentButton.addEventListener(Events.ON_CLICK, event -> { getModel().indent(criterionForThisRow); reloadTree(); }); } Button unindentButton = createButtonUnindent(); unindentButton.setParent(tcOperations); if (getModel().getCriterionType().allowHierarchy()) { unindentButton.addEventListener(Events.ON_CLICK, event -> { getModel().unindent(criterionForThisRow); reloadTree(); }); } Button removeButton = createButtonRemove(criterionForThisRow); removeButton.setParent(tcOperations); if (criterionsModel.isDeletable(criterionForThisRow.getCriterion())) { removeButton.addEventListener(Events.ON_CLICK, event -> { getModel().removeNode(criterionForThisRow); if (!criterionForThisRow.isNewObject()) { criterionsModel.addForRemoval(criterionForThisRow.getCriterion()); } reloadTree(); }); } tcOperations.setParent(tr); tr.addEventListener("onDrop", arg0 -> { DropEvent dropEvent = (DropEvent) arg0; move(dropEvent.getTarget(), dropEvent.getDragged()); }); } } private CostCategory getCostCategory(Treeitem listitem) { return ((CriterionDTO) listitem.getValue()).getCriterion().getCostCategory(); } private Autocomplete appendAutocompleteType(final Treeitem row) { final Autocomplete autocomplete = new Autocomplete(); autocomplete.setAutodrop(true); autocomplete.applyProperties(); autocomplete.setFinder("CostCategoryFinder"); // Getter, show type selected if (getCostCategory(row) != null) { autocomplete.setSelectedItem(getCostCategory(row)); } // Setter, set type selected to HourCost.type autocomplete.addEventListener("onSelect", event -> { final Comboitem comboitem = autocomplete.getSelectedItem(); if (comboitem != null) { // Update resourcesCostCategoryAssignment CriterionDTO assignment = row.getValue(); assignment.getCriterion().setCostCategory(comboitem.getValue()); row.setValue(assignment); } }); autocomplete.addEventListener("onBlur", event -> { if (autocomplete.getText().isEmpty()) { autocomplete.clear(); CriterionDTO assignment = row.getValue(); assignment.getCriterion().setCostCategory(null); } }); return autocomplete; } private Button createButtonUnindent() { Button unindentButton; if ( this.criterionsModel.getCriterionType().allowHierarchy()) { unindentButton = new Button("", "/common/img/ico_izq1.png"); unindentButton.setHoverImage("/common/img/ico_izq.png"); unindentButton.setTooltiptext(_("Unindent")); } else { unindentButton = new Button("", "/common/img/ico_izq_out.png"); unindentButton.setTooltiptext(_("Not indentable")); } unindentButton.setSclass("icono"); return unindentButton; } private Button createButtonIndent() { Button indentButton; if ( this.criterionsModel.getCriterionType().allowHierarchy()) { indentButton = new Button("", "/common/img/ico_derecha1.png"); indentButton.setHoverImage("/common/img/ico_derecha.png"); indentButton.setTooltiptext(_("Indent")); } else { indentButton = new Button("", "/common/img/ico_derecha_out.png"); indentButton.setTooltiptext(_("Not indentable")); } indentButton.setSclass("icono"); return indentButton; } private Button createButtonRemove(CriterionDTO criterion) { Button removeButton; int num = criterionsModel.numberOfRelatedEntities(criterion.getCriterion()); if (criterionsModel.isDeletable(criterion.getCriterion())) { removeButton = new Button("", "/common/img/ico_borrar1.png"); removeButton.setHoverImage("/common/img/ico_borrar.png"); removeButton.setTooltiptext(_("Delete")); } else { removeButton = new Button("", "/common/img/ico_borrar_out.png"); removeButton.setTooltiptext(criterion.getCriterion().getChildren().isEmpty() ? (num + " " + _("references")) : _("Criterion has subelements")); } removeButton.setSclass("icono"); return removeButton; } public void up() { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); if (tree.getSelectedCount() == 1) { getModel().up(getSelectedNode()); } } public void down() { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); if (tree.getSelectedCount() == 1) { getModel().down(getSelectedNode()); } } public void move(Component droppedIn, Component dragged) { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); Treerow from = (Treerow) dragged; CriterionDTO fromNode = ((Treeitem) from.getParent()).getValue(); if ( droppedIn instanceof Tree) { getModel().moveToRoot(fromNode,0); } if ( droppedIn instanceof Treerow) { Treerow to = (Treerow) droppedIn; CriterionDTO toNode = ((Treeitem) to.getParent()).getValue(); getModel().move(fromNode, toNode,0); } reloadTree(); } public void addCriterion() { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); try { if ((tree.getSelectedCount() == 1) && (this.criterionsModel.getAllowHierarchy())) { getModel().addCriterionAt(getSelectedNode(),getName()); } else { getModel().addCriterion(getName()); } clearCriterionName(); reloadTree(); } catch (ValidationException e) { for (InvalidValue invalidValue : e.getInvalidValues()) { messagesForUser.showMessage(Level.ERROR, invalidValue.getMessage()); } } } public void disabledHierarchy() { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); getModel().flattenTree(); reloadTree(); } public void updateEnabledCriterions(boolean isChecked) { snapshotOfOpenedNodes = TreeViewStateSnapshot.snapshotOpened(tree); getModel().updateEnabledCriterions(isChecked); reloadTree(); } public void setCriterionCodeEditionDisabled(boolean disabled) { codeEditionDisabled = disabled; } public void regenerateCodeForUnsavedCriteria(int numberOfDigits) { getModel().regenerateCodeForUnsavedCriteria(numberOfDigits); } public void reloadTree(){ Util.reloadBindings(tree); } private String getName() { String name = criterionName.getValue(); getModel().thereIsOtherWithSameNameAndType(name); getModel().validateNameNotEmpty(name); return name; } private CriterionDTO getSelectedNode() { return (CriterionDTO) tree.getSelectedItem().getValue(); } private static class TreeViewStateSnapshot { private final Set<Object> all; private final Set<Object> dataOpen; private TreeViewStateSnapshot(Set<Object> dataOpen, Set<Object> all) { this.dataOpen = dataOpen; this.all = all; } public static TreeViewStateSnapshot snapshotOpened(Tree tree) { Set<Object> dataOpen = new HashSet<>(); Set<Object> all = new HashSet<>(); if (tree != null && tree.getTreechildren() != null) { for (Treeitem treeitem : tree.getTreechildren().getItems()) { Object value = getAssociatedValue(treeitem); if (treeitem.isOpen()) { dataOpen.add(value); } all.add(value); } } return new TreeViewStateSnapshot(dataOpen, all); } private static Object getAssociatedValue(Treeitem treeitem) { return treeitem.getValue(); } public void openIfRequired(Treeitem item) { Object value = getAssociatedValue(item); item.setOpen(isNewlyCreated(value) || wasOpened(value)); } private boolean wasOpened(Object value) { return dataOpen.contains(value); } private boolean isNewlyCreated(Object value) { return !all.contains(value); } } }