net.certiv.fluentmark.outline.FluentOutlinePage.java Source code

Java tutorial

Introduction

Here is the source code for net.certiv.fluentmark.outline.FluentOutlinePage.java

Source

/*******************************************************************************
 * Copyright (c) 2016 - 2017 Certiv Analytics 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
 ******************************************************************************/
package net.certiv.fluentmark.outline;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.actions.ActionContext;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionGroup;
import org.eclipse.ui.navigator.ICommonMenuConstants;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;

import net.certiv.fluentmark.FluentUI;
import net.certiv.fluentmark.actions.CollapseAllAction;
import net.certiv.fluentmark.actions.CompositeActionGroup;
import net.certiv.fluentmark.actions.ExpandAllAction;
import net.certiv.fluentmark.actions.OpenViewActionGroup;
import net.certiv.fluentmark.actions.ToggleLinkingAction;
import net.certiv.fluentmark.editor.FluentEditor;
import net.certiv.fluentmark.editor.ISourceReference;
import net.certiv.fluentmark.model.ElementChangedEvent;
import net.certiv.fluentmark.model.IElement;
import net.certiv.fluentmark.model.IElementChangedListener;
import net.certiv.fluentmark.model.IParent;
import net.certiv.fluentmark.model.ISourceRange;
import net.certiv.fluentmark.model.PagePart;
import net.certiv.fluentmark.model.PageRoot;
import net.certiv.fluentmark.model.Type;
import net.certiv.fluentmark.outline.dnd.DndConfigurationStrategy;
import net.certiv.fluentmark.preferences.Prefs;

/**
 * The content outline page of the Fluent editor. Publishes its context menu under
 * <code>FluentUI.getDefault().getPluginId() + ".outline"</code>.
 */
public class FluentOutlinePage extends ContentOutlinePage implements IShowInSource, IShowInTarget {

    /**
     * Content provider for the children of a PageRoot
     *
     * @see ITreeContentProvider
     */
    protected class ChildrenProvider implements ITreeContentProvider {

        private ElementChangedListener listener;

        @Override
        public void dispose() {
            if (listener != null) {
                editor.getPageModel().removeElementChangedListener(listener);
                listener = null;
            }
        }

        protected IElement[] filter(IElement[] children) {
            if (store.getBoolean(Prefs.EDITOR_OUTLINE_SHOW_BLANKLINES))
                return children;

            List<IElement> filtered = new ArrayList<>();
            for (IElement child : children) {
                if (child.getKind() != Type.BLANK) {
                    filtered.add(child);
                }
            }
            return filtered.toArray(new IElement[filtered.size()]);
        }

        @Override
        public Object[] getChildren(Object parent) {
            if (parent instanceof IParent) {
                IParent p = (IParent) parent;
                return filter(p.getChildren());
            }
            return FluentOutlinePage.NO_CHILDREN;
        }

        @Override
        public Object[] getElements(Object parent) {
            return getChildren(parent);
        }

        @Override
        public Object getParent(Object child) {
            if (child instanceof PagePart) {
                PagePart e = (PagePart) child;
                return e.getParent();
            }
            return null;
        }

        @Override
        public boolean hasChildren(Object parent) {
            if (parent instanceof IParent) {
                IParent c = (IParent) parent;
                IElement[] children = filter(c.getChildren());
                return children != null && children.length > 0;
            }
            return false;
        }

        @Override
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            boolean isPage = newInput instanceof PageRoot;

            if (isPage && listener == null) {
                listener = new ElementChangedListener();
                editor.getPageModel().addElementChangedListener(listener);
            } else if (!isPage && listener != null) {
                editor.getPageModel().removeElementChangedListener(listener);
                listener = null;
            }
        }

        public boolean isDeleted(Object o) {
            return false;
        }
    }

    /**
     * The model change listener to update the outline viewer.
     */
    protected class ElementChangedListener implements IElementChangedListener {

        @Override
        public void elementChanged(final ElementChangedEvent e) {
            if (getControl() == null)
                return;

            Display display = getControl().getDisplay();
            if (display != null) {
                display.asyncExec(new Runnable() {

                    @Override
                    public void run() {
                        if (viewer != null) {
                            FluentOutlinePage.this.setInput((PageRoot) e.getDelta());
                        }
                    }
                });
            }
        }
    }

    /**
     * The tree viewer used for displaying the outline.
     *
     * @see TreeViewer
     */
    public static class OutlineViewer extends TreeViewer {

        public OutlineViewer(Tree tree) {
            super(tree);
            setAutoExpandLevel(ALL_LEVELS);
            setUseHashlookup(true);
        }

        protected boolean filtered(PagePart parent, PagePart child) {
            Object[] result = new Object[] { child };
            ViewerFilter[] filters = getFilters();
            for (ViewerFilter filter : filters) {
                result = filter.filter(this, parent, result);
                if (result.length == 0)
                    return true;
            }
            return false;
        }

        protected ISourceRange getSourceRange(PagePart element) throws CoreException {
            if (element instanceof ISourceReference) {
                return ((ISourceReference) element).getSourceRange();
            }
            return null;
        }

        private IResource getUnderlyingResource() {
            PageRoot input = (PageRoot) getInput();
            return input.getResource();
        }

        @Override
        protected void handleLabelProviderChanged(LabelProviderChangedEvent event) {
            // Object input = getInput();
            // if (event instanceof ProblemsLabelChangedEvent) {
            // ProblemsLabelChangedEvent e = (ProblemsLabelChangedEvent) event;
            // if (e.isMarkerChange() && input instanceof PageRoot) {
            // return; // marker changes can be ignored
            // }
            // }
            // look if the underlying resource changed
            Object[] changed = event.getElements();
            if (changed != null) {
                IResource resource = getUnderlyingResource();
                if (resource != null) {
                    for (Object element : changed) {
                        if (element != null && element.equals(resource)) {
                            // change event to a full refresh
                            event = new LabelProviderChangedEvent((IBaseLabelProvider) event.getSource());
                            break;
                        }
                    }
                }
            }
            super.handleLabelProviderChanged(event);
        }

        @Override
        public boolean isExpandable(Object element) {
            if (hasFilters()) {
                return getFilteredChildren(element).length > 0;
            }
            return super.isExpandable(element);
        }

        /**
         * Investigates the given element change event and if affected incrementally updates the outline.
         *
         * @param delta the Dsl element delta used to reconcile the Dsl outline
         */
        public void reconcile(PagePart delta) {
            refresh(true);
        }
    }

    private static final String ACTION_EXPAND = "expand";
    private static final String ACTION_COLLAPSE = "collapse";
    private static final String ACTION_TOGGLE = "toggle";
    private static final String ACTION_COPY = "copy";
    private static final String ACTION_CUT = "cut";
    private static final String ACTION_PASTE = "paste";
    private static final String ACTION_DELETE = "delete";

    static Object[] NO_CHILDREN = new Object[0];

    private PageRoot input;
    private Menu menu;
    protected OutlineViewer viewer;

    private FluentEditor editor;
    protected IPreferenceStore store;

    private IPropertyChangeListener fPropertyChangeListener;
    private Clipboard clipboard;

    private CompositeActionGroup actionGroups;
    private Hashtable<String, Action> actions = new Hashtable<>();
    private DndConfigurationStrategy dndStrategy;

    public FluentOutlinePage(FluentEditor editor, IPreferenceStore store) {
        super();
        Assert.isNotNull(editor);
        this.editor = editor;
        this.store = store;

        fPropertyChangeListener = new IPropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent event) {
                // Log.error("preference store changed: " + event.getProperty());
                doPropertyChange(event);
            }
        };
        this.store.addPropertyChangeListener(fPropertyChangeListener);
    }

    public FluentEditor getEditor() {
        return editor;
    }

    @Override
    public void createControl(Composite parent) {
        Tree tree = new Tree(parent, SWT.MULTI);
        viewer = new OutlineViewer(tree);
        viewer.setContentProvider(new ChildrenProvider());
        viewer.setLabelProvider(new FluentOutlineLabelProvider());
        viewer.setInput(input);

        viewer.addOpenListener(new IOpenListener() {

            @Override
            public void open(OpenEvent event) {
                revealInEditor(event.getSelection());
            }
        });

        viewer.addPostSelectionChangedListener(new ISelectionChangedListener() {

            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                revealInEditor(event.getSelection());
            }
        });

        viewer.expandAll();

        // createToolTip();
        createActionControls(tree);
        initDragAndDrop();
        initCopyPaste();
    }

    private void createActionControls(Tree tree) {
        String outlineId = FluentUI.PLUGIN_ID + ".outline";
        MenuManager menuMgr = new MenuManager(outlineId, outlineId);
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener() {

            @Override
            public void menuAboutToShow(IMenuManager m) {
                contextMenuAboutToShow(m);
            }
        });
        menu = menuMgr.createContextMenu(tree);
        tree.setMenu(menu);

        IPageSite site = getSite();
        site.registerContextMenu(outlineId, menuMgr, viewer); // $NON-NLS-1$
        site.setSelectionProvider(viewer);

        actionGroups = new CompositeActionGroup(new ActionGroup[] { new OpenViewActionGroup(this) });

        setAction(ACTION_EXPAND, new ExpandAllAction(viewer));
        setAction(ACTION_COLLAPSE, new CollapseAllAction(viewer));
        setAction(ACTION_TOGGLE, new ToggleLinkingAction(editor));

        setAction(ACTION_COPY, new OutlineCopyAction(this));
        setAction(ACTION_CUT, new OutlineCutAction(this));
        setAction(ACTION_PASTE, new OutlinePasteAction(this));
        setAction(ACTION_DELETE, new OutlineDeleteAction(this));

        createToolBar();
    }

    @SuppressWarnings("unused")
    private void createToolTip() {
        new ToolTip(viewer.getControl(), ToolTip.RECREATE, false) {

            @Override
            protected Composite createToolTipContentArea(Event event, Composite parent) {

                Composite comp = new Composite(parent, SWT.NONE);
                comp.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND));
                GridLayoutFactory.swtDefaults().margins(2, 2).spacing(4, 1).applyTo(comp);

                Object tipItem = getToolTipItem(new Point(event.x, event.y));
                if (tipItem instanceof PagePart) {
                    PagePart part = (PagePart) tipItem;
                    Label label = new Label(comp, SWT.WRAP);
                    label.setBackground(comp.getBackground());
                    label.setText(part.getTooltip());
                }
                return comp;
            }

            @Override
            protected boolean shouldCreateToolTip(Event event) {
                final Object eventItem = getToolTipItem(new Point(event.x, event.y));
                boolean shouldCreate = eventItem != null && eventItem instanceof PagePart
                        && super.shouldCreateToolTip(event);
                if (!shouldCreate)
                    hide();
                return shouldCreate;
            }

            protected Object getToolTipItem(Point point) {
                Tree control = (Tree) viewer.getControl();
                TreeItem item = control.getItem(point);
                if (item != null)
                    return item.getData();
                return null;
            }
        };
    }

    private void createToolBar() {
        IToolBarManager mgr = getSite().getActionBars().getToolBarManager();
        mgr.removeAll();

        mgr.add(new Separator(ICommonMenuConstants.GROUP_SHOW));
        mgr.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));
        mgr.add(new Separator(ICommonMenuConstants.GROUP_VIEWER_SETUP));

        mgr.appendToGroup(ICommonMenuConstants.GROUP_SHOW, actions.get(ACTION_TOGGLE));
        mgr.appendToGroup(ICommonMenuConstants.GROUP_VIEWER_SETUP, actions.get(ACTION_EXPAND));
        mgr.appendToGroup(ICommonMenuConstants.GROUP_VIEWER_SETUP, actions.get(ACTION_COLLAPSE));

        mgr.update(false);
    }

    protected void contextMenuAboutToShow(IMenuManager mgr) {
        if (mgr.isEmpty()) {
            mgr.add(new GroupMarker(ICommonMenuConstants.GROUP_GOTO));
            mgr.add(new Separator(ICommonMenuConstants.GROUP_EDIT));
            mgr.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));
            mgr.add(new Separator(ICommonMenuConstants.GROUP_PROPERTIES));

            mgr.appendToGroup(ICommonMenuConstants.GROUP_EDIT, actions.get(ACTION_COPY));
            mgr.appendToGroup(ICommonMenuConstants.GROUP_EDIT, actions.get(ACTION_CUT));
            mgr.appendToGroup(ICommonMenuConstants.GROUP_EDIT, actions.get(ACTION_PASTE));
            mgr.appendToGroup(ICommonMenuConstants.GROUP_EDIT, actions.get(ACTION_DELETE));
        }

        actionGroups.setContext(new ActionContext(getSite().getSelectionProvider().getSelection()));
        actionGroups.fillContextMenu(mgr);
    }

    public void setInput(PageRoot inputElement) {
        input = inputElement;
        if (viewer != null) {
            Object[] expandedElements = viewer.getExpandedElements();
            TreePath[] expandedTreePaths = viewer.getExpandedTreePaths();

            viewer.setInput(input);

            viewer.setExpandedElements(expandedElements);
            viewer.setExpandedTreePaths(expandedTreePaths);

            actions.get(ACTION_COPY).setEnabled(true);
            actions.get(ACTION_CUT).setEnabled(true);
            actions.get(ACTION_PASTE).setEnabled(true);
            actions.get(ACTION_DELETE).setEnabled(true);
        }
    }

    @Override
    public void dispose() {
        if (editor == null)
            return;
        editor.outlinePageClosed();
        editor = null;
        if (fPropertyChangeListener != null) {
            store.removePropertyChangeListener(fPropertyChangeListener);
            fPropertyChangeListener = null;
        }

        if (menu != null && !menu.isDisposed()) {
            menu.dispose();
            menu = null;
        }

        if (actionGroups != null) {
            actionGroups.dispose();
        }

        this.clipboard.dispose();
        this.clipboard = null;
        viewer = null;
        super.dispose();
    }

    @Override
    public void setFocus() {
        if (viewer != null) {
            viewer.getControl().setFocus();
        }
    }

    public void setAction(String id, Action action) {
        Assert.isNotNull(id);
        if (action == null) {
            actions.remove(id);
        } else {
            actions.put(id, action);
        }
    }

    /**
     * Gets the clipboard. Used by copy paste actions.
     * 
     * @return the clipboard associated with this outline
     */
    public Clipboard getClipboard() {
        return this.clipboard;
    }

    @Override
    public Control getControl() {
        if (viewer != null) {
            return viewer.getControl();
        }
        return null;
    }

    /**
     * Returns the <code>OutlineViewer</code> of this view.
     */
    @Override
    protected OutlineViewer getTreeViewer() {
        return viewer;
    }

    @Override
    public ISelection getSelection() {
        if (viewer == null)
            return StructuredSelection.EMPTY;
        return viewer.getSelection();
    }

    @Override
    public void setSelection(ISelection selection) {
        if (viewer != null) {
            viewer.setSelection(selection);
        }
    }

    @Override
    public ShowInContext getShowInContext() {
        return editor.getShowInContext();
    }

    @Override
    public boolean show(ShowInContext context) {
        return editor.show(context);
    }

    private void doPropertyChange(PropertyChangeEvent event) {
        if (viewer != null) {
            viewer.refresh(false);
        }
    }

    public void select(ISourceReference reference) {
        if (viewer != null) {
            ISelection sel = viewer.getSelection();
            if (sel instanceof IStructuredSelection) {
                IStructuredSelection ssel = (IStructuredSelection) sel;
                List<?> elements = ssel.toList();
                if (!elements.contains(reference)) {
                    sel = reference == null ? StructuredSelection.EMPTY : new StructuredSelection(reference);
                    viewer.setSelection(sel, true);
                }
            }
        }
    }

    private void revealInEditor(ISelection sel) {
        if (sel instanceof IStructuredSelection) {
            IStructuredSelection ssel = (IStructuredSelection) sel;
            Object element = ssel.getFirstElement();
            if (element instanceof PagePart) {
                PagePart part = (PagePart) element;
                editor.revealPart(part);
            }
        }
    }

    /**
     * Gets the text of the currently selected item. Use by copy paste and drag'n'drop operations.
     * 
     * @return text of the currently selected item or null if no item is selected.
     */
    public String getSelectedText() {
        IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection();
        if (selection == null)
            return null;

        PagePart part = (PagePart) selection.getFirstElement();
        ISourceRange range = part.getSourceRange();

        try {
            return editor.getDocument().get(range.getOffset(), range.getLength());
        } catch (BadLocationException e) {
        }
        return null;
    }

    /**
     * Pastes given text after the selected item. Used by the paste action.
     * 
     * @param text the text to be pasted
     * @return true if pasting was succesful, otherwise false
     */
    public boolean paste(String text) {
        IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection();
        if (selection == null)
            return false;

        PagePart part = (PagePart) selection.getFirstElement();
        ISourceRange range = part.getSourceRange();

        try {
            editor.getDocument().replace(range.getOffset() + range.getLength(), 0, text);
        } catch (BadLocationException e) {
            return false;
        }
        return true;
    }

    /**
     * Removes the text of the currently selected item From the document. Used by copy paste and
     * drag'n'drop operations.
     */
    public void removeSelectedText() {
        IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection();
        if (selection == null)
            return;

        PagePart part = (PagePart) selection.getFirstElement();
        ISourceRange range = part.getSourceRange();

        try {
            editor.getDocument().replace(range.getOffset(), range.getLength(), "");
        } catch (BadLocationException e) {
        }
    }

    private void initDragAndDrop() {
        dndStrategy = new DndConfigurationStrategy();
        dndStrategy.configure(editor, getControl(), getTreeViewer());
    }

    private void initCopyPaste() {
        this.clipboard = new Clipboard(getSite().getShell().getDisplay());
        IActionBars bars = getSite().getActionBars();
        bars.setGlobalActionHandler(ActionFactory.CUT.getId(), actions.get(ACTION_CUT));
        bars.setGlobalActionHandler(ActionFactory.COPY.getId(), actions.get(ACTION_COPY));
        bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), actions.get(ACTION_PASTE));
        bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), actions.get(ACTION_DELETE));
    }
}