/******************************************************************************* * Copyright (c) 2013, 2017 Obeo and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Obeo - initial API and implementation * Michael Borkowski - bug 462237, refactoring * Simon Delisle - bug 511172 * Philip Langer - bug 509975 *******************************************************************************/ package org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer; import static org.eclipse.emf.compare.DifferenceState.UNRESOLVED; import static org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer.EMFCompareStructureMergeViewerContentProvider.CallbackType.IN_UI_ASYNC; import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasState; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import java.util.Arrays; import java.util.List; import org.eclipse.compare.INavigatable; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer.EMFCompareStructureMergeViewerContentProvider.CallbackType; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.widgets.TreeItem; /** * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a> */ public class Navigatable implements INavigatable { public static final int NEXT_UNRESOLVED_CHANGE = 80; private final WrappableTreeViewer viewer; private final EMFCompareStructureMergeViewerContentProvider contentProvider; protected CallbackType uiSyncCallbackType = IN_UI_ASYNC; public Navigatable(WrappableTreeViewer viewer, EMFCompareStructureMergeViewerContentProvider contentProvider) { this.viewer = viewer; this.contentProvider = contentProvider; } public boolean selectChange(final int flag) { contentProvider.runWhenReady(uiSyncCallbackType, new Runnable() { public void run() { TreeItem[] selection = viewer.getTree().getSelection(); TreeItem firstSelectedItem = firstOrNull(selection); Object newSelection = calculateNextSelection(firstSelectedItem, flag); if (newSelection != null) { fireOpen(newSelection); } } }); return false; } private static TreeItem firstOrNull(TreeItem[] items) { if (items.length > 0) { return items[0]; } return null; } private Object calculateNextSelection(TreeItem currentSelection, int flag) { switch (flag) { case NEXT_CHANGE: return getNextDiff(thisOrFirstItem(currentSelection)); case PREVIOUS_CHANGE: return getPreviousDiff(thisOrFirstItem(currentSelection)); case FIRST_CHANGE: return getNextDiff(getFirstItemInTree()); case LAST_CHANGE: return getPreviousDiff(getFirstItemInTree()); case NEXT_UNRESOLVED_CHANGE: return getNextUnresolvedDiff(thisOrFirstItem(currentSelection)); default: throw new IllegalStateException(); } } private TreeItem thisOrFirstItem(TreeItem item) { if (item != null) { return item; } return getFirstItemInTree(); } /** * Execute the fireOpen method of the viewer associated to this navigatable. * * @param element * the input of the selection of the open event fired by the fireOpen method. */ public void fireOpen(Object element) { StructuredSelection newSelection = new StructuredSelection(element); viewer.setSelection(newSelection); viewer.fireOpen(new OpenEvent(viewer, newSelection)); } /** * Return the viewer associated with this Navigatable. * * @return the viewer associated with this Navigatable. */ public WrappableTreeViewer getViewer() { return viewer; } public Object getInput() { return viewer.getInput(); } /** * {@inheritDoc} * * @see org.eclipse.compare.INavigatable#openSelectedChange() */ public boolean openSelectedChange() { ISelection selection = viewer.getSelection(); if (selection.isEmpty()) { return false; } else { viewer.fireOpen(new OpenEvent(viewer, selection)); return true; } } /** * {@inheritDoc} * * @see org.eclipse.compare.INavigatable#hasChange(int) */ public boolean hasChange(int changeFlag) { TreeItem[] selection = viewer.getTree().getSelection(); TreeItem firstSelectedItem = selection.length > 0 ? selection[0] : null; switch (changeFlag) { case NEXT_CHANGE: return getNextDiff(thisOrFirstItem(firstSelectedItem)) != null; case PREVIOUS_CHANGE: return getPreviousDiff(thisOrFirstItem(firstSelectedItem)) != null; case NEXT_UNRESOLVED_CHANGE: return getNextUnresolvedDiff(thisOrFirstItem(firstSelectedItem)) != null; case FIRST_CHANGE: TreeItem firstItemInTree = getFirstItemInTree(); return firstItemInTree != null && getNextDiff(firstItemInTree) != null; default: throw new IllegalStateException(); } } /** * Returns, from the given TreeNode, the next TreeNode that contains a diff. * * @param treeNode * the given TreeNode for which we want to find the next. * @return the next TreeNode that contains a diff. */ private Object getNextDiff(TreeItem start) { return getNextData(start, Predicates.alwaysTrue()); } /** * Returns, from the given TreeNode, the previous TreeNode that contains a diff. * * @param treeNode * the given TreeNode for which we want to find the previous. * @return the previous TreeNode that contains a diff. */ private Object getPreviousDiff(TreeItem item) { return getPreviousData(item, Predicates.alwaysTrue()); } /** * Returns, from the given TreeNode, the next TreeNode that contains an unresolved diff. * * @param treeNode * the given TreeNode for which we want to find the next unresolvable diff. * @return the previous TreeNode that contains an unresolvable diff. */ private Object getNextUnresolvedDiff(TreeItem start) { Object nextUnresolvedDiff = getNextData(start, hasState(UNRESOLVED)); if (nextUnresolvedDiff == null) { // we don't have an unresolved diff AFTER the the current one (TreeItem start) // thus, we look for the next unresolved diff starting from the beginning nextUnresolvedDiff = getNextData(getFirstItemInTree(), hasState(UNRESOLVED)); } return nextUnresolvedDiff; } /** * Returns the data of the next tree item meeting the supplied predicate * * @param start * the TreeNode at which to start searching * @param predicate * the predicate to match * @return the next matching TreeNode */ private Object getNextData(TreeItem start, Predicate<? super Diff> predicate) { TreeItem current = getNextItem(start); while (current != null) { EObject data = EMFCompareStructureMergeViewer.getDataOfTreeNodeOfAdapter(current.getData()); if (data instanceof Diff && predicate.apply((Diff)data)) { return current.getData(); } current = getNextItem(current); } return null; } /** * Returns the data of the previous tree item meeting the supplied predicate * * @param start * the TreeNode at which to start searching * @param predicate * the predicate to match * @return the previous matching TreeNode */ private Object getPreviousData(TreeItem start, Predicate<? super Diff> predicate) { TreeItem current = getPreviousItem(start); while (current != null) { EObject data = EMFCompareStructureMergeViewer.getDataOfTreeNodeOfAdapter(current.getData()); if (data instanceof Diff && predicate.apply((Diff)data)) { return current.getData(); } current = getPreviousItem(current); } return null; } /** * Returns the first TreeItem * * @return the first TreeItem */ private TreeItem getFirstItemInTree() { final TreeItem startingItem; TreeItem[] roots = viewer.getTree().getItems(); if (roots != null && roots.length > 0) { startingItem = roots[0]; } else { startingItem = null; } return startingItem; } /** * Starting at the given TreeItem, returns the next item in the tree. * * @param start * the item for which to find the next one * @return the next TreeItem */ // Protected for testing purposes protected TreeItem getNextItem(TreeItem start) { viewer.createChildren(start); if (hasChildren(start)) { return start.getItem(0); } else if (hasNextSibling(start)) { return getNextSibling(start); } else { return getNextAncestor(start); } } /** * Checks whether the given item has children. * * @param item * the item to check * @return whether it has children */ private boolean hasChildren(TreeItem item) { return item.getItems().length != 0; } /** * Checks whether the given item has a next sibling. * * @param item * the item to check * @return whether it has a next sibling */ private boolean hasNextSibling(TreeItem item) { return getNextSibling(item) != null; } /** * Returns the next ancestor of the given TreeItem. "Next" means that it is the closest available sibling * for one of the given TreeNode's ancestors. * * @param item * the item for which to get the next ancestor * @return the next ancestor */ private TreeItem getNextAncestor(TreeItem item) { TreeItem nextAncestor = null; TreeItem ancestor = item.getParentItem(); while (ancestor != null && nextAncestor == null) { nextAncestor = getNextSibling(ancestor); ancestor = ancestor.getParentItem(); } return nextAncestor; } /** * Starting at the given TreeItem, returns the previous item in the tree. * * @param start * the item for which to find the previous one * @return the previous TreeItem */ // Protected for testing purposes protected TreeItem getPreviousItem(TreeItem start) { if (hasPreviousSibling(start)) { return getLastDescendant(getPreviousSibling(start)); } else { return start.getParentItem(); } } /** * Checks whether the given item has a previous sibling. * * @param item * the item to check * @return whether it has a previous sibling */ private boolean hasPreviousSibling(TreeItem item) { return getPreviousSibling(item) != null; } /** * Returns the last item of a depth-first search starting at the given TreeItem * * @param input * the starting item * @return the resulting item */ private TreeItem getLastDescendant(TreeItem input) { TreeItem[] children = getChildren(input); TreeItem deepestChild = input; while (children.length > 0) { deepestChild = children[children.length - 1]; children = getChildren(deepestChild); } return deepestChild; } /** * Return a (possibly empty) array of TreeItem which are the children of the input item. This loads the * children in the tree before returning the array to avoid passing over hidden items (Bug 511172). * * @param input * TreeItem that you want to have the children * @return Direct item children of the input */ private TreeItem[] getChildren(TreeItem input) { viewer.createChildren(input); return input.getItems(); } /** * Returns the previous sibling of the given TreeItem. * * @param item * the item to get the sibling for * @return the sibling */ private TreeItem getPreviousSibling(TreeItem item) { List<TreeItem> siblings = Arrays.asList(getSiblings(item)); int indexOfCurrent = siblings.indexOf(item); if (indexOfCurrent > 0) { return siblings.get(indexOfCurrent - 1); } return null; } /** * Returns the next sibling of the given TreeItem. * * @param item * the item to get the sibling for * @return the sibling */ private TreeItem getNextSibling(TreeItem item) { List<TreeItem> siblings = Arrays.asList(getSiblings(item)); int indexOfItem = siblings.indexOf(item); if (indexOfItem >= 0 && indexOfItem < siblings.size() - 1) { return siblings.get(indexOfItem + 1); } return null; } /** * Returns the siblings for a given TreeItem (including the item itself). * * @param item * the item to get the siblings for * @return the siblings */ private TreeItem[] getSiblings(TreeItem item) { if (item.getParentItem() == null) { return viewer.getTree().getItems(); } else { return item.getParentItem().getItems(); } } }