/******************************************************************************* * Copyright (c) 2007 Gunnar Wagenknecht and others. * 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 * * Contributors: * IBM Corporation - initial API and implementation * Gunnar Wagenknecht - initial API and implementation * Shalom Gibly - Aptana additions and modifications ******************************************************************************/ package com.aptana.ide.core.ui.preferences; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IProjectNatureDescriptor; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.IFontProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; 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.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.actions.CloseResourceAction; import org.eclipse.ui.dialogs.PropertyPage; import org.eclipse.ui.internal.ide.DialogUtil; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; import org.eclipse.ui.internal.progress.ProgressMonitorJobsDialog; import org.eclipse.ui.model.BaseWorkbenchContentProvider; import org.eclipse.ui.progress.UIJob; import com.aptana.ide.core.IdeLog; import com.aptana.ide.epl.Activator; /** * Project property page for viewing and modifying the project natures. * * @since Aptana Studio 1.2.4 */ public class ProjectNaturesPage extends PropertyPage implements ICheckStateListener { protected static final String APTANA_NATURE_PREFIX = "com.aptana.ide.";//$NON-NLS-1$; private static final int NATURES_LIST_MULTIPLIER = 30; private Image aptanaNatureImage = Activator.getImage("icons/aptana_nature.gif"); //$NON-NLS-1$; private IProject project; private boolean modified = false; // widgets private CheckboxTableViewer listViewer; protected boolean showAptanaOnly; private List<String> projectNatures; private HashMap<Object, TableItem> maintainChecked = new HashMap<Object, TableItem>(); private HashMap<String, String> descriptionCache = new HashMap<String, String>(); private String primaryNature; private Button makePrimaryButton; private Button restoreButton; private List<String> initialCheckedItems; public ProjectNaturesPage() { showAptanaOnly = true; } /** * @see PreferencePage#createContents */ protected Control createContents(Composite parent) { Font font = parent.getFont(); Composite composite = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); composite.setLayout(layout); composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); composite.setFont(font); initialize(); Label description = createDescriptionLabel(composite); description.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); Composite tableComposite = new Composite(composite, SWT.NONE); layout = new GridLayout(2, false); tableComposite.setLayout(layout); GridData data = new GridData(GridData.FILL_BOTH); tableComposite.setLayoutData(data); listViewer = CheckboxTableViewer.newCheckList(tableComposite, SWT.TOP | SWT.BORDER); Table table = listViewer.getTable(); table.setFont(font); TableColumn column = new TableColumn(table, SWT.LEFT); column.setWidth(350); data = new GridData(GridData.FILL_BOTH); data.grabExcessHorizontalSpace = true; if (!project.isOpen()) listViewer.getControl().setEnabled(false); // Only set a height hint if it will not result in a cut off dialog if (DialogUtil.inRegularFontMode(parent)) { data.heightHint = getDefaultFontHeight(table, NATURES_LIST_MULTIPLIER); } table.setLayoutData(data); table.setFont(font); listViewer.setLabelProvider(getLabelProvider()); listViewer.setContentProvider(getContentProvider(project)); listViewer.setComparator(getViewerComperator()); listViewer.setInput(project.getWorkspace()); listViewer.addCheckStateListener(this); table.setMenu(createMenu()); initialCheckedItems = initializeCheckedNatures(); Collections.sort(initialCheckedItems); listViewer.setCheckedElements(initialCheckedItems.toArray()); listViewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { updateButtons(); } }); // Add the buttons Composite buttons = new Composite(tableComposite, SWT.NONE); layout = new GridLayout(1, true); layout.marginWidth = 0; layout.marginHeight = 0; buttons.setLayout(layout); buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); makePrimaryButton = createButton(EPLMessages.ProjectNaturesPage_LBL_MakePrimary, buttons, new MakePrimarySelectionListener()); restoreButton = createButton(EPLMessages.ProjectNaturesPage_LBL_Restore, buttons, new RestoreSelectionListener()); updateButtons(); return composite; } /** * Returns a list that contains the items that are checked while loading and resetting this page. */ private List<String> initializeCheckedNatures() { Set<String> projectNatureIds = new HashSet<String>(projectNatures); IProjectNatureDescriptor[] natureDescriptors = project.getWorkspace().getNatureDescriptors(); List<String> checked = new ArrayList<String>(projectNatureIds.size()); for (int i = 0; i < natureDescriptors.length; i++) { if (projectNatureIds.remove(natureDescriptors[i].getNatureId())) { checked.add(fixNatureId(natureDescriptors[i].getNatureId())); } } for (Iterator<String> stream = projectNatureIds.iterator(); stream.hasNext();) { checked.add(stream.next()); } return checked; } private Button createButton(String text, Composite parent, SelectionListener selectionListener) { Button b = new Button(parent, SWT.PUSH); b.setText(text); GridData data = new GridData(GridData.FILL); data.widthHint = getButtonWidthHint(b); b.setLayoutData(data); if (selectionListener != null) { b.addSelectionListener(selectionListener); } return b; } /* * Updates the buttons enablement. */ private void updateButtons() { StructuredSelection selection = (StructuredSelection) listViewer.getSelection(); makePrimaryButton.setEnabled(!selection.isEmpty() && !isPrimary(selection.getFirstElement())); restoreButton.setEnabled(modified || isPrimaryModified()); } /* * Returns a width hint for a button control. */ private static int getButtonWidthHint(Button button) { button.setFont(JFaceResources.getDialogFont()); PixelConverter converter = new PixelConverter(button); int widthHint = converter.convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); return Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x); } /** * Get the defualt widget height for the supplied control. * * @return int * @param control * - the control being queried about fonts * @param lines * - the number of lines to be shown on the table. */ private static int getDefaultFontHeight(Control control, int lines) { FontData[] viewerFontData = control.getFont().getFontData(); int fontHeight = 10; // If we have no font data use our guess if (viewerFontData.length > 0) { fontHeight = viewerFontData[0].getHeight(); } return lines * fontHeight; } /** * Returns a content provider for the list dialog. It will return all available natures as strings. * * @return the content provider that shows the natures (as string children) */ private IStructuredContentProvider getContentProvider(final IProject project) { return new BaseWorkbenchContentProvider() { public Object[] getChildren(Object o) { if (!(o instanceof IWorkspace)) { return new Object[0]; } Set<String> projectNatureIds = new HashSet<String>(projectNatures); // collect all the natures IProjectNatureDescriptor[] natureDescriptors = ((IWorkspace) o).getNatureDescriptors(); HashSet<String> elements = new HashSet<String>(natureDescriptors.length); for (int i = 0; i < natureDescriptors.length; i++) { String natureId = fixNatureId(natureDescriptors[i].getNatureId()); if (natureId != null) { if (natureId.startsWith(APTANA_NATURE_PREFIX) || projectNatureIds.contains(natureId) || !showAptanaOnly) { elements.add(natureId); descriptionCache.put(natureId, natureDescriptors[i].getLabel()); } } } // Add any natures that exists in the project, but do not exist in the workbench // (This can happen when importing a project from a different workspace, or when the nature // provider uninstalled) for (String nature : projectNatures) { if (!elements.contains(nature)) { elements.add(nature); descriptionCache.put(nature, EPLMessages.ProjectNaturesPage_MissingDescription); } } return elements.toArray(); } }; } private ILabelProvider getLabelProvider() { return new NaturesLabelProvider(); } protected String getNatureDescriptorLabel(IProjectNatureDescriptor natureDescriptor) { String id = natureDescriptor.getNatureId(); id = fixNatureId(id); String label = natureDescriptor.getLabel(); if (label.trim().length() != 0) return id + " (" + label + ')'; //$NON-NLS-1$ return id; } /** * Creates the table menu. * * @return A newly created menu */ protected Menu createMenu() { final Menu menu = new Menu(listViewer.getTable()); MenuItem item = new MenuItem(menu, SWT.PUSH); item.setText(EPLMessages.ProjectNaturesPage_LBL_SetPrimary); item.addSelectionListener(new MakePrimarySelectionListener()); return menu; } private ViewerComparator getViewerComperator() { return new ViewerComparator(new Comparator() { public int compare(Object element1, Object element2) { // set the Aptana on top and the rest on the bottom String firstId = element1.toString(); String secondId = element2.toString(); if (firstId.startsWith(APTANA_NATURE_PREFIX)) { if (secondId.startsWith(APTANA_NATURE_PREFIX)) { // Compare 2 aptana natures by name return firstId.compareTo(secondId); } else { // Put aptana as first return -1; } } else if (secondId.startsWith(APTANA_NATURE_PREFIX)) { // put aptana as first return 1; } // The natures does not belong to Aptana, so return a simple string comparison return firstId.compareTo(secondId); } }); } /** * Handle the exception thrown when saving. * * @param e * the exception */ protected void handle(InvocationTargetException e) { IdeLog.logError(Activator.getDefault(), EPLMessages.ProjectNaturesPage_ERR_NaturePage, e); IStatus error; Throwable target = e.getTargetException(); if (target instanceof CoreException) { error = ((CoreException) target).getStatus(); } else { String msg = target.getMessage(); if (msg == null) { msg = IDEWorkbenchMessages.Internal_error; } error = new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, 1, msg, target); } ErrorDialog.openError(getControl().getShell(), null, null, error); } /** * Initializes a ProjectReferencePage. */ private void initialize() { project = (IProject) getElement().getAdapter(IResource.class); try { String[] natureIds = project.getDescription().getNatureIds(); projectNatures = new ArrayList<String>(Arrays.asList(natureIds)); } catch (CoreException e) { handle(new InvocationTargetException(e)); } primaryNature = (projectNatures != null && !projectNatures.isEmpty()) ? projectNatures.get(0) : null; noDefaultAndApplyButton(); String desc = NLS.bind( EPLMessages.ProjectNaturesPage_TXT_AdditionalNatures, project .getName()); setDescription(desc); } /** * Returns true if the given element string is set as the primary nature. * * @param element * @return */ protected boolean isPrimary(Object element) { return primaryNature != null && primaryNature.equals(element); } /** * @see org.eclipse.jface.viewers.ICheckStateListener#checkStateChanged(org.eclipse.jface.viewers.CheckStateChangedEvent) */ public void checkStateChanged(CheckStateChangedEvent event) { if (maintainChecked.containsKey((event.getElement()))) { // revert the change maintainChecked.get(event.getElement()).setChecked(!event.getChecked()); } // Check if the current checked items are the same as the initial ones. Object[] checkedElements = listViewer.getCheckedElements(); Arrays.sort(checkedElements); modified = !Arrays.equals(initialCheckedItems.toArray(), checkedElements); if (primaryNature == null) { // in case that the item was checked, set it as the primary if (event.getChecked()) { primaryNature = event.getElement().toString(); listViewer.refresh(); } } else { if (!event.getChecked() && isPrimary(event.getElement())) { // reset the primary element primaryNature = null; // Find the next available item which is checked and set it to the primary checkedElements = listViewer.getCheckedElements(); if (checkedElements.length > 0) { // take the first checked and set it to defualt primaryNature = checkedElements[0].toString(); } listViewer.refresh(); } } updateButtons(); } /** * @see PreferencePage#performOk */ public boolean performOk() { if (!modified && !isPrimaryModified()) { return true; } // get checked natures Object[] checked = listViewer.getCheckedElements(); final ArrayList<String> natureIds = new ArrayList<String>(); for (int i = 0; i < checked.length; i++) { if (checked[i] instanceof String) { natureIds.add(checked[i].toString()); } else { handle(new InvocationTargetException(new IllegalStateException(NLS.bind( "invalid element \"{0}\" in nature list", checked[i])))); //$NON-NLS-1$ return false; } } // Locate and promote the primary item to the top of the list natureIds.remove(primaryNature); if (primaryNature != null) { natureIds.add(0, primaryNature); } // set nature ids IRunnableWithProgress runnable = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException { try { IProjectDescription description = project.getDescription(); description.setNatureIds(natureIds.toArray(new String[natureIds.size()])); // Use IResource.AVOID_NATURE_CONFIG to avoid any warning about the natures. // We have to use it since not all of the Natures that are defined in the system // are valid and some are forced into the project in a non-standard way. project.setDescription(description, IResource.AVOID_NATURE_CONFIG, monitor); } catch (CoreException e) { throw new InvocationTargetException(e); } } }; try { // This will block until the progress is done new ProgressMonitorJobsDialog(getControl().getShell()).run(true, true, runnable); } catch (InterruptedException e) { // Ignore interrupted exceptions } catch (InvocationTargetException e) { handle(e); return false; } resetProject(); return true; } /* * Returns true only if the primary nature was modified. */ private boolean isPrimaryModified() { // Check if just the primary was modified before exiting if (projectNatures != null && !projectNatures.isEmpty()) { if (!projectNatures.get(0).equals(primaryNature)) { return true; } } else if (primaryNature != null) { return true; } return false; } private String fixNatureId(String natureId) { int secondComIndex = natureId.indexOf(".com."); //$NON-NLS-1$ if (secondComIndex > -1) { natureId = natureId.substring(secondComIndex + 1); } return natureId; } /** * Ask to reset the project (e.g. Close and Open) to apply the changes. */ protected void resetProject() { boolean reset = MessageDialog .openQuestion(getControl().getShell(), EPLMessages.ProjectNaturesPage_ResetTitle, EPLMessages.ProjectNaturesPage_ResetMessage); if (reset) { IRunnableWithProgress close = new IRunnableWithProgress() { public void run(final IProgressMonitor monitor) throws InvocationTargetException { // Use the CloseResourceAction to provide a file saving dialog in case the project has some unsaved // files UIJob job = new UIJob(EPLMessages.ProjectNaturesPage_Job_CloseProject) { public IStatus runInUIThread(IProgressMonitor monitor) { CloseResourceAction closeAction = new CloseResourceAction(Display.getDefault() .getActiveShell()); closeAction.selectionChanged(new StructuredSelection(new Object[] { project })); closeAction.run(); monitor.done(); return Status.OK_STATUS; } }; job.schedule(); try { job.join(); } catch (InterruptedException e) { IdeLog.logError(Activator.getDefault(), EPLMessages.ProjectNaturesPage_ERR_CloseProject, e); } monitor.done(); } }; try { // This will block until the progress is done new ProgressMonitorJobsDialog(getControl().getShell()).run(true, true, close); } catch (InterruptedException e) { // Ignore interrupted exceptions } catch (InvocationTargetException e) { handle(e); } IRunnableWithProgress open = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException { try { project.open(monitor); } catch (CoreException e) { throw new InvocationTargetException(e); } } }; try { // This will block until the progress is done new ProgressMonitorJobsDialog(getControl().getShell()).run(true, true, open); } catch (InterruptedException e) { // Ignore interrupted exceptions } catch (InvocationTargetException e) { handle(e); } } } /** * Natures label provider */ private class NaturesLabelProvider extends LabelProvider implements IFontProvider { public String getText(Object element) { if (element instanceof String) { if (descriptionCache.containsKey(element)) { String desc = descriptionCache.get(element); if (isPrimary(element)) { desc += EPLMessages.ProjectNaturesPage_TXT_Primary; } return desc; } return element.toString(); } if (element instanceof IWorkspace) { return "Unknown"; //$NON-NLS-1$ } return super.getText(element); } public Image getImage(Object element) { String natureId = element.toString(); if (natureId != null && natureId.startsWith(APTANA_NATURE_PREFIX)) return aptanaNatureImage; return super.getImage(element); } public Font getFont(Object element) { updateEnablement(element); if (isPrimary(element)) { return JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT); } return null; } private void updateEnablement(Object element) { if (maintainChecked.containsKey(element)) { return; } TableItem[] items = listViewer.getTable().getItems(); for (TableItem item : items) { if (item.getData() == element) { if (!item.getData().toString().startsWith(APTANA_NATURE_PREFIX)) { item.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY)); maintainChecked.put(element, item); } break; } } } } /* * A selection adapter that handles the 'Make Primary' clicks. */ private class MakePrimarySelectionListener extends SelectionAdapter { public void widgetSelected(SelectionEvent e) { ISelection selection = listViewer.getSelection(); if (!selection.isEmpty() && selection instanceof StructuredSelection) { Object firstElement = ((StructuredSelection) selection).getFirstElement(); // Select the item listViewer.setChecked(firstElement, true); // Make it as primary primaryNature = firstElement.toString(); listViewer.refresh(); updateButtons(); } } } /* * A selection adapter that handles the 'Restore' clicks. */ private class RestoreSelectionListener extends SelectionAdapter { public void widgetSelected(SelectionEvent e) { modified = false; initialize(); listViewer.setCheckedElements(initialCheckedItems.toArray()); listViewer.refresh(); updateButtons(); } } }