/******************************************************************************* * Copyright (c) 2000, 2020 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Alex Blewitt - replace new Boolean with Boolean.valueOf - https://bugs.eclipse.org/470344 * Stefan Xenos (Google) - bug 448968 - Add diagnostic logging * Conrad Groth - Bug 213780 - Compare With direction should be configurable *******************************************************************************/ package org.eclipse.compare.contentmergeviewer; import java.io.IOException; import java.util.ResourceBundle; import org.eclipse.compare.CompareConfiguration; import org.eclipse.compare.CompareEditorInput; import org.eclipse.compare.CompareUI; import org.eclipse.compare.CompareViewerPane; import org.eclipse.compare.ICompareContainer; import org.eclipse.compare.ICompareInputLabelProvider; import org.eclipse.compare.IPropertyChangeNotifier; import org.eclipse.compare.internal.ChangePropertyAction; import org.eclipse.compare.internal.CompareEditor; import org.eclipse.compare.internal.CompareHandlerService; import org.eclipse.compare.internal.CompareMessages; import org.eclipse.compare.internal.ComparePreferencePage; import org.eclipse.compare.internal.CompareUIPlugin; import org.eclipse.compare.internal.ICompareUIConstants; import org.eclipse.compare.internal.IFlushable2; import org.eclipse.compare.internal.ISavingSaveable; import org.eclipse.compare.internal.MergeViewerContentProvider; import org.eclipse.compare.internal.MirroredMergeViewerContentProvider; import org.eclipse.compare.internal.Policy; import org.eclipse.compare.internal.Utilities; import org.eclipse.compare.internal.ViewerSwitchingCancelled; 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.IToolBarManager; import org.eclipse.jface.action.LegacyActionTools; 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.preference.IPersistentPreferenceStore; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ContentViewer; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.window.Window; import org.eclipse.osgi.util.TextProcessor; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; 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.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Layout; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.ISaveablesSource; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.Saveable; /** * 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. *

* ContentMergeViewer *

* *

* A ContentMergeViewer accesses its model by means of a content * provider which must implement the IMergeViewerContentProvider * interface. *

*

* Clients may wish to use the standard concrete subclass * TextMergeViewer, or define their own subclass. *

* * @see IMergeViewerContentProvider * @see TextMergeViewer */ public abstract class ContentMergeViewer extends ContentViewer implements IPropertyChangeNotifier, IFlushable, IFlushable2 { /* 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 { @Override public Point computeSize(Composite c, int w, int h, boolean force) { return new Point(100, 100); } @Override public void layout(Composite composite, boolean force) { if (fLeftLabel == null) { if (composite.isDisposed()) { CompareUIPlugin .log(new IllegalArgumentException("Attempted to perform a layout on a disposed composite")); //$NON-NLS-1$ } if (Policy.debugContentMergeViewer) { logTrace("found bad label. Layout = " + System.identityHashCode(this) + ". composite = " //$NON-NLS-1$//$NON-NLS-2$ + System.identityHashCode(composite) + ". fComposite = " //$NON-NLS-1$ + System.identityHashCode(fComposite) + ". fComposite.isDisposed() = " //$NON-NLS-1$ + fComposite.isDisposed()); logStackTrace(); } // Help to find out the cause for bug 449558 NullPointerException npe= new NullPointerException("fLeftLabel is 'null';fLeftLabelSet is " + fLeftLabelSet + ";fComposite.isDisposed() is " + fComposite.isDisposed()); //$NON-NLS-1$ //$NON-NLS-2$ // Allow to test whether doing nothing helps if (Boolean.getBoolean("ContentMergeViewer.DEBUG")) { //$NON-NLS-1$ CompareUIPlugin.log(npe); return; } throw npe; } // 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( e -> fControl= null ); } @Override public void mouseDoubleClick(MouseEvent e) { if ((fDirection & HORIZONTAL) != 0) fHSplit= -1; if ((fDirection & VERTICAL) != 0) fVSplit= VSPLIT; fComposite.layout(true); } @Override 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; } @Override public void mouseUp(MouseEvent e) { fIsDown= false; if (!fLiveResize) resize(e); } @Override 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 IPropertyChangeListener fPreferenceChangeListener; 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 ActionContributionItem copyLeftToRightItem; // copy from left to right private ActionContributionItem copyRightToLeftItem; // copy from right to left private boolean fIsLeftDirty; private boolean fIsRightDirty; private CompareHandlerService fHandlerService; private final MergeViewerContentProvider fDefaultContentProvider; private Action fSwitchLeftAndRight; // SWT widgets /* package */ Composite fComposite; private CLabel fAncestorLabel; private CLabel fLeftLabel; private boolean fLeftLabelSet= false; // needed for debug output for bug 449558 private CLabel fRightLabel; /* package */ CLabel fDirectionLabel; /* package */ Control fCenter; //---- SWT resources to be disposed private Image fRightArrow; private Image fLeftArrow; private Image fBothArrow; private Cursor fNormalCursor; private Cursor fHSashCursor; private Cursor fVSashCursor; private Cursor fHVSashCursor; private ILabelProviderListener labelChangeListener = event -> { Object[] elements = event.getElements(); for (Object object : elements) { 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) { if (Policy.debugContentMergeViewer) { logTrace("constructed (fLeftLabel == null)"); //$NON-NLS-1$ logStackTrace(); } 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); fCompareInputChangeListener = input -> { if (input == getInput()) handleCompareInputChange(); }; // Make sure the compare configuration is not null fCompareConfiguration = cc != null ? cc : new CompareConfiguration(); fPropertyChangeListener = this::handlePropertyChangeEvent; fCompareConfiguration.addPropertyChangeListener(fPropertyChangeListener); fPreferenceChangeListener = event -> { if (event.getProperty().equals(ComparePreferencePage.SWAPPED)) { getCompareConfiguration().setProperty(CompareConfiguration.MIRRORED, event.getNewValue()); updateContentProvider(); updateToolItems(); } }; cc.getPreferenceStore().addPropertyChangeListener(fPreferenceChangeListener); fDefaultContentProvider = new MergeViewerContentProvider(fCompareConfiguration); updateContentProvider(); fIsLeftDirty = false; fIsRightDirty = false; } private void logStackTrace() { new Exception(" in " + getClass().getName()).printStackTrace(System.out); //$NON-NLS-1$ } private void logTrace(String string) { System.out.println("ContentMergeViewer " + System.identityHashCode(this) + ": " + string); //$NON-NLS-1$//$NON-NLS-2$ } //---- 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 * updateContent. * * @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 ToolBarManager. * It is called when this viewer is installed in its container and if the container * has a ToolBarManager. * The ContentMergeViewer 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 true, the left side is copied to the right side; * if false, 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 null can be returned. * * @param left if true, the byte contents of the left area is returned; * if false, the byte contents of the right area * @return the content as an array of bytes, or null */ 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. * * @return the compare configuration, never null */ protected CompareConfiguration getCompareConfiguration() { return fCompareConfiguration; } /** * The ContentMergeViewer implementation of this * ContentViewer method * checks to ensure that the content provider is an IMergeViewerContentProvider. * @param contentProvider the content provider to set. Must implement IMergeViewerContentProvider. */ @Override public void setContentProvider(IContentProvider contentProvider) { Assert.isTrue(contentProvider instanceof IMergeViewerContentProvider); super.setContentProvider(contentProvider); } private void updateContentProvider() { setContentProvider(getCompareConfiguration().isMirrored() ? new MirroredMergeViewerContentProvider(getCompareConfiguration(), fDefaultContentProvider) : fDefaultContentProvider); } /* package */ IMergeViewerContentProvider getMergeContentProvider() { return (IMergeViewerContentProvider) getContentProvider(); } /** * The ContentMergeViewer implementation of this * Viewer method returns the empty selection. Subclasses may override. * @return empty selection. */ @Override public ISelection getSelection() { return () -> true; } /** * The ContentMergeViewer implementation of this * Viewer method does nothing. Subclasses may reimplement. * @see org.eclipse.jface.viewers.Viewer#setSelection(org.eclipse.jface.viewers.ISelection, boolean) */ @Override 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=c.getDisplay().getSystemCursor(SWT.CURSOR_SIZENS); cursor= fVSashCursor; } else { if (fNormalCursor == null) fNormalCursor= c.getDisplay().getSystemCursor(SWT.CURSOR_ARROW); cursor= fNormalCursor; } break; case HORIZONTAL: if (fHSashCursor == null) fHSashCursor= c.getDisplay().getSystemCursor(SWT.CURSOR_SIZEWE); cursor= fHSashCursor; break; case VERTICAL + HORIZONTAL: if (fAncestorVisible) { if (fHVSashCursor == null) fHVSashCursor= c.getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL); cursor= fHVSashCursor; } else { if (fHSashCursor == null) fHSashCursor= c.getDisplay().getSystemCursor(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, Boolean.valueOf(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. *

* The ContentMergeViewer implementation of this Viewer * method tries to save the old input by calling doSave(...) and * then calls internalRefresh(...). * * @param input the new input of this viewer, or null if there is no new input * @param oldInput the old input element, or null if there was previously no input */ @Override 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 Viewer method inputChanged * to save any unsaved changes of the old input. *

* The ContentMergeViewer implementation of this * method calls saveContent(...). If confirmation has been turned on * with setConfirmSave(true), a confirmation alert is posted before saving. *

* 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 null if there is no new input * @param oldInput the old input element, or null if there was previously no input * @return true 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 doSave(Object, Object) asks for confirmation before saving * the old input with saveContent(Object). * @param enable a value of true enables confirmation * @since 2.0 */ public void setConfirmSave(boolean enable) { fConfirmSave= enable; } @Override public void refresh() { internalRefresh(getInput()); } private void internalRefresh(Object input) { IMergeViewerContentProvider content= getMergeContentProvider(); if (content != null) { 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); updateHeader(); if (Utilities.okToUse(fComposite) && Utilities.okToUse(fComposite.getParent())) { ToolBarManager tbm = (ToolBarManager) getToolBarManager(fComposite.getParent()); if (tbm != null ) { updateToolItems(); tbm.update(true); tbm.getControl().getParent().layout(true); } } } } @Override protected void hookControl(Control control) { if (Policy.debugContentMergeViewer) { logTrace("Attached dispose listener to control " + System.identityHashCode(control)); //$NON-NLS-1$ } super.hookControl(control); } //---- layout & SWT control creation /** * Builds the SWT controls for the three areas of a compare/merge viewer. *

* Calls the hooks createControls and createToolItems * to let subclasses build the specific content areas and to add items to * an enclosing toolbar. *

* 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 @Override public boolean setFocus() { return ContentMergeViewer.this.handleSetFocus(); } }; fComposite.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle()); hookControl(fComposite); // Hook help & dispose listener. fComposite.setLayout(new ContentMergeViewerLayout()); if (Policy.debugContentMergeViewer) { logTrace("Created composite " + System.identityHashCode(fComposite) + " with layout " //$NON-NLS-1$//$NON-NLS-2$ + System.identityHashCode(fComposite.getLayout())); logStackTrace(); } int style= SWT.SHADOW_OUT; fAncestorLabel= new CLabel(fComposite, style | Window.getDefaultOrientation()); fLeftLabel= new CLabel(fComposite, style | Window.getDefaultOrientation()); if (Policy.debugContentMergeViewer) { logTrace("fLeftLabel initialized"); //$NON-NLS-1$ logStackTrace(); } fLeftLabelSet= true; 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; } /** * Returns the toolbar manager for this viewer. * * Subclasses may extend this method and use either the toolbar manager * provided by the inherited method by calling * super.getToolBarManager(parent) or provide an alternate toolbar manager. * * @param parent * a Composite or null * @return a IToolBarManager * @since 3.4 */ protected IToolBarManager getToolBarManager(Composite parent) { return CompareViewerPane.getToolBarManager(parent); } private void initializeToolbars(Composite parent) { ToolBarManager tbm = (ToolBarManager) 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$ copyLeftToRightItem= createCopyAction(true); Utilities.initAction(copyLeftToRightItem.getAction(), getResourceBundle(), "action.CopyLeftToRight."); //$NON-NLS-1$ tbm.appendToGroup("merge", copyLeftToRightItem); //$NON-NLS-1$ fHandlerService.registerAction(copyLeftToRightItem.getAction(), "org.eclipse.compare.copyAllLeftToRight"); //$NON-NLS-1$ copyRightToLeftItem= createCopyAction(false); Utilities.initAction(copyRightToLeftItem.getAction(), getResourceBundle(), "action.CopyRightToLeft."); //$NON-NLS-1$ tbm.appendToGroup("merge", copyRightToLeftItem); //$NON-NLS-1$ fHandlerService.registerAction(copyRightToLeftItem.getAction(), "org.eclipse.compare.copyAllRightToLeft"); //$NON-NLS-1$ fSwitchLeftAndRight = new Action() { @Override public void run() { /* * Bug 552352: When comparing .txt files with corresponding .txt file editors * closed and user modifies the content of one of the .txt file in the compare * view and the compare state becomes 'dirty' and if followed by a 'Swap' * action, modifications get lost. This data loss is considered a severe * problem, so to avoid this modified data loss in this scenario, show a * confirmation dialog to 'save' the file before swapping and let user decide * and take a call on this. Note: Issue not seen with .java & .properties files. */ doSave(null, getInput()); IPreferenceStore preferences = getCompareConfiguration().getPreferenceStore(); preferences.setValue(ComparePreferencePage.SWAPPED, !getCompareConfiguration().isMirrored()); if (preferences instanceof IPersistentPreferenceStore) { try { ((IPersistentPreferenceStore) preferences).save(); } catch (IOException e) { CompareUIPlugin.log(e); } } } }; Utilities.initAction(fSwitchLeftAndRight, getResourceBundle(), "action.SwitchLeftAndRight."); //$NON-NLS-1$ tbm.appendToGroup("modes", fSwitchLeftAndRight); //$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); } } private ActionContributionItem createCopyAction(boolean leftToRight) { return new ActionContributionItem(new Action() { @Override public void run() { copy(leftToRight); } }); } /** * Callback that is invoked when the control of this merge viewer is given focus. * This method should return true if a particular widget was given focus * and false otherwise. By default, false 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; } @Override 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) */ @Override 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; } if (fPreferenceChangeListener != null) { fCompareConfiguration.getPreferenceStore().removePropertyChangeListener(fPreferenceChangeListener); fPreferenceChangeListener= null; } fAncestorLabel= null; fLeftLabel= null; if (Policy.debugContentMergeViewer) { logTrace("handleDispose(...) - fLeftLabel = null. event.widget = " + System.identityHashCode(event.widget)); //$NON-NLS-1$ logStackTrace(); } 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; } super.handleDispose(event); } /** * Updates the enabled state of the toolbar items. *

* This method is called whenever the state of the items needs updating. *

* Subclasses may extend this method, although this is generally not required. */ protected void updateToolItems() { IMergeViewerContentProvider content= getMergeContentProvider(); Object input= getInput(); if (copyLeftToRightItem != null) { boolean rightEditable = content.isRightEditable(input); copyLeftToRightItem.setVisible(rightEditable); copyLeftToRightItem.getAction().setEnabled(rightEditable); } if (copyRightToLeftItem != null) { boolean leftEditable = content.isLeftEditable(input); copyRightToLeftItem.setVisible(leftEditable); copyRightToLeftItem.getAction().setEnabled(leftEditable); } if (fSwitchLeftAndRight != null) { fSwitchLeftAndRight.setChecked(getCompareConfiguration().isMirrored()); } } /** * 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. *

* This method is called whenever the header must be updated. *

* Subclasses may extend this method, although this is generally not required. */ protected void updateHeader() { IMergeViewerContentProvider content= getMergeContentProvider(); 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(LegacyActionTools.escapeMnemonics(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(LegacyActionTools.escapeMnemonics(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(LegacyActionTools.escapeMnemonics(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 @Override public void addPropertyChangeListener(IPropertyChangeListener listener) { if (fListenerList == null) fListenerList= new ListenerList<>(); fListenerList.add(listener); } @Override public void removePropertyChangeListener(IPropertyChangeListener listener) { if (fListenerList != null) { fListenerList.remove(listener); if (fListenerList.isEmpty()) fListenerList= null; } } private void fireDirtyState(boolean state) { Utilities.firePropertyChange(fListenerList, this, CompareEditorInput.DIRTY_STATE, null, Boolean.valueOf(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 PropertyChangeEvent with the * property name CompareEditorInput.DIRTY_STATE. * * @param dirty the state of the left side dirty flag */ protected void setLeftDirty(boolean dirty) { if (isLeftDirty() != dirty) { fIsLeftDirty = dirty; // Always fire the event if the dirty state has changed 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 PropertyChangeEvent with the * property name CompareEditorInput.DIRTY_STATE. * * @param dirty the state of the right side dirty flag */ protected void setRightDirty(boolean dirty) { if (isRightDirty() != dirty) { fIsRightDirty = dirty; // Always fire the event if the dirty state has changed fireDirtyState(dirty); } } /** * Method from the old internal ISavable 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 not thrown anymore * @deprecated use {@link IFlushable#flush(IProgressMonitor)}. */ @Deprecated 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 */ @Override public final void flush(IProgressMonitor monitor) { flushContent(getInput(), monitor); } /** * Flushes 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 null if the method * was call from a place where a progress monitor was not available. * @since 3.3 */ protected void flushContent(Object input, IProgressMonitor monitor) { flushLeftSide(input, monitor); flushRightSide(input, monitor); } void flushLeftSide(Object input, IProgressMonitor monitor) { IMergeViewerContentProvider content = (IMergeViewerContentProvider) getContentProvider(); 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(); 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 The progress monitor to report progress. * @noreference This method is not intended to be referenced by clients. */ @Override public void flushLeft(IProgressMonitor monitor) { flushLeftSide(getInput(), monitor); } /** * @param monitor The progress monitor to report progress. * @noreference This method is not intended to be referenced by clients. */ @Override 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 fIsRightDirty; } /** * @return the dirty state of the right side of this viewer * @since 3.7 * @noreference This method is not intended to be referenced by clients. */ public boolean internalIsRightDirty() { return isRightDirty(); } /** * 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 fIsLeftDirty; } /** * @return the dirty state of the left side of this viewer * @since 3.7 * @noreference This method is not intended to be referenced by clients. */ public boolean internalIsLeftDirty() { return isLeftDirty(); } /** * 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. By default, this method prompts for any unsaved changes and then refreshes * the viewer. Subclasses may override. * @since 3.3 */ protected void handleCompareInputChange() { // Before setting the new input we have to save the old. Object input = getInput(); if (!isSaving() && (isLeftDirty() || isRightDirty())) { if (Utilities.RUNNING_TESTS) { if (Utilities.TESTING_FLUSH_ON_COMPARE_INPUT_CHANGE) { flushContent(input, null); } } else { // post alert Shell shell= fComposite.getShell(); MessageDialog dialog= new MessageDialog(shell, CompareMessages.ContentMergeViewer_resource_changed_title, null, // accept the default window icon CompareMessages.ContentMergeViewer_resource_changed_description, MessageDialog.QUESTION, new String[] { IDialogConstants.YES_LABEL, // 0 IDialogConstants.NO_LABEL, // 1 }, 0); // default button index switch (dialog.open()) { // open returns index of pressed button case 0: flushContent(input, null); break; case 1: setLeftDirty(false); setRightDirty(false); break; } } } if (isSaving() && (isLeftDirty() || isRightDirty())) { return; // Do not refresh until saving both sides is complete. } refresh(); } CompareHandlerService getCompareHandlerService() { return fHandlerService; } /** * @return true if any of the Saveables is being saved */ private boolean isSaving() { ICompareContainer container = fCompareConfiguration.getContainer(); ISaveablesSource source = null; if (container instanceof ISaveablesSource) { source = (ISaveablesSource) container; } else { IWorkbenchPart part = container.getWorkbenchPart(); if (part instanceof ISaveablesSource) { source = (ISaveablesSource) part; } } if (source != null) { Saveable[] saveables = source.getSaveables(); for (Saveable s : saveables) { if (s instanceof ISavingSaveable) { ISavingSaveable saveable = (ISavingSaveable) s; if (saveable.isSaving()) return true; } } } return false; } /** * If the inputs are mirrored, this asks the right model value. * * @return true if the left viewer is editable * @since 3.7 */ protected boolean isLeftEditable() { return fCompareConfiguration.isMirrored() ? fCompareConfiguration.isRightEditable() : fCompareConfiguration.isLeftEditable(); } /** * If the inputs are mirrored, this asks the left model value. * * @return true if the right viewer is editable * @since 3.7 */ protected boolean isRightEditable() { return fCompareConfiguration.isMirrored() ? fCompareConfiguration.isLeftEditable() : fCompareConfiguration.isRightEditable(); } }