/******************************************************************************* * Copyright (c) 2015, 2016 Pivotal, Inc. * 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: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.views.sections; import java.util.Arrays; import java.util.Comparator; import java.util.Set; import org.apache.commons.lang3.ArrayUtils; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.util.LocalSelectionTransfer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.jface.viewers.ViewerSorter; 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.DropTarget; import org.eclipse.swt.dnd.DropTargetAdapter; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Tree; import org.springframework.ide.eclipse.boot.dash.BootDashActivator; import org.springframework.ide.eclipse.boot.dash.livexp.ElementwiseListener; import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelection; import org.springframework.ide.eclipse.boot.dash.livexp.MultiSelectionSource; import org.springframework.ide.eclipse.boot.dash.model.BootDashElement; import org.springframework.ide.eclipse.boot.dash.model.BootDashModel; import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ElementStateListener; import org.springframework.ide.eclipse.boot.dash.model.BootDashModel.ModelStateListener; import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; import org.springframework.ide.eclipse.boot.dash.model.ModifiableModel; import org.springframework.ide.eclipse.boot.dash.model.RunTarget; import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; import org.springframework.ide.eclipse.boot.dash.util.HiddenElementsLabel; import org.springframework.ide.eclipse.boot.dash.views.AbstractBootDashAction; import org.springframework.ide.eclipse.boot.dash.views.AddRunTargetAction; import org.springframework.ide.eclipse.boot.dash.views.BootDashActions; import org.springframework.ide.eclipse.boot.dash.views.RunStateAction; import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression; import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable; import org.springsource.ide.eclipse.commons.livexp.core.ObservableSet; import org.springsource.ide.eclipse.commons.livexp.core.UIValueListener; import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult; import org.springsource.ide.eclipse.commons.livexp.core.Validator; import org.springsource.ide.eclipse.commons.livexp.core.ValueListener; import org.springsource.ide.eclipse.commons.livexp.ui.IPageWithSections; import org.springsource.ide.eclipse.commons.livexp.ui.PageSection; import org.springsource.ide.eclipse.commons.livexp.ui.Stylers; import org.springsource.ide.eclipse.commons.livexp.ui.util.ReflowUtil; import org.springsource.ide.eclipse.commons.livexp.util.Filter; import org.springsource.ide.eclipse.commons.ui.UiUtil; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; /** * Displays all runtargets and elements in a single 'unified' tree viewer. * * @author Kris De Volder */ public class BootDashUnifiedTreeSection extends PageSection implements MultiSelectionSource { private static final boolean DEBUG = false; private <T> void debug(final String name, LiveExpression<T> watchable) { if (DEBUG) { watchable.addListener(new ValueListener<T>() { public void gotValue(LiveExpression<T> exp, T value) { System.out.println(name +": "+ value); } }); } } protected static final Object[] NO_OBJECTS = new Object[0]; private CustomTreeViewer tv; private BootDashViewModel model; private MultiSelection<Object> mixedSelection; // selection that may contain section or element nodes or both. private MultiSelection<BootDashElement> selection; private LiveExpression<BootDashModel> sectionSelection; private BootDashActions actions; private UserInteractions ui; private LiveExpression<Filter<BootDashElement>> searchFilterModel; private Stylers stylers; public static class BootModelViewerSorter extends ViewerSorter { private final BootDashViewModel viewModel; public BootModelViewerSorter(BootDashViewModel viewModel) { this.viewModel = viewModel; } @Override public int compare(Viewer viewer, Object e1, Object e2) { if (e1 instanceof BootDashModel && e2 instanceof BootDashModel) { return this.viewModel.getModelComparator().compare((BootDashModel) e1, (BootDashModel) e2); } else if (e1 instanceof BootDashElement && e2 instanceof BootDashElement) { BootDashElement bde1 = (BootDashElement) e1; BootDashElement bde2 = (BootDashElement) e2; if (bde1.getBootDashModel()==bde2.getBootDashModel()) { Comparator<BootDashElement> comparator = bde1.getBootDashModel().getElementComparator(); if (comparator!=null) { return comparator.compare(bde1, bde2); } } } return super.compare(viewer, e1, e2); } } final private ValueListener<Filter<BootDashElement>> FILTER_LISTENER = new ValueListener<Filter<BootDashElement>>() { public void gotValue(LiveExpression<Filter<BootDashElement>> exp, Filter<BootDashElement> value) { tv.refresh(); final Tree t = tv.getTree(); t.getDisplay().asyncExec(new Runnable() { public void run() { Composite parent = t.getParent(); parent.layout(); } }); } }; final private ElementStateListener ELEMENT_STATE_LISTENER = new ElementStateListener() { public void stateChanged(final BootDashElement e) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (tv != null && !tv.getControl().isDisposed()) { //tv.update(e, null); tv.refresh(e, true); } } }); } }; final private ModelStateListener MODEL_STATE_LISTENER = new ModelStateListener() { public void stateChanged(final BootDashModel model) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (tv != null && !tv.getControl().isDisposed()) { tv.refresh(); /* * TODO: ideally the above should do the repaint of * the control's area where the tree item is * located, but for some reason repaint doesn't * happen. #refresh() didn't trigger the repaint either */ tv.getControl().redraw(); } else { model.removeModelStateListener(MODEL_STATE_LISTENER); } } }); } }; final private ValueListener<ImmutableSet<RunTarget>> RUN_TARGET_LISTENER = new UIValueListener<ImmutableSet<RunTarget>>() { protected void uiGotValue(LiveExpression<ImmutableSet<RunTarget>> exp, ImmutableSet<RunTarget> value) { if (tv != null && !tv.getControl().isDisposed()) { tv.refresh(); } } }; private final ValueListener<ImmutableSet<BootDashElement>> ELEMENTS_SET_LISTENER = new UIValueListener<ImmutableSet<BootDashElement>>() { protected void uiGotValue(LiveExpression<ImmutableSet<BootDashElement>> exp, ImmutableSet<BootDashElement> value) { if (tv != null && !tv.getControl().isDisposed()) { //TODO: refreshing the whole table is overkill, but is a bit tricky to figure out which BDM // this set of elements belong to. If we did know then we could just refresh the node representing its section // only. tv.refresh(); } else { //This listener can't easily be removed because of the intermediary adapter that adds it to a numner of different // things. So at least remove it when model remains chatty after view got disposed. exp.removeListener(this); } } }; /** * Listener which adds element set listener to each section model. */ final private ValueListener<ImmutableSet<BootDashModel>> ELEMENTS_SET_LISTENER_ADAPTER = new ElementwiseListener<BootDashModel>() { protected void added(LiveExpression<ImmutableSet<BootDashModel>> exp, BootDashModel e) { e.getElements().addListener(ELEMENTS_SET_LISTENER); e.addModelStateListener(MODEL_STATE_LISTENER); } protected void removed(LiveExpression<ImmutableSet<BootDashModel>> exp, BootDashModel e) { e.getElements().removeListener(ELEMENTS_SET_LISTENER); e.removeModelStateListener(MODEL_STATE_LISTENER); } }; public static class CustomTreeViewer extends TreeViewer { private LiveVariable<Integer> hiddenElementCount = new LiveVariable<>(0); public CustomTreeViewer(Composite page, int style) { super(page, style); } @Override public void refresh(Object obj) { super.refresh(obj); // Every sub-tree refresh should update the hidden elements label int totalElements = countChildren(getRoot()); int filteredElements = countFilteredChildren(getRoot()); hiddenElementCount.setValue(totalElements - filteredElements); } private int countChildren(Object element) { int count = 0; for (Object o : getRawChildren(element)) { count += 1 + countChildren(o); } return count; } private int countFilteredChildren(Object element) { int count = 0; for (Object o : super.getFilteredChildren(element)) { count += 1 + countFilteredChildren(o); } return count; } } public BootDashUnifiedTreeSection(IPageWithSections owner, BootDashViewModel model, UserInteractions ui) { super(owner); Assert.isNotNull(ui); this.ui = ui; this.model = model; this.searchFilterModel = model.getFilter(); } @Override public void createContents(Composite page) { tv = new CustomTreeViewer(page, SWT.V_SCROLL | SWT.H_SCROLL | SWT.MULTI); tv.setExpandPreCheckFilters(true); tv.setContentProvider(new BootDashTreeContentProvider()); tv.setSorter(new BootModelViewerSorter(this.model)); tv.setInput(model); tv.getTree().setLinesVisible(false); stylers = new Stylers(tv.getTree().getFont()); tv.setLabelProvider(new BootDashTreeLabelProvider(stylers, tv)); ColumnViewerToolTipSupport.enableFor(tv); GridDataFactory.fillDefaults().grab(true, true).applyTo(tv.getTree()); new HiddenElementsLabel(page, tv.hiddenElementCount); tv.getControl().addControlListener(new ControlListener() { public void controlResized(ControlEvent e) { ReflowUtil.reflow(owner, tv.getControl()); } public void controlMoved(ControlEvent e) { } }); actions = new BootDashActions(model, getSelection(), getSectionSelection(), ui); hookContextMenu(); // Careful, either selection or tableviewer might be created first. // in either case we must make sure the listener is added when *both* // have been created. if (selection != null) { addViewerSelectionListener(); } tv.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { if (selection != null) { BootDashElement selected = selection.getSingle(); if (selected != null) { String url = selected.getUrl(); if (url != null) { UiUtil.openUrl(url); } } else if (sectionSelection.getValue() != null) { IAction openCloudAdminConsoleAction = actions.getOpenCloudAdminConsoleAction(); if (openCloudAdminConsoleAction != null) { openCloudAdminConsoleAction.run(); } } } } }); model.getRunTargets().addListener(RUN_TARGET_LISTENER); model.getSectionModels().addListener(ELEMENTS_SET_LISTENER_ADAPTER); model.addElementStateListener(ELEMENT_STATE_LISTENER); if (searchFilterModel != null) { searchFilterModel.addListener(FILTER_LISTENER); tv.addFilter(new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (searchFilterModel.getValue() != null && element instanceof BootDashElement) { return searchFilterModel.getValue().accept((BootDashElement) element); } return true; } }); } tv.getTree().addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { model.removeElementStateListener(ELEMENT_STATE_LISTENER); model.getRunTargets().removeListener(RUN_TARGET_LISTENER); model.getSectionModels().removeListener(ELEMENTS_SET_LISTENER_ADAPTER); for (BootDashModel m : model.getSectionModels().getValue()) { m.removeModelStateListener(MODEL_STATE_LISTENER); } if (searchFilterModel!=null) { searchFilterModel.removeListener(FILTER_LISTENER); } if (actions!=null) { actions.dispose(); actions = null; } if (stylers != null) { stylers.dispose(); stylers = null; } } }); addDragSupport(tv); addDropSupport(tv); } private LiveExpression<BootDashModel> getSectionSelection() { if (sectionSelection==null) { sectionSelection = getMixedSelection().toSingleSelection().filter(BootDashModel.class); debug("sectionSelection", sectionSelection); } return sectionSelection; } private synchronized MultiSelection<Object> getMixedSelection() { if (mixedSelection==null) { mixedSelection = MultiSelection.from(Object.class, new ObservableSet<Object>() { @Override protected ImmutableSet<Object> compute() { if (tv!=null) { ISelection s = tv.getSelection(); if (s instanceof IStructuredSelection) { Object[] elements = ((IStructuredSelection) s).toArray(); return ImmutableSet.copyOf(elements); } } return ImmutableSet.of(); } }); debug("mixedSelection", mixedSelection.getElements()); } if (tv!=null) { addViewerSelectionListener(); } return mixedSelection; } @Override public synchronized MultiSelection<BootDashElement> getSelection() { if (selection==null) { selection = getMixedSelection().filter(BootDashElement.class); debug("selection", selection.getElements()); } return selection; } private void addViewerSelectionListener() { tv.setSelection(new StructuredSelection(Arrays.asList(mixedSelection.getValue().toArray()))); tv.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { mixedSelection.getElements().refresh(); } }); } private void hookContextMenu() { MenuManager menuMgr = new MenuManager("#PopupMenu"); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { fillContextMenu(manager); } }); Menu menu = menuMgr.createContextMenu(tv.getControl()); tv.getControl().setMenu(menu); } private void fillContextMenu(IMenuManager manager) { for (RunStateAction a : actions.getRunStateActions()) { addVisible(manager, a); } addVisible(manager, actions.getOpenBrowserAction()); addVisible(manager, actions.getOpenNgrokAdminUi()); addVisible(manager, actions.getOpenConsoleAction()); addVisible(manager, actions.getOpenInPackageExplorerAction()); addVisible(manager, actions.getShowPropertiesViewAction()); manager.add(new Separator()); addVisible(manager, actions.getOpenConfigAction()); addVisible(manager, actions.getDuplicateConfigAction()); addVisible(manager, actions.getDeleteConfigsAction()); manager.add(new Separator()); addVisible(manager, actions.getExposeRunAppAction()); addVisible(manager, actions.getExposeDebugAppAction()); addSubmenu(manager, "Deploy and Run On...", BootDashActivator.getImageDescriptor("icons/run-on-cloud.png"), actions.getRunOnTargetActions()); addSubmenu(manager, "Deploy and Debug On...", BootDashActivator.getImageDescriptor("icons/debug-on-cloud.png"), actions.getDebugOnTargetActions()); manager.add(new Separator()); for (AddRunTargetAction a : actions.getAddRunTargetActions()) { addVisible(manager, a); } manager.add(new Separator()); addVisible(manager, actions.getRemoveRunTargetAction()); addVisible(manager, actions.getRefreshRunTargetAction()); addVisible(manager, actions.getRestartOnlyApplicationAction()); addVisible(manager, actions.getSelectManifestAction()); addVisible(manager, actions.getRestartWithRemoteDevClientAction()); addVisible(manager, actions.getDeleteAppsAction()); addVisible(manager, actions.getUpdatePasswordAction()); addVisible(manager, actions.getOpenCloudAdminConsoleAction()); addVisible(manager, actions.getToggleTargetConnectionAction()); addVisible(manager, actions.getReconnectCloudConsole()); manager.add(new Separator()); ImmutableList.Builder<IAction> customizeActions = ImmutableList.builder(); customizeActions.add(actions.getCustomizeTargetLabelAction()); customizeActions.add(actions.getCustomizeTargetAppsManagerURLAction()); addSubmenu(manager, "Customize...", null, customizeActions.build()); // manager.add // addVisible(manager, new Separator()); // addVisible(manager, refreshAction); // addVisible(manager, action2); // Other plug-ins can contribute there actions here // manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS)); } /** * Adds a submenu containing a given list of actions. The menu is only added if * there is at least one visible action in the list. * @param imageDescriptor */ private void addSubmenu(IMenuManager parent, String label, ImageDescriptor imageDescriptor, ImmutableList<IAction> actions) { if (actions!=null && !actions.isEmpty()) { boolean notEmpty = false; MenuManager submenu = new MenuManager(label); for (IAction a : actions) { notEmpty |= addVisible(submenu, a); } if (notEmpty) { submenu.setImageDescriptor(imageDescriptor); parent.add(submenu); } } } private boolean addVisible(IMenuManager manager, IAction a) { if (a!=null && isVisible(a)) { manager.add(a); return true; } return false; } private boolean isVisible(IAction a) { if (a instanceof AbstractBootDashAction) { return ((AbstractBootDashAction) a).isVisible(); } return true; } private void addDragSupport(final TreeViewer viewer) { int ops = DND.DROP_COPY; final Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer() }; DragSourceAdapter listener = new DragSourceAdapter() { // @Override // public void dragSetData(DragSourceEvent event) { // IStructuredSelection selection = (IStructuredSelection) viewer.getSelection(); // event.data = selection.getFirstElement(); // LocalSelectionTransfer.getTransfer().setSelection(selection); // } // // @Override // public void dragStart(DragSourceEvent event) { // if (event.detail == DND.DROP_NONE || event.detail == DND.DROP_DEFAULT) { // event.detail = DND.DROP_COPY; // } // dragSetData(event); // } @Override public void dragSetData(DragSourceEvent event) { Set<BootDashElement> selection = getSelection().getValue(); BootDashElement[] elements = selection.toArray(new BootDashElement[selection.size()]); LocalSelectionTransfer.getTransfer().setSelection(new StructuredSelection(elements)); event.detail = DND.DROP_COPY; } @Override public void dragStart(DragSourceEvent event) { if (!canDeploySelection(getSelection().getValue())) { event.doit = false; } else { dragSetData(event); } } }; viewer.addDragSupport(ops, transfers, listener); } private void addDropSupport(final TreeViewer tv) { int ops = DND.DROP_COPY; final LocalSelectionTransfer transfer = LocalSelectionTransfer.getTransfer(); Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer() }; DropTarget dropTarget = new DropTarget(tv.getTree(), ops); dropTarget.setTransfer(transfers); dropTarget.addDropListener(new DropTargetAdapter() { @Override public void dragEnter(DropTargetEvent event) { checkDropable(event); } @Override public void dragOver(DropTargetEvent event) { checkDropable(event); event.feedback = DND.FEEDBACK_SELECT | DND.FEEDBACK_SCROLL; } @Override public void dropAccept(DropTargetEvent event) { checkDropable(event); } private void checkDropable(DropTargetEvent event) { if (canDrop(event)) { event.detail = DND.DROP_COPY & event.operations; } else { event.detail = DND.DROP_NONE; } } private boolean canDrop(DropTargetEvent event) { BootDashModel droppedOn = getDropTarget(event); if (droppedOn!=null && droppedOn instanceof ModifiableModel) { ModifiableModel target = (ModifiableModel) droppedOn; if (transfer.isSupportedType(event.currentDataType)) { Object[] elements = getDraggedElements(); if (ArrayUtils.isNotEmpty(elements) && target.canBeAdded(Arrays.asList(elements))) { return true; } } } return false; } /** * Determines which BootDashModel a droptarget event represents (i.e. what thing * are we dropping or dragging onto? */ private BootDashModel getDropTarget(DropTargetEvent event) { Point loc = tv.getTree().toControl(new Point(event.x, event.y)); ViewerCell cell = tv.getCell(loc); if (cell!=null) { Object el = cell.getElement(); if (el instanceof BootDashModel) { return (BootDashModel) el; } } //Not a valid place to drop return null; } @Override public void drop(DropTargetEvent event) { if (canDrop(event)) { BootDashModel model = getDropTarget(event); final Object[] elements = getDraggedElements(); if (model instanceof ModifiableModel) { final ModifiableModel modifiableModel = (ModifiableModel) model; Job job = new Job("Performing deployment to " + model.getRunTarget().getName()) { @Override protected IStatus run(IProgressMonitor monitor) { if (modifiableModel != null && selection != null) { try { modifiableModel.add(Arrays.asList(elements), ui); } catch (Exception e) { ui.errorPopup("Failed to Add Element", e.getMessage()); } } return Status.OK_STATUS; } }; job.schedule(); } } super.drop(event); } private Object[] getDraggedElements() { ISelection sel = transfer.getSelection(); if (sel instanceof IStructuredSelection) { return ((IStructuredSelection)sel).toArray(); } return NO_OBJECTS; } }); } private boolean canDeploySelection(Set<BootDashElement> selection) { if (selection.isEmpty()) { //Careful... don't return 'true' if nothing is selected. return false; } for (BootDashElement e : selection) { if (!e.getBootDashModel().getRunTarget().canDeployAppsFrom()) { return false; } } return true; } @Override public LiveExpression<ValidationResult> getValidator() { return Validator.OK; } }