org.eclipse.ui.part.MultiPageEditorPart.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ui.part.MultiPageEditorPart.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Lars Vogel <Lars.Vogel@gmail.com> - Bug 440810
 *******************************************************************************/
package org.eclipse.ui.part;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.util.Tracing;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.jface.dialogs.IPageChangeProvider;
import org.eclipse.jface.dialogs.IPageChangedListener;
import org.eclipse.jface.dialogs.PageChangedEvent;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IEditorActionBarContributor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IKeyBindingService;
import org.eclipse.ui.INestableKeyBindingService;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.PartSite;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.misc.Policy;
import org.eclipse.ui.internal.services.INestable;
import org.eclipse.ui.internal.services.IServiceLocatorCreator;
import org.eclipse.ui.services.IDisposable;
import org.eclipse.ui.services.IServiceLocator;

/**
 * A multi-page editor is an editor with multiple pages, each of which may
 * contain an editor or an arbitrary SWT control.
 * <p>
 * Subclasses must implement the following methods:
 * <ul>
 * <li><code>createPages</code> - to create the required pages by calling one of
 * the <code>addPage</code> methods</li>
 * <li><code>IEditorPart.doSave</code> - to save contents of editor</li>
 * <li><code>IEditorPart.doSaveAs</code> - to save contents of editor</li>
 * <li><code>IEditorPart.isSaveAsAllowed</code> - to enable Save As</li>
 * <li><code>IEditorPart.gotoMarker</code> - to scroll to a marker</li>
 * </ul>
 * </p>
 * <p>
 * Multi-page editors have a single action bar contributor, which manages
 * contributions for all the pages. The contributor must be a subclass of
 * <code>MultiPageEditorActionBarContributor</code>. Note that since any nested
 * editors are created directly in code by callers of
 * <code>addPage(IEditorPart,IEditorInput)</code>, nested editors do not have
 * their own contributors.
 * </p>
 * <p>
 * As of 3.5 multi-page editors will post PageChangedEvents at the end of
 * {@link #pageChange(int)}. Subclasses may override {@link #getSelectedPage()}
 * to return a page appropriate to their multi-page editor. IPartListener2
 * listeners registered with the IPartService can implement IPageChangedListener
 * to be notified about all page change events within the workbench page or
 * workbench window.
 * </p>
 *
 * @see org.eclipse.ui.part.MultiPageEditorActionBarContributor
 * @see org.eclipse.jface.dialogs.IPageChangeProvider
 * @see org.eclipse.jface.dialogs.IPageChangedListener
 * @see org.eclipse.ui.IPartService
 */
public abstract class MultiPageEditorPart extends EditorPart implements IPageChangeProvider {

    private static final String COMMAND_NEXT_SUB_TAB = "org.eclipse.ui.navigate.nextSubTab"; //$NON-NLS-1$
    private static final String COMMAND_PREVIOUS_SUB_TAB = "org.eclipse.ui.navigate.previousSubTab"; //$NON-NLS-1$

    /**
     * Subclasses that override {@link #createPageContainer(Composite)} can use
     * this constant to get a site for the container that can be active while
     * the current page is deactivated.
     *
     * @since 3.4
     * @see #activateSite()
     * @see #deactivateSite(boolean, boolean)
     * @see #getPageSite(int)
     */
    protected static final int PAGE_CONTAINER_SITE = 65535;

    /**
     * Private tracing output.
     */
    private static final String TRACING_COMPONENT = "MPE"; //$NON-NLS-1$

    /**
     * The active service locator. This value may be <code>null</code> if
     * there is no selected page, or if the selected page is a control with
     * no site.
     */
    private INestable activeServiceLocator;

    /**
     * The container widget.
     */
    private CTabFolder container;

    /**
     * List of nested editors. Element type: IEditorPart. Need to hang onto them
     * here, in addition to using get/setData on the items, because dispose()
     * needs to access them, but widgetry has already been disposed at that
     * point.
     */
    private ArrayList nestedEditors = new ArrayList(3);

    private List pageSites = new ArrayList(3);

    private IServiceLocator pageContainerSite;

    private ListenerList pageChangeListeners = new ListenerList(ListenerList.IDENTITY);

    /**
     * Creates an empty multi-page editor with no pages.
     */
    protected MultiPageEditorPart() {
        super();
    }

    /**
     * Creates and adds a new page containing the given control to this
     * multi-page editor. The control may be <code>null</code>, allowing it
     * to be created and set later using <code>setControl</code>.
     *
     * @param control
     *            the control, or <code>null</code>
     * @return the index of the new page
     *
     * @see MultiPageEditorPart#setControl(int, Control)
     */
    public int addPage(Control control) {
        int index = getPageCount();
        addPage(index, control);
        return index;
    }

    /**
     * Creates and adds a new page containing the given control to this
     * multi-page editor. The page is added at the given index. The control may
     * be <code>null</code>, allowing it to be created and set later using
     * <code>setControl</code>.
     *
     * @param index
     *            the index at which to add the page (0-based)
     * @param control
     *            the control, or <code>null</code>
     *
     * @see MultiPageEditorPart#setControl(int, Control)
     */
    public void addPage(int index, Control control) {
        createItem(index, control);
    }

    /**
     * Creates and adds a new page containing the given editor to this
     * multi-page editor. This also hooks a property change listener on the
     * nested editor.
     *
     * @param editor
     *            the nested editor
     * @param input
     *            the input for the nested editor
     * @return the index of the new page
     * @exception PartInitException
     *                if a new page could not be created
     *
     * @see MultiPageEditorPart#handlePropertyChange(int) the handler for
     *      property change events from the nested editor
     */
    public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException {
        int index = getPageCount();
        addPage(index, editor, input);
        return index;
    }

    /**
     * Creates and adds a new page containing the given editor to this
     * multi-page editor. The page is added at the given index. This also hooks
     * a property change listener on the nested editor.
     *
     * @param index
     *            the index at which to add the page (0-based)
     * @param editor
     *            the nested editor
     * @param input
     *            the input for the nested editor
     * @exception PartInitException
     *                if a new page could not be created
     *
     * @see MultiPageEditorPart#handlePropertyChange(int) the handler for
     *      property change events from the nested editor
     */
    public void addPage(int index, IEditorPart editor, IEditorInput input) throws PartInitException {
        IEditorSite site = createSite(editor);
        // call init first so that if an exception is thrown, we have created no
        // new widgets
        editor.init(site, input);
        Composite parent2 = new Composite(getContainer(), getOrientation(editor));
        parent2.setLayout(new FillLayout());
        editor.createPartControl(parent2);
        editor.addPropertyListener(new IPropertyListener() {
            @Override
            public void propertyChanged(Object source, int propertyId) {
                MultiPageEditorPart.this.handlePropertyChange(propertyId);
            }
        });
        // create item for page only after createPartControl has succeeded
        Item item = createItem(index, parent2);
        // remember the editor, as both data on the item, and in the list of
        // editors (see field comment)
        item.setData(editor);
        nestedEditors.add(editor);
    }

    /**
     * Get the orientation of the editor.
     *
     * @param editor
     * @return int the orientation flag
     * @see SWT#RIGHT_TO_LEFT
     * @see SWT#LEFT_TO_RIGHT
     * @see SWT#NONE
     */
    private int getOrientation(IEditorPart editor) {
        if (editor instanceof IWorkbenchPartOrientation) {
            return ((IWorkbenchPartOrientation) editor).getOrientation();
        }
        return getOrientation();
    }

    /**
     * Creates an empty container. Creates a CTabFolder with no style bits set,
     * and hooks a selection listener which calls <code>pageChange()</code>
     * whenever the selected tab changes.
     *
     * @param parent
     *            The composite in which the container tab folder should be
     *            created; must not be <code>null</code>.
     * @return a new container
     */
    private CTabFolder createContainer(Composite parent) {
        // use SWT.FLAT style so that an extra 1 pixel border is not reserved
        // inside the folder
        parent.setLayout(new FillLayout());
        final CTabFolder newContainer = new CTabFolder(parent, SWT.BOTTOM | SWT.FLAT);
        newContainer.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                int newPageIndex = newContainer.indexOf((CTabItem) e.item);
                pageChange(newPageIndex);
            }
        });
        newContainer.addTraverseListener(new TraverseListener() {
            // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=199499 : Switching tabs by Ctrl+PageUp/PageDown must not be caught on the inner tab set
            @Override
            public void keyTraversed(TraverseEvent e) {
                switch (e.detail) {
                case SWT.TRAVERSE_PAGE_NEXT:
                case SWT.TRAVERSE_PAGE_PREVIOUS:
                    int detail = e.detail;
                    e.doit = true;
                    e.detail = SWT.TRAVERSE_NONE;
                    Control control = newContainer.getParent();
                    do {
                        if (control.traverse(detail))
                            return;
                        if (control.getListeners(SWT.Traverse).length != 0)
                            return;
                        if (control instanceof Shell)
                            return;
                        control = control.getParent();
                    } while (control != null);
                }
            }
        });
        return newContainer;
    }

    /**
     * Creates a tab item at the given index and places the given control in the
     * new item. The item is a CTabItem with no style bits set.
     *
     * @param index
     *            the index at which to add the control
     * @param control
     *            is the control to be placed in an item
     * @return a new item
     */
    private CTabItem createItem(int index, Control control) {
        CTabItem item = new CTabItem(getTabFolder(), SWT.NONE, index);
        item.setControl(control);
        return item;
    }

    /**
     * Creates the pages of this multi-page editor.
     * <p>
     * Subclasses must implement this method.
     * </p>
     */
    protected abstract void createPages();

    /**
     * The <code>MultiPageEditor</code> implementation of this
     * <code>IWorkbenchPart</code> method creates the control for the
     * multi-page editor by calling <code>createContainer</code>, then
     * <code>createPages</code>. Subclasses should implement
     * <code>createPages</code> rather than overriding this method.
     *
     * @param parent
     *            The parent in which the editor should be created; must not be
     *            <code>null</code>.
     */
    @Override
    public final void createPartControl(Composite parent) {
        Composite pageContainer = createPageContainer(parent);
        this.container = createContainer(pageContainer);
        createPages();
        // set the active page (page 0 by default), unless it has already been
        // done
        if (getActivePage() == -1) {
            setActivePage(0);
            IEditorPart part = getEditor(0);
            if (part != null) {
                final IServiceLocator serviceLocator = part.getEditorSite();
                if (serviceLocator instanceof INestable) {
                    activeServiceLocator = (INestable) serviceLocator;
                    activeServiceLocator.activate();
                }
            }
        }
        initializePageSwitching();
        initializeSubTabSwitching();
    }

    /**
     * Initialize the MultiPageEditorPart to use the page switching command.
     * Clients can override this method with an empty body if they wish to
     * opt-out.
     *
     * @since 3.4
     */
    protected void initializePageSwitching() {
        new PageSwitcher(this) {
            @Override
            public Object[] getPages() {
                int pageCount = getPageCount();
                Object[] result = new Object[pageCount];
                for (int i = 0; i < pageCount; i++) {
                    result[i] = Integer.valueOf(i);
                }
                return result;
            }

            @Override
            public String getName(Object page) {
                return getPageText(((Integer) page).intValue());
            }

            @Override
            public ImageDescriptor getImageDescriptor(Object page) {
                Image image = getPageImage(((Integer) page).intValue());
                if (image == null)
                    return null;

                return ImageDescriptor.createFromImage(image);
            }

            @Override
            public void activatePage(Object page) {
                setActivePage(((Integer) page).intValue());
            }

            @Override
            public int getCurrentPageIndex() {
                return getActivePage();
            }
        };
    }

    /**
     * Initialize the MultiPageEditorPart to use the sub-tab switching commands.
     *
     * @since 3.5
     */
    private void initializeSubTabSwitching() {
        IHandlerService service = getSite().getService(IHandlerService.class);
        service.activateHandler(COMMAND_NEXT_SUB_TAB, new AbstractHandler() {
            /**
             * {@inheritDoc}
             * @throws ExecutionException
             *             if an exception occurred during execution
             */
            @Override
            public Object execute(ExecutionEvent event) throws ExecutionException {
                int n = getPageCount();
                if (n == 0)
                    return null;

                int i = getActivePage() + 1;
                if (i >= n)
                    i = 0;
                setActivePage(i);
                return null;
            }
        });

        service.activateHandler(COMMAND_PREVIOUS_SUB_TAB, new AbstractHandler() {
            /**
             * {@inheritDoc}
             * @throws ExecutionException
             *             if an exception occurred during execution
             */
            @Override
            public Object execute(ExecutionEvent event) throws ExecutionException {
                int n = getPageCount();
                if (n == 0)
                    return null;

                int i = getActivePage() - 1;
                if (i < 0)
                    i = n - 1;
                setActivePage(i);
                return null;
            }
        });
    }

    /**
     * Creates the parent control for the container returned by
     * {@link #getContainer() }.
     *
     * <p>
     * Subclasses may extend and must call super implementation first.
     * </p>
     *
     * @param parent
     *            the parent for all of the editors contents.
     * @return the parent for this editor's container. Must not be
     *         <code>null</code>.
     *
     * @since 3.2
     */
    protected Composite createPageContainer(Composite parent) {
        return parent;
    }

    /**
     * Creates the site for the given nested editor. The
     * <code>MultiPageEditorPart</code> implementation of this method creates
     * an instance of <code>MultiPageEditorSite</code>. Subclasses may
     * reimplement to create more specialized sites.
     *
     * @param editor
     *            the nested editor
     * @return the editor site
     */
    protected IEditorSite createSite(IEditorPart editor) {
        return new MultiPageEditorSite(this, editor);
    }

    /**
     * The <code>MultiPageEditorPart</code> implementation of this
     * <code>IWorkbenchPart</code> method disposes all nested editors.
     * Subclasses may extend.
     */
    @Override
    public void dispose() {
        deactivateSite(true, false);

        pageChangeListeners.clear();
        for (int i = 0; i < nestedEditors.size(); ++i) {
            IEditorPart editor = (IEditorPart) nestedEditors.get(i);
            disposePart(editor);
        }
        nestedEditors.clear();
        if (pageContainerSite instanceof IDisposable) {
            ((IDisposable) pageContainerSite).dispose();
            pageContainerSite = null;
        }
        for (int i = 0; i < pageSites.size(); i++) {
            IServiceLocator sl = (IServiceLocator) pageSites.get(i);
            if (sl instanceof IDisposable) {
                ((IDisposable) sl).dispose();
            }
        }
        pageSites.clear();
        super.dispose();
    }

    /**
     * Returns the active nested editor if there is one.
     * <p>
     * Subclasses should not override this method
     * </p>
     *
     * @nooverride
     * @return the active nested editor, or <code>null</code> if none
     */
    protected IEditorPart getActiveEditor() {
        int index = getActivePage();
        if (index != -1) {
            return getEditor(index);
        }
        return null;
    }

    /**
     * Returns the index of the currently active page, or -1 if there is no
     * active page.
     * <p>
     * Subclasses should not override this method
     * </p>
     *
     * @nooverride
     *
     * @return the index of the active page, or -1 if there is no active page
     * @since 3.5
     */
    public int getActivePage() {
        CTabFolder tabFolder = getTabFolder();
        if (tabFolder != null && !tabFolder.isDisposed()) {
            return tabFolder.getSelectionIndex();
        }
        return -1;
    }

    /**
     * Returns the composite control containing this multi-page editor's pages.
     * This should be used as the parent when creating controls for the
     * individual pages. That is, when calling <code>addPage(Control)</code>,
     * the passed control should be a child of this container.
     * <p>
     * Warning: Clients should not assume that the container is any particular
     * subclass of Composite. The actual class used may change in order to
     * improve the look and feel of multi-page editors. Any code making
     * assumptions on the particular subclass would thus be broken.
     * </p>
     * <p>
     * Subclasses should not override this method
     * </p>
     *
     * @return the composite, or <code>null</code> if
     *         <code>createPartControl</code> has not been called yet
     */
    protected Composite getContainer() {
        return container;
    }

    /**
     * Returns the control for the given page index, or <code>null</code> if
     * no control has been set for the page. The page index must be valid.
     * <p>
     * Subclasses should not override this method
     * </p>
     *
     * @param pageIndex
     *            the index of the page
     * @return the control for the specified page, or <code>null</code> if
     *         none has been set
     */
    protected Control getControl(int pageIndex) {
        return getItem(pageIndex).getControl();
    }

    /**
     * Returns the editor for the given page index. The page index must be
     * valid.
     *
     * @param pageIndex
     *            the index of the page
     * @return the editor for the specified page, or <code>null</code> if the
     *         specified page was not created with
     *         <code>addPage(IEditorPart,IEditorInput)</code>
     */
    protected IEditorPart getEditor(int pageIndex) {
        Item item = getItem(pageIndex);
        if (item != null) {
            Object data = item.getData();
            if (data instanceof IEditorPart) {
                return (IEditorPart) data;
            }
        }
        return null;
    }

    /**
     * Returns the service locator for the given page index. This method can be
     * used to create service locators for pages that are just controls. The
     * page index must be valid.
     * <p>
     * This will return the editor site service locator for an editor, and
     * create one for a page that is just a control.
     * </p>
     *
     * @param pageIndex
     *            the index of the page
     * @return the editor for the specified page, or <code>null</code> if the
     *         specified page was not created with
     *         <code>addPage(IEditorPart,IEditorInput)</code>
     * @since 3.4
     */
    protected final IServiceLocator getPageSite(int pageIndex) {
        if (pageIndex == PAGE_CONTAINER_SITE) {
            return getPageContainerSite();
        }

        Item item = getItem(pageIndex);
        if (item != null) {
            Object data = item.getData();
            if (data instanceof IEditorPart) {
                return ((IEditorPart) data).getSite();
            } else if (data instanceof IServiceLocator) {
                return (IServiceLocator) data;
            } else if (data == null) {
                IServiceLocatorCreator slc = getSite().getService(IServiceLocatorCreator.class);
                IServiceLocator sl = slc.createServiceLocator(getSite(), null, new IDisposable() {
                    @Override
                    public void dispose() {
                        close();
                    }
                });
                item.setData(sl);
                pageSites.add(sl);
                return sl;
            }
        }
        return null;
    }

    void close() {
        // 3.x implementation closes the editor when the ISL is disposed
        PartSite partSite = (PartSite) getSite();
        MPart model = partSite.getModel();
        Widget widget = (Widget) model.getWidget();
        if (widget != null && !widget.isDisposed()) {
            getSite().getPage().closeEditor(MultiPageEditorPart.this, true);
        }
    }

    /**
     * @return A site that can be used with a header.
     * @since 3.4
     * @see #createPageContainer(Composite)
     * @see #PAGE_CONTAINER_SITE
     * @see #getPageSite(int)
     */
    private IServiceLocator getPageContainerSite() {
        if (pageContainerSite == null) {
            IServiceLocatorCreator slc = getSite().getService(IServiceLocatorCreator.class);
            pageContainerSite = slc.createServiceLocator(getSite(), null, new IDisposable() {
                @Override
                public void dispose() {
                    close();
                }
            });
        }
        return pageContainerSite;
    }

    /**
     * Returns the tab item for the given page index (page index is 0-based).
     * The page index must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @return the tab item for the given page index
     */
    private CTabItem getItem(int pageIndex) {
        return getTabFolder().getItem(pageIndex);
    }

    /**
     * Returns the number of pages in this multi-page editor.
     *
     * @return the number of pages
     */
    protected int getPageCount() {
        CTabFolder folder = getTabFolder();
        // May not have been created yet, or may have been disposed.
        if (folder != null && !folder.isDisposed()) {
            return folder.getItemCount();
        }
        return 0;
    }

    /**
     * Returns the image for the page with the given index, or <code>null</code>
     * if no image has been set for the page. The page index must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @return the image, or <code>null</code> if none
     */
    protected Image getPageImage(int pageIndex) {
        return getItem(pageIndex).getImage();
    }

    /**
     * Returns the text label for the page with the given index. Returns the
     * empty string if no text label has been set for the page. The page index
     * must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @return the text label for the page
     */
    protected String getPageText(int pageIndex) {
        return getItem(pageIndex).getText();
    }

    /**
     * Returns the tab folder containing this multi-page editor's pages.
     *
     * @return the tab folder, or <code>null</code> if
     *         <code>createPartControl</code> has not been called yet
     */
    private CTabFolder getTabFolder() {
        return container;
    }

    /**
     * Handles a property change notification from a nested editor. The default
     * implementation simply forwards the change to listeners on this multi-page
     * editor by calling <code>firePropertyChange</code> with the same
     * property id. For example, if the dirty state of a nested editor changes
     * (property id <code>IEditorPart.PROP_DIRTY</code>), this method handles
     * it by firing a property change event for
     * <code>IEditorPart.PROP_DIRTY</code> to property listeners on this
     * multi-page editor.
     * <p>
     * Subclasses may extend or reimplement this method.
     * </p>
     *
     * @param propertyId
     *            the id of the property that changed
     */
    protected void handlePropertyChange(int propertyId) {
        firePropertyChange(propertyId);
    }

    /**
     * The <code>MultiPageEditorPart</code> implementation of this
     * <code>IEditorPart</code> method sets its site to the given site, its
     * input to the given input, and the site's selection provider to a
     * <code>MultiPageSelectionProvider</code>. Subclasses may extend this
     * method.
     *
     * @param site
     *            The site for which this part is being created; must not be
     *            <code>null</code>.
     * @param input
     *            The input on which this editor should be created; must not be
     *            <code>null</code>.
     * @throws PartInitException
     *             If the initialization of the part fails -- currently never.
     */
    @Override
    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
        setSite(site);
        setInput(input);
        site.setSelectionProvider(new MultiPageSelectionProvider(this));
    }

    /**
     * The <code>MultiPageEditorPart</code> implementation of this
     * <code>IEditorPart</code> method returns whether the contents of any of
     * this multi-page editor's nested editors have changed since the last save.
     * Pages created with <code>addPage(Control)</code> are ignored.
     * <p>
     * Subclasses may extend or reimplement this method.
     * </p>
     *
     * @return <code>true</code> if any of the nested editors are dirty;
     *         <code>false</code> otherwise.
     */
    @Override
    public boolean isDirty() {
        // use nestedEditors to avoid SWT requests; see bug 12996
        for (Iterator i = nestedEditors.iterator(); i.hasNext();) {
            IEditorPart editor = (IEditorPart) i.next();
            if (editor.isDirty()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Notifies this multi-page editor that the page with the given id has been
     * activated. This method is called when the user selects a different tab.
     * <p>
     * The <code>MultiPageEditorPart</code> implementation of this method sets
     * focus to the new page, and notifies the action bar contributor (if there
     * is one). This checks whether the action bar contributor is an instance of
     * <code>MultiPageEditorActionBarContributor</code>, and, if so, calls
     * <code>setActivePage</code> with the active nested editor. This also
     * fires a selection change event if required.
     * </p>
     * <p>
     * Subclasses may extend this method.
     * </p>
     *
     * @param newPageIndex
     *            the index of the activated page
     */
    protected void pageChange(int newPageIndex) {
        deactivateSite(false, false);

        IPartService partService = getSite().getService(IPartService.class);
        if (partService.getActivePart() == this) {
            setFocus();
        }

        IEditorPart activeEditor = getEditor(newPageIndex);

        IEditorActionBarContributor contributor = getEditorSite().getActionBarContributor();
        if (contributor != null && contributor instanceof MultiPageEditorActionBarContributor) {
            ((MultiPageEditorActionBarContributor) contributor).setActivePage(activeEditor);
        }

        if (activeEditor != null) {
            ISelectionProvider selectionProvider = activeEditor.getSite().getSelectionProvider();
            if (selectionProvider != null) {
                ISelectionProvider outerProvider = getSite().getSelectionProvider();
                if (outerProvider instanceof MultiPageSelectionProvider) {
                    SelectionChangedEvent event = new SelectionChangedEvent(selectionProvider,
                            selectionProvider.getSelection());

                    MultiPageSelectionProvider provider = (MultiPageSelectionProvider) outerProvider;
                    provider.fireSelectionChanged(event);
                    provider.firePostSelectionChanged(event);
                } else {
                    if (Policy.DEBUG_MPE) {
                        Tracing.printTrace(TRACING_COMPONENT, "MultiPageEditorPart " + getTitle() //$NON-NLS-1$
                                + " did not propogate selection for " //$NON-NLS-1$
                                + activeEditor.getTitle());
                    }
                }
            }
        }

        activateSite();
        Object selectedPage = getSelectedPage();
        if (selectedPage != null) {
            firePageChanged(new PageChangedEvent(this, selectedPage));
        }
    }

    /**
     * This method can be used by implementors of
     * {@link MultiPageEditorPart#createPageContainer(Composite)} to deactivate
     * the active inner editor services while their header has focus. A
     * deactivateSite() must have a matching call to activateSite() when
     * appropriate.
     * <p>
     * An new inner editor will have its site activated on a
     * {@link MultiPageEditorPart#pageChange(int)}.
     * </p>
     * <p>
     * <b>Note:</b> This API is evolving in 3.4 and this might not be its final
     * form.
     * </p>
     *
     * @param immediate
     *            immediately deactivate the legacy keybinding service
     * @param containerSiteActive
     *            Leave the page container site active.
     * @since 3.4
     * @see #activateSite()
     * @see #createPageContainer(Composite)
     * @see #getPageSite(int)
     * @see #PAGE_CONTAINER_SITE
     */
    protected final void deactivateSite(boolean immediate, boolean containerSiteActive) {
        // Deactivate the nested services from the last active service locator.
        if (activeServiceLocator != null) {
            activeServiceLocator.deactivate();
            activeServiceLocator = null;
        }

        final int pageIndex = getActivePage();
        final IKeyBindingService service = getSite().getKeyBindingService();
        if (pageIndex < 0 || pageIndex >= getPageCount() || immediate) {
            // There is no selected page, so deactivate the active service.
            if (service instanceof INestableKeyBindingService) {
                final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
                nestableService.activateKeyBindingService(null);
            } else {
                WorkbenchPlugin.log(
                        "MultiPageEditorPart.deactivateSite()   Parent key binding service was not an instance of INestableKeyBindingService.  It was an instance of " //$NON-NLS-1$
                                + service.getClass().getName() + " instead."); //$NON-NLS-1$
            }
        }

        if (containerSiteActive) {
            IServiceLocator containerSite = getPageContainerSite();
            if (containerSite instanceof INestable) {
                activeServiceLocator = (INestable) containerSite;
                activeServiceLocator.activate();
            }
        }
    }

    /**
     * This method can be used by implementors of
     * {@link #createPageContainer(Composite)} to activate the active inner
     * editor services when their header loses focus.
     * <p>
     * An new inner editor will have its site activated on a
     * {@link #pageChange(int)}.
     * </p>
     * <p>
     * <b>Note:</b> This API is evolving in 3.4 and this might not be its final
     * form.
     * </p>
     *
     * @since 3.4
     * @see #deactivateSite(boolean,boolean)
     * @see #createPageContainer(Composite)
     * @see #getPageSite(int)
     */
    protected final void activateSite() {
        if (activeServiceLocator != null) {
            activeServiceLocator.deactivate();
            activeServiceLocator = null;
        }

        final IKeyBindingService service = getSite().getKeyBindingService();
        final int pageIndex = getActivePage();
        final IEditorPart editor = getEditor(pageIndex);

        if (editor != null) {
            // active the service for this inner editor
            if (service instanceof INestableKeyBindingService) {
                final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
                nestableService.activateKeyBindingService(editor.getEditorSite());

            } else {
                WorkbenchPlugin.log(
                        "MultiPageEditorPart.activateSite()   Parent key binding service was not an instance of INestableKeyBindingService.  It was an instance of " //$NON-NLS-1$
                                + service.getClass().getName() + " instead."); //$NON-NLS-1$
            }
            // Activate the services for the new service locator.
            final IServiceLocator serviceLocator = editor.getEditorSite();
            if (serviceLocator instanceof INestable) {
                activeServiceLocator = (INestable) serviceLocator;
                activeServiceLocator.activate();
            }

        } else {
            Item item = getItem(pageIndex);

            // There is no selected editor, so deactivate the active service.
            if (service instanceof INestableKeyBindingService) {
                final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
                nestableService.activateKeyBindingService(null);
            } else {
                WorkbenchPlugin.log(
                        "MultiPageEditorPart.activateSite()   Parent key binding service was not an instance of INestableKeyBindingService.  It was an instance of " //$NON-NLS-1$
                                + service.getClass().getName() + " instead."); //$NON-NLS-1$
            }

            if (item.getData() instanceof INestable) {
                activeServiceLocator = (INestable) item.getData();
                activeServiceLocator.activate();
            }
        }
    }

    /**
     * Disposes the given part and its site.
     *
     * @param part
     *            The part to dispose; must not be <code>null</code>.
     */
    private void disposePart(final IWorkbenchPart part) {
        SafeRunner.run(new ISafeRunnable() {
            @Override
            public void run() {
                IWorkbenchPartSite partSite = part.getSite();
                part.dispose();
                if (partSite instanceof MultiPageEditorSite) {
                    ((MultiPageEditorSite) partSite).dispose();
                }
            }

            @Override
            public void handleException(Throwable e) {
                // Exception has already being logged by Core. Do nothing.
            }
        });
    }

    /**
     * Removes the page with the given index from this multi-page editor. The
     * controls for the page are disposed of; if the page has an editor, it is
     * disposed of too. The page index must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @see MultiPageEditorPart#addPage(Control)
     * @see MultiPageEditorPart#addPage(IEditorPart, IEditorInput)
     */
    public void removePage(int pageIndex) {
        Assert.isTrue(pageIndex >= 0 && pageIndex < getPageCount());
        // get editor (if any) before disposing item
        IEditorPart editor = getEditor(pageIndex);

        // get control for the item if it's not an editor
        CTabItem item = getItem(pageIndex);
        IServiceLocator pageLocator = null;
        if (item.getData() instanceof IServiceLocator) {
            pageLocator = (IServiceLocator) item.getData();
        }
        Control pageControl = item.getControl();

        // dispose item before disposing editor, in case there's an exception
        // in editor's dispose
        item.dispose();

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

        // dispose editor (if any)
        if (editor != null) {
            nestedEditors.remove(editor);
            disposePart(editor);
        }
        if (pageLocator != null) {
            pageSites.remove(pageLocator);
            if (pageLocator instanceof IDisposable) {
                ((IDisposable) pageLocator).dispose();
            }
        }
    }

    /**
     * Sets the currently active page.
     *
     * @param pageIndex
     *            the index of the page to be activated; the index must be valid
     */
    protected void setActivePage(int pageIndex) {
        Assert.isTrue(pageIndex >= 0 && pageIndex < getPageCount());
        getTabFolder().setSelection(pageIndex);
        pageChange(pageIndex);
    }

    /**
     * Sets the control for the given page index. The page index must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @param control
     *            the control for the specified page, or <code>null</code> to
     *            clear the control
     */
    protected void setControl(int pageIndex, Control control) {
        getItem(pageIndex).setControl(control);
    }

    /**
     * The <code>MultiPageEditor</code> implementation of this
     * <code>IWorkbenchPart</code> method sets focus on the active nested
     * editor, if there is one.
     * <p>
     * Subclasses may extend or reimplement.
     * </p>
     */
    @Override
    public void setFocus() {
        setFocus(getActivePage());
    }

    /**
     * Sets focus to the control for the given page. If the page has an editor,
     * this calls its <code>setFocus()</code> method. Otherwise, this calls
     * <code>setFocus</code> on the control for the page.
     *
     * @param pageIndex
     *            the index of the page
     */
    private void setFocus(int pageIndex) {
        if (pageIndex < 0 || pageIndex >= getPageCount()) {
            // page index out of bounds, don't set focus.
            return;
        }
        final IEditorPart editor = getEditor(pageIndex);
        if (editor != null) {
            editor.setFocus();

        } else {
            // Give the page's control focus.
            final Control control = getControl(pageIndex);
            if (control != null) {
                control.setFocus();
            }
        }
    }

    /**
     * Sets the image for the page with the given index, or <code>null</code>
     * to clear the image for the page. The page index must be valid.
     *
     * @param pageIndex
     *            the index of the page
     * @param image
     *            the image, or <code>null</code>
     */
    protected void setPageImage(int pageIndex, Image image) {
        getItem(pageIndex).setImage(image);
    }

    /**
     * Sets the text label for the page with the given index. The page index
     * must be valid. The text label must not be null.
     *
     * @param pageIndex
     *            the index of the page
     * @param text
     *            the text label
     */
    protected void setPageText(int pageIndex, String text) {
        getItem(pageIndex).setText(text);
    }

    /**
     * If there is an adapter registered against the subclass of
     * MultiPageEditorPart return that. Otherwise, delegate to the internal
     * editor.
     *
     * @see org.eclipse.ui.part.WorkbenchPart#getAdapter(java.lang.Class)
     */
    @Override
    public <T> T getAdapter(Class<T> adapter) {
        T result = super.getAdapter(adapter);
        // restrict delegating to the UI thread for bug 144851
        if (result == null && Display.getCurrent() != null) {
            IEditorPart innerEditor = getActiveEditor();
            // see bug 138823 - prevent some subclasses from causing
            // an infinite loop
            if (innerEditor != null && innerEditor != this) {
                result = Adapters.adapt(innerEditor, adapter);
            }
        }
        return result;
    }

    /**
     * Find the editors contained in this multi-page editor
     * whose editor input match the provided input.
     * @param input the editor input
     * @return the editors contained in this multi-page editor
     * whose editor input match the provided input
     * @since 3.3
     */
    public final IEditorPart[] findEditors(IEditorInput input) {
        List result = new ArrayList();
        int count = getPageCount();
        for (int i = 0; i < count; i++) {
            IEditorPart editor = getEditor(i);
            if (editor != null && editor.getEditorInput() != null && editor.getEditorInput().equals(input)) {
                result.add(editor);
            }
        }
        return (IEditorPart[]) result.toArray(new IEditorPart[result.size()]);
    }

    /**
     * Set the active page of this multi-page editor to the
     * page that contains the given editor part. This method has
     * no effect of the given editor part is not contained in this
     * multi-page editor.
     * @param editorPart the editor part
     * @since 3.3
     */
    public final void setActiveEditor(IEditorPart editorPart) {
        int count = getPageCount();
        for (int i = 0; i < count; i++) {
            IEditorPart editor = getEditor(i);
            if (editor == editorPart) {
                setActivePage(i);
                break;
            }
        }
    }

    /**
     * Returns the selected page for the current active page index, either the
     * IEditorPart for editors or the Control for other pages.
     * <p>
     * <b>Note:</b> clients may override this method to return a page
     * appropriate for their editors. Maybe be <code>null</code>.
     * </p>
     *
     * @return The IEditorPart or Control representing the current active page,
     *         or <code>null</code> if there are no active pages.
     * @since 3.5
     * @see #getActivePage()
     */
    @Override
    public Object getSelectedPage() {
        int index = getActivePage();
        if (index == -1) {
            return null;
        }
        IEditorPart editor = getEditor(index);
        if (editor != null) {
            return editor;
        }
        return getControl(index);
    }

    /**
     * Add the page change listener to be notified when the page changes. The
     * newly selected page will be the Object returned from
     * {@link #getSelectedPage()}. In the default case, this will be the active
     * page Control, IEditorPart, or <code>null</code>.
     * <p>
     * This method has no effect if the listener has already been added.
     * </p>
     *
     * @nooverride
     *
     * @since 3.5
     */
    @Override
    public void addPageChangedListener(IPageChangedListener listener) {
        pageChangeListeners.add(listener);
    }

    /**
     * Remove the page change listener.
     * <p>
     * This method has no effect if the listener is not in the list.
     * </p>
     *
     * @nooverride
     *
     * @since 3.5
     */
    @Override
    public void removePageChangedListener(IPageChangedListener listener) {
        pageChangeListeners.remove(listener);
    }

    private void firePageChanged(final PageChangedEvent event) {
        Object[] listeners = pageChangeListeners.getListeners();
        for (int i = 0; i < listeners.length; ++i) {
            final IPageChangedListener l = (IPageChangedListener) listeners[i];
            SafeRunnable.run(new SafeRunnable() {
                @Override
                public void run() {
                    l.pageChanged(event);
                }
            });
        }
    }
}