blob: 0a485137ab3c73ff80dc499989808b2ae3fc59bf [file] [log] [blame]
/*
* Licensed Materials - Property of IBM,
* WebSphere Studio Workbench
* (c) Copyright IBM Corp 2001
*/
package org.eclipse.compare;
import java.util.HashMap;
import java.util.ResourceBundle;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashSet;
import java.text.*;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Button;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.util.Assert;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFileState;
import org.eclipse.compare.internal.*;
import org.eclipse.compare.structuremergeviewer.*;
/**
* A dialog where one input element can be compared against
* a list of historic variants (editions) of the same input element.
* The dialog can be used to implement functions like "Replace with Version" or
* "Replace with Edition" on workbench resources.
* <p>
* In addition it is possible to specify a subsection of the input element
* (e.g. a method in a Java source file) by means of a "path".
* In this case the dialog compares only the subsection (as specified by the path)
* with the corresponding subsection in the list of editions.
* Only those editions are shown where the subsection differs from the same subsection in
* another edition thereby minimizing the number of presented variants.
* This functionality can be used to implement "Replace with Method Edition"
* for the Java language.
* <p>
* Subsections of an input element are determined by first finding an
* <code>IStructureCreator</code> for the input's type.
* Then the method <code>locate</code> is used to extract the subsection.
* <p>
* Each edition (variant in the list of variants) must implement the <code>IModificationDate</code> interface
* so that the dialog can sort the editions and present them in a tree structure where every
* node corresponds one day.
* <p>
* The functionality is surfaced in a single function <code>selectEdition</code>.
* <p>
* Clients may instantiate this class; it is not intended to be subclassed.
* </p>
*
* @see IModificationDate
* @see ITypedElement
*/
public class EditionSelectionDialog extends Dialog {
private static class Pair {
private ITypedElement fEdition;
private ITypedElement fItem;
private String fContent;
Pair(ITypedElement edition, ITypedElement item, String content) {
fEdition= edition;
fItem= item;
fContent= content;
}
ITypedElement getEdition() {
return fEdition;
}
ITypedElement getItem() {
return fItem;
}
public boolean equals(Object other) {
if (other != null && other.getClass() == getClass()) {
String otherContent= ((Pair)other).fContent;
if (otherContent != null && fContent != null && fContent.equals(otherContent))
return true;
}
return super.equals(other);
}
}
private static final int ONE_DAY_MS= 86400 * 1000; // one day in milli seconds
private static final boolean HIDE_IDENTICAL= true;
private Button fCommitButton;
private boolean fReplaceMode= true;
private ResourceBundle fBundle;
private boolean fTargetIsRight;
/**
* Maps from members to their corresponding editions.
* Has only a single entry if dialog is used in "Replace" (and not "Add") mode.
*/
private HashMap fMemberEditions;
/** The editions of the current selected member */
private List fCurrentEditions;
private Thread fThread;
private ITypedElement fTargetItem;
/** The selected edition in the edition viewer */
private ITypedElement fSelectedItem;
private Table fMemberTable;
private Pane fMemberPane;
private Tree fEditionTree;
private Pane fEditionPane;
private Image fDateImage;
private Image fTimeImage;
private CompareViewerSwitchingPane fContentPane;
private CompareConfiguration fCompareConfiguration;
/**
* Creates a new modal, resizable dialog.
* Various titles, icons, and labels are configured from the given resource bundle.
* The following resource keys are used:
* <pre>
* key type description
* title String dialog title
* width Integer initial width of dialog
* height Integer initial height of dialog
* mode String "replace" or "add"; default is "replace"
* targetSide String whether target is on "right" or "left" side; default is "right"
* treeTitleFormat MessageFormat pane title for edition tree; arg 0 is the target
* dateIcon String icon for node in edition tree; path relative to plugin
* timeIcon String icon for leaf in edition tree; path relative to plugin
* todayFormat MessageFormat format string if date is todays date; arg 0 is date
* yesterdayFormat MessageFormat format string if date is yesterdays date; arg 0 is date
* dayFormat MessageFormat format string if date is any other date; arg 0 is date
* editionLabel String label for editions side of compare viewer; arg 0 is the date
* targetLabel String label for target side of compare viewer
* buttonLabel String label for OK button; default is IDialogConstants.OK_LABEL
* </pre>
*
* @param parent if not <code>null</code> the new dialog stays on top of this parent shell
* @param bundle <code>ResourceBundle</code> to configure the dialog
*/
public EditionSelectionDialog(Shell parent, ResourceBundle bundle) {
super(parent);
setShellStyle(SWT.CLOSE | SWT.APPLICATION_MODAL | SWT.RESIZE);
fBundle= bundle;
fTargetIsRight= "right".equals(Utilities.getString(fBundle, "targetSide", "right"));
fReplaceMode= "replace".equals(Utilities.getString(fBundle, "mode", "replace"));
fCompareConfiguration= new CompareConfiguration();
fCompareConfiguration.setLeftEditable(false);
fCompareConfiguration.setRightEditable(false);
String iconName= Utilities.getString(fBundle, "dateIcon", "obj16/day_obj.gif");
ImageDescriptor id= CompareUIPlugin.getImageDescriptor(iconName);
if (id != null)
fDateImage= id.createImage();
iconName= Utilities.getString(fBundle, "timeIcon", "obj16/resource_obj.gif");
id= CompareUIPlugin.getImageDescriptor(iconName);
if (id != null)
fTimeImage= id.createImage();
}
/**
* Presents this modal dialog with the functionality described in the class comment above.
*
* @param target the input object against which the editions are compared; must not be <code>null</code>
* @param editions the list of editions (element type: <code>ITypedElement</code>s)
* @param path If <code>null</code> dialog shows full input; if non <code>null</code> it extracts a subsection
* @return returns the selected edition or <code>null</code> if dialog was cancelled.
* The returned <code>ITypedElement</code> is one of the original editions
* if <code>path</code> was <code>null</code>; otherwise
* it is an <code>ITypedElement</code> returned from <code>IStructureCreator.locate(path, item)</code>
*/
public ITypedElement selectEdition(final ITypedElement target, ITypedElement[] inputEditions, Object ppath) {
Assert.isNotNull(target);
fTargetItem= target;
// sort input editions
final int count= inputEditions.length;
final IModificationDate[] editions= new IModificationDate[count];
for (int i= 0; i < count; i++)
editions[i]= (IModificationDate) inputEditions[i];
if (count > 1)
internalSort(editions, 0, count-1);
// find StructureCreator if ppath is not null
IStructureCreator structureCreator= null;
if (ppath != null) {
String type= target.getType();
IStructureCreatorDescriptor scd= CompareUIPlugin.getStructureCreator(type);
if (scd != null)
structureCreator= scd.createStructureCreator();
}
if (fReplaceMode) {
if (structureCreator != null) {
Object item= structureCreator.locate(ppath, target);
if (item instanceof ITypedElement)
fTargetItem= (ITypedElement) item;
else
ppath= null; // couldn't extract item
}
// set the left and right labels for the compare viewer
String targetLabel= getTargetLabel(target, fTargetItem);
if (fTargetIsRight)
fCompareConfiguration.setRightLabel(targetLabel);
else
fCompareConfiguration.setLeftLabel(targetLabel);
if (structureCreator != null && ppath != null) { // extract sub element
final IStructureCreator sc= structureCreator;
final Object path= ppath;
// construct the comparer thread
// and perform the background extract
fThread= new Thread() {
public void run() {
// from front (newest) to back (oldest)
for (int i= 0; i < count; i++) {
if (fEditionTree == null || fEditionTree.isDisposed())
break;
ITypedElement edition= (ITypedElement) editions[i];
// extract sub element from edition
Object r= sc.locate(path, edition);
if (r instanceof ITypedElement) { // if not empty
ITypedElement item= (ITypedElement) r;
sendPair(new Pair(edition, item, sc.getContents(item, false)));
}
}
sendPair(null);
}
};
} else {
// create tree widget
create();
// from front (newest) to back (oldest)
for (int i= 0; i < count; i++)
addMemberEdition(new Pair((ITypedElement) editions[i], (ITypedElement) editions[i], null));
}
} else {
final Object container= ppath;
Assert.isNotNull(container);
if (structureCreator == null)
return null; // error
// extract all elements of container
final HashSet current= new HashSet();
IStructureComparator sco= structureCreator.locate(container, target);
if (sco != null) {
Object[] children= sco.getChildren();
if (children != null)
for (int i= 0; i < children.length; i++)
current.add(children[i]);
} else
return null; // error
final IStructureCreator sc= structureCreator;
// construct the comparer thread
// and perform the background extract
fThread= new Thread() {
public void run() {
// from front (newest) to back (oldest)
for (int i= 0; i < count; i++) {
if (fEditionTree == null || fEditionTree.isDisposed())
break;
ITypedElement edition= (ITypedElement) editions[i];
IStructureComparator sco2= sc.locate(container, edition);
if (sco2 != null) {
Object[] children= sco2.getChildren();
if (children != null) {
for (int i2= 0; i2 < children.length; i2++) {
ITypedElement child= (ITypedElement) children[i2];
if (!current.contains(child))
sendPair(new Pair(edition, child, sc.getContents(child, false)));
}
}
}
}
sendPair(null);
}
};
}
open();
if (getReturnCode() == OK)
return fSelectedItem;
return null;
}
/**
* Returns the input target that has been specified with the most recent call
* to <code>selectEdition</code>. If a not <code>null</code> path was specified this method
* returns a subsection of this target (<code>IStructureCreator.locate(path, target)</code>)
* instead of the input target.
* <p>
* For example if the <code>target</code> is a Java compilation unit and <code>path</code> specifies
* a method, the value returned from <code>getTarget</code> will be the method not the compilation unit.
*
* @return the last specified target or a subsection thereof.
*/
public ITypedElement getTarget() {
return fTargetItem;
}
/**
* Returns a label for identifying the target side of a compare viewer.
* This implementation extracts the value for the key "targetLabel" from the resource bundle
* and passes it as the format argument to <code>MessageFormat.format</code>.
* The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
* is the name of the given input element.
* <p>
* Subclasses may override to create their own label.
* </p>
*
* @param target the target element for which a label must be returned
* @param item if a path has been specified in <code>selectEdition</code> a sub element of the given target; otherwise the same as target
* @return a label the target side of a compare viewer
*/
protected String getTargetLabel(ITypedElement target, ITypedElement item) {
String format= Utilities.getString(fBundle, "targetLabel", "targetLabel");
return MessageFormat.format(format, new Object[] { target.getName() });
}
/**
* Returns a label for identifying the edition side of a compare viewer.
* This implementation extracts the value for the key "editionLabel" from the resource bundle
* and passes it as the format argument to <code>MessageFormat.format</code>.
* The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
* is the formatted modification date of the given input element.
* <p>
* Subclasses may override to create their own label.
* </p>
*
* @param selectedEdition the selected edition for which a label must be returned
* @param item if a path has been specified in <code>selectEdition</code> a sub element of the given selectedEdition; otherwise the same as selectedEdition
* @return a label the edition side of a compare viewer
*/
protected String getEditionLabel(ITypedElement selectedEdition, ITypedElement item) {
String label= Utilities.getString(fBundle, "editionLabel", "editionLabel");
if (selectedEdition instanceof IModificationDate) {
long modDate= ((IModificationDate)selectedEdition).getModificationDate();
String date= DateFormat.getDateTimeInstance().format(new Date(modDate));
label= MessageFormat.format(label, new Object[] { date });
}
return label;
}
/* (non Javadoc)
* Returns the size initialized with the constructor.
*/
protected Point getInitialSize() {
Point size= new Point(Utilities.getInteger(fBundle, "width", 0),
Utilities.getInteger(fBundle, "height", 0));
Shell shell= getParentShell();
if (shell != null) {
Point parentSize= shell.getSize();
if (size.x <= 0)
size.x= parentSize.x-300;
if (size.y <= 0)
size.y= parentSize.y-200;
}
if (size.x < 700)
size.x= 700;
if (size.y < 500)
size.y= 500;
return size;
}
/* (non Javadoc)
* Creates SWT control tree.
*/
protected synchronized Control createDialogArea(Composite parent) {
getShell().setText(Utilities.getString(fBundle, "title", "title"));
Splitter vsplitter= new Splitter(parent, SWT.VERTICAL);
vsplitter.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL
| GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
vsplitter.addDisposeListener(
new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
if (fDateImage != null)
fDateImage.dispose();
if (fTimeImage != null)
fTimeImage.dispose();
}
}
);
if (!fReplaceMode) {
Splitter hsplitter= new Splitter(vsplitter, SWT.HORIZONTAL);
fMemberPane= new Pane(hsplitter, SWT.NONE);
fMemberPane.setText("Available Members");
fMemberTable= new Table(fMemberPane, SWT.H_SCROLL + SWT.V_SCROLL);
fMemberTable.addSelectionListener(
new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
handleMemberSelect(e.item);
}
}
);
fMemberPane.setContent(fMemberTable);
fEditionPane= new Pane(hsplitter, SWT.NONE);
} else {
fEditionPane= new Pane(vsplitter, SWT.NONE);
}
String titleFormat= Utilities.getString(fBundle, "treeTitleFormat", "treeTitleFormat");
String title= MessageFormat.format(titleFormat, new Object[] { fTargetItem.getName() });
fEditionPane.setText(title);
fEditionTree= new Tree(fEditionPane, SWT.H_SCROLL + SWT.V_SCROLL);
fEditionTree.addSelectionListener(
new SelectionListener() {
public void widgetDefaultSelected(SelectionEvent e) {
handleDefaultSelected();
}
public void widgetSelected(SelectionEvent e) {
feedInput(e.item);
}
}
);
fEditionPane.setContent(fEditionTree);
// now start the thread (and forget about it)
if (fThread != null) {
fThread.start();
fThread= null;
}
fContentPane= new CompareViewerSwitchingPane(vsplitter, SWT.NONE) {
protected Viewer getViewer(Viewer oldViewer, Object input) {
return CompareUIPlugin.findContentViewer(oldViewer, input, this, fCompareConfiguration);
}
};
vsplitter.setWeights(new int[] { 30, 70 });
return vsplitter;
}
/* (non-Javadoc)
* Method declared on Dialog.
*/
protected void createButtonsForButtonBar(Composite parent) {
String buttonLabel= Utilities.getString(fBundle, "buttonLabel", IDialogConstants.OK_LABEL);
fCommitButton= createButton(parent, IDialogConstants.OK_ID, buttonLabel, true);
fCommitButton.setEnabled(false);
createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
}
//---- private stuff ----------------------------------------------------------------------------------------
/**
* Asynchroneously sends a Pair (or null) to the UI thread.
*/
private void sendPair(final Pair pair) {
if (fEditionTree != null && !fEditionTree.isDisposed()) {
Display display= fEditionTree.getDisplay();
display.asyncExec(
new Runnable() {
public void run() {
addMemberEdition(pair);
}
}
);
}
}
private void handleDefaultSelected() {
if (fSelectedItem != null)
okPressed();
}
private static void internalSort(IModificationDate[] keys, int left, int right) {
int original_left= left;
int original_right= right;
IModificationDate mid= keys[(left + right) / 2];
do {
while (keys[left].getModificationDate() > mid.getModificationDate())
left++;
while (mid.getModificationDate() > keys[right].getModificationDate())
right--;
if (left <= right) {
IModificationDate tmp= keys[left];
keys[left]= keys[right];
keys[right]= tmp;
left++;
right--;
}
} while (left <= right);
if (original_left < right)
internalSort(keys, original_left, right);
if (left < original_right)
internalSort(keys, left, original_right);
}
/**
* Adds the given Pair to
* If HIDE_IDENTICAL is true the new Pair is only added if its contents
* is different from the preceeding Pair.
* If the argument is <code>null</code> the message "No Editions found" is shown
* in the member or edition viewer.
*/
private void addMemberEdition(Pair pair) {
if (pair == null) {
if (fMemberTable != null) {
if (!fMemberTable.isDisposed() && fMemberTable.getItemCount() == 0) {
TableItem ti= new TableItem(fMemberTable, SWT.NONE);
ti.setText("No additional members found");
}
return;
}
if (fEditionTree != null && !fEditionTree.isDisposed() && fEditionTree.getItemCount() == 0) {
TreeItem ti= new TreeItem(fEditionTree, SWT.NONE);
ti.setText("No editions found");
}
return;
}
if (fMemberEditions == null)
fMemberEditions= new HashMap();
ITypedElement item= pair.getItem();
List editions= (List) fMemberEditions.get(item);
if (editions == null) {
editions= new ArrayList();
fMemberEditions.put(item, editions);
if (fMemberTable != null && !fMemberTable.isDisposed()) {
ITypedElement te= (ITypedElement)item;
String name= te.getName();
// find position
TableItem[] items= fMemberTable.getItems();
int where= items.length;
for (int i= 0; i < where; i++) {
String n= items[i].getText();
if (n.compareTo(name) > 0) {
where= i;
break;
}
}
TableItem ti= new TableItem(fMemberTable, where, SWT.NULL);
ti.setImage(te.getImage());
ti.setText(name);
ti.setData(editions);
}
}
if (HIDE_IDENTICAL) {
int size= editions.size();
if (size > 0) {
Pair last= (Pair) editions.get(size-1);
if (last != null && last.equals(pair))
return; // don't add since the new one is equal to old
}
}
editions.add(pair);
if (fReplaceMode || editions == fCurrentEditions)
addEdition(pair);
}
/**
* Adds the given Pair to the edition tree.
* It takes care of creating tree nodes for different dates.
*/
private void addEdition(Pair pair) {
if (fEditionTree == null || fEditionTree.isDisposed())
return;
// find last day
TreeItem[] days= fEditionTree.getItems();
TreeItem lastDay= null;
if (days.length > 0)
lastDay= days[days.length-1];
boolean first= lastDay == null;
ITypedElement edition= pair.getEdition();
ITypedElement item= pair.getItem();
long ldate= ((IModificationDate)edition).getModificationDate();
long day= ldate / ONE_DAY_MS;
Date date= new Date(ldate);
if (lastDay == null || day != ((Date)lastDay.getData()).getTime() / ONE_DAY_MS) {
lastDay= new TreeItem(fEditionTree, SWT.NONE);
lastDay.setImage(fDateImage);
String df= DateFormat.getDateInstance().format(date);
long today= System.currentTimeMillis() / ONE_DAY_MS;
String formatKey;
if (day == today)
formatKey= "todayFormat";
else if (day == today-1)
formatKey= "yesterdayFormat";
else
formatKey= "dayFormat";
String pattern= Utilities.getString(fBundle, formatKey, null);
if (pattern != null)
df= MessageFormat.format(pattern, new Object[] { df });
lastDay.setText(df);
lastDay.setData(date);
}
TreeItem ti= new TreeItem(lastDay, SWT.NONE);
ti.setImage(fTimeImage);
ti.setText(DateFormat.getTimeInstance().format(date));
ti.setData(new Pair(edition, item, null));
if (first) {
fEditionTree.setSelection(new TreeItem[] {ti});
if (fReplaceMode)
fEditionTree.setFocus();
feedInput(ti);
}
if (first) // expand first node
lastDay.setExpanded(true);
}
/**
* Feeds selection from member viewer to edition viewer.
*/
private void handleMemberSelect(Widget w) {
Object data= w.getData();
if (data instanceof List) {
List editions= (List) data;
if (editions != fCurrentEditions) {
fCurrentEditions= editions;
fEditionTree.removeAll();
fEditionPane.setText("Editions of " + ((Item)w).getText());
Iterator iter= editions.iterator();
while (iter.hasNext()) {
Object item= iter.next();
if (item instanceof Pair)
addEdition((Pair) item);
}
}
}
}
/*
* Feeds selection from edition viewer to content viewer.
*/
private void feedInput(Widget w) {
Object input= w.getData();
if (input instanceof Pair) {
Pair pair= (Pair) input;
fSelectedItem= pair.getItem();
String editionLabel= getEditionLabel(pair.getEdition(), fSelectedItem);
if (!fReplaceMode) {
fContentPane.setInput(fSelectedItem);
fContentPane.setText(editionLabel);
} else {
if (fTargetIsRight) {
fCompareConfiguration.setLeftLabel(editionLabel);
fContentPane.setInput(new DiffNode(fSelectedItem, fTargetItem));
} else {
fCompareConfiguration.setRightLabel(editionLabel);
fContentPane.setInput(new DiffNode(fTargetItem, fSelectedItem));
}
}
} else {
fSelectedItem= null;
fContentPane.setInput(null);
}
fCommitButton.setEnabled(fSelectedItem != null);
}
}