001: /*******************************************************************************
002: * Copyright (c) 2005, 2007 BEA Systems, Inc. and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * BEA Systems Inc. - initial API and implementation
010: *******************************************************************************/package org.eclipse.jdt.apt.ui.internal.preferences;
011:
012: import java.util.ArrayList;
013: import java.util.Collections;
014: import java.util.HashMap;
015: import java.util.LinkedHashMap;
016: import java.util.List;
017: import java.util.Map;
018:
019: import org.eclipse.core.resources.IFolder;
020: import org.eclipse.core.resources.IProject;
021: import org.eclipse.core.resources.ProjectScope;
022: import org.eclipse.core.runtime.IStatus;
023: import org.eclipse.core.runtime.preferences.IEclipsePreferences;
024: import org.eclipse.core.runtime.preferences.IScopeContext;
025: import org.eclipse.core.runtime.preferences.InstanceScope;
026: import org.eclipse.jdt.apt.core.internal.AptPlugin;
027: import org.eclipse.jdt.apt.core.util.AptConfig;
028: import org.eclipse.jdt.apt.core.util.AptPreferenceConstants;
029: import org.eclipse.jdt.core.IJavaProject;
030: import org.eclipse.jdt.core.JavaCore;
031: import org.eclipse.jdt.internal.ui.dialogs.StatusInfo;
032: import org.eclipse.jdt.internal.ui.util.PixelConverter;
033: import org.eclipse.jdt.internal.ui.wizards.IStatusChangeListener;
034: import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
035: import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
036: import org.eclipse.jdt.internal.ui.wizards.dialogfields.IListAdapter;
037: import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
038: import org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField;
039: import org.eclipse.jdt.internal.ui.wizards.dialogfields.SelectionButtonDialogField;
040: import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringDialogField;
041: import org.eclipse.jface.dialogs.Dialog;
042: import org.eclipse.jface.viewers.ITableLabelProvider;
043: import org.eclipse.jface.viewers.LabelProvider;
044: import org.eclipse.jface.viewers.Viewer;
045: import org.eclipse.jface.viewers.ViewerComparator;
046: import org.eclipse.jface.window.Window;
047: import org.eclipse.swt.SWT;
048: import org.eclipse.swt.graphics.Image;
049: import org.eclipse.swt.layout.GridData;
050: import org.eclipse.swt.layout.GridLayout;
051: import org.eclipse.swt.widgets.Composite;
052: import org.eclipse.swt.widgets.Control;
053: import org.eclipse.swt.widgets.Label;
054: import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
055: import org.osgi.service.prefs.BackingStoreException;
056:
057: /**
058: * Preference pane for most APT (Java annotation processing) settings.
059: * see org.eclipse.jdt.ui.internal.preferences.TodoTaskConfigurationBlock
060: * for the conceptual source of some of this code.
061: * <p>
062: *
063: */
064: public class AptConfigurationBlock extends BaseConfigurationBlock {
065:
066: private static final Key KEY_APTENABLED = getKey(
067: AptPlugin.PLUGIN_ID, AptPreferenceConstants.APT_ENABLED);
068: private static final Key KEY_RECONCILEENABLED = getKey(
069: AptPlugin.PLUGIN_ID,
070: AptPreferenceConstants.APT_RECONCILEENABLED);
071: private static final Key KEY_GENSRCDIR = getKey(
072: AptPlugin.PLUGIN_ID, AptPreferenceConstants.APT_GENSRCDIR);
073:
074: private static Key[] getAllKeys() {
075: return new Key[] { KEY_APTENABLED, KEY_RECONCILEENABLED,
076: KEY_GENSRCDIR };
077: }
078:
079: private static final int IDX_ADD = 0;
080: private static final int IDX_EDIT = 1;
081: private static final int IDX_REMOVE = 2;
082:
083: private final IJavaProject fJProj;
084:
085: private SelectionButtonDialogField fAptEnabledField;
086: private SelectionButtonDialogField fReconcileEnabledField;
087: private StringDialogField fGenSrcDirField;
088: private ListDialogField fProcessorOptionsField;
089:
090: private PixelConverter fPixelConverter;
091: private Composite fBlockControl;
092:
093: private Map<String, String> fOriginalProcOptions; // cache of saved values
094: private String fOriginalGenSrcDir;
095: private boolean fOriginalAptEnabled;
096: private boolean fOriginalReconcileEnabled;
097:
098: // used to distinguish actual changes from re-setting of same value - see useProjectSpecificSettings()
099: private boolean fPerProjSettingsEnabled;
100:
101: /**
102: * Event handler for Processor Options list control.
103: */
104: private class ProcessorOptionsAdapter implements IListAdapter,
105: IDialogFieldListener {
106:
107: public void customButtonPressed(ListDialogField field, int index) {
108: switch (index) {
109: case IDX_ADD:
110: editOrAddProcessorOption(null);
111: break;
112: case IDX_EDIT:
113: tryToEdit(field);
114: break;
115: }
116: }
117:
118: @SuppressWarnings("unchecked")
119: public void selectionChanged(ListDialogField field) {
120: List selectedElements = field.getSelectedElements();
121: field.enableButton(IDX_EDIT, canEdit(field,
122: selectedElements));
123: }
124:
125: public void doubleClicked(ListDialogField field) {
126: tryToEdit(field);
127: }
128:
129: public void dialogFieldChanged(DialogField field) {
130: updateModel(field);
131: }
132:
133: @SuppressWarnings("unchecked")
134: private boolean canEdit(DialogField field, List selectedElements) {
135: if (!field.isEnabled())
136: return false;
137: return selectedElements.size() == 1;
138: }
139:
140: private void tryToEdit(ListDialogField field) {
141: List<ProcessorOption> selection = getListSelection();
142: if (canEdit(field, selection)) {
143: editOrAddProcessorOption(selection.get(0));
144: }
145: }
146: }
147:
148: /**
149: * An entry in the Processor Options list control.
150: */
151: public static class ProcessorOption {
152: public String key;
153: public String value;
154: }
155:
156: /**
157: * Sorts items in the Processor Options list control.
158: */
159: private static class ProcessorOptionSorter extends ViewerComparator {
160: @SuppressWarnings("unchecked")
161: // getComparator() returns a raw Comparator rather than a Comparator<T>
162: public int compare(Viewer viewer, Object e1, Object e2) {
163: return getComparator().compare(((ProcessorOption) e1).key,
164: ((ProcessorOption) e2).key);
165: }
166: }
167:
168: /**
169: * Controls display of items in the Processor Options list control.
170: */
171: private class ProcessorOptionsLabelProvider extends LabelProvider
172: implements ITableLabelProvider {
173:
174: /* (non-Javadoc)
175: * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
176: */
177: public Image getColumnImage(Object element, int columnIndex) {
178: return null;
179: }
180:
181: /* (non-Javadoc)
182: * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
183: */
184: public String getColumnText(Object element, int columnIndex) {
185: ProcessorOption o = (ProcessorOption) element;
186: if (columnIndex == 0) {
187: return o.key;
188: } else if (columnIndex == 1) {
189: return o.value;
190: } else {
191: return ""; //$NON-NLS-1$
192: }
193: }
194: }
195:
196: public AptConfigurationBlock(IStatusChangeListener context,
197: IProject project, IWorkbenchPreferenceContainer container) {
198: super (context, project, getAllKeys(), container);
199:
200: fJProj = JavaCore.create(project);
201:
202: UpdateAdapter adapter = new UpdateAdapter();
203:
204: if (fJProj != null) {
205: fAptEnabledField = new SelectionButtonDialogField(SWT.CHECK);
206: fAptEnabledField.setDialogFieldListener(adapter);
207: fAptEnabledField
208: .setLabelText(Messages.AptConfigurationBlock_enable);
209: } else {
210: fAptEnabledField = null;
211: }
212:
213: fReconcileEnabledField = new SelectionButtonDialogField(
214: SWT.CHECK);
215: fReconcileEnabledField.setDialogFieldListener(adapter);
216: fReconcileEnabledField
217: .setLabelText(Messages.AptConfigurationBlock_enableReconcileProcessing);
218:
219: fGenSrcDirField = new StringDialogField();
220: fGenSrcDirField.setDialogFieldListener(adapter);
221: fGenSrcDirField
222: .setLabelText(Messages.AptConfigurationBlock_generatedSrcDir);
223:
224: String[] buttons = new String[] {
225: Messages.AptConfigurationBlock_add,
226: Messages.AptConfigurationBlock_edit,
227: Messages.AptConfigurationBlock_remove };
228: ProcessorOptionsAdapter optionsAdapter = new ProcessorOptionsAdapter();
229: fProcessorOptionsField = new ListDialogField(optionsAdapter,
230: buttons, new ProcessorOptionsLabelProvider());
231: fProcessorOptionsField.setDialogFieldListener(optionsAdapter);
232: fProcessorOptionsField.setRemoveButtonIndex(IDX_REMOVE);
233: String[] columnHeaders = new String[] {
234: Messages.AptConfigurationBlock_key,
235: Messages.AptConfigurationBlock_value };
236: fProcessorOptionsField
237: .setTableColumns(new ListDialogField.ColumnsDescription(
238: columnHeaders, true));
239: fProcessorOptionsField
240: .setViewerComparator(new ProcessorOptionSorter());
241: fProcessorOptionsField
242: .setLabelText(Messages.AptConfigurationBlock_options);
243:
244: updateControls();
245:
246: if (fProcessorOptionsField.getSize() > 0) {
247: fProcessorOptionsField.selectFirstElement();
248: } else {
249: fProcessorOptionsField.enableButton(IDX_EDIT, false);
250: }
251:
252: }
253:
254: /*
255: * At workspace level, don't ask for a rebuild.
256: */
257: @Override
258: protected String[] getFullBuildDialogStrings(
259: boolean workspaceSettings) {
260: if (workspaceSettings)
261: return null;
262: // if the only thing that changed was the reconcile setting, return null: a rebuild is not necessary
263: if (fOriginalGenSrcDir.equals(fGenSrcDirField.getText())) {
264: if (fOriginalAptEnabled == fAptEnabledField.isSelected()) {
265: if (!procOptionsChanged()) {
266: return null;
267: }
268: }
269: }
270: return super .getFullBuildDialogStrings(workspaceSettings);
271: }
272:
273: /*
274: * Helper to eliminate unchecked-conversion warning
275: */
276: @SuppressWarnings("unchecked")
277: private List<ProcessorOption> getListElements() {
278: return fProcessorOptionsField.getElements();
279: }
280:
281: /*
282: * Helper to eliminate unchecked-conversion warning
283: */
284: @SuppressWarnings("unchecked")
285: private List<ProcessorOption> getListSelection() {
286: return fProcessorOptionsField.getSelectedElements();
287: }
288:
289: private void editOrAddProcessorOption(ProcessorOption original) {
290: ProcessorOptionInputDialog dialog = new ProcessorOptionInputDialog(
291: getShell(), original, getListElements());
292: if (dialog.open() == Window.OK) {
293: if (original != null) {
294: fProcessorOptionsField.replaceElement(original, dialog
295: .getResult());
296: } else {
297: fProcessorOptionsField.addElement(dialog.getResult());
298: }
299: }
300: }
301:
302: @Override
303: protected Control createContents(Composite parent) {
304: setShell(parent.getShell());
305:
306: fPixelConverter = new PixelConverter(parent);
307: int indent = fPixelConverter.convertWidthInCharsToPixels(4);
308:
309: fBlockControl = new Composite(parent, SWT.NONE);
310: fBlockControl.setFont(parent.getFont());
311:
312: GridLayout layout = new GridLayout();
313: layout.numColumns = 2;
314: layout.marginWidth = 0;
315: layout.marginHeight = 0;
316:
317: fBlockControl.setLayout(layout);
318:
319: DialogField[] fields = fAptEnabledField != null ? new DialogField[] {
320: fAptEnabledField, fReconcileEnabledField,
321: fGenSrcDirField, fProcessorOptionsField, }
322: : new DialogField[] { fReconcileEnabledField,
323: fGenSrcDirField, fProcessorOptionsField, };
324: LayoutUtil.doDefaultLayout(fBlockControl, fields, true,
325: SWT.DEFAULT, SWT.DEFAULT);
326: LayoutUtil.setHorizontalGrabbing(fProcessorOptionsField
327: .getListControl(null));
328:
329: GridData reconcileGD = (GridData) fReconcileEnabledField
330: .getSelectionButton(parent).getLayoutData();
331: reconcileGD.horizontalIndent = indent;
332: fReconcileEnabledField.getSelectionButton(parent)
333: .setLayoutData(reconcileGD);
334:
335: Label description = new Label(fBlockControl, SWT.WRAP);
336: description
337: .setText(Messages.AptConfigurationBlock_classpathAddedAutomaticallyNote);
338: GridData gdLabel = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
339: gdLabel.horizontalSpan = 2;
340: gdLabel.widthHint = fPixelConverter
341: .convertWidthInCharsToPixels(60);
342: description.setLayoutData(gdLabel);
343:
344: Dialog.applyDialogFont(fBlockControl);
345:
346: validateSettings(null, null, null);
347:
348: return fBlockControl;
349: }
350:
351: @Override
352: protected void cacheOriginalValues() {
353: super .cacheOriginalValues();
354: fOriginalProcOptions = AptConfig.getRawProcessorOptions(fJProj);
355: fOriginalGenSrcDir = AptConfig.getGenSrcDir(fJProj);
356: fOriginalAptEnabled = AptConfig.isEnabled(fJProj);
357: fOriginalReconcileEnabled = AptConfig
358: .shouldProcessDuringReconcile(fJProj);
359: fPerProjSettingsEnabled = hasProjectSpecificOptionsNoCache(fProject);
360: }
361:
362: protected void initContents() {
363: loadProcessorOptions(fJProj);
364: }
365:
366: @Override
367: protected void saveSettings() {
368: List<ProcessorOption> elements;
369: boolean isProjSpecificDisabled = (fJProj != null)
370: && !fBlockControl.isEnabled();
371: if (isProjSpecificDisabled) {
372: // We're in a project properties pane but the entire configuration
373: // block control is disabled. That means the per-project settings checkbox
374: // is unchecked. To save that state, we'll clear the proc options map.
375: elements = Collections.<ProcessorOption> emptyList();
376: } else {
377: elements = getListElements();
378: }
379: saveProcessorOptions(elements);
380: super .saveSettings();
381: if (null != fAptProject) {
382: if (isProjSpecificDisabled) { // compare against workspace defaults
383: if (!fOriginalGenSrcDir.equals(AptConfig
384: .getGenSrcDir(null))) {
385: fAptProject
386: .preferenceChanged(AptPreferenceConstants.APT_GENSRCDIR);
387: }
388: if (fOriginalAptEnabled != AptConfig.isEnabled(null)) {
389: fAptProject
390: .preferenceChanged(AptPreferenceConstants.APT_ENABLED);
391: // make JDT "processingEnabled" setting track APT "enabled" setting.
392: setJDTProcessAnnotationsSetting(fAptEnabledField
393: .isSelected());
394: }
395: if (fOriginalReconcileEnabled != AptConfig
396: .shouldProcessDuringReconcile(null)) {
397: fAptProject
398: .preferenceChanged(AptPreferenceConstants.APT_RECONCILEENABLED);
399: }
400: } else { // compare against current settings
401: if (!fOriginalGenSrcDir.equals(fGenSrcDirField
402: .getText()))
403: fAptProject
404: .preferenceChanged(AptPreferenceConstants.APT_GENSRCDIR);
405: boolean isAptEnabled = fAptEnabledField.isSelected();
406: if (fOriginalAptEnabled != isAptEnabled) {
407: fAptProject
408: .preferenceChanged(AptPreferenceConstants.APT_ENABLED);
409: // make JDT "processingEnabled" setting track APT "enabled" setting.
410: setJDTProcessAnnotationsSetting(isAptEnabled);
411: }
412: if (fOriginalReconcileEnabled != fReconcileEnabledField
413: .isSelected())
414: fAptProject
415: .preferenceChanged(AptPreferenceConstants.APT_RECONCILEENABLED);
416: }
417: }
418: }
419:
420: /**
421: * Set the org.eclipse.jdt.core.compiler.processAnnotations setting.
422: * In Eclipse 3.3, this value replaces org.eclipse.jdt.apt.aptEnabled,
423: * but we continue to set both values in order to ensure backward
424: * compatibility with prior versions.
425: * the aptEnabled setting.
426: * @param enable
427: */
428: private void setJDTProcessAnnotationsSetting(boolean enable) {
429: IScopeContext context = (null != fJProj) ? new ProjectScope(
430: fJProj.getProject()) : new InstanceScope();
431: IEclipsePreferences node = context.getNode(JavaCore.PLUGIN_ID);
432: final String value = enable ? AptPreferenceConstants.ENABLED
433: : AptPreferenceConstants.DISABLED;
434: node.put(AptPreferenceConstants.APT_PROCESSANNOTATIONS, value);
435: try {
436: node.flush();
437: } catch (BackingStoreException e) {
438: AptPlugin
439: .log(
440: e,
441: "Failed to save preference: " + AptPreferenceConstants.APT_PROCESSANNOTATIONS); //$NON-NLS-1$
442: }
443: }
444:
445: /**
446: * Check whether any processor options have changed.
447: * @return true if they did.
448: */
449: private boolean procOptionsChanged() {
450: Map<String, String> savedProcOptions = new HashMap<String, String>(
451: fOriginalProcOptions);
452: for (ProcessorOption o : getListElements()) {
453: final String savedVal = savedProcOptions.get(o.key);
454: if (savedVal != null && savedVal.equals(o.value)) {
455: savedProcOptions.remove(o.key);
456: } else {
457: // found an unsaved option in the list
458: return true;
459: }
460: }
461: if (!savedProcOptions.isEmpty()) {
462: // found a saved option that has been removed
463: return true;
464: }
465: return false;
466: }
467:
468: /**
469: * Check whether any processor options have changed, as well as
470: * any of the settings tracked in the "normal" way (as Keys).
471: */
472: @Override
473: protected boolean settingsChanged(IScopeContext currContext) {
474: if (procOptionsChanged())
475: return true;
476: else
477: return super .settingsChanged(currContext);
478: }
479:
480: /**
481: * Call after updating key values, to warn user if new values are invalid.
482: * @param changedKey may be null, e.g. if called from createContents.
483: * @param oldValue may be null
484: * @param newValue may be null
485: */
486: @Override
487: protected void validateSettings(Key changedKey, String oldValue,
488: String newValue) {
489: IStatus status = null;
490:
491: status = validateGenSrcDir();
492: if (status.getSeverity() == IStatus.OK) {
493: status = validateProcessorOptions();
494: }
495:
496: fContext.statusChanged(status);
497: }
498:
499: /**
500: * Validate "generated source directory" setting. It must be a valid
501: * pathname relative to a project, and must not be a source directory.
502: * @return true if current field value is valid
503: */
504: private IStatus validateGenSrcDir() {
505: String dirName = fGenSrcDirField.getText();
506: if (!AptConfig.validateGenSrcDir(fJProj, dirName)) {
507: return new StatusInfo(
508: IStatus.ERROR,
509: Messages.AptConfigurationBlock_genSrcDirMustBeValidRelativePath);
510: }
511: if (fJProj != null && !dirName.equals(fOriginalGenSrcDir)) {
512: IFolder folder = fJProj.getProject().getFolder(dirName);
513: if (folder != null && folder.exists()
514: && !folder.isDerived()) {
515: return new StatusInfo(
516: IStatus.WARNING,
517: Messages.AptConfigurationBlock_warningContentsMayBeDeleted);
518: }
519: }
520: return new StatusInfo();
521: }
522:
523: /**
524: * Validate the currently set processor options. We do this by
525: * looking at the table contents rather than the packed string,
526: * just because it's easier.
527: * @return a StatusInfo containing a warning if appropriate.
528: */
529: private IStatus validateProcessorOptions() {
530: List<ProcessorOption> elements = getListElements();
531: for (ProcessorOption o : elements) {
532: if (AptConfig.isAutomaticProcessorOption(o.key)) {
533: return new StatusInfo(
534: IStatus.WARNING,
535: Messages.AptConfigurationBlock_warningIgnoredOptions
536: + ": " + o.key); //$NON-NLS-1$
537: }
538: }
539: return new StatusInfo();
540: }
541:
542: /**
543: * Update the UI based on the values presently stored in the keys.
544: */
545: @Override
546: protected void updateControls() {
547: if (fAptEnabledField != null) {
548: boolean aptEnabled = Boolean.valueOf(
549: getValue(KEY_APTENABLED)).booleanValue();
550: fAptEnabledField.setSelection(aptEnabled);
551: }
552: boolean reconcileEnabled = Boolean.valueOf(
553: getValue(KEY_RECONCILEENABLED)).booleanValue();
554: fReconcileEnabledField.setSelection(reconcileEnabled);
555: String str = getValue(KEY_GENSRCDIR);
556: fGenSrcDirField.setText(str == null ? "" : str); //$NON-NLS-1$
557: }
558:
559: /**
560: * Update the values stored in the keys based on the UI.
561: */
562: protected final void updateModel(DialogField field) {
563:
564: if (fAptEnabledField != null && field == fAptEnabledField) {
565: String newVal = String.valueOf(fAptEnabledField
566: .isSelected());
567: setValue(KEY_APTENABLED, newVal);
568: } else if (field == fGenSrcDirField) {
569: String newVal = fGenSrcDirField.getText();
570: setValue(KEY_GENSRCDIR, newVal);
571: } else if (field == fReconcileEnabledField) {
572: String newVal = String.valueOf(fReconcileEnabledField
573: .isSelected());
574: setValue(KEY_RECONCILEENABLED, newVal);
575: }
576: validateSettings(null, null, null); // params are ignored
577: }
578:
579: /**
580: * Bugzilla 136498: when project-specific settings are enabled, force APT to be enabled.
581: */
582: @Override
583: public void useProjectSpecificSettings(boolean enable) {
584: super .useProjectSpecificSettings(enable);
585: if (enable ^ fPerProjSettingsEnabled) {
586: fAptEnabledField.setSelection(enable);
587: fPerProjSettingsEnabled = enable;
588: }
589: }
590:
591: /**
592: * Save the contents of the options list.
593: */
594: private void saveProcessorOptions(List<ProcessorOption> elements) {
595: Map<String, String> map = new LinkedHashMap<String, String>(
596: elements.size());
597: for (ProcessorOption o : elements) {
598: map.put(o.key, (o.value.length() > 0) ? o.value : null);
599: }
600: AptConfig.setProcessorOptions(map, fJProj);
601: }
602:
603: /**
604: * Set the processor options list contents
605: */
606: private void loadProcessorOptions(IJavaProject jproj) {
607: List<ProcessorOption> options = new ArrayList<ProcessorOption>();
608: Map<String, String> parsedOptions = AptConfig
609: .getRawProcessorOptions(jproj);
610: for (Map.Entry<String, String> entry : parsedOptions.entrySet()) {
611: ProcessorOption o = new ProcessorOption();
612: o.key = entry.getKey();
613: if (o.key == null || o.key.length() < 1) {
614: // Don't allow defective entries
615: continue;
616: }
617: o.value = (entry.getValue() == null) ? "" : entry.getValue(); //$NON-NLS-1$
618: options.add(o);
619: }
620: fProcessorOptionsField.setElements(options);
621: }
622:
623: @Override
624: public void performDefaults() {
625: fPerProjSettingsEnabled = false;
626: if (fJProj != null) {
627: // If project-specific, load workspace settings
628: loadProcessorOptions(null);
629: } else {
630: // If workspace, load "factory default," which is empty.
631: fProcessorOptionsField.removeAllElements();
632: }
633: super.performDefaults();
634: }
635:
636: }
|