org.eclipse.php.internal.ui.compare.ContentMergeViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.php.internal.ui.compare.ContentMergeViewer.java

Source

/*******************************************************************************
 * Copyright (c) 2009 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
 *     Zend Technologies
 *******************************************************************************/

package org.eclipse.php.internal.ui.compare;

import java.util.ResourceBundle;

import org.eclipse.compare.*;
import org.eclipse.compare.contentmergeviewer.IFlushable;
import org.eclipse.compare.contentmergeviewer.IMergeViewerContentProvider;
import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
import org.eclipse.compare.internal.*;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.ICompareInputChangeListener;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.php.internal.ui.PHPUIMessages;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.*;

/**
 * An abstract compare and merge viewer with two side-by-side content areas and
 * an optional content area for the ancestor. The implementation makes no
 * assumptions about the content type.
 * <p>
 * <code>ContentMergeViewer</code>
 * <ul>
 * <li>implements the overall layout and defines hooks so that subclasses can
 * easily provide an implementation for a specific content type,
 * <li>implements the UI for making the areas resizable,
 * <li>has an action for controlling whether the ancestor area is visible or
 * not,
 * <li>has actions for copying one side of the input to the other side,
 * <li>tracks the dirty state of the left and right sides and send out
 * notification on state changes.
 * </ul>
 * A <code>ContentMergeViewer</code> accesses its model by means of a content
 * provider which must implement the <code>IMergeViewerContentProvider</code>
 * interface.
 * </p>
 * <p>
 * Clients may wish to use the standard concrete subclass
 * <code>TextMergeViewer</code>, or define their own subclass.
 * 
 * @see IMergeViewerContentProvider
 * @see TextMergeViewer
 */
public abstract class ContentMergeViewer extends ContentViewer
        implements IPropertyChangeNotifier, IFlushable, IFlushable2 {
    // https://bugs.eclipse.org/bugs/show_bug.cgi?id=330672
    org.eclipse.compare.contentmergeviewer.ContentMergeViewer cmv;

    class SaveAction extends MergeViewerAction {

        SaveAction(boolean left) {
            super(true, false, false);

            setText(PHPUIMessages.ContentMergeViewer_0);
            setToolTipText(PHPUIMessages.ContentMergeViewer_1);
            setDescription(PHPUIMessages.ContentMergeViewer_2);
        }

        public void run() {
            flush(null);
        }
    }

    /* package */static final int HORIZONTAL = 1;
    /* package */static final int VERTICAL = 2;

    static final double HSPLIT = 0.5;
    static final double VSPLIT = 0.3;

    private class ContentMergeViewerLayout extends Layout {

        public Point computeSize(Composite c, int w, int h, boolean force) {
            return new Point(100, 100);
        }

        public void layout(Composite composite, boolean force) {

            // determine some derived sizes
            int headerHeight = fLeftLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y;
            Rectangle r = composite.getClientArea();

            int centerWidth = getCenterWidth();
            int width1 = (int) ((r.width - centerWidth) * getHorizontalSplitRatio());
            int width2 = r.width - width1 - centerWidth;

            int height1 = 0;
            int height2 = 0;
            if (fIsThreeWay && fAncestorVisible) {
                height1 = (int) ((r.height - (2 * headerHeight)) * fVSplit);
                height2 = r.height - (2 * headerHeight) - height1;
            } else {
                height1 = 0;
                height2 = r.height - headerHeight;
            }

            int y = 0;

            if (fIsThreeWay && fAncestorVisible) {
                fAncestorLabel.setBounds(0, y, r.width, headerHeight);
                fAncestorLabel.setVisible(true);
                y += headerHeight;
                handleResizeAncestor(0, y, r.width, height1);
                y += height1;
            } else {
                fAncestorLabel.setVisible(false);
                handleResizeAncestor(0, 0, 0, 0);
            }

            fLeftLabel.getSize(); // without this resizing would not always work

            if (centerWidth > 3) {
                fLeftLabel.setBounds(0, y, width1 + 1, headerHeight);
                fDirectionLabel.setVisible(true);
                fDirectionLabel.setBounds(width1 + 1, y, centerWidth - 1, headerHeight);
                fRightLabel.setBounds(width1 + centerWidth, y, width2, headerHeight);
            } else {
                fLeftLabel.setBounds(0, y, width1, headerHeight);
                fDirectionLabel.setVisible(false);
                fRightLabel.setBounds(width1, y, r.width - width1, headerHeight);
            }

            y += headerHeight;

            if (fCenter != null && !fCenter.isDisposed())
                fCenter.setBounds(width1, y, centerWidth, height2);

            handleResizeLeftRight(0, y, width1, centerWidth, width2, height2);
        }

        private double getHorizontalSplitRatio() {
            if (fHSplit < 0) {
                Object input = getInput();
                if (input instanceof ICompareInput) {
                    ICompareInput ci = (ICompareInput) input;
                    if (ci.getLeft() == null)
                        return 0.1;
                    if (ci.getRight() == null)
                        return 0.9;
                }
                return HSPLIT;
            }
            return fHSplit;
        }
    }

    class Resizer extends MouseAdapter implements MouseMoveListener {

        Control fControl;
        int fX, fY;
        int fWidth1, fWidth2;
        int fHeight1, fHeight2;
        int fDirection;
        boolean fLiveResize;
        boolean fIsDown;

        public Resizer(Control c, int dir) {
            fDirection = dir;
            fControl = c;
            fLiveResize = !(fControl instanceof Sash);
            updateCursor(c, dir);
            fControl.addMouseListener(this);
            fControl.addMouseMoveListener(this);
            fControl.addDisposeListener(new DisposeListener() {
                public void widgetDisposed(DisposeEvent e) {
                    fControl = null;
                }
            });
        }

        public void mouseDoubleClick(MouseEvent e) {
            if ((fDirection & HORIZONTAL) != 0)
                fHSplit = -1;
            if ((fDirection & VERTICAL) != 0)
                fVSplit = VSPLIT;
            fComposite.layout(true);
        }

        public void mouseDown(MouseEvent e) {
            Composite parent = fControl.getParent();

            Point s = parent.getSize();
            Point as = fAncestorLabel.getSize();
            Point ys = fLeftLabel.getSize();
            Point ms = fRightLabel.getSize();

            fWidth1 = ys.x;
            fWidth2 = ms.x;
            fHeight1 = fLeftLabel.getLocation().y - as.y;
            fHeight2 = s.y - (fLeftLabel.getLocation().y + ys.y);

            fX = e.x;
            fY = e.y;
            fIsDown = true;
        }

        public void mouseUp(MouseEvent e) {
            fIsDown = false;
            if (!fLiveResize)
                resize(e);
        }

        public void mouseMove(MouseEvent e) {
            if (fIsDown && fLiveResize)
                resize(e);
        }

        private void resize(MouseEvent e) {
            int dx = e.x - fX;
            int dy = e.y - fY;

            int centerWidth = fCenter.getSize().x;

            if (fWidth1 + dx > centerWidth && fWidth2 - dx > centerWidth) {
                fWidth1 += dx;
                fWidth2 -= dx;
                if ((fDirection & HORIZONTAL) != 0)
                    fHSplit = (double) fWidth1 / (double) (fWidth1 + fWidth2);
            }
            if (fHeight1 + dy > centerWidth && fHeight2 - dy > centerWidth) {
                fHeight1 += dy;
                fHeight2 -= dy;
                if ((fDirection & VERTICAL) != 0)
                    fVSplit = (double) fHeight1 / (double) (fHeight1 + fHeight2);
            }

            fComposite.layout(true);
            fControl.getDisplay().update();
        }
    }

    /** Style bits for top level composite */
    private int fStyles;
    private ResourceBundle fBundle;
    private final CompareConfiguration fCompareConfiguration;
    private IPropertyChangeListener fPropertyChangeListener;
    private ICompareInputChangeListener fCompareInputChangeListener;
    private ListenerList fListenerList;
    boolean fConfirmSave = true;

    private double fHSplit = -1; // width ratio of left and right panes
    private double fVSplit = VSPLIT; // height ratio of ancestor and bottom
    // panes

    private boolean fIsThreeWay; // whether their is an ancestor
    private boolean fAncestorVisible; // whether the ancestor pane is visible
    private ActionContributionItem fAncestorItem;

    private Action fCopyLeftToRightAction; // copy from left to right
    private Action fCopyRightToLeftAction; // copy from right to left

    MergeViewerAction fLeftSaveAction;
    MergeViewerAction fRightSaveAction;

    private CompareHandlerService fHandlerService;

    // SWT widgets
    /* package */Composite fComposite;
    private CLabel fAncestorLabel;
    private CLabel fLeftLabel;
    private CLabel fRightLabel;
    /* package */CLabel fDirectionLabel;
    /* package */Control fCenter;

    // ---- SWT resources to be disposed
    private Image fRightArrow;
    private Image fLeftArrow;
    private Image fBothArrow;
    Cursor fNormalCursor;
    private Cursor fHSashCursor;
    private Cursor fVSashCursor;
    private Cursor fHVSashCursor;

    private ILabelProviderListener labelChangeListener = new ILabelProviderListener() {
        public void labelProviderChanged(LabelProviderChangedEvent event) {
            Object[] elements = event.getElements();
            for (int i = 0; i < elements.length; i++) {
                Object object = elements[i];
                if (object == getInput())
                    updateHeader();
            }
        }
    };

    // ---- end

    /**
     * Creates a new content merge viewer and initializes with a resource bundle
     * and a configuration.
     * 
     * @param style
     *            SWT style bits
     * @param bundle
     *            the resource bundle
     * @param cc
     *            the configuration object
     */
    protected ContentMergeViewer(int style, ResourceBundle bundle, CompareConfiguration cc) {

        fStyles = style & ~(SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT); // remove
        // BIDI
        // direction
        // bits
        fBundle = bundle;

        fAncestorVisible = Utilities.getBoolean(cc, ICompareUIConstants.PROP_ANCESTOR_VISIBLE, fAncestorVisible);
        fConfirmSave = Utilities.getBoolean(cc, CompareEditor.CONFIRM_SAVE_PROPERTY, fConfirmSave);

        setContentProvider(new MergeViewerContentProvider(cc));

        fCompareInputChangeListener = new ICompareInputChangeListener() {
            public void compareInputChanged(ICompareInput input) {
                if (input == getInput()) {
                    handleCompareInputChange();
                }
            }
        };

        // Make sure the compare configuration is not null
        if (cc == null)
            fCompareConfiguration = new CompareConfiguration();
        else
            fCompareConfiguration = cc;
        fPropertyChangeListener = new IPropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent event) {
                ContentMergeViewer.this.handlePropertyChangeEvent(event);
            }
        };
        fCompareConfiguration.addPropertyChangeListener(fPropertyChangeListener);

        fLeftSaveAction = new SaveAction(true);
        fLeftSaveAction.setEnabled(false);
        fRightSaveAction = new SaveAction(false);
        fRightSaveAction.setEnabled(false);

        // this is used to update the dirty status,if we use
        // org.eclipse.php.internal.ui.compare.ContentMergeViewer,we will get a
        // ClassCastException
        cmv = new org.eclipse.compare.contentmergeviewer.ContentMergeViewer(fStyles, fBundle,
                fCompareConfiguration) {

            @Override
            protected void createControls(Composite composite) {

            }

            @Override
            protected void handleResizeAncestor(int x, int y, int width, int height) {

            }

            @Override
            protected void handleResizeLeftRight(int x, int y, int leftWidth, int centerWidth, int rightWidth,
                    int height) {

            }

            @Override
            protected void updateContent(Object ancestor, Object left, Object right) {

            }

            @Override
            protected void copy(boolean leftToRight) {

            }

            @Override
            protected byte[] getContents(boolean left) {
                return null;
            }

            @Override
            public boolean internalIsLeftDirty() {
                return ContentMergeViewer.this.isLeftDirty();
            }

            @Override
            public boolean internalIsRightDirty() {
                return ContentMergeViewer.this.isRightDirty();
            }
        };
    }

    // ---- hooks ---------------------

    /**
     * Returns the viewer's name.
     * 
     * @return the viewer's name
     */
    public String getTitle() {
        return Utilities.getString(getResourceBundle(), "title"); //$NON-NLS-1$
    }

    /**
     * Creates the SWT controls for the ancestor, left, and right content areas
     * of this compare viewer. Implementations typically hold onto the controls
     * so that they can be initialized with the input objects in method
     * <code>updateContent</code>.
     * 
     * @param composite
     *            the container for the three areas
     */
    abstract protected void createControls(Composite composite);

    /**
     * Lays out the ancestor area of the compare viewer. It is called whenever
     * the viewer is resized or when the sashes between the areas are moved to
     * adjust the size of the areas.
     * 
     * @param x
     *            the horizontal position of the ancestor area within its
     *            container
     * @param y
     *            the vertical position of the ancestor area within its
     *            container
     * @param width
     *            the width of the ancestor area
     * @param height
     *            the height of the ancestor area
     */
    abstract protected void handleResizeAncestor(int x, int y, int width, int height);

    /**
     * Lays out the left and right areas of the compare viewer. It is called
     * whenever the viewer is resized or when the sashes between the areas are
     * moved to adjust the size of the areas.
     * 
     * @param x
     *            the horizontal position of the left area within its container
     * @param y
     *            the vertical position of the left and right area within its
     *            container
     * @param leftWidth
     *            the width of the left area
     * @param centerWidth
     *            the width of the gap between the left and right areas
     * @param rightWidth
     *            the width of the right area
     * @param height
     *            the height of the left and right areas
     */
    abstract protected void handleResizeLeftRight(int x, int y, int leftWidth, int centerWidth, int rightWidth,
            int height);

    /**
     * Contributes items to the given <code>ToolBarManager</code>. It is called
     * when this viewer is installed in its container and if the container has a
     * <code>ToolBarManager</code>. The <code>ContentMergeViewer</code>
     * implementation of this method does nothing. Subclasses may reimplement.
     * 
     * @param toolBarManager
     *            the toolbar manager to contribute to
     */
    protected void createToolItems(ToolBarManager toolBarManager) {
        // empty implementation
    }

    /**
     * Initializes the controls of the three content areas with the given input
     * objects.
     * 
     * @param ancestor
     *            the input for the ancestor area
     * @param left
     *            the input for the left area
     * @param right
     *            the input for the right area
     */
    abstract protected void updateContent(Object ancestor, Object left, Object right);

    /**
     * Copies the content of one side to the other side. Called from the
     * (internal) actions for copying the sides of the viewer's input object.
     * 
     * @param leftToRight
     *            if <code>true</code>, the left side is copied to the right
     *            side; if <code>false</code>, the right side is copied to the
     *            left side
     */
    abstract protected void copy(boolean leftToRight);

    /**
     * Returns the byte contents of the left or right side. If the viewer has no
     * editable content <code>null</code> can be returned.
     * 
     * @param left
     *            if <code>true</code>, the byte contents of the left area is
     *            returned; if <code>false</code>, the byte contents of the
     *            right area
     * @return the content as an array of bytes, or <code>null</code>
     */
    abstract protected byte[] getContents(boolean left);

    // ----------------------------

    /**
     * Returns the resource bundle of this viewer.
     * 
     * @return the resource bundle
     */
    protected ResourceBundle getResourceBundle() {
        return fBundle;
    }

    /**
     * Returns the compare configuration of this viewer, or <code>null</code> if
     * this viewer does not yet have a configuration.
     * 
     * @return the compare configuration, or <code>null</code> if none
     */
    protected CompareConfiguration getCompareConfiguration() {
        return fCompareConfiguration;
    }

    /**
     * The <code>ContentMergeViewer</code> implementation of this
     * <code>ContentViewer</code> method checks to ensure that the content
     * provider is an <code>IMergeViewerContentProvider</code>.
     * 
     * @param contentProvider
     *            the content provider to set. Must implement
     *            IMergeViewerContentProvider.
     */
    public void setContentProvider(IContentProvider contentProvider) {
        Assert.isTrue(contentProvider instanceof IMergeViewerContentProvider);
        super.setContentProvider(contentProvider);
    }

    /* package */IMergeViewerContentProvider getMergeContentProvider() {
        return (IMergeViewerContentProvider) getContentProvider();
    }

    /**
     * The <code>ContentMergeViewer</code> implementation of this
     * <code>Viewer</code> method returns the empty selection. Subclasses may
     * override.
     * 
     * @return empty selection.
     */
    public ISelection getSelection() {
        return new ISelection() {
            public boolean isEmpty() {
                return true;
            }
        };
    }

    /**
     * The <code>ContentMergeViewer</code> implementation of this
     * <code>Viewer</code> method does nothing. Subclasses may reimplement.
     * 
     * @see org.eclipse.jface.viewers.Viewer#setSelection(org.eclipse.jface.viewers.ISelection,
     *      boolean)
     */
    public void setSelection(ISelection selection, boolean reveal) {
        // empty implementation
    }

    /**
     * Callback that is invoked when a property in the compare configuration (
     * {@link #getCompareConfiguration()} changes.
     * 
     * @param event
     *            the property change event
     * @since 3.3
     */
    protected void handlePropertyChangeEvent(PropertyChangeEvent event) {

        String key = event.getProperty();

        if (key.equals(ICompareUIConstants.PROP_ANCESTOR_VISIBLE)) {
            fAncestorVisible = Utilities.getBoolean(getCompareConfiguration(),
                    ICompareUIConstants.PROP_ANCESTOR_VISIBLE, fAncestorVisible);
            fComposite.layout(true);

            updateCursor(fLeftLabel, VERTICAL);
            updateCursor(fDirectionLabel, HORIZONTAL | VERTICAL);
            updateCursor(fRightLabel, VERTICAL);

            return;
        }

        if (key.equals(ICompareUIConstants.PROP_IGNORE_ANCESTOR)) {
            setAncestorVisibility(false, !Utilities.getBoolean(getCompareConfiguration(),
                    ICompareUIConstants.PROP_IGNORE_ANCESTOR, false));
            return;
        }
    }

    void updateCursor(Control c, int dir) {
        if (!(c instanceof Sash)) {
            Cursor cursor = null;
            switch (dir) {
            case VERTICAL:
                if (fAncestorVisible) {
                    if (fVSashCursor == null)
                        fVSashCursor = new Cursor(c.getDisplay(), SWT.CURSOR_SIZENS);
                    cursor = fVSashCursor;
                } else {
                    if (fNormalCursor == null)
                        fNormalCursor = new Cursor(c.getDisplay(), SWT.CURSOR_ARROW);
                    cursor = fNormalCursor;
                }
                break;
            case HORIZONTAL:
                if (fHSashCursor == null)
                    fHSashCursor = new Cursor(c.getDisplay(), SWT.CURSOR_SIZEWE);
                cursor = fHSashCursor;
                break;
            case VERTICAL + HORIZONTAL:
                if (fAncestorVisible) {
                    if (fHVSashCursor == null)
                        fHVSashCursor = new Cursor(c.getDisplay(), SWT.CURSOR_SIZEALL);
                    cursor = fHVSashCursor;
                } else {
                    if (fHSashCursor == null)
                        fHSashCursor = new Cursor(c.getDisplay(), SWT.CURSOR_SIZEWE);
                    cursor = fHSashCursor;
                }
                break;
            }
            if (cursor != null)
                c.setCursor(cursor);
        }
    }

    private void setAncestorVisibility(boolean visible, boolean enabled) {
        if (fAncestorItem != null) {
            Action action = (Action) fAncestorItem.getAction();
            if (action != null) {
                action.setChecked(visible);
                action.setEnabled(enabled);
            }
        }
        getCompareConfiguration().setProperty(ICompareUIConstants.PROP_ANCESTOR_VISIBLE, new Boolean(visible));
    }

    // ---- input

    /**
     * Return whether the input is a three-way comparison.
     * 
     * @return whether the input is a three-way comparison
     * @since 3.3
     */
    protected boolean isThreeWay() {
        return fIsThreeWay;
    }

    /**
     * Internal hook method called when the input to this viewer is initially
     * set or subsequently changed.
     * <p>
     * The <code>ContentMergeViewer</code> implementation of this
     * <code>Viewer</code> method tries to save the old input by calling
     * <code>doSave(...)</code> and then calls <code>internalRefresh(...)</code>.
     * 
     * @param input
     *            the new input of this viewer, or <code>null</code> if there is
     *            no new input
     * @param oldInput
     *            the old input element, or <code>null</code> if there was
     *            previously no input
     */
    protected final void inputChanged(Object input, Object oldInput) {

        if (input != oldInput && oldInput != null) {
            ICompareInputLabelProvider lp = getCompareConfiguration().getLabelProvider();
            if (lp != null)
                lp.removeListener(labelChangeListener);
        }

        if (input != oldInput && oldInput instanceof ICompareInput) {
            ICompareContainer container = getCompareConfiguration().getContainer();
            container.removeCompareInputChangeListener((ICompareInput) oldInput, fCompareInputChangeListener);
        }

        boolean success = doSave(input, oldInput);

        if (input != oldInput && input instanceof ICompareInput) {
            ICompareContainer container = getCompareConfiguration().getContainer();
            container.addCompareInputChangeListener((ICompareInput) input, fCompareInputChangeListener);
        }

        if (input != oldInput && input != null) {
            ICompareInputLabelProvider lp = getCompareConfiguration().getLabelProvider();
            if (lp != null)
                lp.addListener(labelChangeListener);
        }

        if (success) {
            setLeftDirty(false);
            setRightDirty(false);
        }

        if (input != oldInput)
            internalRefresh(input);
    }

    /**
     * This method is called from the <code>Viewer</code> method
     * <code>inputChanged</code> to save any unsaved changes of the old input.
     * <p>
     * The <code>ContentMergeViewer</code> implementation of this method calls
     * <code>saveContent(...)</code>. If confirmation has been turned on with
     * <code>setConfirmSave(true)</code>, a confirmation alert is posted before
     * saving.
     * </p>
     * Clients can override this method and are free to decide whether they want
     * to call the inherited method.
     * 
     * @param newInput
     *            the new input of this viewer, or <code>null</code> if there is
     *            no new input
     * @param oldInput
     *            the old input element, or <code>null</code> if there was
     *            previously no input
     * @return <code>true</code> if saving was successful, or if the user didn't
     *         want to save (by pressing 'NO' in the confirmation dialog).
     * @since 2.0
     */
    protected boolean doSave(Object newInput, Object oldInput) {

        // before setting the new input we have to save the old
        if (isLeftDirty() || isRightDirty()) {

            if (Utilities.RUNNING_TESTS) {
                if (Utilities.TESTING_FLUSH_ON_COMPARE_INPUT_CHANGE) {
                    flushContent(oldInput, null);
                }
            } else if (fConfirmSave) {
                // post alert
                Shell shell = fComposite.getShell();

                MessageDialog dialog = new MessageDialog(shell,
                        Utilities.getString(getResourceBundle(), "saveDialog.title"), //$NON-NLS-1$
                        null, // accept the default window icon
                        Utilities.getString(getResourceBundle(), "saveDialog.message"), //$NON-NLS-1$
                        MessageDialog.QUESTION,
                        new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL, }, 0); // default
                // button
                // index

                switch (dialog.open()) { // open returns index of pressed button
                case 0:
                    flushContent(oldInput, null);
                    break;
                case 1:
                    setLeftDirty(false);
                    setRightDirty(false);
                    break;
                case 2:
                    throw new ViewerSwitchingCancelled();
                }
            } else
                flushContent(oldInput, null);
            return true;
        }
        return false;
    }

    /**
     * Controls whether <code>doSave(Object, Object)</code> asks for
     * confirmation before saving the old input with
     * <code>saveContent(Object)</code>.
     * 
     * @param enable
     *            a value of <code>true</code> enables confirmation
     * @since 2.0
     */
    public void setConfirmSave(boolean enable) {
        fConfirmSave = enable;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.viewers.Viewer#refresh()
     */
    public void refresh() {
        internalRefresh(getInput());
    }

    private void internalRefresh(Object input) {

        IMergeViewerContentProvider content = getMergeContentProvider();
        if (content == null) {
            return;
        }
        Object ancestor = content.getAncestorContent(input);
        boolean oldFlag = fIsThreeWay;
        if (Utilities.isHunk(input)) {
            fIsThreeWay = true;
        } else if (input instanceof ICompareInput)
            fIsThreeWay = (((ICompareInput) input).getKind() & Differencer.DIRECTION_MASK) != 0;
        else
            fIsThreeWay = ancestor != null;

        if (fAncestorItem != null)
            fAncestorItem.setVisible(fIsThreeWay);

        if (fAncestorVisible && oldFlag != fIsThreeWay)
            fComposite.layout(true);

        Object left = content.getLeftContent(input);
        Object right = content.getRightContent(input);
        updateContent(ancestor, left, right);

        // https://bugs.eclipse.org/bugs/show_bug.cgi?id=453799
        // Content provider may be disposed after call to updateContent()
        content = getMergeContentProvider();
        if (content == null) {
            return;
        }

        updateHeader();
        ToolBarManager tbm = CompareViewerPane.getToolBarManager(fComposite.getParent());
        if (tbm != null) {
            updateToolItems();
            tbm.update(true);
            tbm.getControl().getParent().layout(true);
        }

    }

    // ---- layout & SWT control creation

    /**
     * Builds the SWT controls for the three areas of a compare/merge viewer.
     * <p>
     * Calls the hooks <code>createControls</code> and
     * <code>createToolItems</code> to let subclasses build the specific content
     * areas and to add items to an enclosing toolbar.
     * <p>
     * This method must only be called in the constructor of subclasses.
     * 
     * @param parent
     *            the parent control
     * @return the new control
     */
    protected final Control buildControl(Composite parent) {

        fComposite = new Composite(parent, fStyles | SWT.LEFT_TO_RIGHT) { // we
            // force
            // a
            // specific
            // direction
            public boolean setFocus() {
                return ContentMergeViewer.this.handleSetFocus();
            }
        };
        fComposite.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle());

        hookControl(fComposite); // hook help & dispose listener

        fComposite.setLayout(new ContentMergeViewerLayout());

        int style = SWT.SHADOW_OUT;
        fAncestorLabel = new CLabel(fComposite, style | Window.getDefaultOrientation());

        fLeftLabel = new CLabel(fComposite, style | Window.getDefaultOrientation());
        new Resizer(fLeftLabel, VERTICAL);

        fDirectionLabel = new CLabel(fComposite, style);
        fDirectionLabel.setAlignment(SWT.CENTER);
        new Resizer(fDirectionLabel, HORIZONTAL | VERTICAL);

        fRightLabel = new CLabel(fComposite, style | Window.getDefaultOrientation());
        new Resizer(fRightLabel, VERTICAL);

        if (fCenter == null || fCenter.isDisposed())
            fCenter = createCenterControl(fComposite);

        createControls(fComposite);

        fHandlerService = CompareHandlerService.createFor(getCompareConfiguration().getContainer(),
                fComposite.getShell());

        initializeToolbars(parent);

        return fComposite;
    }

    private void initializeToolbars(Composite parent) {
        ToolBarManager tbm = CompareViewerPane.getToolBarManager(parent);
        if (tbm != null) {
            tbm.removeAll();

            // define groups
            tbm.add(new Separator("modes")); //$NON-NLS-1$
            tbm.add(new Separator("merge")); //$NON-NLS-1$
            tbm.add(new Separator("navigation")); //$NON-NLS-1$

            CompareConfiguration cc = getCompareConfiguration();

            if (cc.isRightEditable()) {
                fCopyLeftToRightAction = new Action() {
                    public void run() {
                        copy(true);
                    }
                };
                Utilities.initAction(fCopyLeftToRightAction, getResourceBundle(), "action.CopyLeftToRight."); //$NON-NLS-1$
                tbm.appendToGroup("merge", fCopyLeftToRightAction); //$NON-NLS-1$
                fHandlerService.registerAction(fCopyLeftToRightAction, "org.eclipse.compare.copyAllLeftToRight"); //$NON-NLS-1$
            }

            if (cc.isLeftEditable()) {
                fCopyRightToLeftAction = new Action() {
                    public void run() {
                        copy(false);
                    }
                };
                Utilities.initAction(fCopyRightToLeftAction, getResourceBundle(), "action.CopyRightToLeft."); //$NON-NLS-1$
                tbm.appendToGroup("merge", fCopyRightToLeftAction); //$NON-NLS-1$
                fHandlerService.registerAction(fCopyRightToLeftAction, "org.eclipse.compare.copyAllRightToLeft"); //$NON-NLS-1$
            }

            final ChangePropertyAction a = new ChangePropertyAction(fBundle, getCompareConfiguration(),
                    "action.EnableAncestor.", ICompareUIConstants.PROP_ANCESTOR_VISIBLE); //$NON-NLS-1$
            a.setChecked(fAncestorVisible);
            fAncestorItem = new ActionContributionItem(a);
            fAncestorItem.setVisible(false);
            tbm.appendToGroup("modes", fAncestorItem); //$NON-NLS-1$
            tbm.getControl().addDisposeListener(a);

            createToolItems(tbm);
            updateToolItems();

            tbm.update(true);
        }
    }

    /**
     * Callback that is invoked when the control of this merge viewer is given
     * focus. This method should return <code>true</code> if a particular widget
     * was given focus and false otherwise. By default, <code>false</code> is
     * returned. Subclasses may override.
     * 
     * @return whether particular widget was given focus
     * @since 3.3
     */
    protected boolean handleSetFocus() {
        return false;
    }

    /**
     * Return the desired width of the center control. This width is used to
     * calculate the values used to layout the ancestor, left and right sides.
     * 
     * @return the desired width of the center control
     * @see #handleResizeLeftRight(int, int, int, int, int, int)
     * @see #handleResizeAncestor(int, int, int, int)
     * @since 3.3
     */
    protected int getCenterWidth() {
        return 3;
    }

    /**
     * Return whether the ancestor pane is visible or not.
     * 
     * @return whether the ancestor pane is visible or not
     * @since 3.3
     */
    protected boolean isAncestorVisible() {
        return fAncestorVisible;
    }

    /**
     * Create the control that divides the left and right sides of the merge
     * viewer.
     * 
     * @param parent
     *            the parent composite
     * @return the center control
     * @since 3.3
     */
    protected Control createCenterControl(Composite parent) {
        Sash sash = new Sash(parent, SWT.VERTICAL);
        new Resizer(sash, HORIZONTAL);
        return sash;
    }

    /**
     * Return the center control that divides the left and right sides of the
     * merge viewer. This method returns the control that was created by calling
     * {@link #createCenterControl(Composite)}.
     * 
     * @see #createCenterControl(Composite)
     * @return the center control
     * @since 3.3
     */
    protected Control getCenterControl() {
        return fCenter;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.viewers.Viewer#getControl()
     */
    public Control getControl() {
        return fComposite;
    }

    /**
     * Called on the viewer disposal. Unregisters from the compare
     * configuration. Clients may extend if they have to do additional cleanup.
     * 
     * @see org.eclipse.jface.viewers.ContentViewer#handleDispose(org.eclipse.swt.events.DisposeEvent)
     */
    protected void handleDispose(DisposeEvent event) {

        if (fHandlerService != null)
            fHandlerService.dispose();

        Object input = getInput();
        if (input instanceof ICompareInput) {
            ICompareContainer container = getCompareConfiguration().getContainer();
            container.removeCompareInputChangeListener((ICompareInput) input, fCompareInputChangeListener);
        }
        if (input != null) {
            ICompareInputLabelProvider lp = getCompareConfiguration().getLabelProvider();
            if (lp != null)
                lp.removeListener(labelChangeListener);
        }

        if (fPropertyChangeListener != null) {
            fCompareConfiguration.removePropertyChangeListener(fPropertyChangeListener);
            fPropertyChangeListener = null;
        }

        fAncestorLabel = null;
        fLeftLabel = null;
        fDirectionLabel = null;
        fRightLabel = null;
        fCenter = null;

        if (fRightArrow != null) {
            fRightArrow.dispose();
            fRightArrow = null;
        }
        if (fLeftArrow != null) {
            fLeftArrow.dispose();
            fLeftArrow = null;
        }
        if (fBothArrow != null) {
            fBothArrow.dispose();
            fBothArrow = null;
        }

        if (fNormalCursor != null) {
            fNormalCursor.dispose();
            fNormalCursor = null;
        }
        if (fHSashCursor != null) {
            fHSashCursor.dispose();
            fHSashCursor = null;
        }
        if (fVSashCursor != null) {
            fVSashCursor.dispose();
            fVSashCursor = null;
        }
        if (fHVSashCursor != null) {
            fHVSashCursor.dispose();
            fHVSashCursor = null;
        }

        super.handleDispose(event);
    }

    /**
     * Updates the enabled state of the toolbar items.
     * <p>
     * This method is called whenever the state of the items needs updating.
     * <p>
     * Subclasses may extend this method, although this is generally not
     * required.
     */
    protected void updateToolItems() {

        IMergeViewerContentProvider content = getMergeContentProvider();
        if (content == null) {
            return;
        }
        Object input = getInput();

        if (fCopyLeftToRightAction != null) {
            boolean enable = content.isRightEditable(input);
            // if (enable && input instanceof ICompareInput) {
            // ITypedElement e= ((ICompareInput) input).getLeft();
            // if (e == null)
            // enable= false;
            // }
            fCopyLeftToRightAction.setEnabled(enable);
        }

        if (fCopyRightToLeftAction != null) {
            boolean enable = content.isLeftEditable(input);
            // if (enable && input instanceof ICompareInput) {
            // ITypedElement e= ((ICompareInput) input).getRight();
            // if (e == null)
            // enable= false;
            // }
            fCopyRightToLeftAction.setEnabled(enable);
        }
    }

    /**
     * Updates the headers of the three areas by querying the content provider
     * for a name and image for the three sides of the input object.
     * <p>
     * This method is called whenever the header must be updated.
     * <p>
     * Subclasses may extend this method, although this is generally not
     * required.
     */
    protected void updateHeader() {

        IMergeViewerContentProvider content = getMergeContentProvider();
        if (content == null) {
            return;
        }
        Object input = getInput();

        // Only change a label if there is a new label available
        if (fAncestorLabel != null) {
            Image ancestorImage = content.getAncestorImage(input);
            if (ancestorImage != null)
                fAncestorLabel.setImage(ancestorImage);
            String ancestorLabel = content.getAncestorLabel(input);
            if (ancestorLabel != null)
                fAncestorLabel.setText(TextProcessor.process(ancestorLabel));
        }
        if (fLeftLabel != null) {
            Image leftImage = content.getLeftImage(input);
            if (leftImage != null)
                fLeftLabel.setImage(leftImage);
            String leftLabel = content.getLeftLabel(input);
            if (leftLabel != null)
                fLeftLabel.setText(TextProcessor.process(leftLabel));
        }
        if (fRightLabel != null) {
            Image rightImage = content.getRightImage(input);
            if (rightImage != null)
                fRightLabel.setImage(rightImage);
            String rightLabel = content.getRightLabel(input);
            if (rightLabel != null)
                fRightLabel.setText(TextProcessor.process(rightLabel));
        }
    }

    /*
     * Calculates the height of the header.
     */
    /* package */int getHeaderHeight() {
        int headerHeight = fLeftLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y;
        headerHeight = Math.max(headerHeight, fDirectionLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y);
        return headerHeight;
    }

    // ---- dirty state & saving state

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.compare.IPropertyChangeNotifier#addPropertyChangeListener
     * (org.eclipse.jface.util.IPropertyChangeListener)
     */
    public void addPropertyChangeListener(IPropertyChangeListener listener) {
        if (fListenerList == null)
            fListenerList = new ListenerList();
        fListenerList.add(listener);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.eclipse.compare.IPropertyChangeNotifier#removePropertyChangeListener
     * (org.eclipse.jface.util.IPropertyChangeListener)
     */
    public void removePropertyChangeListener(IPropertyChangeListener listener) {
        if (fListenerList != null) {
            fListenerList.remove(listener);
            if (fListenerList.isEmpty())
                fListenerList = null;
        }
    }

    private void fireDirtyState(boolean state) {
        Utilities.firePropertyChange(fListenerList, cmv, CompareEditorInput.DIRTY_STATE, null, new Boolean(state));
    }

    /**
     * Sets the dirty state of the left side of this viewer. If the new value
     * differs from the old all registered listener are notified with a
     * <code>PropertyChangeEvent</code> with the property name
     * <code>CompareEditorInput.DIRTY_STATE</code>.
     * 
     * @param dirty
     *            the state of the left side dirty flag
     */
    protected void setLeftDirty(boolean dirty) {
        if (isLeftDirty() != dirty) {
            fLeftSaveAction.setEnabled(dirty);
            // Only fire the event if the combined dirty state has changed
            if ((!isRightDirty() && !isLeftDirty()) || (!isRightDirty() && isLeftDirty()))
                fireDirtyState(dirty);
        }
    }

    /**
     * Sets the dirty state of the right side of this viewer. If the new value
     * differs from the old all registered listener are notified with a
     * <code>PropertyChangeEvent</code> with the property name
     * <code>CompareEditorInput.DIRTY_STATE</code>.
     * 
     * @param dirty
     *            the state of the right side dirty flag
     */
    protected void setRightDirty(boolean dirty) {
        if (isRightDirty() != dirty) {
            fRightSaveAction.setEnabled(dirty);
            // Only fire the event if the combined dirty state has changed
            if ((!isRightDirty() && !isLeftDirty()) || (isRightDirty() && !isLeftDirty()))
                fireDirtyState(dirty);
        }
    }

    /**
     * Method from the old internal <code>ISavable</code> interface Save the
     * viewers's content. Note: this method is for internal use only. Clients
     * should not call this method.
     * 
     * @param monitor
     *            a progress monitor
     * @throws CoreException
     * @deprecated use {@link IFlushable#flush(IProgressMonitor)}.
     */
    public void save(IProgressMonitor monitor) throws CoreException {
        flush(monitor);
    }

    /**
     * Flush any modifications made in the viewer into the compare input. This
     * method calls {@link #flushContent(Object, IProgressMonitor)} with the
     * compare input of the viewer as the first parameter.
     * 
     * @param monitor
     *            a progress monitor
     * @see org.eclipse.compare.contentmergeviewer.IFlushable#flush(org.eclipse.core.runtime.IProgressMonitor)
     * @since 3.3
     */
    public final void flush(IProgressMonitor monitor) {
        flushContent(getInput(), monitor);
    }

    /**
     * Flush the modified content back to input elements via the content
     * provider. The provided input may be the current input of the viewer or it
     * may be the previous input (i.e. this method may be called to flush
     * modified content during an input change).
     * 
     * @param input
     *            the compare input
     * @param monitor
     *            a progress monitor or <code>null</code> if the method was call
     *            from a place where a progress monitor was not available.
     * @since 3.3
     */
    protected void flushContentOld(Object input, IProgressMonitor monitor) {

        // write back modified contents
        IMergeViewerContentProvider content = (IMergeViewerContentProvider) getContentProvider();
        if (content == null) {
            return;
        }

        boolean leftEmpty = content.getLeftContent(input) == null;
        boolean rightEmpty = content.getRightContent(input) == null;

        if (getCompareConfiguration().isLeftEditable() && isLeftDirty()) {
            byte[] bytes = getContents(true);
            if (rightEmpty && bytes != null && bytes.length == 0)
                bytes = null;
            setLeftDirty(false);
            content.saveLeftContent(input, bytes);
        }

        if (getCompareConfiguration().isRightEditable() && isRightDirty()) {
            byte[] bytes = getContents(false);
            if (leftEmpty && bytes != null && bytes.length == 0)
                bytes = null;
            setRightDirty(false);
            content.saveRightContent(input, bytes);
        }
    }

    protected void flushContent(Object input, IProgressMonitor monitor) {
        flushLeftSide(input, monitor);
        flushRightSide(input, monitor);
    }

    void flushLeftSide(Object input, IProgressMonitor monitor) {
        IMergeViewerContentProvider content = (IMergeViewerContentProvider) getContentProvider();
        if (content == null) {
            return;
        }

        boolean rightEmpty = content.getRightContent(input) == null;

        if (getCompareConfiguration().isLeftEditable() && isLeftDirty()) {
            byte[] bytes = getContents(true);
            if (rightEmpty && bytes != null && bytes.length == 0)
                bytes = null;
            setLeftDirty(false);
            content.saveLeftContent(input, bytes);
        }
    }

    void flushRightSide(Object input, IProgressMonitor monitor) {
        IMergeViewerContentProvider content = (IMergeViewerContentProvider) getContentProvider();
        if (content == null) {
            return;
        }

        boolean leftEmpty = content.getLeftContent(input) == null;

        if (getCompareConfiguration().isRightEditable() && isRightDirty()) {
            byte[] bytes = getContents(false);
            if (leftEmpty && bytes != null && bytes.length == 0)
                bytes = null;
            setRightDirty(false);
            content.saveRightContent(input, bytes);
        }
    }

    /**
     * @param monitor
     * @noreference This method is not intended to be referenced by clients.
     */
    public void flushLeft(IProgressMonitor monitor) {
        flushLeftSide(getInput(), monitor);
    }

    /**
     * @param monitor
     * @noreference This method is not intended to be referenced by clients.
     */
    public void flushRight(IProgressMonitor monitor) {
        flushRightSide(getInput(), monitor);
    }

    /**
     * Return the dirty state of the right side of this viewer.
     * 
     * @return the dirty state of the right side of this viewer
     * @since 3.3
     */
    protected boolean isRightDirty() {
        return fRightSaveAction.isEnabled();
    }

    /**
     * Return the dirty state of the left side of this viewer.
     * 
     * @return the dirty state of the left side of this viewer
     * @since 3.3
     */
    protected boolean isLeftDirty() {
        return fLeftSaveAction.isEnabled();
    }

    /**
     * Handle a change to the given input reported from an
     * {@link org.eclipse.compare.structuremergeviewer.ICompareInputChangeListener}
     * . This class registers a listener with its input and reports any change
     * events through this method. Subclasses may override.
     * 
     * @since 3.3
     */
    protected void handleCompareInputChange() {
        // before setting the new input we have to save the old
        Object input = getInput();
        if (isLeftDirty() || isRightDirty()) {
            flushContent(input, null);
        }
        refresh();
    }
}