/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.ui.dialogs;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.dialogs.ISelectionStatusValidator;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.uiutils.DialogMemento;
import org.python.pydev.plugin.PydevPlugin;
/**
* This dialog will select an existing entry or give the user a chance to create a new one.
*
* It's always persisted in the preference store and values are gotten from there (users just
* need to pass the key which it should manage -- internally values are stored as a list separated
* by '|').
*/
public class SelectExistingOrCreateNewDialog extends TreeSelectionDialog implements SelectionListener {
/**
* Text when nothing is selected (so, anything written will be the text executed).
*/
private static final String NEW_ENTRY_TEXT = "New entry (whathever is written in the text field).";
/**
* Helper for saving/restoring the dialog position
*/
private final DialogMemento memento;
/**
* Preference store from where the preferences key is gotten.
*/
private final IPreferenceStore preferenceStore;
/**
* Key where the commands available should be set.
*/
private String preferenceKey;
/**
* List of commands available (obtained from the preferences). Note that it may change on delete
* and it should be used when the user presses OK to save the current commands.
*/
private List<String> input;
private Button btAdd;
private Button btRemove;
/**
* @param preferenceStore the store from where we should get the preferences key.
* @param preferenceKey this is a key where the value is a string with substrings separated by '|'.
* @param shellMementoId the id for saving the memento settings for the dialog.
*/
public SelectExistingOrCreateNewDialog(Shell parent, IPreferenceStore preferenceStore, String preferenceKey,
String shellMementoId) {
super(parent, new ToStringLabelProvider(), new StringFromListContentProvider());
this.memento = new DialogMemento(parent, shellMementoId);
this.preferenceStore = preferenceStore;
final String initialValue = preferenceStore.getString(preferenceKey);
this.preferenceKey = preferenceKey;
this.setInput(StringUtils.split(initialValue, '|'));
this.setAllowMultiple(false);
this.setValidator(createValidator());
this.setHelpAvailable(false);
//as we have many special things about deleting, filtering in this class, it's important that
//elements are up to date.
this.updateInThread = false;
}
/**
* Creates a special validator that considers that items may be gotten from what's filtered (not only actually selected).
*/
private ISelectionStatusValidator createValidator() {
return new ISelectionStatusValidator() {
public IStatus validate(Object[] selection) {
if (selection != null && selection.length == 1) {
return new Status(IStatus.OK, PydevPlugin.getPluginID(), getEntry(selection[0].toString()));
}
TreeItem[] items = getTreeViewer().getTree().getItems();
if (selection == null || selection.length == 0) {
//not available in selection
if (items != null) {
if (items.length == 1) {
return new Status(IStatus.OK, PydevPlugin.getPluginID(), getEntry(items[0].getData()
.toString()));
}
if (items.length > 0) {
String textInEditor = text.getText();
for (TreeItem item : items) {
if (item.getData().toString().equals(textInEditor)) {
//exact match of what's written to an item, so, just use it.
return new Status(IStatus.OK, PydevPlugin.getPluginID(), textInEditor);
}
}
}
}
}
if ((selection == null || selection.length == 0) && (items == null || items.length == 0)) {
return new Status(IStatus.ERROR, PydevPlugin.getPluginID(), "No selection available.");
}
return new Status(IStatus.ERROR, PydevPlugin.getPluginID(), "Only 1 entry may be selected or visible.");
}
private String getEntry(String string) {
if (NEW_ENTRY_TEXT.equals(string)) {
return text.getText();
}
return string;
}
};
}
public boolean close() {
memento.writeSettings(getShell());
return super.close();
}
public Control createDialogArea(Composite parent) {
memento.readSettings();
Control ret = super.createDialogArea(parent);
getTreeViewer().getTree().addKeyListener(new KeyListener() {
/**
* Support for deleting the current selection on del or backspace.
*/
public void keyReleased(KeyEvent e) {
if (e.keyCode == SWT.DEL || e.keyCode == SWT.BS) {
removeSelection();
}
}
public void keyPressed(KeyEvent e) {
}
});
Tree tree = getTreeViewer().getTree();
GridData layoutData = (GridData) tree.getLayoutData();
layoutData.grabExcessHorizontalSpace = true;
layoutData.grabExcessVerticalSpace = true;
layoutData.horizontalAlignment = GridData.FILL;
layoutData.verticalAlignment = GridData.FILL;
return ret;
}
/* (non-Javadoc)
* @see org.eclipse.ui.dialogs.ElementTreeSelectionDialog#createTreeViewer(org.eclipse.swt.widgets.Composite)
*/
@Override
protected TreeViewer createTreeViewer(Composite parent) {
Composite composite = new Composite(parent, SWT.None);
GridLayout gridLayout = new GridLayout(2, false);
gridLayout.marginWidth = 0;
composite.setLayout(gridLayout);
GridData layoutData = new GridData(GridData.FILL_BOTH);
composite.setLayoutData(layoutData);
TreeViewer ret = super.createTreeViewer(composite);
Composite buttonBox = new Composite(composite, SWT.NULL);
GridData gridData = new GridData(SWT.END, SWT.FILL, false, false);
buttonBox.setLayoutData(gridData);
GridLayout layout = new GridLayout(1, true);
layout.marginWidth = 0;
buttonBox.setLayout(layout);
btAdd = createPushButton(buttonBox, "Add");
btRemove = createPushButton(buttonBox, "Remove (DEL)");
return ret;
}
private Button createPushButton(Composite parent, String text) {
Button button = new Button(parent, SWT.PUSH);
button.setText(text);
button.setFont(parent.getFont());
GridData data = new GridData(GridData.FILL_HORIZONTAL);
int widthHint = convertHorizontalDLUsToPixels(button, IDialogConstants.BUTTON_WIDTH);
data.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
button.setLayoutData(data);
button.addSelectionListener(this);
return button;
}
/**
* Returns the number of pixels corresponding to the
* given number of horizontal dialog units.
* <p>
* Clients may call this framework method, but should not override it.
* </p>
*
* @param control the control being sized
* @param dlus the number of horizontal dialog units
* @return the number of pixels
*/
protected int convertHorizontalDLUsToPixels(Control control, int dlus) {
GC gc = new GC(control);
gc.setFont(control.getFont());
int averageWidth = gc.getFontMetrics().getAverageCharWidth();
gc.dispose();
double horizontalDialogUnitSize = averageWidth * 0.25;
return (int) Math.round(dlus * horizontalDialogUnitSize);
}
protected Point getInitialSize() {
return memento.getInitialSize(super.getInitialSize(), getShell());
}
protected Point getInitialLocation(Point initialSize) {
return memento.getInitialLocation(initialSize, super.getInitialLocation(initialSize), getShell());
}
/*
* @see SelectionStatusDialog#computeResult()
*/
@SuppressWarnings("unchecked")
protected void computeResult() {
doFinalUpdateBeforeComputeResult();
IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection();
List list = selection.toList();
if (list.size() == 1) {
Object selected = list.get(0);
if (NEW_ENTRY_TEXT.equals(selected)) {
list = newCommand();
}
setResult(list);
} else {
TreeItem[] items = getTreeViewer().getTree().getItems();
if (items.length == 1) {
//there is only one item filtered in the tree it may be that one (or a custom).
list = new ArrayList();
Object entry = items[0].getData();
if (NEW_ENTRY_TEXT.equals(entry)) {
list = newCommand();
} else {
list.add(entry);
}
setResult(list);
} else if (items.length > 1) {
String textInEditor = text.getText();
for (TreeItem item : items) {
if (item.getData().toString().equals(textInEditor)) {
//exact match of what's written to an item, so, just use it.
list = new ArrayList();
list.add(textInEditor);
}
}
setResult(list);
}
}
}
public void widgetSelected(SelectionEvent e) {
Object source = e.getSource();
if (source == btAdd) {
InputDialog dialog = new InputDialog(getShell(), "Add custom command to list",
"Add custom command to list", "", new IInputValidator() {
public String isValid(String newText) {
if (newText.trim().length() == 0) {
return "Command not entered.";
}
if (input.contains(newText)) {
return "Command already entered.";
}
return null;
}
});
int open = dialog.open();
if (open == InputDialog.OK) {
String value = dialog.getValue();
input.add(value);
saveCurrentCommands(value); //Save it.
updateGui();
}
} else if (source == btRemove) {
removeSelection();
}
}
public void widgetDefaultSelected(SelectionEvent e) {
//Do nothing.
}
/**
* Creates a new command (should be used only when OK is pressed, as it will add that
* command to the key in the preferences)
*
* @return a list with a single command gotten from the text in the text field.
*/
private List<String> newCommand() {
ArrayList<String> list = new ArrayList<String>();
String newCommand = this.text.getText().trim();
if (newCommand.length() > 0) {
list.add(newCommand);
saveCurrentCommands(newCommand);
}
return list;
}
/**
* Saves the current list of commands in the preferences, adding the one passed as a parameter
* if it is not null.
*/
private void saveCurrentCommands(String newCommand) {
ArrayList<String> newCommands = new ArrayList<String>(input);
if (newCommand != null && !input.contains(newCommand)) {
newCommands.add(newCommand);
}
newCommands.remove(NEW_ENTRY_TEXT); //never save this entry.
preferenceStore.setValue(preferenceKey, com.aptana.shared_core.string.StringUtils.join("|", newCommands));
}
/**
* Caches the entries that are currently accepted to show in the tree.
*/
Set<String> currentlyAccepted = new HashSet<String>();
/**
* Overridden because we want to update the pre-computed list of accepted entries.
*/
@Override
protected void setFilter(String text, IProgressMonitor monitor, boolean updateFilter) {
if (updateFilter) {
if (fFilterMatcher.lastPattern.equals(text)) {
//no actual change...
return;
}
fFilterMatcher.setFilter(text);
if (monitor.isCanceled())
return;
}
updateFilterEntries(monitor);
//the filter is already updated in this class.
super.setFilter(text, monitor, false);
}
/**
* Whenever the update finishes, we have to update our OK status because it depends not only
* on the selection, but also on the visible items.
*/
@Override
protected void onFinishUpdateJob() {
updateOKStatus();
}
/**
* Updates what should be shown in the tree. We have to override because if we
* don't match anything we want to add a NEW_ENTRY_TEXT.
*/
private void updateFilterEntries(IProgressMonitor monitor) {
currentlyAccepted.clear();
for (String s : input) {
if (NEW_ENTRY_TEXT.equals(s)) {
continue;
}
if (fFilterMatcher.match(s)) {
currentlyAccepted.add(s);
}
if (monitor.isCanceled())
return;
}
if (currentlyAccepted.size() == 0) {
currentlyAccepted.add(NEW_ENTRY_TEXT);
}
}
/**
* Overridden to get the input set and always add a NEW_ENTRY_TEXT if it's still not there.
* (and also update the pre-computed filter entries accepted on a new input).
*/
@Override
public void setInput(Object input) {
this.input = (List<String>) input;
if (this.input.indexOf(NEW_ENTRY_TEXT) == -1) {
this.input.add(NEW_ENTRY_TEXT);
}
super.setInput(input);
this.updateFilterEntries(new NullProgressMonitor());
}
/**
* Overridden because of the special support for having a NEW_ENTRY_TEXT if nothing matches
* the current text (so, we pre-compute what's accepted and only check that here).
*/
@Override
protected boolean matchItemToShowInTree(Object element) {
return this.currentlyAccepted.contains(element);
}
private void updateGui() {
setFilter(text.getText(), new NullProgressMonitor(), false);
updateSelectionIfNothingSelected(getTreeViewer().getTree());
}
private void removeSelection() {
IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection();
List<String> list = selection.toList();
for (String s : list) {
if (NEW_ENTRY_TEXT.equals(s)) {
continue; //don't delete this one.
}
input.remove(s);
}
saveCurrentCommands(null);
//updates the selection
updateGui();
}
}
/**
* Transform anything that gets here into a string.
*/
final class ToStringLabelProvider extends LabelProvider {
public Image getImage(Object element) {
return null;
}
public String getText(Object element) {
return "" + element;
}
}
/**
* Works with lists of strings
*/
final class StringFromListContentProvider implements ITreeContentProvider {
public Object[] getChildren(Object element) {
if (element instanceof List) {
List list = (List) element;
return list.toArray();
}
return new Object[0];
}
public Object getParent(Object element) {
return null;
}
public boolean hasChildren(Object element) {
return element instanceof List && ((List) element).size() > 0;
}
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}
public void dispose() {
//do nothing
}
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
//do nothing
}
}