/*******************************************************************************
* Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) 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:
* Thomas Holland - initial API and implementation
*******************************************************************************/
package de.innot.avreclipse.ui.editors.targets;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.GC;
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.ui.forms.IManagedForm;
import org.eclipse.ui.forms.IMessage;
import org.eclipse.ui.forms.IMessageManager;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.forms.widgets.TableWrapData;
import org.eclipse.ui.forms.widgets.TableWrapLayout;
import de.innot.avreclipse.core.targets.ITargetConfigChangeListener;
import de.innot.avreclipse.core.targets.ITargetConfiguration;
import de.innot.avreclipse.core.targets.ITargetConfigurationWorkingCopy;
import de.innot.avreclipse.core.targets.ITargetConfiguration.Result;
import de.innot.avreclipse.core.targets.ITargetConfiguration.ValidationResult;
/**
* Default implementation of the {@link ITCEditorPart} which creates a Section.
* <p>
* This implementation completely hides the <code>IFormPart</code> interface and its extension, the
* {@link ITCEditorPart}.
* </p>
* <p>
* Subclasses only need to implement the following methods:
* <ul>
* <li>{@link #getTitle()} to set the title of the section.</li>
* <li>{@link #getPartAttributes()} for a list of managed attributes.</li>
* <li>{@link #createSectionContent(Composite, FormToolkit)} to create the static parts of the user
* interface.</li>
* <li>{@link #refreshSectionContent()} to refresh the static parts and create the dynamic parts of
* the user interface.</li>
* </ul>
* Optionally the following methods can be overridden to give this part more information:
* <ul>
* <li>{@link #getDescription()} for an optional Section description.</li>
* <li>{@link #getDependentAttributes()} for a list of attributes that will cause this part to
* become stale when changed.</li>
* <li>{@link #getSectionStyle()} to override the style bits for the section.</li>
* <li>{@link #refreshMessages()} to let the part update the messages in the shared header, even
* when the form is not active.</li>
* </ul>
*
* @author Thomas Holland
* @since 2.4
*
*/
public abstract class AbstractTCSectionPart implements ITCEditorPart {
/** The managed form this object is a part of. */
private IManagedForm fManagedForm;
/**
* The current message manager. Can be changed with the
* {@link #setMessageManager(IMessageManager)} method.
*/
private IMessageManager fMessageManager;
/** The parent composite for this part. */
private Composite fParent;
/** The Section control created by this part. */
private Section fSection;
/** The content of the Section created by this part. */
private Composite fContentCompo;
/** The target configuration (working copy) this form part works on. */
private ITargetConfigurationWorkingCopy fTCWC;
/** Map of attributes managed by this part and their last saved values. */
private final Map<String, String> fLastValues = new HashMap<String, String>();
/** Map of attributes that will cause this part to become stale and their last saved values. */
private final Map<String, String> fLastDependentValues = new HashMap<String, String>();
private final ITargetConfigChangeListener fListener = new ChangeListener();
/** <code>true</code> if the part has been initialized. The part may only be initialized once. */
private boolean fIsInitialized = false;
/** <code>true</code> if the part has become stale because a dependent attribute was changed. */
private boolean fIsStale = false;
public final static String[] EMPTY_LIST = new String[] {};
/**
* A target configuration change listener that will mark this part as stale when any of the
* attributes in the list returned by {@link AbstractTCSectionPart#getDependentAttributes()} has
* a value different from its last saved value.
*/
private class ChangeListener implements ITargetConfigChangeListener {
/*
* (non-Javadoc)
* @seede.innot.avreclipse.core.targets.TargetConfiguration.ITargetConfigChangeListener#
* attributeChange(de.innot.avreclipse.core.targets.ITargetConfiguration, java.lang.String,
* java.lang.String, java.lang.String)
*/
public void attributeChange(ITargetConfiguration config, String attribute, String oldvalue,
String newvalue) {
if (fLastDependentValues.containsKey(attribute)) {
String lastValue = fLastDependentValues.get(attribute);
// the attribute is on the list of dependent values.
// The part is stale when the new value is different from the last saved value
if (lastValue == null) {
fIsStale = true;
} else {
fIsStale = !lastValue.equals(newvalue);
}
fManagedForm.staleStateChanged();
fLastDependentValues.put(attribute, newvalue);
}
// When the form is marked as stale it will only be refreshed when it is (or
// becomes)
// active. But the changes might cause some problems with this form that need to be
// shown immediately in the form header.
// So we call this method here to give the subclass a chance to update its problem
// messages.
refreshMessages();
}
}
/**
* Returns a list of attributes that are managed by this form part.
* <p>
* This list is used to associate attributes with the form part that edits them. Internally the
* list is also used to manage the dirty state of the form part.
* </p>
*
* @return Array with attributes. May be empty but never <code>null</code>
*/
protected abstract String[] getPartAttributes();
/**
* Returns a list of attributes whose changes cause the form part to become stale.
* <p>
* Changes to any of these attributes will cause {@link #refresh()} to be called, although the
* call may be delayed if the part is not currently visible.
* </p>
*
* @return Array with attributes. May be empty but never <code>null</code>
*/
protected String[] getDependentAttributes() {
return EMPTY_LIST;
}
/**
* Returns a String to be used as the title for this section part.
*
* @return The title string.
*/
protected abstract String getTitle();
/**
* Returns an optional description for this section part.
* <p>
* If <code>null</code> is returned the section will not have a description.
* </p>
*
* @return A description string or <code>null</code>.
*/
protected String getDescription() {
return null;
}
/**
* Returns the style bits used for creating the section.
* <p>
* The default is: EXPANDED, TITLE_BAR and CLIENT_INDENT. Subclasses can override this method if
* they need different style bits.
* </p>
*
* @return
*/
protected int getSectionStyle() {
return Section.EXPANDED | Section.TITLE_BAR | Section.CLIENT_INDENT;
}
/**
* Create the actual content of the section.
* <p>
* This method is called during the initialization of this form part. Implementations can add
* their static user interface parts to the given parent here.
* </p>
* <p>
* The given parent composite does not have a layout. It is up to the implementation to add the
* appropriate layout to the composite.
* </p>
*
* @param parent
* @param toolkit
*/
abstract protected void createSectionContent(Composite parent, FormToolkit toolkit);
/**
* Update the content in reaction to changes in the source target configuration.
* <p>
* This method is called when the form part receives a <code>refresh()</code> event.
* Implementations can update the static parts of the user interface and/or create the dynamic
* parts (dependent on the current target configuration).
* </p>
*/
abstract protected void refreshSectionContent();
/**
* Update the messages in the shared header of the editor.
* <p>
* This method is called every time the part becomes stale, i.e. a dependent attribute has
* changed its value. The implementation must then create or remove the appropriate messages via
* the message manager of this form part.
* </p>
* <p>
* Unlike {@link #refreshSectionContent()} this method will be called even when the part is not
* active and therefore not visible.
* </p>
*
* @see #getMessageManager();
*
*/
protected void refreshMessages() {
// empty default.
}
/**
* Create the section which is the root control of this form part.
* <p>
* This method calls {@link #createSectionContent(Composite, FormToolkit)} from the subclass to
* fill the content of the section.
* </p>
* <p>
* If the {@link #getDescription()} method returns a non-<code>null</code> string, then a label
* with the description is created as the first child of the section. In this case it is
* important that the parent and all composites above it (up to the managed form body) use a
* TableWrapLayout so that the description will get wrapped correctly when the form is resized.
* </p>
*
* @param parent
* Parent composite from the {@link #setParent(Composite)} method.
* @param toolkit
* the form toolkit
* @return the new section control.
*/
protected Control createContent(Composite parent, FormToolkit toolkit) {
fSection = toolkit.createSection(parent, getSectionStyle());
fSection.setText(getTitle());
Composite sectionClient = toolkit.createComposite(fSection);
String description = getDescription();
if (description != null) {
// Why not use section.setDescription()?
// Well, I had problems with that:
// a) This is implemented as an read-only Text control instead of a label, so the
// description could get the focus, have a cursor and participate in the tab cycle - not
// very professional looking!
// b) Probably due to a) the description would flicker whenever the editor was resized.
//
// So instead we use a normal Label to display the description. To have the label wrap
// correctly on resize it is put into a TableWrapLayout().
sectionClient.setLayout(new TableWrapLayout());
Label label = toolkit.createLabel(sectionClient, description, SWT.WRAP);
label.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB));
// Create new virgin Composite for the subclass content.
Composite compo = toolkit.createComposite(sectionClient);
compo.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB));
fContentCompo = compo;
} else {
fContentCompo = sectionClient;
}
// Call the subclass to have it create its user interface stuff.
createSectionContent(fContentCompo, toolkit);
fSection.setClient(sectionClient);
// If the Section is expandable we add a listener that will reflow the complete form
// whenever our section is expanded or collapsed.
if ((fSection.getExpansionStyle() & Section.TWISTIE) != 0
|| (fSection.getExpansionStyle() & Section.TREE_NODE) != 0) {
fSection.addExpansionListener(new ExpansionAdapter() {
public void expansionStateChanging(ExpansionEvent e) {
// Do nothing
}
public void expansionStateChanged(ExpansionEvent e) {
getManagedForm().getForm().reflow(false);
}
});
}
return fSection;
}
/*
* (non-Javadoc)
* @see
* de.innot.avreclipse.ui.editors.targets.ITCEditorPart#setParent(org.eclipse.swt.widgets.Composite
* )
*/
public void setParent(Composite parent) {
fParent = parent;
}
/*
* (non-Javadoc)
* @see
* de.innot.avreclipse.ui.editors.targets.ITCEditorPart#setMessageManager(org.eclipse.ui.forms
* .IMessageManager)
*/
public void setMessageManager(IMessageManager manager) {
fMessageManager = manager;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm)
*/
public void initialize(IManagedForm form) {
// Only initialize once (to avoid duplicate UI elements)
if (!fIsInitialized) {
fIsInitialized = true;
fManagedForm = form;
if (fParent == null) {
fParent = form.getForm().getBody();
}
createContent(fParent, form.getToolkit());
}
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#setFormInput(java.lang.Object)
*/
public boolean setFormInput(Object input) {
if (input instanceof ITargetConfigurationWorkingCopy) {
fTCWC = (ITargetConfigurationWorkingCopy) input;
// Save the current values for dirty state tracking
String[] managedAttributes = getPartAttributes();
for (String attr : managedAttributes) {
String currValue = fTCWC.getAttribute(attr);
fLastValues.put(attr, currValue);
}
// Save the current values for stale state tracking
String[] dependentAttributes = getDependentAttributes();
for (String attr : dependentAttributes) {
String currValue = fTCWC.getAttribute(attr);
fLastDependentValues.put(attr, currValue);
}
// Add the listener to the config to do the stale state management
fTCWC.addPropertyChangeListener(fListener);
// Let the form redraw itself for the new input.
refresh();
return true;
}
return false;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#refresh()
*/
public void refresh() {
refreshSectionContent();
fIsStale = false;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#commit(boolean)
*/
public void commit(boolean onSave) {
// if the form is actually saved (not just a page change),
// then we take all managed attributes of the subclass and store their current
// value. This is used to check if the form part is dirty or not.
if (onSave) {
String[] managedAttributes = getPartAttributes();
for (String attr : managedAttributes) {
String newvalue = fTCWC.getAttribute(attr);
fLastValues.put(attr, newvalue);
}
}
// this will in turn cause isDirty() to be called which will determine if this form part
// still has unsaved changes.
getManagedForm().dirtyStateChanged();
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#dispose()
*/
public void dispose() {
// Remove the listener.
// overriding classes need to call super.dispose();
if (fTCWC != null) {
fTCWC.removePropertyChangeListener(fListener);
}
Control control = getControl();
if (control != null) {
control.dispose();
}
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#isDirty()
*/
public boolean isDirty() {
// Compare the current values of the target configuration with the last saved values. If at
// least on of them is different, then the part is dirty.
boolean isDirty = false;
for (String attr : fLastValues.keySet()) {
String currValue = fTCWC.getAttribute(attr);
String lastValue = fLastValues.get(attr);
if (!currValue.equals(lastValue)) {
isDirty = true;
break; // no need to compare further
}
}
return isDirty;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#isStale()
*/
public boolean isStale() {
// The stale flag is set by the config change listener.
return fIsStale;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.IFormPart#setFocus()
*/
public void setFocus() {
if (fSection != null) {
Control client = fSection.getClient();
if (client != null) {
client.setFocus();
}
}
}
/**
* Set the focus to the control that can change the given attribute.
*
* @param attribute
* A target configuration attribute.
* @return <code>false</code> if this form part has no controls for the attribute.
*/
public boolean setFocus(String attribute) {
// This is the default.
// Subclasses should override to set the focus on the correct control for the attribute.
return false;
}
/*
* (non-Javadoc)
* @see de.innot.avreclipse.ui.editors.targets.ITCEditorPart#getControl()
*/
public Section getControl() {
return fSection;
}
/**
* Returns the Managed Form used by this form part.
* <p>
* This method must not be called before the part has been initialized.
* </p>
*
* @return
*/
public IManagedForm getManagedForm() {
Assert.isNotNull(fManagedForm, "getManagedForm() called before initialize()");
return fManagedForm;
}
/**
* Gets the current message manager for this form.
* <p>
* If a message manager has not been set via {@link #setMessageManager(IMessageManager)}, then
* the message manager from the managed form is used.
* </p>
* <p>
* This method must not be called before the part has been initialized.
* </p>
*
* @return
*/
protected IMessageManager getMessageManager() {
if (fMessageManager == null) {
Assert.isNotNull(fManagedForm, "getMessageManager() called before initialize()");
fMessageManager = fManagedForm.getMessageManager();
}
return fMessageManager;
}
/**
* Get the target configuration that the editor works on.
* <p>
* This method must not be called before the form input has been set (
* {@link #setFormInput(Object)}).
* </p>
*
* @return A reference to the current target configuration (working copy)
*/
protected ITargetConfigurationWorkingCopy getTargetConfiguration() {
Assert.isNotNull(fTCWC, "getTargetConfiguration() called before setFormInput()");
return fTCWC;
}
/**
* Get the width in pixels of the given String.
* <p>
* The width is calculated by using the current font of the given control.
* </p>
*
* @param control
* The parent control
* @param text
* The text for which to get the width.
* @return Width in pixels.
*/
protected int calcTextWidth(Control control, String text) {
GC gc = new GC(control);
gc.setFont(control.getFont());
int value = gc.stringExtent(text).x;
gc.dispose();
return value;
}
/**
* Enable / Disable the given Composite.
* <p>
* This method will call the <code>setEnabled(value)</code> method of all children of the given
* composite.
* </p>
* <p>
* Note: The method is not recursive. If any child is a composite itself, then its children will
* not be affected by this method.
* </p>
*
* @param compo
* A <code>Composite</code> with some controls.
* @param value
* <code>true</code> to enable, <code>false</code> to disable the given composite.
*/
protected void setEnabled(Composite compo, boolean value) {
Control[] children = compo.getChildren();
for (Control child : children) {
child.setEnabled(value);
}
}
public interface IValidationListener {
public void result(ValidationResult result);
}
/**
* Validate an attribute and set the form messages for the given control.
* <p>
* If the validation result is either {@link Result#WARNING} or {@link Result#ERROR}, then a
* message is added to the control with the result of the validation. If the validation
* indicates no problems ({@link Result#OK}), then any existing message for the attribute /
* control pair is removed.
* </p>
*
* @param attribute
* The attribute to be validated
* @param control
* The control that will receive any warning or error messages.
*/
protected void validate(final String attribute, final Control control) {
validate(attribute, control, null);
}
protected void validate(final String attribute, final Control control,
final IValidationListener validationhook) {
// Validation can take some time, so we run it in a separate background job.
// Once the validation is completed we need to check if the control still exists (the user
// might have closed the editor), and if yes we need to run the actual update in the GUI
// thread again.
Job validatejob = new Job("validate") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
monitor.beginTask("Validating " + attribute, 100);
ITargetConfiguration tc = getTargetConfiguration();
final ValidationResult res = tc.validateAttribute(attribute);
monitor.worked(90);
if (control != null && !control.isDisposed()) {
Display display = control.getDisplay();
if (display != null && !display.isDisposed()) {
// The actual update is run in the GUI threat.
display.syncExec(new Runnable() {
public void run() {
IMessageManager msgmngr = getMessageManager();
switch (res.result) {
case OK:
msgmngr.removeMessage(attribute, control);
break;
case WARNING:
msgmngr.addMessage(attribute, res.description,
attribute, IMessage.WARNING, control);
break;
case ERROR:
msgmngr.addMessage(attribute, res.description,
attribute, IMessage.ERROR, control);
break;
default:
// do nothing
}
// Now execute the optional hook method from the caller
if (validationhook != null) {
validationhook.result(res);
}
}
});
}
}
monitor.worked(10);
} catch (SWTException e) {
// Probably the control has been disposed after the check.
// In this case we just ignore the validation result.
e.printStackTrace();
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
};
validatejob.setSystem(true);
validatejob.setPriority(Job.SHORT);
validatejob.schedule();
}
}