/*******************************************************************************
* Copyright (c) 2009, 2010 Progress Software Corporation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.fusesource.tools.core.ui.url.urlchooser;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.fusesource.tools.core.ui.url.urlchooser.filesystemchooser.LocalFileSystemProvider;
import org.fusesource.tools.core.ui.url.util.ImageConstants;
import org.fusesource.tools.core.ui.url.util.ImagesUtil;
/**
*/
public abstract class AbstractChooser {
/*
* The null type (value is 0).
*/
public static final int STYLE_NONE = 0;
/**
* Style constant to activate multi file selection.
*/
public static final int STYLE_MULTI_SELECTION = 1;
/**
* Style constant to enable F3 file open - selected file is opened in a new editor.
*/
public static final int STYLE_SUPPORT_OPEN = 2;
/**
* Style constant to hide the text field, i.e. only show the button bar.
*/
public static final int STYLE_HIDE_TEXT_FIELD = 4;
/**
* Style constant to hide the New menu (default is visible).
*/
public static final int STYLE_HIDE_NEW_MENU = 8;
public static final int STYLE_TEXT_READONLY = 0x100;
public static final String CP_SUPPRESS_EDITOR = "suppressEditor";
public static final String CP_PARENT_FOLDER = "parentFolder";
public static final String CP_FILE_NAME = "fileName";
/**
* The separating character used to separate multiple url strings (when STYLE_MULTI_SELECTION is
* on).
*/
private static final String MULTI_SEPARATOR = ",";
private ActionProvider lastExeced = null;
private Composite composite;
protected Composite parent;
private List userActionProviders;
private Text textField;
protected Menu menu;
private final List listeners = Collections.synchronizedList(new ArrayList(3));
protected Object[] selectedValues;
private boolean isSingleSelection = true;
private boolean isTextFieldShown = true;
protected String browseBtnText = "...";
private List defaultActionProviders;
protected List actionProviders;
protected boolean isTextModified = false;
private boolean supportOpen = false;
protected int style = STYLE_NONE;
protected boolean showRelativeFilePath = false;
public static final Object ACTION_SEPERATOR = new Object();
protected Button mainButton;
protected Button openButton;
private boolean textReadonly;
protected AbstractChooser() {
}
public AbstractChooser(Composite parent) {
this(parent, Collections.EMPTY_LIST, Collections.EMPTY_LIST, STYLE_NONE);
}
public AbstractChooser(Composite parent, int style) {
this(parent, Collections.EMPTY_LIST, Collections.EMPTY_LIST, style);
}
public AbstractChooser(Composite parent, List supportedProviderIds, int style) {
this(parent, supportedProviderIds, Collections.EMPTY_LIST, style);
}
/**
* Provide a list of provider ids that is supported by this instance of the FileChooser. This
* method may not be called again.
*
* @param supportedProviderIds
* MAY BE NULL
*/
public AbstractChooser(Composite parent, List supportedProviderIds, List tempFSProviders, int style) {
this(parent, supportedProviderIds, tempFSProviders, null, style);
}
/**
* Provide a list of provider ids that is supported by this instance of the FileChooser. This
* method may not be called again.
*
* @param supportedProviderIds
* MAY BE NULL
* @param tempFSProviders
* a list or temporary (just for this instance) FS Providers
* @param listener
* a quick way to specify a listener
*/
public AbstractChooser(Composite parent, List supportedProviderIds, List tempFSProviders,
SelectionChangeListener listener, int style) {
init(parent, tempFSProviders, supportedProviderIds, listener, style);
}
protected void init(Composite parent, List tempFSProviders, List supportedProviderIds,
SelectionChangeListener listener, int style) {
this.parent = parent;
this.userActionProviders = tempFSProviders == null ? Collections.EMPTY_LIST : tempFSProviders;
this.defaultActionProviders = supportedProviderIds == null ? Collections.EMPTY_LIST : supportedProviderIds;
this.style = style;
if ((style & STYLE_MULTI_SELECTION) > 0) {
setSingleSelection(false);
}
setSupportsOpen((style & STYLE_SUPPORT_OPEN) > 0);
setTextFieldShown(!((style & STYLE_HIDE_TEXT_FIELD) > 0));
setTextFieldReadonly(((style & STYLE_TEXT_READONLY) > 0));
createUI(this.parent, this.defaultActionProviders, this.userActionProviders);
if (listener != null) {
addSelectionChangeListener(listener);
}
}
private void setTextFieldReadonly(boolean b) {
textReadonly = b;
}
private void createUI(Composite parent, List supportedProviderIds, List tempFSProviders) {
composite = createUIImpl(parent, supportedProviderIds, tempFSProviders);
}
public Composite getUI() {
return composite;
}
protected Composite createUIImpl(final Composite parent, List providersList, List tempFSProviders) {
setMenu(new Menu(parent.getShell()));
Composite composite = createParent(parent);
composite.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
getMenu().dispose();
}
});
GridLayout layout = new GridLayout(isTextFieldShown() ? 2 : 1, false);
layout.marginWidth = 1; // We do this to remove the default (5 pixel) border
layout.marginHeight = 0;
composite.setLayout(layout);
if (isTextFieldShown()) {
prepareTextField(composite);
}
Control button = createBrowseButton(composite);
if (isTextFieldShown()) {
textField.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
}
button.setLayoutData(new GridData(SWT.FILL, SWT.FILL, isTextFieldShown() ? false : true, false));
addProviders(providersList, tempFSProviders);
addDropTarget();
return composite;
}
public void setEnabled(boolean enabled) {
if (isTextFieldShown()) {
textField.setEnabled(enabled);
}
mainButton.setEnabled(enabled);
openButton.setEnabled(enabled);
}
protected Control createBrowseButton(Composite composite) {
Composite comp = new Composite(composite, SWT.NONE);
GridLayout gridLayout = new GridLayout(2, false);
gridLayout.horizontalSpacing = 0;
gridLayout.marginHeight = 0;
gridLayout.marginWidth = 0;
comp.setLayout(gridLayout);
mainButton = new Button(comp, SWT.PUSH);
mainButton.setText(browseBtnText);
GridData gridData = new GridData();
mainButton.setLayoutData(gridData);
mainButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
execActionProvider();
}
});
openButton = new Button(comp, SWT.PUSH);
gridData = new GridData(SWT.FILL, SWT.FILL, false, true);
openButton.setLayoutData(gridData);
openButton.setImage(ImagesUtil.getInstance().getImage(ImageConstants.WIDGET_OPEN));
openButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
Button item = (Button) e.widget;
Rectangle rect = item.getBounds();
Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y));
pt.y += rect.height;
showMenu(pt, true);
}
});
return comp;
}
protected Composite createParent(final Composite parent) {
Composite c = new Composite(parent, SWT.NONE);
c.setBackground(parent.getBackground()); // Have to do this since SWT has no concept of
// transparent widgets
// (see SWT.NO_BACKGROUND & SWT FAQ).
return c;
}
protected Control prepareTextField(Composite parent) {
textField = createText(parent);
hookTextFieldListeners();
return textField;
}
protected Text createText(Composite parent) {
Text text = new Text(parent, SWT.NONE | SWT.SINGLE | SWT.BORDER | (getTextReadonly() ? SWT.READ_ONLY : 0));
return text;
}
private boolean getTextReadonly() {
return textReadonly;
}
private void hookTextFieldListeners() {
textField.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (!internalUpdate) {
selectedValues = parseText();
fireSelectionChanged();
}
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
widgetSelected(e);
}
});
textField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
if (!internalUpdate) {
isTextModified = true;
selectedValues = parseText();
}
}
});
textField.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
if (isTextModified) {
selectedValues = parseText();
fireSelectionChanged(); // isTextModified is set to false in fireURLChanged
isTextModified = false;
}
}
});
if (isSingleSelection() && supportOpen()) {
// Irene's URL Open code
textField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (e.keyCode == SWT.F3) {
openSelectedValue();
}
}
});
textField.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if ((e.stateMask & SWT.CTRL) > 0) {
openSelectedValue();
}
}
@Override
public void mouseDoubleClick(MouseEvent e) {
if ((e.stateMask & SWT.CTRL) > 0) {
openSelectedValue();
}
}
});
}
}
protected abstract void openSelectedValue();
public boolean supportOpen() {
return supportOpen;
}
protected void setSupportsOpen(boolean open) {
supportOpen = open;
}
protected void addProviders(List defaultActions, List userActions) {
Collection list = getProvidersList(defaultActions);
list.remove(ACTION_SEPERATOR);
this.actionProviders = new ArrayList(list);
this.actionProviders.addAll(userActions);
}
protected void addDropTarget() {
if (!isTextFieldShown) {
return;
}
int operations = DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK | DND.DROP_DEFAULT;
DropTarget target = new DropTarget(textField, operations);
target.setTransfer(getTransferTypes());
target.addDropListener(new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetEvent event) {
if (event.detail == DND.DROP_DEFAULT) {
event.detail = (event.operations & DND.DROP_COPY) != 0 ? DND.DROP_COPY : DND.DROP_NONE;
}
}
@Override
public void drop(DropTargetEvent event) {
// A drop has occurred, copy over the data
if (event.data == null) { // no data to copy, indicate failure in event.detail
event.detail = DND.DROP_NONE;
return;
}
Object[] selection = acceptObjectDrop(event);
if (selection != null) {
setSelectedValues(selection);
}
}
});
}
private Transfer[] getTransferTypes() {
Set transfers = new HashSet();
for (Iterator iterator = actionProviders.iterator(); iterator.hasNext();) {
ActionProvider provider = (ActionProvider) iterator.next();
if (provider.supportsDnd()) {
Transfer[] types = provider.getTransferTypes();
transfers.addAll(Arrays.asList(types));
}
}
return (Transfer[]) transfers.toArray(new Transfer[transfers.size()]);
}
public Object[] acceptObjectDrop(DropTargetEvent data) {
for (Iterator iterator = actionProviders.iterator(); iterator.hasNext();) {
ActionProvider provider = (ActionProvider) iterator.next();
Object[] urls = provider.acceptDrop(data);
if (urls != null) {
return urls;
}
}
return null;
}
public Object[] parseText() {
if (textField.isDisposed()) {
return new Object[0];
}
String text = textField.getText();
StringTokenizer st = new StringTokenizer(text, MULTI_SEPARATOR);
List res = new ArrayList(st.countTokens());
while (st.hasMoreTokens()) {
res.add(st.nextToken().trim());
}
return res.toArray(new Object[0]);
}
protected abstract Collection getProvidersList(List providersList);
public void setBrowseButtonText(String text) {
this.browseBtnText = text;
mainButton.setText(browseBtnText);
}
private void setTextFieldShown(boolean show) {
this.isTextFieldShown = show;
}
public boolean isTextFieldShown() {
return isTextFieldShown;
}
/**
* Returns objects currently selected. The type of the objects will depend on the ActionProvider
* which add selections.
*
* @return array of selected objects
*/
public Object[] getSelectedObjects() {
return selectedValues;
}
public Object getSelectedObject() {
if (selectedValues == null || selectedValues.length == 0) {
return null;
}
return selectedValues[0];
}
public void setSelectedValue(Object value) {
setSelectedValue(value, false);
}
public void setSelectedValue(Object value, boolean suppressEvents) {
setSelectedValues((value != null) ? new Object[] { value } : null, suppressEvents);
}
public void setSelectedValues(Object[] values, boolean suppressEvents) {
// Only update if there's a change
if (Arrays.equals(selectedValues, values)) {
return;
}
selectedValues = new Object[(values != null) ? values.length : 0];
if (selectedValues.length > 0) {
// Check because arraycopy will throw a NPE if 'values' is null even if the copy length
// is ZERO :(
System.arraycopy(values, 0, selectedValues, 0, selectedValues.length);
}
updateSelectedValues(selectedValues, suppressEvents);
}
public void setSelectedValues(Object[] values) {
setSelectedValues(values, false);
}
protected void addSelectionChangeListener(SelectionChangeListener l) {
if (l != null && !listeners.contains(l)) {
listeners.add(l);
}
}
protected void removeSelectionChangeListener(SelectionChangeListener l) {
listeners.remove(l);
}
public Text getTextControl() {
return textField;
}
protected void showMenu(Point pt, boolean visible) {
populateMenu();
updateMenuEnablement(getMenu());
getMenu().setLocation(pt.x, pt.y);
getMenu().setVisible(visible);
}
private boolean m_menuInit = false;
private boolean internalUpdate;
private void populateMenu() {
if (!m_menuInit) {
addProviderItems(getProvidersList(this.defaultActionProviders), this.userActionProviders);
m_menuInit = true;
}
}
protected Menu getMenu() {
return menu;
}
protected void setMenu(Menu menu) {
this.menu = menu;
}
protected void addProviderItems(Collection list, List tempFSProviders) {
addOtherMenus();
addActionProviderMenuItems(list, tempFSProviders);
}
protected void addActionProviderMenuItems(Collection list, List tempFSProviders) {
for (Iterator iterator = list.iterator(); iterator.hasNext();) {
Object action = iterator.next();
if (action.equals(ACTION_SEPERATOR)) {
createSeperator();
} else {
createProviderItem((ActionProvider) action);
}
}
for (Iterator iterator = tempFSProviders.iterator(); iterator.hasNext();) {
createProviderItem((ActionProvider) iterator.next());
}
}
protected void addOtherMenus() {
boolean bShowMenu = !((style & STYLE_HIDE_NEW_MENU) > 0);
boolean bShowOpen = ((style & STYLE_SUPPORT_OPEN) > 0);
if (bShowMenu) {
_addNewMenuSupport();
}
if (bShowOpen) {
_addOpenMenuSupport();
}
addCustomMenus();
boolean showSep = bShowMenu || bShowOpen;
addSeparator(showSep);
}
/** override to always show separator */
protected void addSeparator(boolean showSep) {
if (showSep) {
new MenuItem(menu, SWT.SEPARATOR);
}
}
protected void addCustomMenus() {
}
protected void createSeperator() {
new MenuItem(menu, SWT.SEPARATOR);
}
protected void createProviderItem(final ActionProvider provider) {
MenuItem menuItem = new MenuItem(menu, SWT.NONE);
menuItem.setText(provider.getDisplayName());
menuItem.setData(provider);
menuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
execActionProvider(provider);
}
});
}
// ------------------------------------------------------------------------
protected void updateMenuEnablement(Menu menu) {
boolean bOpen = false;
MenuItem miOpen = findMenuItemWithData(menu, "OPEN");
if (miOpen != null) {
// if (getSelectedValueAsURL() != null)
if (isSelectedValueValid()) {
// TODO - need to find out a way of checking to see if URL is valid
bOpen = true;
}
miOpen.setEnabled(bOpen);
}
boolean bNew = false;
MenuItem miNew = findMenuItemWithData(menu, "NEW");
if ((miNew != null) && (miNew.getMenu() != null)) {
bNew = (miNew.getMenu().getItemCount() > 0);
}
if (miNew != null) {
miNew.setEnabled(bNew);
}
}
protected abstract boolean isSelectedValueValid();
private void _addOpenMenuSupport() {
MenuItem miOpen = new MenuItem(menu, SWT.NONE);
miOpen.setText("Open");
miOpen.setData("OPEN");
miOpen.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
openSelectedValue();
}
});
}
private void _addNewMenuSupport() {
MenuItem miNew = new MenuItem(menu, SWT.CASCADE);
miNew.setText("New");
miNew.setData("NEW");
Menu mNew = new Menu(menu);
miNew.setMenu(mNew);
addNewMenuItems(mNew);
}
protected abstract void addNewMenuItems(Menu newMenu);
// ------------------------------------------------------------------------
/**
* Method executes the specific (supplied) File System Provider.
*/
protected void execActionProvider(final ActionProvider provider) {
lastExeced = provider.isSelectionSupported() ? provider : lastExeced;
String initialPath = null;
if (isTextFieldShown) {
initialPath = textField.getText();
}
provider.setSelectionType(isSingleSelection);
provider.run(selectedValues);
Object[] selection = null;
if (provider.isSelectionSupported()) {
selection = provider.getSelection();
}
if (selection != null) {
updateSelectedValues(selection);
}
}
/**
* Method executes the default (1st) or last executed File System Provider's chooser.
*/
protected void execActionProvider() {
if (lastExeced != null) {
execActionProvider(lastExeced);
} else {
if ((actionProviders != null) && !actionProviders.isEmpty()) {
for (Object provider : actionProviders) {
ActionProvider p = (ActionProvider) provider;
if (p != null && "Resource".equals(p.getID())) {
execActionProvider(p);
}
}
}
}
}
private MenuItem findMenuItemWithData(Menu menu, Object data) {
MenuItem[] items = menu.getItems();
for (MenuItem item : items) {
if ((item.getData() != null) && (item.getData().equals(data))) {
return item;
}
}
return null;
}
public boolean isTextModified() {
return isTextModified;
}
protected void updateSelectedValues(Object[] values, boolean suppressEvents) {
if (isSingleSelection() && values.length > 1) {
selectedValues = new Object[1];
selectedValues[0] = values[0];
} else {
selectedValues = values;
}
if (isTextFieldShown) {
internalUpdate = true;
if (showRelativeFilePath && !LocalFileSystemProvider.isFileChooserDialogOpen()) {
try {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
String path = root.getLocation().toString();
File workspace = new File(path);
URL workspacleUrl = workspace.toURL();
String workspacePath = workspacleUrl.toString();
int workSpacePathLength = workspacePath.length();
String text = getText(selectedValues);
int index = text.lastIndexOf(workspacePath);
String subtext = "/" + text.substring(index + workSpacePathLength);
textField.setText(subtext);
} catch (MalformedURLException e) {
e.printStackTrace();
}
} else {
textField.setText(getText(selectedValues));
}
LocalFileSystemProvider.setFileChooserDialogOpen(false);
internalUpdate = false;
}
if (!suppressEvents) {
fireSelectionChanged();
} else {
isTextModified = false;
}
}
protected void updateSelectedValues(Object[] values) {
updateSelectedValues(values, false);
}
protected void fireSelectionChanged() {
isTextModified = false; // this is critical or the text field fires this method again
List cloned = new ArrayList(listeners);
for (Iterator iterator = cloned.iterator(); iterator.hasNext();) {
Object listener = iterator.next();
if (listener instanceof SelectionChangeListener) {
try {
((SelectionChangeListener) listener).selectionChanged(selectedValues);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
public boolean isSingleSelection() {
return isSingleSelection;
}
public void setSingleSelection(boolean isSingleOrMulti) {
isSingleSelection = isSingleOrMulti;
}
public interface NewCustomizationProvider {
Map getCustomizationMap();
}
public static Map getDefaultCustomizationMap(IFile refEditorFile) {
Map map = new HashMap();
map.put(CP_FILE_NAME, refEditorFile.getName());
map.put(CP_PARENT_FOLDER, refEditorFile.getParent().getFullPath());
return map;
}
private String getText(Object[] selectedValues) {
StringBuffer buf = new StringBuffer();
for (Object selectedValue : selectedValues) {
// Don't output empty (null) urls to the string
if (selectedValue == null) {
continue;
}
// If the buffer is not empty then we are adding an additional URL
// so we put the MULTI character in to separate the URLs.
if (buf.length() > 0) {
buf.append(MULTI_SEPARATOR).append(" ");
}
// Add the url to the buffer
buf.append(getDisplayValue(selectedValue));
}
return buf.toString();
}
protected String getDisplayValue(Object object) {
return object.toString();
}
}