blob: 269d721fd867182b27a5a1cfd3d3020cdba3cddc [file] [log] [blame]
/*
* Copyright (c) Eclipse contributors and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*/
package org.eclipse.emf.edit.ui.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.ui.viewer.IStyledLabelDecorator;
import org.eclipse.emf.common.ui.viewer.IViewerProvider;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.edit.provider.IItemLabelProvider;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.ItemPropertyDescriptorDecorator;
import org.eclipse.emf.edit.ui.EMFEditUIPlugin;
import org.eclipse.emf.edit.ui.action.EditingDomainActionBarContributor;
import org.eclipse.emf.edit.ui.action.FindAction;
import org.eclipse.emf.edit.ui.provider.DecoratingColumLabelProvider;
import org.eclipse.emf.edit.ui.provider.DelegatingStyledCellLabelProvider;
import org.eclipse.emf.edit.ui.provider.PropertyDescriptor;
import org.eclipse.emf.edit.ui.provider.PropertySource;
import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage;
import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage.ExtendedPropertySheetEntry;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;
import org.eclipse.jface.text.IFindReplaceTargetExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.part.WorkbenchPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.IPropertySourceProvider;
import org.eclipse.ui.views.properties.PropertySheet;
import org.eclipse.ui.views.properties.PropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheetSorter;
/**
* An implementation of a find and replace target specialized to work with EMF Editors and EMF's {@link ExtendedPropertySheetPage}.
* Because platform internals are used, which are subject to arbitrary change, this class is final and the constructor is private.
* The only public entry point for creating an instance is {@link #getAdapter(Class, IWorkbenchPart, AbstractUIPlugin)}.
* <p>
* This find and replace target expects to be used by the platform's {@code org.eclipse.ui.texteditor.FindReplaceDialog},
* i.e., it reuses the same find and replace dialog that is used by the platform's text editors,
* specializing that dialog with addition controls, i.e., support for a {@link SearchType search type} control.
* </p>
* <p>
* To exploit this implementation, {@link FindAction create} a find action the constructor of a derived {@link EditingDomainActionBarContributor}
* and specialize the {@link WorkbenchPart#getAdapter(Class)} method in the editor implementation class to call the {@link #getAdapter(Class, IWorkbenchPart, AbstractUIPlugin) getAdapter} method of this class.
* This will result in the {@code Find/Replace...} appearing in the context menu and {@code Edit} menu, bound to the search hot key, generally {@code Ctrl-F},
* and will result in an instance of this class being created, bound to the viewers in your editor.
* But take note, this implementation's behavior is <b>invasive</b>, i.e., it replaces the label provider of the structure viewer with a wrapper label provider
* that is able to show boxes around the matching search text.
* <p>
* <p>
* If your content trees are potentially infinite, or you otherwise wish to control which parts of the tree are searched, you can specialize each structured viewer's {@link StructuredViewerTreeIterator}.
* If you have specialized property sheet's {@link PropertySheetPage#setSorter(PropertySheetSorter) sorter}, you must {@link #setCollator(Collator) set the collator} used by this implementation to sort properties and property categories.
* </p>
*
* @since 2.14
*/
public final class FindAndReplaceTarget implements IFindReplaceTarget, IFindReplaceTargetExtension, IFindReplaceTargetExtension3
{
/**
* A map for managing the association between a workbench part and it associated find and replace target.
*/
private static final Map<IWorkbenchPart, FindAndReplaceTarget> FIND_AND_REPLACE_TARGETS = new HashMap<IWorkbenchPart, FindAndReplaceTarget>();
/**
* A styler for drawing a box around matching text.
*/
private static final Styler MATCH_STYLER = new Styler()
{
@Override
public void applyStyles(TextStyle textStyle)
{
textStyle.borderStyle = SWT.BORDER_SOLID;
}
};
/**
* Provides access to the {@code fIsRegExCheckBox} of a {@code org.eclipse.ui.texteditor.FindReplaceDialog}.
*/
private static final Field F_IS_REGEX_CHECK_BOX_FIELD;
/**
* Provides access to the {@code fSelectedRangeRadioButton} of a {@code org.eclipse.ui.texteditor.FindReplaceDialog}.
*/
private static final Field F_SELECTED_RANGE_RADIO_BUTTON_FIELD;
/**
* Provides access to the {@code fTarget} of a {@code org.eclipse.ui.texteditor.FindReplaceDialog}.
*/
private static final Field F_TARGET_FIELD;
static
{
Field fIsRegExCheckBoxField = null;
Field fSelectedRangeRadioButtonField = null;
Field fTarget = null;
try
{
Class<?> findReplaceDialogClass = CommonPlugin.loadClass("org.eclipse.ui.workbench.texteditor", "org.eclipse.ui.texteditor.FindReplaceDialog");
fIsRegExCheckBoxField = findReplaceDialogClass.getDeclaredField("fIsRegExCheckBox");
fIsRegExCheckBoxField.setAccessible(true);
fSelectedRangeRadioButtonField = findReplaceDialogClass.getDeclaredField("fSelectedRangeRadioButton");
fSelectedRangeRadioButtonField.setAccessible(true);
fTarget = findReplaceDialogClass.getDeclaredField("fTarget");
fTarget.setAccessible(true);
}
catch (Throwable throwable)
{
// Ignore.
}
F_IS_REGEX_CHECK_BOX_FIELD = fIsRegExCheckBoxField;
F_SELECTED_RANGE_RADIO_BUTTON_FIELD = fSelectedRangeRadioButtonField;
F_TARGET_FIELD = fTarget;
}
/**
* We need a tiny font to produce a bit of white space in the labels.
* @see #hookLabelProvider()
*/
private static final Map<Font, Font> TINY_FONT = new HashMap<Font, Font>();
/**
* A listener for when parts are closed to clean up the {@link #FIND_AND_REPLACE_TARGETS}.
* @see #getAdapter(Class, IWorkbenchPart, AbstractUIPlugin)
*/
private static final IPartListener PART_LISTENER = new IPartListener()
{
public void partOpened(IWorkbenchPart part)
{
}
public void partDeactivated(IWorkbenchPart part)
{
}
public void partClosed(IWorkbenchPart part)
{
synchronized (FIND_AND_REPLACE_TARGETS)
{
FindAndReplaceTarget findAndReplaceTarget = FIND_AND_REPLACE_TARGETS.remove(part);
if (findAndReplaceTarget != null)
{
findAndReplaceTarget.dispose();
}
}
}
public void partBroughtToTop(IWorkbenchPart part)
{
}
public void partActivated(IWorkbenchPart part)
{
}
};
/**
* The workbench part of this find and replace target.
*/
private final IWorkbenchPart workbenchPart;
/**
* The plugin used for saving and fetching the dialog settings of the find/replace dialog.
*/
private final AbstractUIPlugin plugin;
/**
* The collator used to {@link org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.TextData#sort(List, Object) sort properties}.
*/
Collator collator = Collator.getInstance();
/**
* A cleanup action for removing the paint lister from the properties view and for refreshing its labels.
*
* @see FindAndReplaceTarget#propertiesCleanup()
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private Runnable propertiesCleanup;
/**
* A cleanup action for when the session ends.
*
* @see #addSearchTypeControl()
* @see #endSession()
*/
private Runnable sessionCleanup;
/**
* A cleanup action for when the workbench part is disposed.
*
* @see #addSearchTypeControl()
* @see #dispose()
*/
private Runnable disposeCleanup;
/**
* If an advanced property needs to be displayed for search results and that action is currently not checked,
* it will be checked and this cleanup action will be set to restore the state when the session ends.
*
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private Runnable filterChangeCleanup;
/**
* Properties are always search assuming the are categorized.
* The properties view state will be changed to match, and if it was changed, this cleanup action will be set to restore the state when the session ends.
*
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private Runnable categoriesChangeCleanup;
/**
* Set to the objects in the selection of the viewer by {@link #getLineSelection()} and used by {@link #setScope(IRegion)}.
*/
private List<?> selectionScope;
/**
* Use to control the scope of all objects that will be searched.
*/
private Set<Object> selectionScopeObjects;
/**
* Initialized with the initial text to search based on the text of whatever is select in whatever viewer is active.
*/
private String selectionText;
/**
* The current selected item.
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private Data.Item selectedItem;
/**
* The current selection start.
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private int selectedItemStart;
/**
* The current pattern being selected.
* @see #setSelection(boolean, StructuredViewer, org.eclipse.emf.edit.ui.util.FindAndReplaceTarget.Data.Item, int, Pattern)
*/
private Pattern selectedItemPattern;
/**
* The command for doing replace-all.
* @see #replaceSelectionAll(String, boolean)
* @see #setReplaceAllMode(boolean)
*/
private CompoundCommand replaceAllCommand;
/**
* Used for processing all replacements.
*/
private int pendingReplacements = -1;
/**
* Used to control whether replace-all is enabled. Of course you can only modify editable properties.
*/
private boolean findReplaceable;
/**
* The types of things to search.
*/
private FindAndReplaceTarget.SearchType searchType;
/**
* A tree item in the properties view that's currently being processed for search/replace.
*/
private TreeItem specialTreeItem;
/**
* If the special tree item can't show the matching text, e.g., because there are control characters, this handles the display of the matching text.
*/
private int specialStart;
/**
* This avoids changes in {@link #setScope(IRegion)} while the properties view is being made visible.
*/
private boolean suspendScopeChanges;
/**
* Adapter the workbench part to an {@link IFindReplaceTarget}.
* @param adapter the adapter type; only {@code IFindReplaceTarget.class} will return a non-null result.
* @param workbenchPart the workbench part to adapt.
* @param plugin the plugin used for {@link AbstractUIPlugin#getDialogSettings() dialog settings}.
* @return a {@link FindAndReplaceTarget} or {@code null}.
*/
public static <T> T getAdapter(Class<T> adapter, IWorkbenchPart workbenchPart, AbstractUIPlugin plugin)
{
if (adapter == IFindReplaceTarget.class)
{
synchronized (FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS)
{
FindAndReplaceTarget findAndReplaceTarget = FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS.get(workbenchPart);
if (findAndReplaceTarget == null)
{
workbenchPart.getSite().getPage().addPartListener(PART_LISTENER);
findAndReplaceTarget = new FindAndReplaceTarget(workbenchPart, plugin);
FindAndReplaceTarget.FIND_AND_REPLACE_TARGETS.put(workbenchPart, findAndReplaceTarget);
}
return adapter.cast(findAndReplaceTarget);
}
}
return null;
}
/**
* Creates an instance for the given workbench part and the plugin used for {@link AbstractUIPlugin#getDialogSettings() dialog settings}.
* @param workbenchPart the workbench part.
* @param plugin the plugin used for dialog settings.
*/
private FindAndReplaceTarget(IWorkbenchPart workbenchPart, AbstractUIPlugin plugin)
{
this.workbenchPart = workbenchPart;
this.plugin = plugin;
}
/**
* Returns the collator used to sort properties and property categories.
* @return the collator used to sort properties and property categories.
*/
public Collator getCollator()
{
return collator;
}
/**
* Sets the collator used to sort properties and property categories.
* @param collator the collator used to sort properties and property categories.
*/
public void setCollator(Collator collator)
{
this.collator = collator;
}
/**
* Extracts the viewer from the workbench part.
*/
protected StructuredViewer getViewer()
{
if (workbenchPart instanceof IViewerProvider)
{
IViewerProvider viewerProvider = (IViewerProvider)workbenchPart;
Viewer viewer = viewerProvider.getViewer();
if (viewer instanceof StructuredViewer)
{
return (StructuredViewer)viewer;
}
}
return null;
}
/**
* Returns the property sheet page of the workbench page's active property sheet.
*/
protected ExtendedPropertySheetPage getActivePropertySheetPage()
{
IWorkbenchPart activePart = workbenchPart.getSite().getPage().getActivePart();
if (activePart instanceof PropertySheet)
{
PropertySheet propertySheet = (PropertySheet)activePart;
IPage currentPage = propertySheet.getCurrentPage();
if (currentPage instanceof ExtendedPropertySheetPage)
{
ExtendedPropertySheetPage propertySheetPage = (ExtendedPropertySheetPage)currentPage;
return propertySheetPage;
}
}
return null;
}
/**
* Returns the tree of the active property sheet page.
*/
protected Tree getActivePropertySheetTree()
{
ExtendedPropertySheetPage propertySheetPage = getActivePropertySheetPage();
if (propertySheetPage != null)
{
Control control = propertySheetPage.getControl();
if (control instanceof Tree)
{
Tree tree = (Tree)control;
return tree;
}
}
return null;
}
public boolean isEditable()
{
// Editing is always supported.
// Replace is selectively disabled when a particular selection is not editable.
return true;
}
public boolean canPerformFind()
{
// As long as there is a viewer that has appropriate label and content providers, we can support find.
StructuredViewer viewer = getViewer();
return viewer != null && viewer.getLabelProvider() instanceof ILabelProvider && viewer.getContentProvider() instanceof IStructuredContentProvider;
}
/**
* Called by {@link #beginSession()} to populate the initial {@link #selectionText}.
*/
protected void initialize(IWorkbenchPart workbenchPart)
{
// This method is called by the find and replace action before it opens the find and replace dialog.
// It has the opportunity to see the state before the find and replace dialog takes focus away.
// In particular, it can see the selected text in an active cell editor in the properties view.
StructuredViewer viewer = getViewer();
Tree propertySheetTree = getActivePropertySheetTree();
if (propertySheetTree != null)
{
// If there is an active property sheet with a tree, iterate over the selected tree items.
for (TreeItem treeItem : propertySheetTree.getSelection())
{
// Determine if there is an EMF property descriptor associated with it.
Collection<? extends PropertyDescriptor> propertyDescriptors = getPropertyDescriptors(treeItem).keySet();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
{
// If so, extract the object and look for it in the data.
Object object = propertyDescriptor.getObject();
for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
if (data.object == object)
{
// Look for the feature in the data items.
Object feature = getFeature(propertyDescriptor);
for (Data.Item item : data.items)
{
Object itemFeature = item.getFeature();
if (itemFeature == feature)
{
// If there is a focus text control, it must be the cell editor of this property.
Control control = workbenchPart.getSite().getShell().getDisplay().getFocusControl();
if (control instanceof Text)
{
// Extract the selected text, if any...
Text text = (Text)control;
selectionText = text.getSelectionText();
if (selectionText.length() > 0)
{
// Use this item's selected text as our initial selection.
Point selection = text.getSelection();
setSelection(true, viewer, item, selection.x, Pattern.compile(Pattern.quote(selectionText)));
// Then we're done.
return;
}
}
// Use this overall item as our initial selection.
setSelection(true, viewer, item, 0, Pattern.compile(Pattern.quote(item.value)));
return;
}
}
// Once we've passed the object of interest, there is nothing left to do.
break;
}
}
}
}
}
else
{
// Otherwise, use the first item selected in the viewer as our initial selection.
List<?> list = ((IStructuredSelection)viewer.getSelection()).toList();
for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
if (list.contains(data.object))
{
Data.Item item = data.items.get(0);
setSelection(true, viewer, item, 0, Pattern.compile(Pattern.quote(item.value)));
return;
}
}
}
}
public void beginSession()
{
initialize(workbenchPart);
// When the session start, we add our search-type control.
addSearchTypeControl();
}
/**
* Used to access the private fields of {@org.eclipse.ui.texteditor.FindReplaceDialog}.
*/
private static Object getFieldValue(Dialog dialog, Field field)
{
try
{
return field.get(dialog);
}
catch (Exception exception)
{
return null;
}
}
/**
* Modifies the {@org.eclipse.ui.texteditor.FindReplaceDialog} to add our {@link SearchType search type controls}.
*/
protected void addSearchTypeControl()
{
// Look through all child shells...
Shell workbenchShell = workbenchPart.getSite().getShell();
Shell[] shells = workbenchShell.getShells();
for (final Shell shell : shells)
{
// If this is a shell for the find and replace dialog...
Object data = shell.getData();
if (data instanceof Dialog && "org.eclipse.ui.texteditor.FindReplaceDialog".equals(data.getClass().getName()))
{
// Find the last checkbox in the dialog.
final Dialog dialog = (Dialog)data;
Object checkBox = getFieldValue(dialog, F_IS_REGEX_CHECK_BOX_FIELD);
if (checkBox instanceof Button)
{
// It should have grid data.
Button checkBoxButton = (Button)checkBox;
Object layoutData = checkBoxButton.getLayoutData();
if (layoutData instanceof GridData)
{
// Change it's span and alignment to make room for our additional control.
final GridData checkBoxGridData = (GridData)layoutData;
if (checkBoxGridData.horizontalSpan == 2)
{
checkBoxGridData.verticalAlignment = SWT.TOP;
checkBoxGridData.horizontalSpan = 1;
}
// Keep state in a section of our plugin's dialog settings.
IDialogSettings pluginDialogSettings = plugin.getDialogSettings();
final IDialogSettings dialogSettings = pluginDialogSettings.getSection("org.eclipse.ui.texteditor.FindReplaceDialog") == null
? pluginDialogSettings.addNewSection("org.eclipse.ui.texteditor.FindReplaceDialog") : pluginDialogSettings.getSection("org.eclipse.ui.texteditor.FindReplaceDialog");
// Create a search-type combo in the same group as the checkbox.
final Composite group = checkBoxButton.getParent();
final Combo combo = new Combo(group, SWT.READ_ONLY | SWT.DROP_DOWN);
combo.setItems(SearchType.getLabels());
GridData gridData = new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1);
combo.setLayoutData(gridData);
// The initial choice is remembered from the dialog settings.
searchType = SearchType.getSearchType(dialogSettings.get("search-type"));
combo.select(searchType.ordinal());
// Listen for changes in the choice.
combo.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
// Not only remember the choice, but record it in the dialog settings.
searchType = SearchType.values()[combo.getSelectionIndex()];
dialogSettings.put("search-type", searchType.key());
}
});
String oldLabel = null;
Button selectedRangeButton = null;
Object selectedRange = getFieldValue(dialog, F_SELECTED_RANGE_RADIO_BUTTON_FIELD);
if (selectedRange instanceof Button)
{
selectedRangeButton = (Button)selectedRange;
String label = selectedRangeButton.getText();
if (label.endsWith("lines"))
{
oldLabel = label;
selectedRangeButton.setText(label.substring(0, label.length() - 5) + " elements");
}
}
// Relayout the shell.
shell.layout(true, true);
// If we've never checked that the default size of the dialog is big enough to fit our controls,
// do it this time.
if (!dialogSettings.getBoolean("resized"))
{
// Determine the bottom right corner location of the combo in absolute coordinates.
Rectangle comboBounds = combo.getBounds();
Point comboBottomRightLocation = group.toDisplay(comboBounds.x + comboBounds.width, comboBounds.y + comboBounds.height);
// Determine the bottom right corner location of the containing group in absolute coordinates.
Rectangle groupBounds = group.getBounds();
Point groupBottomRightLocation = group.getParent().toDisplay(groupBounds.x + groupBounds.width, groupBounds.y + groupBounds.height);
// Determine how much too small each dimension might be.
// If there is less that 8 pixes of padding in either direction...
int widthDelta = groupBottomRightLocation.x - comboBottomRightLocation.x;
int heightDelta = groupBottomRightLocation.y - comboBottomRightLocation.y;
if (widthDelta < 8 || heightDelta < 8)
{
// Increase the shell size so that the search-type combo fits nicely.
Point shellSize = shell.getSize();
if (widthDelta < 8)
{
shellSize.x -= widthDelta - 8;
}
if (heightDelta < 8)
{
shellSize.y -= heightDelta - 8;
}
shell.setSize(shellSize);
}
// Only do this once in this workspace.
dialogSettings.put("resized", true);
}
// Setup the task that needs to be done to undo what we've done here.
final String finalOldLabel = oldLabel;
final Button finalSelectRangeButton = selectedRangeButton;
sessionCleanup = new Runnable()
{
public void run()
{
// Restore the grid data and dispose our control.
checkBoxGridData.horizontalSpan = 2;
checkBoxGridData.verticalAlignment = SWT.CENTER;
combo.dispose();
// Clean up the label change that we did.
if (finalOldLabel != null)
{
finalSelectRangeButton.setText(finalOldLabel);
}
group.getDisplay().asyncExec(new Runnable()
{
public void run()
{
// Defer the layout so that when the editor is switched to another EMF editor that supports find and replace,
// we don't see a lot of flickering.
if (!group.isDisposed() && !shell.isDisposed())
{
shell.layout(true, true);
}
}
});
if (filterChangeCleanup != null)
{
filterChangeCleanup.run();
}
if (categoriesChangeCleanup != null)
{
categoriesChangeCleanup.run();
}
sessionCleanup = null;
}
};
disposeCleanup = new Runnable()
{
public void run()
{
// It may be the case that the properties view has focus and the editor being closed is the last one.
// The find-and-replace target is not updated in this case.
// So set it to null to ensure that the
//
Object fieldValue = getFieldValue(dialog, F_TARGET_FIELD);
if (fieldValue == FindAndReplaceTarget.this)
{
try
{
Method method = dialog.getClass().getMethod("updateTarget", IFindReplaceTarget.class, boolean.class, boolean.class);
method.setAccessible(true);
method.invoke(dialog, null, false, false);
}
catch (Exception exception)
{
// Ignore.
}
}
disposeCleanup = null;
}
};
}
}
}
}
}
private void dispose()
{
if (disposeCleanup != null)
{
disposeCleanup.run();
}
}
public void endSession()
{
// When the session ends, we clean up or search-type control.
if (sessionCleanup != null)
{
sessionCleanup.run();
}
// Also do the clean that's done when the find and replace dialog loses focus.
setScope(null);
}
public Point getLineSelection()
{
// This method is used only to compute region to pass to setScope.
// So instead of computing something useless we use this opportunity to remember the viewer's selection.
StructuredViewer viewer = getViewer();
selectionScope = ((IStructuredSelection)viewer.getSelection()).toList();
return new Point(0, 0);
}
public IRegion getScope()
{
// This method is kind of useless, we remember our scope when the getLineSelection is called.
return new Region(0, 0);
}
public void setScope(IRegion scope)
{
// When the properties view is activated, it's sometimes given focus.
// In that case we restore the focus to the dialog, but we need to given the scope changes that are caused by the transient focus changes.
if (suspendScopeChanges)
{
return;
}
StructuredViewer viewer = getViewer();
if (scope == null)
{
// Remember the objects that need label updating.
Object[] selectionScopeObjectsToUpdate = selectionScopeObjects == null ? null : selectionScopeObjects.toArray();
Object selectionToUpdate = selectedItem != null && selectedItem.itemPropertyDescriptor == null ? selectedItem.data.object : null;
// The scope is set to null when the dialog loses focus.
// We should forget about most of our state at this point.
selectionText = null;
selectedItem = null;
findReplaceable = false;
selectionScopeObjects = null;
propertiesCleanup();
// Update update the selection scope objects.
if (selectionScopeObjectsToUpdate != null)
{
viewer.update(selectionScopeObjectsToUpdate, null);
}
// Update the selection object.
if (selectionToUpdate != null)
{
viewer.update(selectionToUpdate, null);
}
}
else
{
// Record the objects in the selection scope.
// These will be painted in a special way in the first to highlight them.
selectionScopeObjects = new HashSet<Object>();
int depth = -1;
for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
// If we hit the next object at the same depend, reset the depth.
if (data.depth == depth)
{
depth = -1;
}
// If the object is directly in scope.
if (selectionScope.contains(data.object))
{
// Remember the depth if we haven't remember one already.
if (depth == -1)
{
depth = data.depth;
}
}
// If the object is a selected object or nested under a selected object, include it.
if (depth != -1)
{
selectionScopeObjects.add(data.object);
}
}
// Hook up our decorating label provider for the current viewer.
hookLabelProvider();
// Eliminate the selection.
viewer.setSelection(StructuredSelection.EMPTY);
}
}
/**
* Handles the cleanup of the properties view.
* @see #propertiesCleanup
*/
protected void propertiesCleanup()
{
// Clean up the stuff we did to the properties view.
if (propertiesCleanup != null)
{
propertiesCleanup.run();
}
}
public void setScopeHighlightColor(Color color)
{
// This is never called by the find and replace dialog.
}
public String getSelectionText()
{
// This method is called for three different reasons.
// 1 - For the initial text in the find field.
// 2 - For enabling the replace button.
// 3 - For recording a "selection" history; goodness knows what that's for though!
//
// If we're updating the button state, the selection text is used to enable/disable the replace button state.
// We want it enabled only if we've selected an editable property.
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
if ("updateButtonState".equals(stackTrace[2].getMethodName())
&& (selectedItem == null || selectedItem.itemPropertyDescriptor == null || !selectedItem.itemPropertyDescriptor.canSetProperty(selectedItem.data.object)))
{
return "";
}
// We use this only to initialize the text selection from a cell editor.
// Once we've done a find, we clean this field.
if (selectionText != null)
{
return selectionText;
}
// If there is a selected item, return its value.
if (selectedItem != null)
{
return selectedItem.value;
}
// Otherwise we have no selection; we must return a non-null value.
return "";
}
public Point getSelection()
{
if (pendingReplacements >= 0)
{
return new Point(pendingReplacements, 0);
}
// This method is super important for determining the point from which the next find processing will proceed.
// If there is a selected item, we should proceed from the end of the current match.
if (selectedItem != null)
{
Matcher matcher = selectedItemPattern.matcher(selectedItem.value);
int size = 0;
if (selectedItem.value.length() >= selectedItemStart && matcher.find(selectedItemStart))
{
size = matcher.group().length();
}
Point point = new Point(selectedItem.index + selectedItemStart, size);
return point;
}
// If there is an active property sheet tree.
StructuredViewer viewer = getViewer();
Tree propertySheetTree = getActivePropertySheetTree();
if (propertySheetTree != null)
{
// Look through the selection.
for (final TreeItem treeItem : propertySheetTree.getSelection())
{
// If it has and EMF property descriptor
Collection<? extends PropertyDescriptor> propertyDescriptors = getPropertyDescriptors(treeItem).keySet();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
{
// Determine it's wrapped object and look for it in the induced text data.
Object object = propertyDescriptor.getObject();
for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
// If we find it...
if (data.object == object)
{
// Determine which feature is the selected feature.
Object feature = getFeature(propertyDescriptor);
// Collection all features before the selected feature.
List<Object> features = new ArrayList<Object>();
LOOP: for (final TreeItem otherTreeIem : propertySheetTree.getItems())
{
Collection<? extends PropertyDescriptor> otherPropertyDescriptors = getPropertyDescriptors(otherTreeIem).keySet();
for (PropertyDescriptor otherPropertyDescriptor : otherPropertyDescriptors)
{
Object otherFeature = getFeature(otherPropertyDescriptor);
features.add(otherFeature);
if (otherFeature == feature)
{
break LOOP;
}
}
}
// Consider all the items.
Data.Item candidate = null;
for (Data.Item item : data.items)
{
// If we find an exact match, return the information for it immediately, otherwise consider it a candidate.
Object itemFeature = item.getFeature();
if (itemFeature == feature)
{
return new Point(item.index, 0);
}
else if (features.contains(itemFeature))
{
candidate = item;
}
}
// If there is a candidate, return the information for it.
if (candidate != null)
{
return new Point(candidate.index, 0);
}
// If we find nothing, there's no point in looking anywhere else.
break;
}
}
}
}
}
// Otherwise find the first item of an object in the selection.
List<?> list = ((IStructuredSelection)viewer.getSelection()).toList();
for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
if (list.contains(data.object))
{
return new Point(data.items.get(0).index, 0);
}
}
// Start at the beginning.
return new Point(0, 0);
}
public void setSelection(int offset, int length)
{
// This method is always called right before setScope.
// The information it provides is not useful and can be ignored.
}
public int findAndSelect(int offset, String findString, boolean searchForward, boolean caseSensitive, boolean wholeWord)
{
// This is never called, but we forward it sensibly anyway.
return findAndSelect(offset, findString, searchForward, caseSensitive, wholeWord, false);
}
public int findAndSelect(int offset, String findString, boolean searchForward, boolean caseSensitive, boolean wholeWord, boolean regExSearch)
{
// Clear out the text used to populate the search text field once we've done a search.
selectionText = null;
// Compile the raw pattern early so it can throw an exception if it's not well formed.
// The information in that exception is displayed to the user.
if (regExSearch)
{
Pattern.compile(findString);
}
// A pattern will be constructed depending on the search parameters.
String impliedPattern = findString;
// If we're not doing a regular expression search, quote the pattern.
if (!regExSearch)
{
impliedPattern = Pattern.quote(impliedPattern);
}
// If we want case-insensitive matching, encode that in the pattern.
if (!caseSensitive)
{
impliedPattern = "(?i)" + impliedPattern;
}
// If we want whole word matching, add the word break delimiters to the pattern.
if (wholeWord)
{
impliedPattern = "\\b" + impliedPattern + "\\b";
}
// This should always compile correctly.
Pattern pattern = Pattern.compile(impliedPattern);
// If there are pending replacements, don't bother searching.
if (pendingReplacements >= 0)
{
selectedItemPattern = pattern;
return --pendingReplacements;
}
StructuredViewer viewer = getViewer();
// Iterate over the induced text, keeping track of a candidates for the case of backward searching.
Data.Item candidate = null;
int candidateStart = -1;
LOOP: for (FindAndReplaceTarget.Data data : new TextData(viewer, collator))
{
// If we have no restricted scope or the we and the object is in that scope...
if (selectionScopeObjects == null || selectionScopeObjects.contains(data.object))
{
// Iterate over the items.
for (Data.Item item : data.items)
{
// If we're searching forward and the item is at or above the offset or the end of the string is above the offset...
// Otherwise if were's searching backward and the offset is -1, because we're wrapped, or the item is before the offset...
if (searchForward ? item.index >= offset || item.index + item.value.length() > offset : offset == -1 || item.index < offset)
{
// If we've just done a replace, we make sure that the next find will find something that can be replaced, i.e., a modifiable attribute.
// Otherwise we make sure that the item is included by the search type.
if (findReplaceable ? !SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(item) : searchType != null && !searchType.isIncluded(item))
{
continue;
}
// When searching forward, we need to make sure that we begin the pattern match where it skips the stuff before the current offset.
int begin = 0;
if (searchForward && item.index < offset)
{
begin = offset - item.index;
}
// Look for a match from the starting point.
Matcher matcher = pattern.matcher(item.value);
if (matcher.find(begin))
{
// Determine the offset of the match.
int start = matcher.start();
if (searchForward)
{
// If we're searching forward, so record and mark this selection point.
setSelection(false, viewer, item, start, pattern);
// Mark the fact that we've done a find but not a replace yet.
findReplaceable = false;
// Return the appropriate absolute index where we matched.
return item.index + start;
}
// Otherwise, if we're searching backward all the way to the end, or the match is not past the target offset...
else if (offset == -1 || item.index + start <= offset)
{
// This is definitely a candidate.
candidate = item;
candidateStart = start;
// But keep searching for a better candidate later in the string.
while (matcher.find())
{
// Repeat the same logic.
start = matcher.start();
if (offset == -1 || item.index + start <= offset)
{
candidate = item;
candidateStart = start;
}
}
}
}
}
// If we're searching backward and we've gone as far as we need to go...
else if (!searchForward && item.index > offset)
{
// Break from the loop.
break LOOP;
}
}
}
}
// If there is a candidate (which is only possible if we're searching backwards...
if (candidate != null)
{
// Record and mark this selection point.
setSelection(false, viewer, candidate, candidateStart, pattern);
// Mark the fact that we've done a find but not a replace yet.
findReplaceable = false;
// Return the appropriate absolute index where we matched.
return candidate.index + candidateStart;
}
// There is no match.
return -1;
}
/**
* This records a match either initially or as a result of a find.
*/
protected void setSelection(boolean preserve, StructuredViewer viewer, Data.Item item, final int start, Pattern pattern)
{
Object selectedObjectToUpdate = selectedItem != null && selectedItem.itemPropertyDescriptor == null ? selectedItem.data.object : null;
// Remember the information about the item, pattern, and offset within the item of the match.
selectedItem = item;
selectedItemPattern = pattern;
selectedItemStart = start;
if (selectedObjectToUpdate != null)
{
viewer.update(selectedObjectToUpdate, null);
}
// There is no special tree item anymore.
specialTreeItem = null;
// If we haven't already done so, hook up the special label provider for providing selection feedback.
hookLabelProvider();
// Clean any previous stuff we did to decorate the properties view.
propertiesCleanup();
// In replace all mode, we don't want to provide any further feedback.
if (replaceAllCommand == null)
{
// Select the item in the viewer, unless we're preserving the selection, i.e., during the initial feedback.
ISelection selection = new StructuredSelection(new TreePath(item.data.getPath()));
if (!preserve)
{
viewer.setSelection(selection, true);
selection = viewer.getSelection();
// If there is an active property page, update its selection immediately.
ExtendedPropertySheetPage activePropertySheetPage = getActivePropertySheetPage();
if (activePropertySheetPage != null)
{
activePropertySheetPage.selectionChanged(workbenchPart, selection);
}
}
// Make the properties view visible, creating it if necessary.
IWorkbenchPartSite site = workbenchPart.getSite();
IWorkbenchPage page = site.getPage();
IViewPart viewPart = page.findView("org.eclipse.ui.views.PropertySheet");
// Sometimes showing the properties view gives it focus, e.g., when the editor is maximized.
Display display = site.getShell().getDisplay();
Control oldFocusControl = display.getFocusControl();
try
{
// Ignore scope changes while showing the properties view.
suspendScopeChanges = true;
if (item.itemPropertyDescriptor != null)
{
viewPart = page.showView("org.eclipse.ui.views.PropertySheet", null, IWorkbenchPage.VIEW_VISIBLE);
if (viewPart == null)
{
viewPart = page.showView("org.eclipse.ui.views.PropertySheet", null, IWorkbenchPage.VIEW_CREATE);
}
}
// Restore the focus.
Control newFocusControl = display.getFocusControl();
if (oldFocusControl != newFocusControl)
{
oldFocusControl.setFocus();
}
}
catch (PartInitException exception)
{
plugin.getLog().log(new Status(IStatus.WARNING, plugin.getBundle().getSymbolicName(), 0, exception.getLocalizedMessage(), null));
}
finally
{
suspendScopeChanges = false;
}
// If it is a property sheet, as expected...
if (viewPart instanceof PropertySheet)
{
// And the current page is a property sheet page as expected...
final PropertySheet propertySheet = (PropertySheet)viewPart;
IPage currentPage = propertySheet.getCurrentPage();
if (currentPage instanceof ExtendedPropertySheetPage)
{
// And the control is a tree...
Control control = currentPage.getControl();
if (control instanceof Tree)
{
final Tree tree = (Tree)control;
if (item.itemPropertyDescriptor != null)
{
// If the property has filter flags...
String[] filterFlags = item.itemPropertyDescriptor.getFilterFlags(item.data.object);
if (filterFlags != null)
{
for (String filterFlag : filterFlags)
{
// If the filter is one for expert property...
if ("org.eclipse.ui.views.properties.expert".equals(filterFlag))
{
final IAction action = ((ExtendedPropertySheetPage)currentPage).getFilterAction();
if (!action.isChecked())
{
// Run the action to show advanced properties, and remember that.
action.setChecked(true);
action.run();
filterChangeCleanup = new Runnable()
{
public void run()
{
action.setChecked(false);
action.run();
filterChangeCleanup = null;
}
};
}
}
}
}
// Remember the categories action that we need to ensure is checked because we search in an order that assumes this.
final IAction categoriesAction = ((ExtendedPropertySheetPage)currentPage).getCategoriesAction();
if (!categoriesAction.isChecked())
{
categoriesAction.setChecked(true);
categoriesAction.run();
categoriesChangeCleanup = new Runnable()
{
public void run()
{
categoriesAction.setChecked(false);
categoriesAction.run();
categoriesChangeCleanup = null;
}
};
}
// Walk the tree items.
for (TreeItem rootTreeItem : tree.getItems())
{
// If there is an EMF property descriptor with a feature for the selected item...
Map<? extends PropertyDescriptor, TreeItem> propertyDescriptors = getPropertyDescriptors(rootTreeItem);
for (Map.Entry<? extends PropertyDescriptor, TreeItem> entry : propertyDescriptors.entrySet())
{
PropertyDescriptor propertyDescriptor = entry.getKey();
final TreeItem propertyDescriptorTreeItem = entry.getValue();
if (propertyDescriptor.getFeature() == item.getFeature())
{
// Consider the label shown in the tree versus the value of the selected item...
String treeItemText = propertyDescriptorTreeItem.getText(1);
String itemValue = item.value;
// We might need to replace the tree item's text with a special representation...
specialStart = -1;
// If they are're identical....
if (!treeItemText.equals(itemValue))
{
// Find the match, which really must be there, do we can determine the length of the match.
Matcher matcher = pattern.matcher(itemValue);
if (matcher.find(start))
{
// Remember this special item, because we'll want to update it after we do a replace to show the replaced text.
specialTreeItem = propertyDescriptorTreeItem;
// If the end of the match is after the end of the tree item's text, or the strings up until the end of the match are not
// identical...
int end = matcher.end();
if (treeItemText.length() < end || !treeItemText.substring(0, end).equals(itemValue.substring(0, end)))
{
// Consider the starting point of the match, and work our way backward for 20 characters or until the preceding control
// character.
int begin = matcher.start();
specialStart = 2;
while (begin >= 0 && specialStart < 20 && !Character.isISOControl(itemValue.charAt(begin)))
{
++specialStart;
--begin;
}
// Work our way forward until the end of the string or until we hit a control character.
int itemValueLength = itemValue.length();
while (end < itemValueLength && !Character.isISOControl(itemValue.charAt(end)))
{
++end;
}
// Create a special string with ellipses at both ends.
String specialText = "..." + itemValue.substring(begin + 1, end) + "...";
// But that back into the item.
propertyDescriptorTreeItem.setText(1, specialText);
// Get the tree to redraw itself.
tree.redraw();
}
}
}
// Create a paint listener to select the match.
final Listener paintItemListener = new Listener()
{
private void paintItem(Event event, TreeItem item, int matchStart)
{
String text = item.getText(1);
Matcher matcher = selectedItemPattern.matcher(text);
if (matchStart < text.length() && matcher.find(matchStart))
{
// Compute the offset of the start of the matching, relative to the start of the text.
int start = matcher.start();
int x = event.gc.textExtent(text.substring(0, start)).x + item.getTextBounds(1).x - propertyDescriptorTreeItem.getBounds(1).x;
// Compute the offset at the end of the match, taking into account the width of the matching text.
int width = event.gc.textExtent(matcher.group()).x;
event.gc.drawRectangle(event.x + x + 1, event.y, width + 1, event.height - 1);
}
else if (text.endsWith("..."))
{
int x = event.gc.textExtent(text.substring(0, text.length() - 3)).x + propertyDescriptorTreeItem.getTextBounds(1).x
- propertyDescriptorTreeItem.getBounds(1).x;
int width = event.gc.textExtent("...").x;
event.gc.drawRectangle(event.x + x + 1, event.y, width + 1, event.height - 1);
}
}
public void handleEvent(Event event)
{
// If we're painting or special item...
TreeItem item = (TreeItem)event.item;
if (item == propertyDescriptorTreeItem && event.index == 1)
{
paintItem(event, item, specialStart == -1 ? start : specialStart);
}
}
};
// Add the listener.
tree.addListener(SWT.PaintItem, paintItemListener);
// Set up the runnable to clean up what we've done here.
final ExtendedPropertySheetPage propertySheetPage = (ExtendedPropertySheetPage)currentPage;
propertiesCleanup = new Runnable()
{
public void run()
{
// Remove the listener.
tree.removeListener(SWT.PaintItem, paintItemListener);
// Refresh the view.
propertySheetPage.refreshLabels();
propertiesCleanup = null;
}
};
// Select the item, and force a repaint.
tree.setSelection(propertyDescriptorTreeItem);
tree.redraw();
// We're done.
return;
}
}
}
}
// If we didn't find it at all, clear out the selection.
tree.setSelection(new TreeItem [0]);
}
}
}
}
}
/**
* The styled text decorator used to show highlight box around the matching text in structured viewer's label.
*/
protected final class StyledLabelDecorator implements IStyledLabelDecorator
{
private final ILabelProvider labelProvider;
// Use the color from the theme that the editor uses to highlight the scope.
final Color color = workbenchPart.getSite().getWorkbenchWindow().getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry().get("org.eclipse.ui.editors.findScope");
final Styler scopeStyler = new Styler()
{
@Override
public void applyStyles(TextStyle textStyle)
{
textStyle.background = color;
}
};
private final Styler tinyFontStyler;
private StyledLabelDecorator(ILabelProvider labelProvider, Styler tinyFontStyler)
{
this.labelProvider = labelProvider;
this.tinyFontStyler = tinyFontStyler;
}
public void removeListener(ILabelProviderListener listener)
{
labelProvider.removeListener(listener);
}
public boolean isLabelProperty(Object element, String property)
{
return labelProvider.isLabelProperty(element, property);
}
public void dispose()
{
labelProvider.dispose();
}
public void addListener(ILabelProviderListener listener)
{
labelProvider.addListener(listener);
}
public String decorateText(String text, Object element)
{
if (labelProvider instanceof ILabelDecorator)
{
ILabelDecorator labelDecorator = (ILabelDecorator)labelProvider;
return labelDecorator.decorateText(text, element);
}
return text;
}
public Image decorateImage(Image image, Object element)
{
if (labelProvider instanceof ILabelDecorator)
{
ILabelDecorator labelDecorator = (ILabelDecorator)labelProvider;
return labelDecorator.decorateImage(image, element);
}
return image;
}
public StyledString decorateStyledText(StyledString styledString, Object element)
{
if (labelProvider instanceof IStyledLabelDecorator)
{
IStyledLabelDecorator styledLabelDecorator = (IStyledLabelDecorator)labelProvider;
styledString = styledLabelDecorator.decorateStyledText(styledString, element);
}
// If we have a selected item, it's the item for the label, and this element is that selected element's object...
if (selectedItem != null && selectedItem.itemPropertyDescriptor == null && element == selectedItem.data.object)
{
// Convert the styled string to just a string.
String string = styledString.getString();
// Find the pattern match within that string.
Matcher matcher = selectedItemPattern.matcher(string);
if (matcher.find(selectedItemStart))
{
// Create a new styles string.
StyledString result = new StyledString();
// Recompose the string with the match styled to show a selection box.
String group = matcher.group();
int start = matcher.start();
int end = matcher.end();
result.append(string.substring(0, start));
Styler tinyPadding = new Styler()
{
@Override
public void applyStyles(TextStyle textStyle)
{
MATCH_STYLER.applyStyles(textStyle);
tinyFontStyler.applyStyles(textStyle);
}
};
result.append(" ", tinyPadding);
result.append(group, MATCH_STYLER);
result.append(" ", tinyPadding);
result.append(string.substring(end));
result.append(" ", tinyFontStyler);
return result;
}
}
// If we have scope objects and the element is one of those...
if (selectionScopeObjects != null && selectionScopeObjects.contains(element))
{
// Mark the entire string with the scope styling.
StyledString result = new StyledString();
result.append(styledString.getString(), scopeStyler);
result.append(" ", tinyFontStyler);
return result;
}
// Otherwise just pass through the string with a tiny bit of extra white space at the end.
styledString.append(" ", tinyFontStyler);
return styledString;
}
}
/**
* We use this special class so we can detect if the label provider is already hooked up.
*/
protected static class DecoratingLabelProvider extends DelegatingStyledCellLabelProvider.FontAndColorProvider implements IStyledLabelProvider
{
public DecoratingLabelProvider(IStyledLabelProvider styledLabelProvider)
{
super(styledLabelProvider);
}
@Override
public StyledString getStyledText(Object element)
{
return super.getStyledText(element);
}
}
/**
* This sets up a special label provider in the viewer to be able to highlight the scope and show the selected match.
*/
protected void hookLabelProvider()
{
final StructuredViewer viewer = getViewer();
// Create a very tiny font that can be used to make very tiny spaces.
Font font = viewer.getControl().getFont();
Font tinyFont = TINY_FONT.get(font);
if (tinyFont == null)
{
FontData[] fontData = font.getFontData();
for (FontData data : fontData)
{
data.setHeight(data.getHeight() / 4);
}
tinyFont = new Font(font.getDevice(), fontData);
TINY_FONT.put(font, tinyFont);
}
// If the label provider is already hooked up...
final ILabelProvider labelProvider = (ILabelProvider)viewer.getLabelProvider();
if (labelProvider instanceof DecoratingLabelProvider)
{
// Update the selection scope objects.
if (selectionScopeObjects != null)
{
viewer.update(selectionScopeObjects.toArray(), null);
}
// Update the selected object if it's for a label.
if (selectedItem != null && selectedItem.itemPropertyDescriptor == null)
{
viewer.update(selectedItem.data.object, null);
}
}
else
{
// Create a styler for creating very small spaces.
final Font finalTinyFont = tinyFont;
Styler tinyFontStyler = new Styler()
{
@Override
public void applyStyles(TextStyle textStyle)
{
textStyle.font = finalTinyFont;
}
};
// Create a styled label provider that can decorate the text.
IStyledLabelProvider styledProvider = new DecoratingColumLabelProvider.StyledLabelProvider(labelProvider, new StyledLabelDecorator(labelProvider, tinyFontStyler))
{
{
if (labelProvider instanceof CellLabelProvider)
{
cellLabelProvider = (CellLabelProvider)labelProvider;
}
}
};
// Hook up the label provider to be the one used by the view.
ILabelProvider delegatingLabelProvider = new DecoratingLabelProvider(styledProvider);
viewer.setLabelProvider(delegatingLabelProvider);
}
}
public void replaceSelection(String text)
{
// This is never called, but delegate it appropriately nevertheless.
replaceSelection(text, false);
}
public void replaceSelection(String text, boolean regExReplace)
{
// If we're in replace all mode.
if (replaceAllCommand != null)
{
// And this is the first call to replace selection...
if (pendingReplacements == Integer.MAX_VALUE - 1)
{
// Determine all the replacements that are applicable.
pendingReplacements = replaceSelectionAll(text, regExReplace) - 1;
}
// Both find and replace will ignore the next pendingReplacements number of calls.
return;
}
// If the selected item can't be modified...
if (!SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(selectedItem))
{
return;
}
// If the pattern doesn't match...
Matcher matcher = selectedItemPattern.matcher(selectedItem.value);
if (!matcher.find(selectedItemStart))
{
return;
}
// Build up the replacement.
StringBuffer result = new StringBuffer();
// Remember the start of the match...
int start = matcher.start();
// Escape the $ if we're not doing a regular expression replacement.
String replacement = regExReplace ? text : text.replace("$", "\\$");
// Append the replacement
matcher.appendReplacement(result, replacement);
// Remember exactly what we've replaced the pattern with.
String actualReplacement = result.substring(start);
// Complete the composition.
matcher.appendTail(result);
// We must have an editing domain and the selected item's feature must be a modifiable attribute.
EditingDomain domain = ((IEditingDomainProvider)workbenchPart).getEditingDomain();
EAttribute eAttribute = (EAttribute)selectedItem.getFeature();
// Try to convert the value modified string to a value; this can fail.
Object value;
try
{
value = EcoreUtil.createFromString(eAttribute.getEAttributeType(), result.toString());
}
catch (RuntimeException exception)
{
return;
}
// If there is a special item in the properties view, we want to update the text to show the replacement.
String replacementSpecialText = null;
if (specialTreeItem != null && !specialTreeItem.isDisposed())
{
String specialText = specialTreeItem.getText(1);
Matcher specialMatcher = selectedItemPattern.matcher(specialText);
if (specialMatcher.find(specialStart))
{
StringBuffer specialResult = new StringBuffer();
specialMatcher.appendReplacement(specialResult, replacement);
specialMatcher.appendTail(specialResult);
replacementSpecialText = specialResult.toString();
}
}
// Remember the replacement as the pattern so that views showing the matching selection continue to show the matching replacement.
selectedItemPattern = Pattern.compile(Pattern.quote(actualReplacement));
Command setCommand;
if (eAttribute.isMany())
{
Object propertyValue = selectedItem.itemPropertyDescriptor.getPropertyValue(selectedItem.data.object);
if (propertyValue instanceof IItemPropertySource)
{
propertyValue = ((IItemPropertySource)propertyValue).getEditableValue(selectedItem.data.object);
}
// Compute the new overall value for the list.
List<Object> values = new ArrayList<Object>((List<?>)propertyValue);
values.set(selectedItem.itemIndex, value);
// Create a command to set the overall list value.
setCommand = SetCommand.create(domain, selectedItem.data.object, eAttribute, values);
}
else
{
// Create a command to set the value.
setCommand = SetCommand.create(domain, selectedItem.data.object, eAttribute, value);
}
// If this is not in replace all mode, we need to be careful that command execution will cause notification that will try to select the affected
// objects.
// This messes up or attempts to track the selection progress in the case of replace/find.
CompoundCommand wrapper = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL)
{
boolean isFirst = true;
@Override
public Collection<?> getAffectedObjects()
{
// The first time this is called, it returns the empty list, so that no selection takes place.
if (isFirst)
{
isFirst = false;
return Collections.emptyList();
}
return super.getAffectedObjects();
}
};
// Put the set command in the wrapper and execute the wrapper.
wrapper.append(setCommand);
domain.getCommandStack().execute(wrapper);
// We need to wait for async executed updates to finish.
Display display = getViewer().getControl().getShell().getDisplay();
final boolean[] run = new boolean []{ true };
display.asyncExec(new Runnable()
{
public void run()
{
run[0] = false;
}
});
// Process the event queue until our runnable has run, at which point other runnables queued before ours will also have been completed.
while (run[0] && display.readAndDispatch())
{
display.sleep();
}
// If we have a special item.
if (specialTreeItem != null)
{
// And it's not disposed yet.
if (!specialTreeItem.isDisposed() && replacementSpecialText != null)
{
// Update it to the replacement text, and force it to repaint to highlight the replacement.
specialTreeItem.setText(1, replacementSpecialText);
specialTreeItem.getParent().redraw();
}
// We can forget about it now.
specialTreeItem = null;
}
// It's important to relocate our selected item, because the replacement could have updated text that appears earlier in our induced text view.
// This would mess up the current selected item's index.
for (FindAndReplaceTarget.Data data : new TextData(getViewer(), collator))
{
if (data.object == selectedItem.data.object)
{
for (Data.Item item : data.items)
{
if (selectedItem.itemPropertyDescriptor == item.itemPropertyDescriptor && selectedItem.itemIndex == item.itemIndex)
{
selectedItem = item;
}
}
}
}
// We want the next find (especially for find/replace) to find only modifiable selections.
findReplaceable = true;
}
/**
* Use to help process all matches to be replaced.
* @see #replaceSelection(String, boolean)
*/
protected int replaceSelectionAll(String text, boolean regExReplace)
{
// Escape the $ if we're not doing a regular expression replacement.
String replacement = regExReplace ? text : text.replace("$", "\\$");
// We must have an editing domain.
EditingDomain domain = ((IEditingDomainProvider)workbenchPart).getEditingDomain();
int total = 0;
for (Data data : new TextData(getViewer(), collator))
{
// We must defer creation of the set command for a multi-valued feature until after all its items are processed.
EAttribute currentListAttribute = null;
List<Object> currentListValue = null;
for (Data.Item item : data.items)
{
// If the selected item can't be modified...
if (!SearchType.MODIFIABLE_ATTRIBUTE.isIncluded(item))
{
continue;
}
// If the pattern doesn't match...
Matcher matcher = selectedItemPattern.matcher(item.value);
if (!matcher.find())
{
continue;
}
// Build up the replacement.
StringBuffer result = new StringBuffer();
// Keep track of the number of matches.
int count = 0;
do
{
// Append the replacement
matcher.appendReplacement(result, replacement);
++count;
}
while (matcher.find());
// Complete the composition.
matcher.appendTail(result);
// The feature must be an attribute.
EAttribute eAttribute = (EAttribute)item.getFeature();
// If we've deferred a multi-valued feature change, and this is a different feature
if (currentListAttribute != null && eAttribute != currentListAttribute)
{
Command setCommand = SetCommand.create(domain, data.object, currentListAttribute, currentListValue);
replaceAllCommand.append(setCommand);
currentListAttribute = null;
currentListValue = null;
}
// Try to convert the modified string to a value; this can fail.
Object value;
try
{
value = EcoreUtil.createFromString(eAttribute.getEAttributeType(), result.toString());
}
catch (RuntimeException exception)
{
continue;
}
total += count;
if (eAttribute.isMany())
{
if (currentListValue == null)
{
currentListAttribute = eAttribute;
Object propertyValue = item.itemPropertyDescriptor.getPropertyValue(data.object);
if (propertyValue instanceof IItemPropertySource)
{
propertyValue = ((IItemPropertySource)propertyValue).getEditableValue(data.object);
}
// Compute the new overall value for the list.
currentListValue = new ArrayList<Object>((List<?>)propertyValue);
}
currentListValue.set(item.itemIndex, value);
}
else
{
// Create a command to set the value.
Command setCommand = SetCommand.create(domain, data.object, eAttribute, value);
replaceAllCommand.append(setCommand);
}
}
// If we've deferred a multi-valued feature change and it's not been processed in the above loop.
if (currentListAttribute != null)
{
Command setCommand = SetCommand.create(domain, data.object, currentListAttribute, currentListValue);
replaceAllCommand.append(setCommand);
}
}
return total;
}
public void setReplaceAllMode(boolean replaceAll)
{
if (replaceAll)
{
// We want the next find to find only modifiable selections.
findReplaceable = true;
// We want all the commands to be batched into a single undoable command.
replaceAllCommand = new CompoundCommand(CompoundCommand.MERGE_COMMAND_ALL, "Replace All")
{
};
pendingReplacements = Integer.MAX_VALUE;
}
else
{
try
{
// We must have an editing domain.
EditingDomain domain = ((IEditingDomainProvider)workbenchPart).getEditingDomain();
domain.getCommandStack().execute(replaceAllCommand);
}
finally
{
// We're done now.
findReplaceable = false;
replaceAllCommand = null;
pendingReplacements = -1;
}
}
}
/**
* Exacts the textual representation of the attribute-based property descriptor.
* Returns <code>null</code> if the property is not attribute-based, or if the value is not representable as text.
*/
protected static List<String> getText(IItemPropertyDescriptor itemPropertyDescriptor, Object object)
{
if (itemPropertyDescriptor instanceof ItemPropertyDescriptorDecorator)
{
ItemPropertyDescriptorDecorator itemPropertyDescriptorDecorator = (ItemPropertyDescriptorDecorator)itemPropertyDescriptor;
return getText(itemPropertyDescriptorDecorator.getDecoratedItemPropertyDescriptor(), itemPropertyDescriptorDecorator.getDecoratedObject());
}
List<String> result = new ArrayList<String>();
// Extract the property value and unwrap it if necessary.
Object propertyValue = itemPropertyDescriptor.getPropertyValue(object);
if (propertyValue instanceof IItemPropertySource)
{
propertyValue = ((IItemPropertySource)propertyValue).getEditableValue(object);
}
// If the feature is an attribute...
Object feature = itemPropertyDescriptor.getFeature(object);
if (feature instanceof EAttribute)
{
// If the attribute's type is serializeable...
EAttribute eAttribute = (EAttribute)feature;
EDataType eDataType = eAttribute.getEAttributeType();
if (eDataType.isSerializable())
{
// If there is a value.
if (propertyValue != null)
{
boolean useLabel = false;
// Always create a list.
if (eAttribute.isMany())
{
// Add the textual representation of each value.
for (Object item : (List<?>)propertyValue)
{
try
{
result.add(EcoreUtil.convertToString(eDataType, item));
}
catch (Exception exception)
{
useLabel = true;
break;
}
}
}
else
{
// Add the textual representation of the one value.
try
{
result.add(EcoreUtil.convertToString(eDataType, propertyValue));
}
catch (RuntimeException exception)
{
useLabel = true;
}
}
if (!useLabel)
{
// return result;
}
}
}
}
// Use the label as displayed in the properties view's value column.
IItemLabelProvider labelProvider = itemPropertyDescriptor.getLabelProvider(object);
if (labelProvider != null && propertyValue != null)
{
// Make sure it's not duplicated text.
String text = labelProvider.getText(propertyValue);
for (String otherText : result)
{
if (otherText.contains(text))
{
return result;
}
}
result.add(text);
}
return result;
}
/**
* Returns the EMF property descriptors, mapped to the corresponding the tree item.
* It will walked nested properties and property categories.
*/
protected static Map<? extends PropertyDescriptor, TreeItem> getPropertyDescriptors(TreeItem treeItem)
{
Map<PropertyDescriptor, TreeItem> result = new LinkedHashMap<PropertyDescriptor, TreeItem>();
Object data = treeItem.getData();
if (data instanceof ExtendedPropertySheetEntry)
{
ExtendedPropertySheetEntry propertySheetEntry = (ExtendedPropertySheetEntry)data;
IPropertyDescriptor descriptor = propertySheetEntry.getDescriptor();
if (descriptor instanceof PropertyDescriptor)
{
PropertyDescriptor propertyDescriptor = (PropertyDescriptor)descriptor;
result.put(propertyDescriptor, treeItem);
if (treeItem.getItemCount() != 0)
{
// Force the content provider to create the child items without actually expanding the tree.
Event event = new Event();
event.item = treeItem;
treeItem.getParent().notifyListeners(SWT.Expand, event);
for (TreeItem childItem : treeItem.getItems())
{
result.putAll(getPropertyDescriptors(childItem));
}
}
}
}
else
{
for (TreeItem childTreeItem : treeItem.getItems())
{
result.putAll(getPropertyDescriptors(childTreeItem));
}
}
return result;
}
/**
* Returns the feature of item property descriptor of the EMF property descriptor.
*/
protected static Object getFeature(PropertyDescriptor propertyDescriptor)
{
Object object = propertyDescriptor.getObject();
IItemPropertyDescriptor itemPropertyDescriptor = propertyDescriptor.getItemPropertyDescriptor();
return itemPropertyDescriptor.getFeature(object);
}
/**
* This represents an element associated with each object in a tree.
* @see TextData
*/
protected static class Data
{
/**
* The object in the tree.
*/
public Object object;
/**
* The depth of the object in the tree.
*/
public int depth;
/**
* The parent data in the tree.
*/
public Data parent;
/**
* The items associated with the object.
*/
public List<Data.Item> items;
public Data(int depth, Object object, List<Data.Item> items)
{
this.depth = depth;
this.object = object;
this.items = items;
}
public Object[] getPath()
{
List<Object> path = new ArrayList<Object>();
for (Data data = this; data != null; data = data.parent)
{
path.add(0, data.object);
}
return path.toArray();
}
/**
* This represents an item associated with each data item.
*/
protected static class Item
{
/**
* The containing data of this item.
*/
public FindAndReplaceTarget.Data data;
/**
* The index of this item in the overall {@link TextData induced} textual representation.
*/
public int index;
/**
* The property descriptor associated with this item.
* The <code>null</value> represents the label value of the data object.
*/
public IItemPropertyDescriptor itemPropertyDescriptor;
/**
* Each value in a multi-valued feature is represented as an item.
* This is the index of that item in its list.
*/
public int itemIndex;
/**
* The textual value of the item.
*/
public String value;
public Item(FindAndReplaceTarget.Data data, int index, IItemPropertyDescriptor itemPropertyDescriptor, int itemIndex, String value)
{
this.data = data;
this.index = index;
this.itemPropertyDescriptor = itemPropertyDescriptor;
this.itemIndex = itemIndex;
this.value = value;
}
public Object getFeature()
{
if (itemPropertyDescriptor != null)
{
return itemPropertyDescriptor.getFeature(data.object);
}
return null;
}
}
}
/**
* This supports iterating over an induced textual representation of a structured viewer's structure.
*/
protected static class TextData implements Iterable<FindAndReplaceTarget.Data>
{
private final StructuredViewer viewer;
private final Collator collator;
private final ILabelProvider labelProvider;
private final IPropertySourceProvider propertySourceProvider;
public TextData(StructuredViewer viewer, Collator collator)
{
this.viewer = viewer;
this.collator = collator;
labelProvider = (ILabelProvider)viewer.getLabelProvider();
IContentProvider contentProvider = viewer.getContentProvider();
propertySourceProvider = contentProvider instanceof IPropertySourceProvider ? (IPropertySourceProvider)contentProvider : null;
}
public Iterator<FindAndReplaceTarget.Data> iterator()
{
// This is an iterator that delegates to an iterator that walks the structure of the viewer.
final StructuredViewerTreeIterator structuredViewerTreeIterator = StructuredViewerTreeIterator.create(viewer);
return new Iterator<FindAndReplaceTarget.Data>()
{
private List<Data> parents = new ArrayList<Data>();
// This keeps track of the textual index as we iterate.
private int index;
public boolean hasNext()
{
return structuredViewerTreeIterator.hasNext();
}
public FindAndReplaceTarget.Data next()
{
// Keep track of the depth before calling next.
int depth = structuredViewerTreeIterator.size();
Object object = structuredViewerTreeIterator.next();
// Create a list of items for this object and use that to create a new data representation.
List<Data.Item> items = new ArrayList<Data.Item>();
FindAndReplaceTarget.Data data = new Data(depth - 1, object, items);
if (parents.size() < depth)
{
parents.add(data);
}
else
{
parents.set(depth - 1, data);
}
if (depth > 1)
{
data.parent = parents.get(depth - 2);
}
// Add an item for the label.
String label = labelProvider.getText(object);
items.add(new Data.Item(data, index, null, 0, label));
index += label.length();
// If we have a source provider...
if (propertySourceProvider != null)
{
// And we have an EMF property source for the object...
IPropertySource propertySource = propertySourceProvider.getPropertySource(object);
if (propertySource instanceof PropertySource)
{
// Extract the EMF property source so we can iterate directly over the EMF property descriptors.
PropertySource emfPropertySource = (PropertySource)propertySource;
IItemPropertySource itemPropertySource = emfPropertySource.getItemPropertySource();
visit(items, data, itemPropertySource, object);
}
}
return data;
}
private void visit(List<Data.Item> items, FindAndReplaceTarget.Data data, IItemPropertySource itemPropertySource, Object object)
{
for (IItemPropertyDescriptor itemPropertyDescriptor : sort(itemPropertySource.getPropertyDescriptors(object), object))
{
// Extract the textual values of the property, if there are any.
List<String> text = getText(itemPropertyDescriptor, object);
if (text != null)
{
// Create an item for each value.
for (int i = 0, size = text.size(); i < size; ++i)
{
String value = text.get(i);
items.add(new Data.Item(data, index, itemPropertyDescriptor, i, value));
index += value.length();
}
}
Object propertyValue = itemPropertyDescriptor.getPropertyValue(object);
if (propertyValue instanceof IItemPropertySource)
{
IItemPropertySource childPropertySource = (IItemPropertySource)propertyValue;
visit(items, data, childPropertySource, propertyValue);
}
}
}
public void remove()
{
throw new UnsupportedOperationException("remove");
}
};
}
protected List<? extends IItemPropertyDescriptor> sort(List<IItemPropertyDescriptor> propertyDescriptors, Object object)
{
Map<String, Map<String, IItemPropertyDescriptor>> categorizedPropertyDescriptors = collator == null
? new LinkedHashMap<String, Map<String, IItemPropertyDescriptor>>() : new TreeMap<String, Map<String, IItemPropertyDescriptor>>(collator);
for (IItemPropertyDescriptor itemPropertyDescriptor : propertyDescriptors)
{
String category = itemPropertyDescriptor.getCategory(object);
if (category == null)
{
category = EMFEditUIPlugin.INSTANCE.getString("_UI_Misc_property_category");
}
Map<String, IItemPropertyDescriptor> sortedItemPropertyDescriptors = categorizedPropertyDescriptors.get(category);
if (sortedItemPropertyDescriptors == null)
{
sortedItemPropertyDescriptors = collator == null
? new LinkedHashMap<String, IItemPropertyDescriptor>() : new TreeMap<String, IItemPropertyDescriptor>(Collator.getInstance());
categorizedPropertyDescriptors.put(category, sortedItemPropertyDescriptors);
}
sortedItemPropertyDescriptors.put(itemPropertyDescriptor.getDisplayName(object), itemPropertyDescriptor);
}
List<IItemPropertyDescriptor> result = new ArrayList<IItemPropertyDescriptor>(propertyDescriptors.size());
for (Map<String, IItemPropertyDescriptor> sortedItemPropertyDescriptors : categorizedPropertyDescriptors.values())
{
result.addAll(sortedItemPropertyDescriptors.values());
}
return result;
}
}
/**
* This enumerates the types of searches that are possible for an EMF-based structured editor.
* <ul>
* <li>Labels and properties - Searches the labels in the structure viewer as well as the values of all properties in the properties view.</li>
* <li>Labels - Searches the only the labels in the structure viewer.</li>
* <li>Properties - Searches the only the properties in the properties view.</li>
* <li>Modifiable attributes - Searches the only the properties in the properties view that correspond to modified attributes of the model.</li>
* </ul>
*/
protected enum SearchType
{
LABEL_AND_PROPERTY()
{
@Override
public boolean isIncluded(Data.Item item)
{
return item != null;
}
@Override
public String key()
{
return "label+property";
}
@Override
public String label()
{
return EMFEditUIPlugin.INSTANCE.getString("_UI_LabelAndProperties_label");
}
},
LABEL()
{
@Override
public boolean isIncluded(Data.Item item)
{
return item != null && item.itemPropertyDescriptor == null;
}
@Override
public String key()
{
return "label";
}
@Override
public String label()
{
return EMFEditUIPlugin.INSTANCE.getString("_UI_Labels_label");
}
},
PROPERTY()
{
@Override
public boolean isIncluded(Data.Item item)
{
return item != null && item.itemPropertyDescriptor != null;
}
@Override
public String key()
{
return "property";
}
@Override
public String label()
{
return EMFEditUIPlugin.INSTANCE.getString("_UI_Properties_label");
}
},
MODIFIABLE_ATTRIBUTE
{
@Override
public boolean isIncluded(Data.Item item)
{
return item != null && item.itemPropertyDescriptor != null && item.getFeature() instanceof EAttribute && item.itemPropertyDescriptor.canSetProperty(item.data.object);
}
@Override
public String key()
{
return "modifiable-attribute";
}
@Override
public String label()
{
return EMFEditUIPlugin.INSTANCE.getString("_UI_ModifiableAttributes_label");
}
};
public abstract boolean isIncluded(Data.Item item);
public abstract String key();
public abstract String label();
public static FindAndReplaceTarget.SearchType getSearchType(String key)
{
for (FindAndReplaceTarget.SearchType searchType : SearchType.values())
{
if (searchType.key().equals(key))
{
return searchType;
}
}
return LABEL_AND_PROPERTY;
}
public static String[] getLabels()
{
FindAndReplaceTarget.SearchType[] values = SearchType.values();
String[] labels = new String [values.length];
for (int i = 0; i < values.length; ++i)
{
labels[i] = values[i].label();
}
return labels;
}
}
}