/*
* Carrot2 project.
*
* Copyright (C) 2002-2014, Dawid Weiss, Stanisław Osiński.
* All rights reserved.
*
* Refer to the full license file "carrot2.LICENSE"
* in the root folder of the repository checkout or at:
* http://www.carrot2.org/carrot2.LICENSE
*/
package org.carrot2.workbench.core.ui;
import static org.eclipse.swt.SWT.NONE;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.commons.lang.StringUtils;
import org.carrot2.core.IProcessingComponent;
import org.carrot2.core.attribute.*;
import org.carrot2.util.attribute.AttributeDescriptor;
import org.carrot2.util.attribute.BindableDescriptor;
import org.carrot2.util.attribute.BindableDescriptor.GroupingMethod;
import org.carrot2.workbench.core.helpers.GUIFactory;
import org.carrot2.workbench.core.helpers.Utils;
import org.carrot2.workbench.editors.*;
import org.carrot2.workbench.editors.factory.EditorFactory;
import org.carrot2.workbench.editors.factory.EditorNotFoundException;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.*;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* An SWT composite capable of displaying groups of {@link IAttributeEditor}s, sorted by
* {@link GroupingMethod} and filtered using {@link #setFilter(Predicate)}.
* <p>
* This component does not store its own state, you should save and restore the state from
* within parent components.
*/
public final class AttributeGroups extends Composite implements IAttributeEventProvider
{
/**
* Padding between a given group (when it is expanded) and the following group.
*/
public static final int SPACE_TO_NEXT_GROUP = 10;
/**
* Section name for ungrouped attributes.
*/
private static final String UNGROUPED_ATTRIBUTES_GROUP = "Ungrouped";
/**
* Method of grouping attribute editors.
*/
private GroupingMethod grouping;
/**
* Main control in which editors are embedded.
*/
private Composite mainControl;
/**
* External attribute change listeners.
*/
private final List<IAttributeListener> listeners = new CopyOnWriteArrayList<IAttributeListener>();
/**
* Forward events from editors to external listeners.
*/
private final IAttributeListener forwardListener = new ForwardingAttributeListener(
listeners);
/**
* Update {@link #currentValues}.
*/
private final IAttributeListener updateListener = new AttributeListenerAdapter()
{
public void valueChanged(AttributeEvent event)
{
currentValues.put(event.key, event.value);
}
@Override
public void valueChanging(AttributeEvent event)
{
valueChanged(event);
}
};
/**
* Descriptors of attribute editors to be created.
*/
private BindableDescriptor descriptor;
/**
* A hashmap of attribute IDs to {@link AttributeList} that contain them.
*
* @see #setAttribute(String, Object)
*/
private Map<String, AttributeList> attributeEditors = Maps.newLinkedHashMap();
/**
* Predicate used to filter attribute descriptors shown in the editor or
* <code>null</code>.
*/
private Predicate<AttributeDescriptor> filterPredicate;
/**
* A copy of the initial default values, kept in sync with changes reported by the
* editors.
*/
private HashMap<String, Object> currentValues;
/**
* {@link Section} objects for attribute groups, if any.
*/
private Map<String, Section> attributeGroupSections = Maps.newHashMap();
/**
* Expansion state for sections (regardless if they are shown or not).
*/
private Map<String, Boolean> attributeGroupExpansionState = Maps.newHashMap();
/**
* A predicate that filters out all descriptors without an editor.
*/
private class HasEditorPredicate implements Predicate<AttributeDescriptor>
{
public boolean apply(AttributeDescriptor ad)
{
try
{
Class<?> clazz = AttributeGroups.this.descriptor.type;
if (clazz != null && IProcessingComponent.class.isAssignableFrom(clazz))
{
EditorFactory.getEditorFor(clazz
.asSubclass(IProcessingComponent.class), ad);
}
else
{
EditorFactory.getEditorFor(null, ad);
}
return true;
}
catch (EditorNotFoundException e)
{
return false;
}
}
}
/**
* Builds the component for a given {@link BindableDescriptor}.
*/
public AttributeGroups(Composite parent, BindableDescriptor descriptor,
GroupingMethod grouping, Predicate<AttributeDescriptor> filter,
Map<String, Object> defaultValues)
{
super(parent, SWT.NONE);
this.descriptor = descriptor;
this.grouping = grouping;
this.filterPredicate = filter;
createComponents();
this.currentValues = Maps.newHashMap(defaultValues);
refreshUI();
}
/**
* Create GUI components (sections, editors).
*/
private void createComponents()
{
final GridLayout layout = GUIFactory.zeroMarginGridLayout();
this.setLayout(layout);
final Composite mainControl = new Composite(this, NONE);
mainControl.setLayout(GUIFactory.zeroMarginGridLayout());
mainControl.setLayoutData(GridDataFactory.fillDefaults().grab(true, true)
.create());
this.mainControl = mainControl;
}
/**
* Reset the grouping of attributes inside this component.
*/
public void setGrouping(GroupingMethod newGrouping)
{
if (newGrouping == grouping)
{
return;
}
this.grouping = newGrouping;
refreshUI();
}
/**
* Reset attribute filter.
*/
public void setFilter(Predicate<AttributeDescriptor> filter)
{
if (this.filterPredicate == filter)
{
return;
}
this.filterPredicate = filter;
refreshUI();
}
/**
* Refresh user interface after a change to the displayed editors. This may take a
* longer time.
*/
private void refreshUI()
{
this.mainControl.setRedraw(false);
disposeEditors();
createEditors(mainControl);
this.mainControl.setRedraw(true);
forceReflow();
}
/**
* Force reflow of parent {@link SharedScrolledComposite}s, if there are any.
*/
private void forceReflow()
{
Control c = this;
while (c != null)
{
if (c instanceof SharedScrolledComposite)
{
((SharedScrolledComposite) c).reflow(true);
break;
}
c = c.getParent();
}
}
/**
* Returns the current grouping state.
*/
public GroupingMethod getGrouping()
{
return grouping;
}
/**
* Create editors based on the given {@link GroupingMethod}.
*/
private void createEditors(Composite parent)
{
/*
* Create a filtered view of attribute descriptors.
*/
BindableDescriptor descriptor = this.descriptor.only(
Predicates.not(new InternalAttributePredicate(false))).only(
new HasEditorPredicate());
if (filterPredicate != null)
{
descriptor = descriptor.only(filterPredicate);
}
/*
* CARROT-818: Check if there is anything for display.
*/
if (descriptor.attributeDescriptors.isEmpty())
{
createMessage(mainControl, this, "No attributes.");
}
/*
* Group attributes.
*/
descriptor = descriptor.group(grouping);
/*
* Create sections for attribute groups.
*/
for (Object groupKey : descriptor.attributeGroups.keySet())
{
final String groupLabel;
if (groupKey instanceof Class<?>)
{
groupLabel = ((Class<?>) groupKey).getSimpleName();
}
else
{
// Anything else, we simply convert to a string.
groupLabel = StringUtils.abbreviate(groupKey.toString(), 160);
}
createEditorGroup(mainControl, groupKey.toString(), groupLabel, descriptor,
descriptor.attributeGroups.get(groupKey), this);
}
/*
* If we have a NONE grouping, then skip creating section headers.
*/
if (grouping == GroupingMethod.NONE)
{
if (descriptor.attributeGroups.size() > 0)
{
Utils.logError("There should be no groups if grouping is NONE.", false);
}
if (!descriptor.attributeDescriptors.isEmpty())
{
createUntitledEditorGroup(mainControl, descriptor,
descriptor.attributeDescriptors, this);
}
}
else
{
/*
* Otherwise, add remaining attributes under a synthetic group.
*/
if (!descriptor.attributeDescriptors.isEmpty())
{
createEditorGroup(mainControl, UNGROUPED_ATTRIBUTES_GROUP,
UNGROUPED_ATTRIBUTES_GROUP, descriptor,
descriptor.attributeDescriptors, this);
}
}
}
/**
* Creates a message control showing a given message.
*/
private void createMessage(Composite parent, AttributeGroups attributeGroups, String message)
{
final GridData gd = new GridData();
gd.horizontalAlignment = GridData.FILL;
gd.verticalAlignment = GridData.FILL;
gd.grabExcessHorizontalSpace = true;
gd.grabExcessVerticalSpace = false;
final CLabel label = new CLabel(parent, SWT.NONE);
label.setLayoutData(gd);
label.setText(message);
label.setAlignment(SWT.LEFT);
}
/**
* Create an unnamed (no section title, no twistie) editor group associated with a
* group of attributes.
*/
private void createUntitledEditorGroup(final Composite parent,
BindableDescriptor descriptor, Map<String, AttributeDescriptor> attributes,
IAttributeEventProvider globalEventsProvider)
{
final GridLayout layout = GUIFactory.zeroMarginGridLayout();
layout.numColumns = 1;
final Composite inner = new Composite(parent, SWT.NONE);
inner.setLayout(layout);
final GridData gd = new GridData();
gd.horizontalAlignment = GridData.FILL;
gd.verticalAlignment = GridData.FILL;
gd.grabExcessHorizontalSpace = true;
gd.grabExcessVerticalSpace = false;
inner.setLayoutData(gd);
final AttributeList editorList = new AttributeList(inner, descriptor, attributes,
globalEventsProvider, currentValues);
final GridData data = new GridData();
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
data.grabExcessVerticalSpace = false;
editorList.setLayoutData(data);
/*
* Update mapping between attribute keys and editors (for event routing).
*/
for (String key : attributes.keySet())
{
attributeEditors.put(key, editorList);
}
/*
* Register for event notifications from editors.
*/
editorList.addAttributeListener(forwardListener);
editorList.addAttributeListener(updateListener);
}
/**
* Create an editor group associated with a group of attributes.
*/
private void createEditorGroup(final Composite parent, final String groupKey,
String groupName, BindableDescriptor descriptor,
Map<String, AttributeDescriptor> attributes,
IAttributeEventProvider globalEventsProvider)
{
final int style = ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT;
final Section group = new Section(parent, style);
group.setText(groupName);
group.setExpanded(getExpansionState(groupKey));
group.setSeparatorControl(new Label(group, SWT.SEPARATOR | SWT.HORIZONTAL));
final GridLayout layout = GUIFactory.zeroMarginGridLayout();
layout.numColumns = 1;
// Vertical space between groups of attributes
layout.marginBottom = SPACE_TO_NEXT_GROUP;
layout.marginTop = AttributeList.SPACE_BEFORE_LABEL;
/*
* Prepare editors inside the group widget. Add some extra space at the bottom of
* the control to separate from the next group
*/
final Composite inner = new Composite(group, SWT.NONE);
inner.setLayout(layout);
final AttributeList editorList = new AttributeList(inner, descriptor, attributes,
globalEventsProvider, currentValues);
final GridData data = new GridData();
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
data.grabExcessVerticalSpace = false;
editorList.setLayoutData(data);
/*
* Update mapping between attribute keys and editors (for event routing).
*/
for (String key : attributes.keySet())
{
attributeEditors.put(key, editorList);
}
/*
* Register for event notifications from editors.
*/
editorList.addAttributeListener(forwardListener);
editorList.addAttributeListener(updateListener);
group.setClient(inner);
/*
* Grid data for the entire group.
*/
final GridData gd = new GridData();
gd.horizontalAlignment = GridData.FILL;
gd.verticalAlignment = GridData.FILL;
gd.grabExcessHorizontalSpace = true;
gd.grabExcessVerticalSpace = false;
group.setLayoutData(gd);
/*
* On group expand/contract, reflow the scrollable composite.
*/
group.addExpansionListener(new ExpansionAdapter()
{
public void expansionStateChanged(ExpansionEvent e)
{
forceReflow();
attributeGroupExpansionState.put(groupKey, group.isExpanded());
}
});
// Store the history of sections to remember folding state.
attributeGroupSections.put(groupKey, group);
}
/**
* Returns expansion state for a given group.
*/
private boolean getExpansionState(String groupKey)
{
if (!attributeGroupExpansionState.containsKey(groupKey))
{
attributeGroupExpansionState.put(groupKey, true);
}
return attributeGroupExpansionState.containsKey(groupKey);
}
/**
*
*/
public void addAttributeListener(IAttributeListener listener)
{
this.listeners.add(listener);
}
/**
*
*/
public void removeAttributeListener(IAttributeListener listener)
{
this.listeners.remove(listener);
}
/**
*
*/
public void setAttribute(String key, Object value)
{
final AttributeList attributeEditorList = this.attributeEditors.get(key);
if (attributeEditorList != null)
{
attributeEditorList.setAttribute(key, value);
}
else
{
forwardListener.valueChanged(new AttributeEvent(this, key, value));
}
}
/**
*
*/
public void setAttributes(Map<String, Object> attributeValues)
{
for (Map.Entry<String, Object> e : attributeValues.entrySet())
{
setAttribute(e.getKey(), e.getValue());
}
}
/**
* Dispose and unregister any editors currently held.
*/
private void disposeEditors()
{
/*
* Unsubscribe from attribute editors (unique).
*/
final HashSet<AttributeList> values = Sets.newHashSet(attributeEditors.values());
for (AttributeList attEditor : values)
{
attEditor.removeAttributeListener(forwardListener);
}
/*
* Dispose controls.
*/
if (!mainControl.isDisposed())
{
for (Control c : this.mainControl.getChildren())
{
if (!c.isDisposed()) c.dispose();
}
}
this.attributeEditors.clear();
this.attributeGroupSections.clear();
}
/**
* Set the focus to the {@link AttributeList} containing {@link AttributeNames#QUERY},
* if possible.
*/
@Override
public boolean setFocus()
{
if (this.attributeEditors.containsKey(AttributeNames.QUERY))
{
return attributeEditors.get(AttributeNames.QUERY).setFocus();
}
return false;
}
/*
*
*/
@Override
public void dispose()
{
disposeEditors();
this.listeners.clear();
super.dispose();
}
/**
* Expand or collapse all currently shown attribute groups.
*
* @see Section#setExpanded(boolean)
*/
public void setExpanded(boolean expanded)
{
assert Display.getCurrent() != null;
for (String key : attributeGroupSections.keySet())
{
setExpanded(key, expanded);
}
}
/**
* Expand or collapse a given attribute group section.
*/
public void setExpanded(String groupKey, boolean expanded)
{
assert Display.getCurrent() != null;
this.attributeGroupExpansionState.put(groupKey, expanded);
final Section section = this.attributeGroupSections.get(groupKey);
if (section != null)
{
section.setExpanded(expanded);
}
}
/**
* Returns a snapshot of expansion states of this component's sections.
*/
public Map<String, Boolean> getExpansionStates()
{
assert Display.getCurrent() != null;
return Maps.newHashMap(this.attributeGroupExpansionState);
}
/**
* Calls {@link #setExpanded(String, boolean)} for all entries.
*/
public void setExpanded(Map<String, Boolean> map)
{
assert Display.getCurrent() != null;
for (Map.Entry<String, Boolean> e : map.entrySet())
{
setExpanded(e.getKey(), e.getValue());
}
}
}