com.buildml.eclipse.outline.OutlinePage.java Source code

Java tutorial

Introduction

Here is the source code for com.buildml.eclipse.outline.OutlinePage.java

Source

/*******************************************************************************
 * Copyright (c) 2012 Arapiki Solutions 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:
 *    psmith - initial API and 
 *        implementation and/or initial documentation
 *******************************************************************************/

package com.buildml.eclipse.outline;

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.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationListener;
import org.eclipse.jface.viewers.ColumnViewerEditorDeactivationEvent;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.operations.RedoActionHandler;
import org.eclipse.ui.operations.UndoActionHandler;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;

import com.buildml.eclipse.MainEditor;
import com.buildml.eclipse.bobj.UIInteger;
import com.buildml.eclipse.bobj.UIPackage;
import com.buildml.eclipse.bobj.UIPackageFolder;
import com.buildml.eclipse.outline.dialogs.ChangeRootsDialog;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.UndoOpAdapter;
import com.buildml.model.FatalBuildStoreError;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageMgrListener;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.undo.PackageUndoOp;
import com.buildml.utils.errors.ErrorCode;

/**
 * An Eclipse view, providing content for the "Outline View" associated with the BuildML
 * editor.
 * 
 * @author Peter Smith <psmith@arapiki.com>
 */
public class OutlinePage extends ContentOutlinePage implements IPackageMgrListener {

    /*=====================================================================================*
     * FIELDS/TYPES
     *=====================================================================================*/

    /** The SWT TreeViewer that is displayed within this outline view page */
    private TreeViewer treeViewer;

    /** The main BuildML editor that we're showing the outline of */
    private MainEditor mainEditor;

    /** The IBuildStore associated with this content view */
    private IBuildStore buildStore;

    /** The IPackageMgr that we'll be displaying information from */
    private IPackageMgr pkgMgr;

    /** The IPackageRootMgr that we'll be displaying information from */
    private IPackageRootMgr pkgRootMgr;

    /** The tree element that's currently selected */
    private UIInteger selectedNode = null;

    /** Based on the current tree selection, can the selected node be removed? */
    private boolean removeEnabled = false;

    /** Based on the current tree selection, can the selected node be renamed? */
    private boolean renameEnabled = false;

    /** Based on the current tree selection, does the selected package have roots? */
    private boolean changeRootsEnabled = false;

    /** Based on the current tree selection, can the package be opened? */
    private boolean openEnabled = false;

    /** The undo handler from our main BuildML editor */
    private UndoActionHandler undoAction;

    /** The redo handler from our main BuildML editor */
    private RedoActionHandler redoAction;

    /*=====================================================================================*
     * CONSTRUCTORS
     *=====================================================================================*/

    /**
     * Create a new OutlinePage object. There should be exactly one of these objects for
     * each BuildML MainEditor object.
     * 
     * @param mainEditor The associate MainEditor object.
     * @param redoAction The MainEditor's redo action (for redoing operations).
     * @param undoAction The MainEditor's undo action (for undoing operations).
     * 
     */
    public OutlinePage(MainEditor mainEditor, UndoActionHandler undoAction, RedoActionHandler redoAction) {
        super();

        /* 
         * Save these handlers for later. We'll apply them to our action bar in
         * the createControl method.
         */
        this.undoAction = undoAction;
        this.redoAction = redoAction;

        /* our outline view will display information from this IPackageMgr object. */
        this.mainEditor = mainEditor;
        buildStore = mainEditor.getBuildStore();
        pkgMgr = buildStore.getPackageMgr();
        pkgRootMgr = buildStore.getPackageRootMgr();

        /* add ourselves as a listener for package changes */
        pkgMgr.addListener(this);
    }

    /*=====================================================================================*
     * PUBLIC METHODS
     *=====================================================================================*/

    /* (non-Javadoc)
     * @see org.eclipse.ui.views.contentoutline.ContentOutlinePage#createControl(org.eclipse.swt.widgets.Composite)
     */
    @Override
    public void createControl(Composite parent) {
        super.createControl(parent);

        /* 
         * Configure the view's (pre-existing) TreeViewer with necessary helper objects that
         * will display the BuildML editor's package structure.
         */
        treeViewer = getTreeViewer();
        treeViewer.setContentProvider(new OutlineContentProvider(pkgMgr, true));
        treeViewer.setLabelProvider(new OutlineLabelProvider(pkgMgr));
        treeViewer.addSelectionChangedListener(this);
        treeViewer.setInput(new UIPackageFolder[] { new UIPackageFolder(pkgMgr.getRootFolder()) });
        treeViewer.expandToLevel(2);

        /*
         * Create the context menu. It'll be populated by the rules in plugin.xml.
         */
        MenuManager menuMgr = new MenuManager();
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener() {
            @Override
            public void menuAboutToShow(IMenuManager manager) {
                manager.add(new Separator("buildmladditions"));
            }
        });
        Menu menu = menuMgr.createContextMenu(treeViewer.getControl());
        treeViewer.getControl().setMenu(menu);
        getSite().registerContextMenu("org.eclipse.ui.views.ContentOutline", menuMgr, treeViewer);
        getSite().setSelectionProvider(treeViewer);

        /*
         * When the user double-clicks on a folder name, automatically expand the content
         * of that folder. If they double-click on a package name, open that package
         * as a new Diagram in the main editor.
         */
        treeViewer.addDoubleClickListener(new IDoubleClickListener() {
            @Override
            public void doubleClick(DoubleClickEvent event) {
                IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                Object node = selection.getFirstElement();

                if (treeViewer.isExpandable(node)) {
                    treeViewer.setExpandedState(node, !treeViewer.getExpandedState(node));
                }

                /* else, open the package diagram editor */
                else {
                    if (node instanceof UIPackage) {
                        int selectedPkgId = ((UIPackage) node).getId();
                        if (selectedPkgId != pkgMgr.getImportPackage()) {
                            mainEditor.openPackageDiagram(selectedPkgId);
                        }
                    }
                }
            }
        });

        /*
         * Configure the ability to edit cells in the package/folder tree. The 
         * OutlineContentCellModifier class does most of the hard work.
         */
        treeViewer.setColumnProperties(new String[] { "NAME" });
        treeViewer.setCellModifier(new OutlineContentCellModifier(mainEditor));
        treeViewer.setCellEditors(new CellEditor[] { new TextCellEditor(treeViewer.getTree()) });

        /*
         * Arrange it so that cell editing is only possible when we call editElement(), rather
         * than when the user clicks on the label.
         */
        treeViewer.getColumnViewerEditor().addEditorActivationListener(new ColumnViewerEditorActivationListener() {
            public void beforeEditorActivated(ColumnViewerEditorActivationEvent event) {
                if (event.eventType != ColumnViewerEditorActivationEvent.PROGRAMMATIC) {
                    event.cancel = true;
                }
            }

            public void beforeEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {
            }

            public void afterEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {
            }

            public void afterEditorActivated(ColumnViewerEditorActivationEvent event) {
            }
        });

        /*
         * Listen to our own selection events, which is necessary to learn which element is
         * selected when we want to add or delete elements.
         */
        addSelectionChangedListener(this);

        /*
         * Add the DragSource and DropTarget so that we can copy/move packages around.
         */
        new OutlineDragSource(treeViewer, this);
        new OutlineDropTarget(treeViewer, this);

        /*
         * Add the undo/redo actions from the main editor to our action bar. This allows
         * the user to use Ctrl-Z etc. to undo/redo while focused on our window.
         */
        getSite().getActionBars().setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
        getSite().getActionBars().setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Add a new package or folder to the BuildML build system. This is a UI method which will
     * update the view and report necessary error messages. The new package or folder will be
     * added at the same level in the tree as the currently selected element (or under
     * the root, if there's no selection).
     * 
     * @param createFolder If true, create a folder, else create a package.
     */
    public void newPackageOrFolder(boolean createFolder) {

        /* figure out where in the tree we'll add the new node */
        int parentId = getParentForNewNode();
        String newName = getNameForNewNode();

        /* add the new package/folder; it'll be positioned under the top root (for now) */
        int id;
        if (createFolder) {
            id = pkgMgr.addFolder(newName);
        } else {
            id = pkgMgr.addPackage(newName);
        }

        /* these errors should never occur */
        if ((id == ErrorCode.INVALID_NAME) || (id == ErrorCode.ALREADY_USED)) {
            throw new FatalBuildStoreError("Unable to create new package/folder: " + newName);
        }

        /* 
         * Move the new node underneath its destined parent. Error cases have already
         * been handled, so if we see an error, that's a coding problem.
         */
        if (pkgMgr.setParent(id, parentId) != ErrorCode.OK) {
            throw new FatalBuildStoreError("Couldn't move new tree element under parent");
        }

        /* 
         * Refresh the tree so that the new folder appears. We also need to make sure that
         * the parent node is expanded, since it might not be right now.
         */
        UIPackageFolder parentNode = new UIPackageFolder(parentId);
        treeViewer.setExpandedState(parentNode, true);
        treeViewer.refresh();

        /* now mark the new node for editing, to encourage the user to change the name */
        UIInteger newNode;
        PackageUndoOp op = new PackageUndoOp(buildStore, id);
        if (createFolder) {
            newNode = new UIPackageFolder(id);
            op.recordNewFolder(newName, parentId);
        } else {
            newNode = new UIPackage(id);
            op.recordNewPackage(newName, parentId);
        }
        treeViewer.editElement(newNode, 0);

        /* record the undo/redo operation */
        new UndoOpAdapter(createFolder ? "Create Package Folder" : "Create Package", op).record();
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Remove the currently-selected package or package folder from the BuildML build system.
     * This is a UI method which will update the view and report necessary error messages.
     * Packages can only be removed if they don't contain any files/actions. Package folders
     * can only be removed if they don't contain any sub-packages (or package folders).
     */
    public void remove() {

        /* if nothing is selected, we can't delete anything */
        if (selectedNode == null) {
            return;
        }

        /* determine the name and type of the thing we're deleting */
        int id = selectedNode.getId();
        String name = pkgMgr.getName(id);
        boolean isFolder = pkgMgr.isFolder(id);

        int status = AlertDialog.displayOKCancelDialog(
                "Are you sure you want to delete the " + "\"" + name + "\" " + (isFolder ? "folder?" : "package?"));
        if (status == IDialogConstants.CANCEL_ID) {
            return;
        }

        /* record the item's parent, in case we need to undo later */
        int parentId = pkgMgr.getParent(id);

        /* go ahead and remove it, possibly with an error code being returned */
        int rc = pkgMgr.remove(id);

        /* An error occured while removing... */
        if (rc != ErrorCode.OK) {

            /* for some reason, the element couldn't be deleted */
            if (rc == ErrorCode.CANT_REMOVE) {

                /* give an appropriate error message */
                if (selectedNode instanceof UIPackage) {
                    AlertDialog.displayErrorDialog("Can't Delete Package",
                            "The selected package couldn't be deleted because it still "
                                    + "contains files and actions.");

                } else {
                    AlertDialog.displayErrorDialog("Can't Delete Package Folder",
                            "The selected package folder couldn't be deleted because it still "
                                    + "contains sub-packages");
                }
            } else {
                throw new FatalBuildStoreError(
                        "Unexpected error when attempting to delete package " + "or package folder");
            }
        }

        /* Success - element removed. Update view accordingly. */
        else {
            PackageUndoOp op = new PackageUndoOp(buildStore, id);
            if (isFolder) {
                op.recordRemoveFolder(name, parentId);
            } else {
                op.recordRemovePackage(name, parentId);
            }
            new UndoOpAdapter(isFolder ? "Remove Folder" : "Remove Package", op).record();
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Rename the currently-selected element in the content outline view.
     */
    public void rename() {

        /*
         * Initiate the editing of the selected cell. Note that most of the work for this
         * operation is performed by the OutlineContentCellModifier class. All we need to
         * do here is start the edit in motion.
         */
        if (selectedNode != null) {
            treeViewer.editElement(selectedNode, 0);
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Change the source/generated package roots for the selected package.
     */
    public void changeRoots() {
        int pkgId = selectedNode.getId();
        boolean success = true;
        int srcRootPathId, genRootPathId;

        /* record the old root value, in case we need to undo */
        int oldSrcRootPathId = pkgRootMgr.getPackageRoot(pkgId, IPackageRootMgr.SOURCE_ROOT);
        int oldGenRootPathId = pkgRootMgr.getPackageRoot(pkgId, IPackageRootMgr.GENERATED_ROOT);

        /* 
         * Show the dialog, repeating if one or more bad paths were provided. Note that we
         * assume the dialog only gives us 'existing' paths, but we still need to check that the
         * new roots are in range.
         */
        do {
            ChangeRootsDialog dialog = new ChangeRootsDialog(buildStore, pkgId);
            if (dialog.open() == ChangeRootsDialog.OK) {
                srcRootPathId = dialog.getSourceRootPathId();
                genRootPathId = dialog.getGeneratedRootPathId();

                String errMsg = "The root could not be moved to that location. It must not be above "
                        + "the @workspace root, and must encompass all the package's existing files.";

                int srcRc = pkgRootMgr.setPackageRoot(pkgId, IPackageRootMgr.SOURCE_ROOT, srcRootPathId);
                if (srcRc == ErrorCode.OUT_OF_RANGE) {
                    AlertDialog.displayErrorDialog("Failed to Change Source Root", errMsg);
                    success = false;
                }

                int genRc = pkgRootMgr.setPackageRoot(pkgId, IPackageRootMgr.GENERATED_ROOT, genRootPathId);
                if (genRc == ErrorCode.OUT_OF_RANGE) {
                    AlertDialog.displayErrorDialog("Failed to Change Generated Root", errMsg);
                    success = false;
                }
            } else {
                /* operation cancelled */
                return;
            }
        } while (!success);

        /* if anything changed, create a history item so we can undo/redo */
        if ((oldSrcRootPathId != srcRootPathId) || (oldGenRootPathId != genRootPathId)) {
            PackageUndoOp op = new PackageUndoOp(buildStore, pkgId);
            op.recordRootChange(oldSrcRootPathId, oldGenRootPathId, srcRootPathId, genRootPathId);
            new UndoOpAdapter("Change Package Root", op).invoke();
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Invoke the "open package" command on the currently selected node, opening the package's
     * diagram in the main editor.
     */
    public void openPackage() {
        if (selectedNode instanceof UIPackage) {
            mainEditor.openPackageDiagram(selectedNode.getId());
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * This method is called whenever the user clicks on a node in the tree viewer. We
     * make note of the currently-selected node so that other operations (add, delete, etc)
     * know what they're operating on. Based on the selection, we also determine whether
     * the "remove" or "rename" operations are currently permitted.
     */
    public void selectionChanged(SelectionChangedEvent event) {

        IStructuredSelection selection = (IStructuredSelection) event.getSelection();
        Object node = selection.getFirstElement();

        if (node instanceof UIInteger) {
            selectedNode = (UIInteger) node;
            int nodeId = selectedNode.getId();

            /* start by assuming the all buttons will be active */
            removeEnabled = renameEnabled = changeRootsEnabled = openEnabled = true;

            /* 
             * Based on the selection, determine whether the buttons 
             * should be disabled. We can't remove/rename the root folder, and we can't
             * remove a folder that has children.
             */
            if (selectedNode instanceof UIPackageFolder) {
                if (nodeId == pkgMgr.getRootFolder()) {
                    removeEnabled = renameEnabled = false;
                } else if (pkgMgr.getFolderChildren(nodeId).length != 0) {
                    removeEnabled = false;
                }
                changeRootsEnabled = openEnabled = false;
            }

            /* else, for the UIPackage, the <import> package can't be touched. */
            else {
                if (nodeId == pkgMgr.getImportPackage()) {
                    removeEnabled = renameEnabled = changeRootsEnabled = openEnabled = false;
                } else if (nodeId == pkgMgr.getMainPackage()) {
                    removeEnabled = renameEnabled = false;
                }
            }
        }
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * @return true if the "remove" command should be enabled, based on the current tree
     * selection.
     */
    public boolean getRemoveEnabled() {
        return removeEnabled;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * @return true if the "rename" command should be enabled, based on the current tree
     * selection.
     */
    public boolean getRenameEnabled() {
        return renameEnabled;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * @return true if the "change roots" command should be enabled, based on the current tree
     * selection.
     */
    public boolean getChangeRootsEnabled() {
        return changeRootsEnabled;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * @return true if the "open" menu command should be enabled, based on the current tree
     * selection.
     */
    public boolean getOpenEnabled() {
        return openEnabled;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * @return The main BuildML editor associated with this outline view.
     */
    public MainEditor getMainEditor() {
        return mainEditor;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Refresh this page's view, due to an external change in the underlying model.
     */
    public void refresh() {
        treeViewer.refresh();
    }

    /*=====================================================================================*
     * PROTECTED METHODS
     *=====================================================================================*/

    /**
     * Inform our parent class to create a single-selection tree.
     */
    protected int getTreeStyle() {
        return super.getTreeStyle() | SWT.SINGLE;
    }

    /*-------------------------------------------------------------------------------------*/

    /*
     * When the underlying IPackageMgr is changed in some way, we must refresh our outline
     * view so it reflects the latest changes.
     */
    @Override
    public void packageChangeNotification(int pkgId, int how) {
        refresh();
    }

    /*=====================================================================================*
     * PRIVATE METHODS
     *=====================================================================================*/

    /**
     * Given that the currently-selected node is used as an indication of where new packages
     * (or folders) should be inserted, compute the parent of the node we're about to add.
     * If the current selection is a folder, use that. If it's a package, use the package's
     * parent. If there's no selection, use the root node.
     * 
     * @return The ID of the parent folder, into which a new package/folder will be added.
     */
    private int getParentForNewNode() {

        /* no selection => return top root */
        if (selectedNode == null) {
            return pkgMgr.getRootFolder();
        }

        /* current selection is a folder => return current selection */
        int selectedId = selectedNode.getId();
        if (pkgMgr.isFolder(selectedId)) {
            return selectedId;
        }

        /* else, return parent of current selection (or root if there's an error) */
        int parentId = pkgMgr.getParent(selectedId);
        if (parentId == ErrorCode.NOT_FOUND) {
            return pkgMgr.getRootFolder();
        }
        return parentId;
    }

    /*-------------------------------------------------------------------------------------*/

    /**
     * Compute a unique name for a newly added package or folder. The default name will
     * be "Untitled", but if that name already exists, return "Untitled-N" where N is the
     * lowest integer (starting at 1) that isn't in use.
     * 
     * @return A unique name for a new package or folder.
     */
    private String getNameForNewNode() {
        String chosenName;
        int attemptNum = 0;

        /* 
         * Repeat until we find an available name. The assumption is that we'll find an
         * available name before we run out of integers.
         */
        while (true) {
            chosenName = "Untitled";
            if (attemptNum != 0) {
                chosenName += "-" + attemptNum;
            }

            if (pkgMgr.getId(chosenName) == ErrorCode.NOT_FOUND) {
                return chosenName;
            }

            attemptNum++;
        }
    }

    /*-------------------------------------------------------------------------------------*/
}