/******************************************************************************* * Copyright (c) 2005 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 *******************************************************************************/ package org.eclipse.bpel.common.ui.composite; import java.util.ArrayList; import java.util.List; import org.eclipse.bpel.common.ui.CommonUIPlugin; import org.eclipse.bpel.common.ui.Messages; import org.eclipse.bpel.common.ui.Policy; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IPartListener; import org.eclipse.ui.IPropertyListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PartInitException; import org.eclipse.ui.part.EditorPart; /** * The CompositeEditor contains a collection of embedded editors. * Each embedded editor provides its own contributions. The contributions * are available for the user when the embedded editor is activated. * Embedded editors have to be registered through the embeddedEditors * extension point like the example bellow: * * <extension point="org.eclipse.bpel.common.ui.embeddedEditors"> * <editor * id="com.examples.MyEmbeddedEditor" * class="com.examples.MyEmbeddedEditor" * contributorClass="com.examples.MyEmbeddedEditorActionBarContributor"/> * </extension> * * Notes: * Embedded editors can never be the workbench active part. The * active part is aways the CompositeEditor. */ public abstract class CompositeEditor extends EditorPart { protected EmbeddedEditorsCollection embeddedEditors; protected IPropertyListener propertyListener; protected CompositeEditorManager editorManager; protected IEditorPart mainEditor; /** * Since subclasses might implement their own part listeners * we make this attribute private so that there are no name * conflicts and subclasses do not overwrite this listener by accident. */ private IPartListener partListener; /** * Represents the collection of embedded editors * and the active editor index. It also helps to * maintain the collection and the index in sync. */ class EmbeddedEditorsCollection { private List<IEditorPart> editors; private IEditorPart[] editorsArrayCache; private int activeEditorIndex; EmbeddedEditorsCollection() { editors = new ArrayList<IEditorPart>(10); editorsArrayCache = null; activeEditorIndex = -1; // -1 means there are no active editors } void add(IEditorPart editor) { editors.add(editor); editorsArrayCache = null; } /** * We should never be able to remove the active editor. */ void remove(IEditorPart editor) { IEditorPart currentActiveEditor = getActiveEditor(); if (currentActiveEditor == editor) { throw new IllegalArgumentException(Messages.CompositeEditor_0); } editors.remove(editor); editorsArrayCache = null; // we need to keep the index updated after removing an editor setActiveEditor(currentActiveEditor); } IEditorPart[] getEmbeddedEditors() { if (editorsArrayCache == null) { editorsArrayCache = (IEditorPart[]) editors.toArray(new IEditorPart[editors.size()]); } return editorsArrayCache; } IEditorPart getActiveEditor() { // there are no active editors yet if (activeEditorIndex == -1) { return null; } IEditorPart[] editors = getEmbeddedEditors(); return editors[activeEditorIndex]; } boolean contains(IEditorPart editor) { return editors.contains(editor); } void setActiveEditor(IEditorPart editor) { if (editor == null) { activeEditorIndex = -1; return; } IEditorPart[] editors = getEmbeddedEditors(); for (int i = 0; i < editors.length; i++) { if (editor == editors[i]) { activeEditorIndex = i; return; } } } void clearEditors() { setActiveEditor(null); editors.clear(); editorsArrayCache = null; } } public CompositeEditor() { embeddedEditors = new EmbeddedEditorsCollection(); } /** * Creates an embedded editor corresponding to the given editorId and * connects it to the CompositeEditor. * <p> * It is important to notice that the returned editor will never be * the workbench active part. * <p> * For each composite editor one of its embedded editors should be * designated as the main editor. In order to do that you have to * call <code>CompositeEditor#setMainEditor</code>. * * @param editor the embedded editor * @param editorComposite the editor's parent composite * @return IEditorPart the embedded editor instance * * @see CompositeEditor#setMainEditor */ public IEditorPart connectEditor(String editorId, IEditorInput input, Composite editorComposite) throws CoreException { final IEditorPart editor = editorManager.createEditor(editorId, input); embeddedEditors.add(editor); Composite composite = new Composite(editorComposite, SWT.NONE); composite.setLayout(new FillLayout()); editor.createPartControl(composite); editor.addPropertyListener(getPropertyListener()); // This is the fundamental part of our implementation. // Editors are activated when we get an Activate // event from the editor's composite. associate(editor, composite); return editor; } /** * Associates the control with the given editor. When the control * is activated all the editor contributions will be activated * as well. This is useful when the CompositeEditor is not only * composed by embedded editor but also by other controls. If no editor * is associated with these controls the available contributions * (menus, toolbars, status line, etc...) will be the contributions * for the last active editor and might cause confusion to the user. */ public void associate(final IEditorPart editor, Control control) { control.addListener(SWT.Activate, new Listener() { public void handleEvent(Event event) { activateEditor(editor); } }); } /** * Activates contributions from the given editor and * deactivates contributions from the current editor. */ protected void activateEditor(IEditorPart newEditor) { IEditorPart oldEditor = embeddedEditors.getActiveEditor(); // It can only be null when the first editor is activated. // If that is the case just go ahead and activate newEditor if (oldEditor != null) { if (oldEditor == newEditor) { return; } editorManager.deactivate(oldEditor); } editorManager.activate(newEditor); embeddedEditors.setActiveEditor(newEditor); } /** * Removes the given editor from this composite editor. If the given * editor is the current active editor the main editor will be activated * instead. The main editor cannot be disconnected by calling this * method. * * @exception CoreException if the editor cannot be disconnected. Reasons include: * <ul> * <li> The editor is the main editor.</li> * <li> The editor is the current active editor and the main editor was not defined.</li> * </ul> */ public void disconnectEditor(IEditorPart editor) throws CoreException { if (editor == embeddedEditors.getActiveEditor()) { if (mainEditor != null && editor != mainEditor) { activateEditor(mainEditor); } else { IStatus status = new Status(IStatus.ERROR, CommonUIPlugin.PLUGIN_ID, ICompositeEditorConstants.COULD_NOT_DISCONNECT_EDITOR, NLS.bind(Messages.CompositeEditor_Cannot_disconnect_active_editor, (new Object[] { editor.getTitle() })), null); throw new CoreException(status); } } embeddedEditors.remove(editor); editor.removePropertyListener(getPropertyListener()); CompositeEditorSite site = (CompositeEditorSite) editor.getSite(); CompositeEditorActionBars actionBars = (CompositeEditorActionBars) site.getActionBars(); actionBars.dispose(); site.dispose(); editor.dispose(); } /** * Listens and propagates all property changes from embedded editors. * This is the default implementation. Subclasses may overwrite * and specialize the behaviour. */ protected IPropertyListener getPropertyListener() { if (propertyListener == null) { propertyListener = new IPropertyListener() { public void propertyChanged(Object source, int propId) { firePropertyChange(propId); } }; } return propertyListener; } /** * Disconects all of the embedded editors. Subclasses should implement * internalDispose() instead of dispose(); */ @Override public final void dispose() { embeddedEditors.setActiveEditor(null); internalDispose(); IEditorPart[] editors = embeddedEditors.getEmbeddedEditors(); for (int i = 0; i < editors.length; i++) { try { disconnectEditor(editors[i]); } catch (CoreException e) { // should never hapen because there should // be no active editors at this point } } getEditorSite().getPage().removePartListener(getPartlistener()); super.dispose(); getEditorSite().setSelectionProvider(null); // discard our references to editors (to help reduce memory leakage) embeddedEditors.clearEditors(); embeddedEditors = null; } /** * Should be implemented by clients. */ protected void internalDispose() { } @Override public boolean isDirty() { IEditorPart[] editors = embeddedEditors.getEmbeddedEditors(); for (int i = 0; i < editors.length; i++) { if (editors[i].isDirty()) { return true; } } return false; } /** * Default implementation saves all of the embedded editors but * it can be overwritten by subclasses. */ @Override public void doSave(IProgressMonitor monitor) { monitor = Policy.monitorFor(monitor); try { IEditorPart[] editors = getEmbeddedEditorsSaveOrder(embeddedEditors.getEmbeddedEditors()); String message = Messages.CompositeEditor_2; monitor.beginTask(message, Math.max(1, editors.length)); for (int i = 0; i < editors.length; i++) { final IProgressMonitor subMonitor = Policy.subMonitorFor(monitor, 1); final IEditorPart editor = editors[i]; ISafeRunnable runnable = new ISafeRunnable() { public void run() throws Exception { editor.doSave(subMonitor); } public void handleException(Throwable exception) { } }; SafeRunnable.run(runnable); } } finally { monitor.done(); } } /** * Defines the order the embedded editors should be saved. * * @param editors the embedded editors that will be saved * @return the same editors in the order they should be saved */ protected IEditorPart[] getEmbeddedEditorsSaveOrder(IEditorPart[] editors) { return editors; } /** * Default implementation. It can be overwritten by subclasses. */ @Override public void setFocus() { IEditorPart active = embeddedEditors.getActiveEditor(); if (active != null) { active.setFocus(); } } /** * Designate the given editor to be the main editor. * The editor will be activated. * * @param editor the main editor * @exception CoreException if the editor cannot be set as the main editor. Reasons include: * <ul> * <li> The editor is not connected to this composite editor.</li> * </ul> */ public void setMainEditor(IEditorPart editor) throws CoreException { if (!embeddedEditors.contains(editor)) { IStatus status = new Status(IStatus.ERROR, CommonUIPlugin.PLUGIN_ID, ICompositeEditorConstants.EDITOR_NOT_CONNECTED, Messages.CompositeEditor_3 + editor.getTitle(), null); throw new CoreException(status); } mainEditor = editor; activateEditor(editor); } /** * Returns the main editor or null if none has been defined. */ public IEditorPart getMainEditor() { return mainEditor; } /** * Returns the current active editor. */ public IEditorPart getActiveEditor() { return embeddedEditors.getActiveEditor(); } @Override public void init(IEditorSite site, IEditorInput input) throws PartInitException { setSite(site); setInput(input); editorManager = new CompositeEditorManager(site); getEditorSite().getPage().addPartListener(getPartlistener()); getEditorSite().setSelectionProvider(new CompositeEditorSelectionProvider(this)); } /** * The workbench caches IActionBars according to the editor type (editor id), * so editors of the same type share the same IActionBars instance. * The CompositeEditor controls embedded editors' contributions by deactivating * the previous active embedded editor and activating the new active embedded editor. * If the actions are not deactivated they remain visible for the IActionBars. * When switching between two CompositeEditor instances of the same type (id) * the origin CompositeEditor does not know it has to deactivate its current active * embedded editor and it causes incorrect or duplicate contributions to appear. * The solution is to coordinate the current embedded editor activation and deactivation * with the CompositeEditor (IWorkbenchPart) activation and deactivation. */ private IPartListener getPartlistener() { if (partListener == null) { partListener = new IPartListener() { public void partActivated(IWorkbenchPart part) { if (part == CompositeEditor.this) { IEditorPart lastActiveEditor = embeddedEditors.getActiveEditor(); if (lastActiveEditor != null) { editorManager.activate(lastActiveEditor); } } } public void partBroughtToTop(IWorkbenchPart part) { } public void partClosed(IWorkbenchPart part) { } public void partDeactivated(IWorkbenchPart part) { if (part == CompositeEditor.this) { editorManager.deactivate(embeddedEditors.getActiveEditor()); } } public void partOpened(IWorkbenchPart part) { } }; } return partListener; } }