package name.abuchen.portfolio.ui.views.taxonomy; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.UUID; import javax.inject.Inject; import org.eclipse.e4.core.di.extensions.Preference; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.layout.TreeColumnLayout; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerDropAdapter; import org.eclipse.jface.window.ToolTip; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DragSourceAdapter; import org.eclipse.swt.dnd.DragSourceEvent; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.dnd.TransferData; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import name.abuchen.portfolio.model.Classification; import name.abuchen.portfolio.model.Classification.Assignment; import name.abuchen.portfolio.model.InvestmentVehicle; import name.abuchen.portfolio.model.Named; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.ExchangeRate; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPart; import name.abuchen.portfolio.ui.UIConstants; import name.abuchen.portfolio.ui.dnd.SecurityTransfer; import name.abuchen.portfolio.ui.util.BookmarkMenu; import name.abuchen.portfolio.ui.util.Colors; import name.abuchen.portfolio.ui.util.ContextMenu; import name.abuchen.portfolio.ui.util.SimpleAction; import name.abuchen.portfolio.ui.util.TreeViewerCSVExporter; import name.abuchen.portfolio.ui.util.viewers.Column; import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport; import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport.ModificationListener; import name.abuchen.portfolio.ui.util.viewers.ShowHideColumnHelper; import name.abuchen.portfolio.ui.util.viewers.StringEditingSupport; import name.abuchen.portfolio.ui.util.viewers.ValueEditingSupport; import name.abuchen.portfolio.ui.views.columns.AttributeColumn; import name.abuchen.portfolio.ui.views.columns.IsinColumn; import name.abuchen.portfolio.ui.views.columns.NameColumn; import name.abuchen.portfolio.ui.views.columns.NameColumn.NameColumnLabelProvider; import name.abuchen.portfolio.ui.views.columns.NoteColumn; @SuppressWarnings("restriction") /* package */abstract class AbstractNodeTreeViewer extends Page implements ModificationListener { private static class ItemContentProvider implements ITreeContentProvider { private TaxonomyModel model; @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { model = (TaxonomyModel) newInput; } @Override public Object[] getElements(Object inputElement) { return model.getVirtualRootNode().getChildren().toArray(); } @Override public boolean hasChildren(Object element) { return !((TaxonomyNode) element).getChildren().isEmpty(); } @Override public Object[] getChildren(Object parentElement) { return ((TaxonomyNode) parentElement).getChildren().toArray(); } @Override public Object getParent(Object element) { return ((TaxonomyNode) element).getParent(); } @Override public void dispose() { // no resources allocated } } private static class NodeDragListener extends DragSourceAdapter { private TreeViewer treeViewer; public NodeDragListener(TreeViewer treeViewer) { this.treeViewer = treeViewer; } @Override public void dragSetData(DragSourceEvent event) { @SuppressWarnings("unchecked") List<TaxonomyNode> nodes = ((TreeSelection) treeViewer.getSelection()).toList(); TaxonomyNodeTransfer.getTransfer().setTaxonomyNodes(nodes); // if only one node is dragged and the node is of type security, // then also enable the drag and drop of securities into a watchlist Assignment assignment = nodes.size() == 1 ? nodes.get(0).getAssignment() : null; if (assignment != null && assignment.getInvestmentVehicle() instanceof Security) SecurityTransfer.getTransfer().setSecurity((Security) assignment.getInvestmentVehicle()); else SecurityTransfer.getTransfer().setSecurity(null); event.data = nodes; } @Override public void dragStart(DragSourceEvent event) { // drag data must not include the two visible root nodes, i.e. the // unassigned category and the root category of the classification @SuppressWarnings("unchecked") List<TaxonomyNode> nodes = ((TreeSelection) treeViewer.getSelection()).toList(); event.doit = !nodes.stream().filter(n -> n.getParent().isRoot()).findAny().isPresent(); } } private static class NodeDropListener extends ViewerDropAdapter { private AbstractNodeTreeViewer viewer; public NodeDropListener(AbstractNodeTreeViewer viewer) { super(viewer.getNodeViewer()); this.viewer = viewer; } @Override public boolean performDrop(Object data) // NOSONAR { List<TaxonomyNode> droppedNodes = getSubtreeNodes(TaxonomyNodeTransfer.getTransfer().getTaxonomyNodes()); final TaxonomyNode target = (TaxonomyNode) getCurrentTarget(); // do not drop upon itself if (droppedNodes.contains(target)) return false; // do not allow dragging a parent into the child for (TaxonomyNode n : target.getPath()) { if (droppedNodes.contains(n)) return false; } // do not allow dragging of categories into the "unassigned // category" (must be deleted via right-click instead) if (target.getPath().stream().filter(TaxonomyNode::isUnassignedCategory).findAny().isPresent() && droppedNodes.stream().filter(TaxonomyNode::isClassification).findAny().isPresent()) return false; switch (getCurrentLocation()) { case ViewerDropAdapter.LOCATION_AFTER: // NOSONAR TaxonomyNode t = target; for (TaxonomyNode node : droppedNodes) { node.insertAfter(t); t = node; } viewer.onTaxnomyNodeEdited(viewer.getModel().getVirtualRootNode()); break; case ViewerDropAdapter.LOCATION_BEFORE: for (TaxonomyNode node : droppedNodes) node.insertBefore(target); viewer.onTaxnomyNodeEdited(viewer.getModel().getVirtualRootNode()); break; case ViewerDropAdapter.LOCATION_ON: // NOSONAR // do not drag parent into child if (target.getPath().stream().filter(droppedNodes::contains).findAny().isPresent()) return false; // do not move node into its own parent droppedNodes.stream().filter(n -> !n.getParent().equals(target)).forEach(n -> n.moveTo(target)); viewer.onTaxnomyNodeEdited(viewer.getModel().getVirtualRootNode()); break; case ViewerDropAdapter.LOCATION_NONE: default: break; } return true; } /** * Returns the unique subtrees, e.g. removes the all nodes whose parent * is already selected */ private List<TaxonomyNode> getSubtreeNodes(List<TaxonomyNode> nodes) { List<TaxonomyNode> answer = new ArrayList<>(); for (TaxonomyNode node : nodes) { List<TaxonomyNode> path = node.getPath(); if (!path.subList(0, path.size() - 1).stream().filter(nodes::contains).findAny().isPresent()) answer.add(node); } return answer; } @Override public boolean validateDrop(Object target, int operation, TransferData transferType) { if (!(target instanceof TaxonomyNode)) return false; TaxonomyNode targetNode = (TaxonomyNode) target; int location = determineLocation(this.getCurrentEvent()); if (targetNode.isClassification()) return true; else if (targetNode.isAssignment()) return location == LOCATION_AFTER || location == LOCATION_BEFORE; else return false; } } protected static final String MENU_GROUP_DEFAULT_ACTIONS = "defaultActions"; //$NON-NLS-1$ protected static final String MENU_GROUP_CUSTOM_ACTIONS = "customActions"; //$NON-NLS-1$ protected static final String MENU_GROUP_DELETE_ACTIONS = "deleteActions"; //$NON-NLS-1$ @Inject private PortfolioPart part; private boolean useIndirectQuotation = false; private TreeViewer nodeViewer; private ShowHideColumnHelper support; private Color warningColor; private boolean isFirstView = true; public AbstractNodeTreeViewer(TaxonomyModel model, TaxonomyNodeRenderer renderer) { super(model, renderer); } @Inject public void setUseIndirectQuotation( @Preference(value = UIConstants.Preferences.USE_INDIRECT_QUOTATION) boolean useIndirectQuotation) { this.useIndirectQuotation = useIndirectQuotation; if (nodeViewer != null) nodeViewer.refresh(); } protected abstract String readExpansionState(); protected abstract void storeExpansionState(String expanded); protected final TreeViewer getNodeViewer() { return nodeViewer; } public Color getWarningColor() { return warningColor; } @Override public void onModified(Object element, Object newValue, Object oldValue) { onTaxnomyNodeEdited((TaxonomyNode) element); } public void onWeightModified(Object element, Object newValue, Object oldValue) { TaxonomyNode node = (TaxonomyNode) element; if (node.getWeight() > Classification.ONE_HUNDRED_PERCENT) node.setWeight(Classification.ONE_HUNDRED_PERCENT); else if (node.getWeight() < 0) node.setWeight(0); if (node.isAssignment()) { int oldWeight = (Integer) oldValue; doChangeAssignmentWeight(node, oldWeight); } onModified(element, newValue, oldValue); } @Override public void configMenuAboutToShow(IMenuManager manager) { support.menuAboutToShow(manager); } @Override public void exportMenuAboutToShow(IMenuManager manager) { manager.add(new SimpleAction(Messages.MenuExportData, action -> new TreeViewerCSVExporter(nodeViewer) .export(getModel().getTaxonomy().getName() + ".csv"))); //$NON-NLS-1$ } @Override public final Control createControl(Composite parent) { Composite container = new Composite(parent, SWT.NONE); TreeColumnLayout layout = new TreeColumnLayout(); container.setLayout(layout); warningColor = new Color(container.getDisplay(), Colors.WARNING.swt()); container.addDisposeListener(e -> warningColor.dispose()); nodeViewer = new TreeViewer(container, SWT.FULL_SELECTION | SWT.MULTI); ColumnEditingSupport.prepare(nodeViewer); ColumnViewerToolTipSupport.enableFor(nodeViewer, ToolTip.NO_RECREATE); support = new ShowHideColumnHelper(getClass().getSimpleName() + '-' + getModel().getTaxonomy().getId(), getPreferenceStore(), nodeViewer, layout); addColumns(support); support.createColumns(); nodeViewer.getTree().setHeaderVisible(true); nodeViewer.getTree().setLinesVisible(true); nodeViewer.setContentProvider(new ItemContentProvider()); nodeViewer.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] { TaxonomyNodeTransfer.getTransfer(), SecurityTransfer.getTransfer() }, new NodeDragListener(nodeViewer)); nodeViewer.addDropSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] { TaxonomyNodeTransfer.getTransfer() }, new NodeDropListener(this)); nodeViewer.setInput(getModel()); new ContextMenu(nodeViewer.getControl(), this::fillContextMenu).hook(); return container; } protected abstract void addColumns(ShowHideColumnHelper support); protected void addDimensionColumn(ShowHideColumnHelper support) { Column column = new NameColumn("txname", Messages.ColumnLevels, SWT.NONE, 400); //$NON-NLS-1$ column.setLabelProvider(new NameColumnLabelProvider() // NOSONAR { @Override public Image getImage(Object e) { if (((TaxonomyNode) e).isUnassignedCategory()) return Images.UNASSIGNED_CATEGORY.image(); return super.getImage(e); } }); new StringEditingSupport(Named.class, "name") //$NON-NLS-1$ { @Override public boolean canEdit(Object element) { if (((TaxonomyNode) element).isUnassignedCategory()) return false; else return super.canEdit(element); } }.setMandatory(true).addListener(this).attachTo(column); column.setRemovable(false); // drag & drop sorting does not work well with auto sorting column.setSorter(null); support.addColumn(column); column = new IsinColumn(); column.getEditingSupport().addListener(this); column.setSorter(null); column.setVisible(false); support.addColumn(column); column = new NoteColumn(); column.getEditingSupport().addListener(this); column.setSorter(null); column.setVisible(false); support.addColumn(column); addWeightColumn(support); } private void addWeightColumn(ShowHideColumnHelper support) // NOSONAR { Column column; column = new Column("weight", Messages.ColumnWeight, SWT.RIGHT, 70); //$NON-NLS-1$ column.setDescription(Messages.ColumnWeight_Description); column.setLabelProvider(new ColumnLabelProvider() // NOSONAR { @Override public String getText(Object element) { TaxonomyNode node = (TaxonomyNode) element; return node.isAssignment() ? Values.Weight.format(node.getWeight()) : null; } @Override public Color getForeground(Object element) { TaxonomyNode node = (TaxonomyNode) element; return node.isAssignment() && getModel().hasWeightError(node) ? Display.getDefault().getSystemColor(SWT.COLOR_BLACK) : null; } @Override public Color getBackground(Object element) { TaxonomyNode node = (TaxonomyNode) element; return node.isAssignment() && getModel().hasWeightError(node) ? warningColor : null; } @Override public Image getImage(Object element) { TaxonomyNode node = (TaxonomyNode) element; return node.isAssignment() && getModel().hasWeightError(node) ? Images.QUICKFIX.image() : null; } }); new ValueEditingSupport(TaxonomyNode.class, "weight", Values.Weight) //$NON-NLS-1$ { @Override public boolean canEdit(Object element) { if (((TaxonomyNode) element).isUnassignedCategory()) return false; if (((TaxonomyNode) element).isClassification()) return false; return super.canEdit(element); } }.addListener(this::onWeightModified).attachTo(column); support.addColumn(column); } protected void addActualColumns(ShowHideColumnHelper support) { Column column = new Column("act%", Messages.ColumnActualPercent, SWT.RIGHT, 60); //$NON-NLS-1$ column.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { TaxonomyNode node = (TaxonomyNode) element; // actual % // --> root is compared to target = total assets long actual = node.getActual().getAmount(); long base = node.getParent() == null ? node.getActual().getAmount() : node.getParent().getActual().getAmount(); if (base == 0) return Values.Percent.format(0d); else return Values.Percent.format((double) actual / (double) base); } }); support.addColumn(column); column = new Column("act", Messages.ColumnActualValue, SWT.RIGHT, 100); //$NON-NLS-1$ column.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { TaxonomyNode node = (TaxonomyNode) element; return Values.Money.format(node.getActual(), getModel().getCurrencyCode()); } }); support.addColumn(column); } protected void addAdditionalColumns(ShowHideColumnHelper support) // NOSONAR { Column column = new Column("exchangeRate", Messages.ColumnExchangeRate, SWT.RIGHT, 80); //$NON-NLS-1$ column.setGroupLabel(Messages.ColumnForeignCurrencies); column.setLabelProvider(new ColumnLabelProvider() // NOSONAR { @Override public String getText(Object element) { TaxonomyNode node = (TaxonomyNode) element; if (!node.isAssignment()) return null; String baseCurrency = node.getAssignment().getInvestmentVehicle().getCurrencyCode(); if (baseCurrency == null) return null; CurrencyConverter converter = getModel().getCurrencyConverter(); ExchangeRate rate = converter.getRate(LocalDate.now(), baseCurrency); if (useIndirectQuotation) rate = rate.inverse(); return Values.ExchangeRate.format(rate.getValue()); } @Override public String getToolTipText(Object e) { String text = getText(e); if (text == null) return null; String term = getModel().getCurrencyConverter().getTermCurrency(); String base = ((TaxonomyNode) e).getAssignment().getInvestmentVehicle().getCurrencyCode(); return text + ' ' + (useIndirectQuotation ? base + '/' + term : term + '/' + base); } }); column.setVisible(false); support.addColumn(column); column = new Column("actBaseCurrency", Messages.ColumnActualValue + Messages.BaseCurrencyCue, SWT.RIGHT, 100); //$NON-NLS-1$ column.setDescription(Messages.ColumnActualValueBaseCurrency); column.setGroupLabel(Messages.ColumnForeignCurrencies); column.setLabelProvider(new ColumnLabelProvider() // NOSONAR { @Override public String getText(Object element) { TaxonomyNode node = (TaxonomyNode) element; if (node.isClassification() || getModel().getCurrencyCode() .equals(node.getAssignment().getInvestmentVehicle().getCurrencyCode())) { // if it is a classification // *or* it is an assignment, but currency code matches // then no currency conversion is needed return Values.Money.format(node.getActual(), getModel().getCurrencyCode()); } else if (node.getAssignment().getInvestmentVehicle().getCurrencyCode() != null) { // convert into target currency if investment vehicle has a // currency code (e.g. is not an stock market index) return Values.Money.format( getModel().getCurrencyConverter() .with(node.getAssignment().getInvestmentVehicle().getCurrencyCode()) .convert(LocalDate.now(), node.getActual()), getModel().getCurrencyCode()); } else { return null; } } }); column.setVisible(false); support.addColumn(column); getModel().getClient() // .getSettings() // .getAttributeTypes() // .filter(a -> a.supports(Security.class)) // .forEach(attribute -> { Column col = new AttributeColumn(attribute); col.setVisible(false); col.setSorter(null); col.getEditingSupport().addListener(this); support.addColumn(col); }); } private void expandNodes() { List<TaxonomyNode> expanded = new ArrayList<>(); // check if we have expansion state in preferences String expansion = readExpansionState(); if (expansion != null && !expansion.isEmpty()) { Set<String> uuid = new HashSet<>(Arrays.asList(expansion.split(","))); //$NON-NLS-1$ getModel().visitAll(node -> { if (node.isClassification() && uuid.contains(node.getClassification().getId())) expanded.add(node); }); } else { // fall back -> expand all classification nodes with children LinkedList<TaxonomyNode> stack = new LinkedList<>(); stack.push(getModel().getVirtualRootNode()); while (!stack.isEmpty()) { TaxonomyNode node = stack.pop(); if (node.isClassification() && !node.getClassification().getChildren().isEmpty()) { expanded.add(node); stack.addAll(node.getChildren()); } } } nodeViewer.getTree().setRedraw(false); try { nodeViewer.setExpandedElements(expanded.toArray()); } finally { nodeViewer.getTree().setRedraw(true); } } protected void onTaxnomyNodeEdited(TaxonomyNode node) { getModel().recalculate(); getModel().fireTaxonomyModelChange(node); getModel().markDirty(); } @Override public void nodeChange(TaxonomyNode node) { if (!nodeViewer.getTree().isDisposed()) nodeViewer.refresh(); } @Override public void beforePage() { if (isFirstView) { expandNodes(); isFirstView = false; } } @Override public void afterPage() {} @Override public void dispose() { // store expansion state to model StringJoiner expansionState = new StringJoiner(","); //$NON-NLS-1$ for (Object element : nodeViewer.getExpandedElements()) { TaxonomyNode node = (TaxonomyNode) element; if (!node.isClassification()) continue; expansionState.add(node.getClassification().getId()); } storeExpansionState(expansionState.toString()); super.dispose(); } protected void fillContextMenu(IMenuManager manager) // NOSONAR { // do not show context menu if multiple nodes are selected IStructuredSelection selection = (IStructuredSelection) nodeViewer.getSelection(); if (selection.isEmpty() || selection.size() > 1) return; TaxonomyNode node = (TaxonomyNode) selection.getFirstElement(); if (node.isUnassignedCategory()) return; // allow inherited views to contribute to the context menu manager.add(new Separator(MENU_GROUP_CUSTOM_ACTIONS)); manager.add(new Separator(MENU_GROUP_DEFAULT_ACTIONS)); if (node.isClassification()) { manager.add(new SimpleAction(Messages.MenuTaxonomyClassificationCreate, a -> doAddClassification(node))); TaxonomyNode unassigned = getModel().getUnassignedNode(); if (!unassigned.getChildren().isEmpty()) { MenuManager subMenu = new MenuManager(Messages.MenuTaxonomyMakeAssignment); addAvailableAssignments(subMenu, node); manager.add(subMenu); } manager.add(new Separator()); MenuManager sorting = new MenuManager(Messages.MenuTaxonomySortTreeBy); sorting.add(new SimpleAction(Messages.MenuTaxonomySortByTypName, a -> doSort(node, true))); sorting.add(new SimpleAction(Messages.MenuTaxonomySortByName, a -> doSort(node, false))); manager.add(sorting); if (!node.isRoot()) { manager.add(new Separator(MENU_GROUP_DELETE_ACTIONS)); manager.add(new SimpleAction(Messages.MenuTaxonomyClassificationDelete, a -> doDeleteClassification(node))); } } else { // node is assignment, but not in unassigned category if (!node.getParent().isUnassignedCategory()) { manager.add(new SimpleAction(Messages.MenuTaxonomyAssignmentRemove, a -> { int oldWeight = node.getWeight(); node.setWeight(0); doChangeAssignmentWeight(node, oldWeight); onTaxnomyNodeEdited(getModel().getVirtualRootNode()); })); } Security security = node.getBackingSecurity(); if (security != null) { manager.add(new Separator()); manager.add(new BookmarkMenu(part, security)); } } } private void addAvailableAssignments(MenuManager manager, TaxonomyNode targetNode) { for (final TaxonomyNode assignment : getModel().getUnassignedNode().getChildren()) { String label = assignment.getName(); if (assignment.getWeight() < Classification.ONE_HUNDRED_PERCENT) label += " (" + Values.Weight.format(assignment.getWeight()) + "%)"; //$NON-NLS-1$ //$NON-NLS-2$ manager.add(new Action(label) { @Override public void run() { assignment.moveTo(targetNode); nodeViewer.setExpandedState(targetNode, true); onTaxnomyNodeEdited(targetNode); } }); } } private void doAddClassification(TaxonomyNode parent) { Classification newClassification = new Classification(null, UUID.randomUUID().toString(), Messages.LabelNewClassification); TaxonomyNode newNode = parent.addChild(newClassification); nodeViewer.setExpandedState(parent, true); onTaxnomyNodeEdited(parent); nodeViewer.editElement(newNode, 0); } private void doDeleteClassification(TaxonomyNode node) { if (node.isRoot() || node.isUnassignedCategory()) return; node.getParent().removeChild(node); node.accept(node1 -> { if (node1.isAssignment()) node1.moveTo(getModel().getUnassignedNode()); }); onTaxnomyNodeEdited(getModel().getVirtualRootNode()); } private void doChangeAssignmentWeight(TaxonomyNode node, int oldWeight) { int change = oldWeight - node.getWeight(); if (change == 0) // was 'fixed' after editing, e.g. was >= 100% return; // if new weight = 0, then remove assignment if (node.getWeight() == 0) node.getParent().removeChild(node); // change total weight as recorded in the model InvestmentVehicle investmentVehicle = node.getAssignment().getInvestmentVehicle(); final int totalWeight = getModel().getWeightByInvestmentVehicle(investmentVehicle) - change; getModel().setWeightByInvestmentVehicle(investmentVehicle, totalWeight); // check if change is fixing weight errors -> no new unassigned vehicles change = Math.min(change, Classification.ONE_HUNDRED_PERCENT - totalWeight); if (change == 0) return; // change existing unassigned node *or* create new unassigned node TaxonomyNode unassigned = getModel().getUnassignedNode().getChildByInvestmentVehicle(investmentVehicle); if (unassigned != null) { // change existing node in unassigned category int newWeight = unassigned.getWeight() + change; if (newWeight <= 0) { getModel().getUnassignedNode().removeChild(unassigned); getModel().setWeightByInvestmentVehicle(investmentVehicle, totalWeight - unassigned.getWeight()); } else { unassigned.setWeight(newWeight); getModel().setWeightByInvestmentVehicle(investmentVehicle, totalWeight + change); } } else if (change > 0) { // create a new node, but only if change is positive Assignment assignment = new Assignment(investmentVehicle); assignment.setWeight(change); getModel().getUnassignedNode().addChild(assignment); getModel().setWeightByInvestmentVehicle(investmentVehicle, totalWeight + change); } // do not fire model change -> called within modification listener } private void doSort(TaxonomyNode node, final boolean byType) // NOSONAR { Collections.sort(node.getChildren(), (node1, node2) -> { // NOSONAR // unassigned category always stays at the end of the list if (node1.isUnassignedCategory()) return 1; if (node2.isUnassignedCategory()) return -1; if (byType && node1.isClassification() && !node2.isClassification()) return -1; if (byType && !node1.isClassification() && node2.isClassification()) return 1; return node1.getName().compareToIgnoreCase(node2.getName()); }); int rank = 0; for (TaxonomyNode child : node.getChildren()) child.setRank(rank++); getModel().markDirty(); getModel().fireTaxonomyModelChange(node); } }