/*
* 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 java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.carrot2.core.Cluster;
import org.carrot2.core.ProcessingComponentDescriptor;
import org.carrot2.core.ProcessingResult;
import org.carrot2.core.attribute.AttributeNames;
import org.carrot2.core.attribute.InternalAttributePredicate;
import org.carrot2.core.attribute.Processing;
import org.carrot2.util.attribute.BindableDescriptor;
import org.carrot2.util.attribute.BindableDescriptor.GroupingMethod;
import org.carrot2.util.attribute.Input;
import org.carrot2.workbench.core.WorkbenchActionFactory;
import org.carrot2.workbench.core.WorkbenchCorePlugin;
import org.carrot2.workbench.core.helpers.ActionDelegateProxy;
import org.carrot2.workbench.core.helpers.DisposeBin;
import org.carrot2.workbench.core.helpers.GUIFactory;
import org.carrot2.workbench.core.helpers.PartListenerAdapter;
import org.carrot2.workbench.core.helpers.PostponableJob;
import org.carrot2.workbench.core.helpers.SimpleXmlMemento;
import org.carrot2.workbench.core.helpers.Utils;
import org.carrot2.workbench.core.preferences.PreferenceConstants;
import org.carrot2.workbench.core.ui.actions.GroupingMethodAction;
import org.carrot2.workbench.core.ui.actions.SaveAsXMLActionDelegate;
import org.carrot2.workbench.core.ui.sash.SashForm;
import org.carrot2.workbench.core.ui.widgets.CScrolledComposite;
import org.carrot2.workbench.editors.AttributeEvent;
import org.carrot2.workbench.editors.AttributeListenerAdapter;
import org.carrot2.workbench.editors.IAttributeListener;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IPersistableEditor;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Form;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.part.EditorPart;
import org.eclipse.ui.progress.UIJob;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
import com.google.common.base.Predicates;
import com.google.common.collect.Maps;
/**
* Editor accepting {@link SearchInput} and performing operations on it. The editor also
* exposes the model of processing results.
*/
public final class SearchEditor extends EditorPart implements IPersistableEditor
{
/**
* Public identifier of this editor.
*/
public static final String ID = "org.carrot2.workbench.core.editors.searchEditor";
/**
* Options required for {@link #doSave(IProgressMonitor)}.
*/
@Root(name = "save-dialog-options")
public static final class SaveOptions implements Cloneable
{
/**
* Preference key for global options.
*/
@org.simpleframework.xml.Transient
private static final String GLOBAL_OPTIONS_PREFKEY =
SearchEditorSaveAsDialog.class.getName() + ".saveOptions";
@org.simpleframework.xml.Attribute(required = false)
public String directory;
@org.simpleframework.xml.Attribute(required = false)
public String fileName;
@org.simpleframework.xml.Attribute(required = false)
public Boolean includeDocuments = true;
@org.simpleframework.xml.Attribute(required = false)
public Boolean includeClusters = true;
@org.simpleframework.xml.Attribute(required = false)
public Boolean includeAttributes = false;
public String getFullPath()
{
Path path = FileDialogs.checkOrDefault(directory);
return new File(path.toFile(), fileName).getAbsolutePath();
}
/**
* Preserve save options in a global preference key.
*/
public static SaveOptions getGlobalOrDefault()
{
try
{
String xml =
WorkbenchCorePlugin.getPreferences().get(GLOBAL_OPTIONS_PREFKEY, null);
if (xml != null)
{
return SimpleXmlMemento.fromString(SaveOptions.class, xml);
}
}
catch (IOException e)
{
// fall through
}
return new SaveOptions();
}
/**
* Preserve save options in a global preference key.
*/
public void saveGlobal()
{
try
{
WorkbenchCorePlugin.getPreferences().put(
GLOBAL_OPTIONS_PREFKEY, SimpleXmlMemento.toString(this));
}
catch (IOException e)
{
Utils.logError(e, false);
}
}
}
/**
* Most recent save options.
*/
private SaveOptions saveOptions;
/**
* Part property indicating current grouping of attributes on the
* {@link PanelName#ATTRIBUTES}.
*/
private static final String GROUPING_LOCAL =
PreferenceConstants.GROUPING_EDITOR_PANEL + ".local";
/**
* Global memento key.
*/
private static final String GLOBAL_MEMENTO_KEY = SearchEditor.class + ".memento";
/**
* {@link SearchEditor} has several panels. These panels are identifier with constants
* in this enum. Their visual attributes and preference keys are also configured here.
* <p>
* These panels are <b>required</b> by {@link SearchEditor} and you should not remove
* any of these constants.
*/
public static enum PanelName
{
CLUSTERS("Clusters", ClusterTreeView.ID),
DOCUMENTS("Documents", DocumentListView.ID),
ATTRIBUTES("Attributes", AttributeView.ID);
/** Default name. */
public final String name;
/** Icon identifier. */
public final String iconID;
/** Visibility preference key. */
public final String prefKeyVisibility;
/** Width preference key. */
public final String prefKeyWeight;
private PanelName(String name, String iconID)
{
this.name = name;
this.iconID = iconID;
final String prefKey = PanelName.class.getName() + "." + name;
this.prefKeyVisibility = prefKey + ".visibility";
this.prefKeyWeight = prefKey + ".weight";
}
}
/**
* All attributes of a single panel.
*/
final static class PanelReference
{
private final Section section;
private final int sashIndex;
PanelState state;
PanelReference(Section self, int sashIndex)
{
this.section = self;
this.sashIndex = sashIndex;
}
}
/**
* Panel state.
*/
@Root
public final static class PanelState
{
@Element
public int weight;
@Element
public boolean visibility;
}
/**
* Panels inside the editor.
*/
private EnumMap<PanelName, PanelReference> panels;
/**
* Search result model is the core model around which all other views revolve
* (editors, views, actions). It can perform transformation of {@link SearchInput}
* into a {@link ProcessingResult} and inform listeners about changes going on in the
* model.
*/
private SearchResult searchResult;
/**
* Resources to be disposed of in {@link #dispose()}.
*/
private DisposeBin resources;
/**
* Image from the {@link SearchInput} used to run the query.
*/
private Image sourceImage;
/**
* If <code>true</code>, then the editor's {@link #searchResult} contain a stale value
* with regard to its input.
*/
private boolean dirty = true;
/*
* GUI layout, state restoration.
*/
private FormToolkit toolkit;
private Form rootForm;
private SashForm sashForm;
/**
* This editor's restore state.
*/
private SearchEditorMemento state;
/**
* Selection handling.
*/
private SearchEditorSelectionProvider selectionProvider;
/**
* There is only one {@link SearchJob} assigned to each editor. The job is
* re-scheduled when re-processing is required.
*
* @see #reprocess()
*/
private SearchJob searchJob;
/**
* Auto-update listener calls {@link #reprocess()} after
* {@link PreferenceConstants#AUTO_UPDATE} property changes.
*/
private IAttributeListener autoUpdateListener = new AttributeListenerAdapter()
{
/** Postponable reschedule job. */
private PostponableJob job = new PostponableJob(new UIJob("Auto update...")
{
public IStatus runInUIThread(IProgressMonitor monitor)
{
reprocess();
return Status.OK_STATUS;
}
});
public void valueChanged(AttributeEvent event)
{
final IPreferenceStore store = WorkbenchCorePlugin.getDefault().getPreferenceStore();
if (store.getBoolean(PreferenceConstants.AUTO_UPDATE))
{
final int delay = store.getInt(PreferenceConstants.AUTO_UPDATE_DELAY);
job.reschedule(delay);
}
}
};
/**
* When auto-update key in the preference store changes, force re-processing in case
* the editor is dirty.
*/
private IPropertyChangeListener autoUpdateListener2 = new IPropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent event)
{
if (PreferenceConstants.AUTO_UPDATE.equals(event.getProperty()))
{
if (isDirty())
{
reprocess();
}
}
}
};
/**
* Attribute editors.
*/
private AttributeGroups attributesPanel;
/**
* Create main GUI components, hook up events, schedule initial processing.
*/
@Override
public void createPartControl(Composite parent)
{
this.resources = new DisposeBin(WorkbenchCorePlugin.getDefault());
sourceImage = getEditorInput().getImageDescriptor().createImage();
resources.add(sourceImage);
toolkit = new FormToolkit(parent.getDisplay());
resources.add(toolkit);
rootForm = toolkit.createForm(parent);
rootForm.setText(getPartName());
rootForm.setImage(getTitleImage());
toolkit.decorateFormHeading(rootForm);
sashForm = new SashForm(rootForm.getBody(), SWT.HORIZONTAL)
{
protected boolean onDragSash(Event event)
{
final boolean modified = super.onDragSash(event);
if (modified)
{
/*
* Update globally remembered weights.
*/
final int [] weights = sashForm.getWeights();
for (PanelReference sr : panels.values())
{
sr.state.weight = weights[sr.sashIndex];
}
saveGlobalPanelsState(getPanelState());
}
return modified;
}
};
toolkit.adapt(sashForm);
final GridLayout layout = GridLayoutFactory.swtDefaults().margins(
sashForm.SASH_WIDTH, sashForm.SASH_WIDTH).create();
rootForm.getBody().setLayout(layout);
createControls(sashForm);
createActions();
updatePartHeaders();
sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
/*
* Hook to post-update events.
*/
this.searchResult.addListener(new SearchResultListenerAdapter()
{
public void processingResultUpdated(ProcessingResult result)
{
updatePartHeaders();
}
});
/*
* Create jobs and schedule initial processing after the editor is shown.
*/
createJobs();
getSite().getPage().addPartListener(new PartListenerAdapter()
{
public void partClosed(IWorkbenchPart part)
{
getSite().getPage().removePartListener(this);
}
public void partOpened(IWorkbenchPart part)
{
if (part == SearchEditor.this)
{
reprocess();
}
}
});
}
/**
* Update part name and root form's title
*/
private void updatePartHeaders()
{
final SearchResult searchResult = getSearchResult();
final SearchInput input = getSearchResult().getInput();
final String full = getFullInputTitle(searchResult);
final String abbreviated = getAbbreviatedInputTitle(searchResult);
setPartName(abbreviated);
setTitleToolTip(full);
/*
* Add the number of documents and clusters to the root form's title.
*/
final ProcessingResult result = searchResult.getProcessingResult();
if (result != null)
{
final int documents = result.getDocuments().size();
final int clusters = result.getClusters().size();
rootForm.setText(abbreviated + " (" + pluralize(documents, "document")
+ " from " + componentName(input.getSourceId()) + ", "
+ pluralize(clusters, "cluster") + " from "
+ componentName(input.getAlgorithmId()) + ")");
}
else
{
rootForm.setText(abbreviated);
}
}
/**
* Returns the component label or id if not available
*/
private String componentName(String componentId)
{
final ProcessingComponentDescriptor component = WorkbenchCorePlugin.getDefault()
.getComponent(componentId);
if (component != null && !StringUtils.isEmpty(component.getLabel()))
{
return component.getLabel();
}
return componentId;
}
/**
* Pluralize a given number.
*/
private String pluralize(int value, String title)
{
return Integer.toString(value) + " " + title + (value != 1 ? "s" : "");
}
/**
* Abbreviates the input's title (and adds an ellipsis at end if needed).
*/
private String getAbbreviatedInputTitle(SearchResult searchResult)
{
final int MAX_WIDTH = 40;
return StringUtils.abbreviate(getFullInputTitle(searchResult), MAX_WIDTH);
}
/**
* Attempts to construct an input title from either query attribute or attributes
* found in processing results.
*/
private String getFullInputTitle(SearchResult searchResult)
{
/*
* Initially, set to dummy name.
*/
String title = ObjectUtils.toString(this.searchResult.getInput().getAttribute(
AttributeNames.QUERY), null);
/*
* If we have a processing result...
*/
if (searchResult.hasProcessingResult())
{
final ProcessingResult result = searchResult.getProcessingResult();
/*
* Check if there is a query in the output attributes (may be different from
* the one set on input).
*/
title = ObjectUtils.toString(
result.getAttributes().get(AttributeNames.QUERY), null);
/*
* Override with custom title, if present.
*/
title = ObjectUtils.toString(result.getAttributes().get(
AttributeNames.PROCESSING_RESULT_TITLE), title);
}
if (StringUtils.isEmpty(title))
{
title = "(empty query)";
}
return title;
}
/*
*
*/
@Override
public void init(IEditorSite site, IEditorInput input) throws PartInitException
{
if (!(input instanceof SearchInput)) throw new PartInitException(
"Invalid input: must be an instance of: " + SearchInput.class.getName());
setSite(site);
setInput(input);
/*
* Set default local grouping if not already restored. We must set it here because
* it is used to create components later (and before restoreState()).
*/
if (StringUtils.isEmpty(getPartProperty(GROUPING_LOCAL)))
{
final IPreferenceStore preferenceStore = WorkbenchCorePlugin.getDefault()
.getPreferenceStore();
setPartProperty(GROUPING_LOCAL, preferenceStore
.getString(PreferenceConstants.GROUPING_EDITOR_PANEL));
}
this.searchResult = new SearchResult((SearchInput) input);
}
/*
*
*/
@Override
public Image getTitleImage()
{
return sourceImage;
}
/*
*
*/
@Override
public void setFocus()
{
rootForm.setFocus();
}
/*
*
*/
@Override
public void dispose()
{
this.resources.dispose();
super.dispose();
}
/*
*
*/
public void saveState(IMemento memento)
{
if (memento == null) return;
try
{
final SearchEditorMemento state = new SearchEditorMemento();
state.panels = getPanelState();
state.sectionsExpansionState = this.attributesPanel.getExpansionStates();
state.saveOptions = this.saveOptions;
SimpleXmlMemento.addChild(memento, state);
}
catch (IOException e)
{
Utils.logError(e, false);
}
}
/*
*
*/
public void restoreState(IMemento memento)
{
if (memento == null) return;
try
{
this.state = SimpleXmlMemento.getChild(SearchEditorMemento.class, memento);
}
catch (IOException e)
{
Utils.logError(e, false);
}
}
/**
* Save this editor's state as global.
*/
void saveAsGlobalState()
{
saveGlobalPanelsState(getPanelState());
// Save global sections expansion state (serialized).
this.state = new SearchEditorMemento();
this.state.panels = getPanelState();
this.state.sectionsExpansionState = this.attributesPanel.getExpansionStates();
SimpleXmlMemento.toPreferenceStore(GLOBAL_MEMENTO_KEY, state);
}
/**
* Save the given panels state as global state.
*/
public static void saveGlobalPanelsState(Map<PanelName, PanelState> globalState)
{
final IPreferenceStore prefStore =
WorkbenchCorePlugin.getDefault().getPreferenceStore();
for (Map.Entry<PanelName, PanelState> e : globalState.entrySet())
{
prefStore.setValue(e.getKey().prefKeyVisibility, e.getValue().visibility);
prefStore.setValue(e.getKey().prefKeyWeight, e.getValue().weight);
}
}
/**
* Restore global state shared among editors.
*/
private static SearchEditorMemento restoreGlobalState()
{
SearchEditorMemento memento = SimpleXmlMemento.fromPreferenceStore(
SearchEditorMemento.class, GLOBAL_MEMENTO_KEY);
if (memento == null)
{
memento = new SearchEditorMemento();
memento.sectionsExpansionState = Maps.newHashMap();
}
final IPreferenceStore prefStore =
WorkbenchCorePlugin.getDefault().getPreferenceStore();
final Map<PanelName, PanelState> panels = Maps.newEnumMap(PanelName.class);
for (PanelName n : EnumSet.allOf(PanelName.class))
{
final PanelState s = new PanelState();
s.visibility = prefStore.getBoolean(n.prefKeyVisibility);
s.weight = prefStore.getInt(n.prefKeyWeight);
panels.put(n, s);
}
memento.panels = panels;
return memento;
}
/*
*
*/
private void restoreState()
{
/*
* Restore from global state, if possible.
*/
final SearchEditorMemento globalState = restoreGlobalState();
for (Map.Entry<PanelName, PanelState> e : globalState.panels.entrySet())
{
panels.get(e.getKey()).state = e.getValue();
}
this.attributesPanel.setExpanded(globalState.sectionsExpansionState);
/*
* Restore state from this editor's memento, if possible.
*/
if (state != null)
{
for (Map.Entry<PanelName, PanelState> e : state.panels.entrySet())
{
panels.get(e.getKey()).state = e.getValue();
}
this.attributesPanel.setExpanded(state.sectionsExpansionState);
this.saveOptions = state.saveOptions;
}
/*
* Update weights and visibility.
*/
final int [] weights = sashForm.getWeights();
for (Map.Entry<PanelName, PanelReference> e : panels.entrySet())
{
final PanelReference p = e.getValue();
weights[p.sashIndex] = p.state.weight;
setPanelVisibility(e.getKey(), p.state.visibility);
}
sashForm.setWeights(weights);
}
/**
* Returns the current state of all editor panels.
*/
Map<PanelName, PanelState> getPanelState()
{
final HashMap<PanelName, PanelState> m = Maps.newHashMap();
for (Map.Entry<PanelName, PanelReference> e : panels.entrySet())
{
m.put(e.getKey(), e.getValue().state);
}
return m;
}
/*
*
*/
public void doSave(IProgressMonitor monitor)
{
if (saveOptions == null)
{
doSaveAs();
return;
}
doSave(saveOptions);
}
/**
* Show a dialog prompting for file name and options and save the result to an XML
* file.
*/
public void doSaveAs()
{
if (isDirty() || this.searchJob.getState() == Job.RUNNING)
{
final MessageDialog dialog = new MessageDialog(getEditorSite().getShell(),
"Modified parameters", null, "Search parameters"
+ " have been changed. Save stale results?", MessageDialog.WARNING,
new String []
{
IDialogConstants.OK_LABEL, IDialogConstants.CANCEL_LABEL
}, 0);
if (dialog.open() == MessageDialog.CANCEL)
{
return;
}
}
SaveOptions newOptions = saveOptions;
if (newOptions == null)
{
newOptions = SaveOptions.getGlobalOrDefault();
newOptions.fileName =
FileDialogs.sanitizeFileName(getFullInputTitle(getSearchResult())) + ".xml";
}
final Shell shell = this.getEditorSite().getShell();
if (new SearchEditorSaveAsDialog(shell, newOptions).open() == Window.OK)
{
this.saveOptions = newOptions;
doSave(saveOptions);
}
}
/**
*
*/
private void doSave(SaveOptions options)
{
final ProcessingResult result = getSearchResult().getProcessingResult();
if (result == null)
{
Utils.showError(new Status(Status.ERROR, WorkbenchCorePlugin.PLUGIN_ID,
"No search result yet."));
return;
}
final IAction saveAction = new SaveAsXMLActionDelegate(result, options);
final Job job = new Job("Saving search result...")
{
@Override
protected IStatus run(IProgressMonitor monitor)
{
saveAction.run();
return Status.OK_STATUS;
}
};
job.setPriority(Job.SHORT);
job.schedule();
}
/*
*
*/
public boolean isSaveAsAllowed()
{
return true;
}
/*
* Don't require save-on-close.
*/
@Override
public boolean isSaveOnCloseNeeded()
{
return false;
}
/*
*
*/
@Override
public boolean isDirty()
{
return dirty;
}
/**
* Mark the editor status as dirty (input parameters changed, for example).
*/
private void setDirty(boolean value)
{
this.dirty = value;
firePropertyChange(PROP_DIRTY);
}
/**
* {@link SearchEditor} adaptations.
*/
@SuppressWarnings("rawtypes")
@Override
public Object getAdapter(Class adapter)
{
return super.getAdapter(adapter);
}
/**
* Shows or hides a given panel.
*/
public void setPanelVisibility(PanelName panel, boolean visible)
{
panels.get(panel).state.visibility = visible;
panels.get(panel).section.setVisible(visible);
sashForm.layout();
}
/**
* Schedule a re-processing job to update {@link #searchResult}.
*/
public void reprocess()
{
/*
* There is a race condition between the search job and the dirty flag. The editor
* becomes 'clean' when the search job is initiated, so that further changes of
* parameters will simply re-schedule another job after the one started before
* ends. This may lead to certain inconsistencies between the view and the
* attributes (temporal), but is much simpler than trying to pool/ cache/ stack
* dirty tokens and manage them in synchronization with running jobs.
*/
setDirty(false);
searchJob.schedule();
}
/**
*
*/
public SearchResult getSearchResult()
{
return searchResult;
}
/**
* Add actions to the root form's toolbar.
*/
private void createActions()
{
final IToolBarManager toolbar = rootForm.getToolBarManager();
// Choose visible panels.
final IAction selectPanelsAction = new SearchEditorPanelsAction(
"Choose visible panels", this);
toolbar.add(selectPanelsAction);
toolbar.update(true);
}
/**
* Update grouping state of the {@link #attributesPanel} and reset its editor values.
*/
private void updateGroupingState(GroupingMethod grouping)
{
attributesPanel.setGrouping(grouping);
attributesPanel.setAttributes(getSearchResult().getInput().getAttributeValueSet()
.getAttributeValues());
}
/**
* Create internal panels and hook up listener infrastructure.
*/
private void createControls(SashForm parent)
{
/*
* Create and add panels in order of their declaration in the enum type.
*/
this.panels = Maps.newEnumMap(PanelName.class);
int index = 0;
for (final PanelName s : EnumSet.allOf(PanelName.class))
{
final Section section;
switch (s)
{
case CLUSTERS:
section = createClustersPart(parent, getSite());
break;
case DOCUMENTS:
section = createDocumentsPart(parent, getSite());
break;
case ATTRIBUTES:
section = createAttributesPart(parent, getSite());
break;
default:
throw new RuntimeException("Unknown section: " + s);
}
final PanelReference sr = new PanelReference(section, index++);
panels.put(s, sr);
}
/*
* Set up selection event forwarding.
*/
this.selectionProvider = new SearchEditorSelectionProvider(this);
this.getSite().setSelectionProvider(selectionProvider);
final ClusterTree tree =
(ClusterTree) panels.get(PanelName.CLUSTERS).section.getClient();
/* Link bidirectional selection synchronization. */
new ClusterTreeSelectionAdapter(selectionProvider, tree);
/*
* Set up an event callback making editor dirty when attributes change.
*/
this.getSearchResult().getInput().addAttributeListener(
new AttributeListenerAdapter()
{
public void valueChanged(AttributeEvent event)
{
setDirty(true);
}
});
/*
* Set up an event callback to spawn auto-update jobs on changes to attributes.
*/
resources.registerAttributeChangeListener(this.getSearchResult().getInput(),
autoUpdateListener);
/*
* Set up an event callback to restart processing after auto-update is enabled and
* the editor is dirty.
*/
resources.registerPropertyChangeListener(WorkbenchCorePlugin.getDefault()
.getPreferenceStore(), autoUpdateListener2);
/*
* Install a synchronization agent between the current selection in the editor and
* the document list panel.
*/
final DocumentList documentList =
(DocumentList) panels.get(PanelName.DOCUMENTS).section.getClient();
new DocumentListSelectionAdapter(selectionProvider, documentList, this);
/*
* Restore state information.
*/
restoreState();
}
/*
*
*/
private Section createSection(Composite parent)
{
return toolkit.createSection(parent, ExpandableComposite.EXPANDED
| ExpandableComposite.TITLE_BAR);
}
/**
* Create internal panel with the list of clusters (if present).
*/
private Section createClustersPart(Composite parent, IWorkbenchSite site)
{
final PanelName section = PanelName.CLUSTERS;
final Section sec = createSection(parent);
sec.setText(section.name);
final ClusterTree clustersTree = new ClusterTree(sec, SWT.NONE);
resources.add(clustersTree);
/*
* Hook the clusters tree to search result's events.
*/
this.searchResult.addListener(new SearchResultListenerAdapter()
{
public void processingResultUpdated(ProcessingResult result)
{
final List<Cluster> clusters = result.getClusters();
if (clusters != null && clusters.size() > 0)
{
clustersTree.show(clusters);
}
else
{
clustersTree.show(Collections.<Cluster> emptyList());
}
}
});
sec.setClient(clustersTree);
// Add expand/collapse action to the toolbar.
final ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
final ToolBar toolbar = toolBarManager.createControl(sec);
toolBarManager.add(new ActionDelegateProxy(
new ClusterTreeExpanderAction(
ClusterTreeExpanderAction.CollapseAction.EXPAND,
clustersTree, this.getSearchResult()),
IAction.AS_PUSH_BUTTON));
toolBarManager.add(new ActionDelegateProxy(
new ClusterTreeExpanderAction(
ClusterTreeExpanderAction.CollapseAction.COLLAPSE,
clustersTree, this.getSearchResult()),
IAction.AS_PUSH_BUTTON));
toolBarManager.update(true);
sec.setTextClient(toolbar);
return sec;
}
/**
* Create internal panel with the document list.
*/
private Section createDocumentsPart(Composite parent, IWorkbenchSite site)
{
final PanelName section = PanelName.DOCUMENTS;
final Section sec = createSection(parent);
sec.setText(section.name);
final DocumentList documentList = new DocumentList(sec, SWT.NONE);
resources.add(documentList);
/*
* Hook the document list to search result's events.
*/
this.searchResult.addListener(new SearchResultListenerAdapter()
{
public void processingResultUpdated(ProcessingResult result)
{
documentList.show(result);
}
});
sec.setClient(documentList);
return sec;
}
/**
* Create internal panel with the set of algorithm component's attributes.
*/
private Section createAttributesPart(Composite parent, IWorkbenchSite site)
{
final PanelName section = PanelName.ATTRIBUTES;
final Section sec = createSection(parent);
sec.setText(section.name);
final BindableDescriptor descriptor = getAlgorithmDescriptor();
final CScrolledComposite scroller = new CScrolledComposite(sec, SWT.H_SCROLL
| SWT.V_SCROLL);
resources.add(scroller);
final Composite spacer = GUIFactory.createSpacer(scroller);
resources.add(spacer);
final String groupingValue = getPartProperty(GROUPING_LOCAL);
final GroupingMethod grouping = GroupingMethod.valueOf(groupingValue);
final Map<String, Object> defaultValues = getSearchResult().getInput()
.getAttributeValueSet().getAttributeValues();
attributesPanel = new AttributeGroups(spacer, descriptor, grouping, null,
defaultValues);
attributesPanel.setLayoutData(GridDataFactory.fillDefaults().grab(true, true)
.create());
resources.add(attributesPanel);
toolkit.paintBordersFor(scroller);
toolkit.adapt(scroller);
scroller.setExpandHorizontal(true);
scroller.setExpandVertical(false);
scroller.setContent(spacer);
/*
* Link attribute value changes: attribute panel -> search result
*/
final IAttributeListener panelToEditorSync = new AttributeListenerAdapter()
{
public void valueChanged(AttributeEvent event)
{
getSearchResult().getInput().setAttribute(event.key, event.value);
}
};
attributesPanel.addAttributeListener(panelToEditorSync);
/*
* Link attribute value changes: search result -> attribute panel
*/
final IAttributeListener editorToPanelSync = new AttributeListenerAdapter()
{
public void valueChanged(AttributeEvent event)
{
/*
* temporarily unsubscribe from events from the attributes list to avoid
* event looping.
*/
attributesPanel.removeAttributeListener(panelToEditorSync);
attributesPanel.setAttribute(event.key, event.value);
attributesPanel.addAttributeListener(panelToEditorSync);
}
};
getSearchResult().getInput().addAttributeListener(editorToPanelSync);
/*
* Add toolbar actions.
*/
final ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
final ToolBar toolbar = toolBarManager.createControl(sec);
// Auto update.
final IWorkbenchWindow window = getSite().getWorkbenchWindow();
toolBarManager.add(WorkbenchActionFactory.AUTO_UPDATE_ACTION.create(window));
// Attribute grouping.
final IAction attributesAction = new GroupingMethodAction(GROUPING_LOCAL, this);
toolBarManager.add(attributesAction);
// Save/ load attributes.
final IAction saveLoadAction = new SaveAlgorithmAttributesAction(getSearchResult().getInput());
toolBarManager.add(saveLoadAction);
toolBarManager.update(true);
sec.setTextClient(toolbar);
/*
* Update global preferences when local change.
*/
final String globalPreferenceKey = PreferenceConstants.GROUPING_EDITOR_PANEL;
addPartPropertyListener(new PropertyChangeListenerAdapter(GROUPING_LOCAL)
{
protected void propertyChangeFiltered(PropertyChangeEvent event)
{
final IPreferenceStore prefStore = WorkbenchCorePlugin.getDefault()
.getPreferenceStore();
final String currentValue = getPartProperty(GROUPING_LOCAL);
prefStore.setValue(globalPreferenceKey, currentValue);
updateGroupingState(GroupingMethod.valueOf(currentValue));
Utils.adaptToFormUI(toolkit, attributesPanel);
if (!panels.get(PanelName.ATTRIBUTES).state.visibility)
{
setPanelVisibility(PanelName.ATTRIBUTES, true);
}
}
});
/*
* Perform GUI adaptations.
*/
Utils.adaptToFormUI(toolkit, attributesPanel);
Utils.adaptToFormUI(toolkit, scroller);
sec.setClient(scroller);
return sec;
}
/**
* Get hold of the algorithm instance, extract its attribute descriptors.
*/
@SuppressWarnings("unchecked")
BindableDescriptor getAlgorithmDescriptor()
{
final WorkbenchCorePlugin core = WorkbenchCorePlugin.getDefault();
final String algorithmID = getSearchResult().getInput().getAlgorithmId();
BindableDescriptor componentDescriptor = core.getComponentDescriptor(algorithmID);
if (componentDescriptor == null) {
throw new RuntimeException("No descriptor for algorithm: " + algorithmID);
}
return componentDescriptor.only(Input.class,
Processing.class).only(Predicates.not(new InternalAttributePredicate(false)));
}
/**
* Creates reusable jobs used in the editor.
*/
private void createJobs()
{
/*
* Create search job.
*/
final String title = getAbbreviatedInputTitle(searchResult);
this.searchJob = new SearchJob("Searching for '" + title + "'...", searchResult);
// Try to push search jobs into the background, if possible.
this.searchJob.setPriority(Job.DECORATE);
/*
* Add a job listener to update root form's busy state.
*/
searchJob.addJobChangeListener(new JobChangeAdapter()
{
@Override
public void aboutToRun(IJobChangeEvent event)
{
setBusy(true);
}
@Override
public void done(IJobChangeEvent event)
{
setBusy(false);
}
private void setBusy(final boolean busy)
{
Utils.asyncExec(new Runnable()
{
public void run()
{
if (!rootForm.isDisposed())
{
rootForm.setBusy(busy);
}
}
});
}
});
}
/*
*
*/
@SuppressWarnings("unused")
private IToolBarManager createToolbarManager(Section section)
{
ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
ToolBar toolbar = toolBarManager.createControl(section);
final Cursor handCursor = new Cursor(Display.getCurrent(), SWT.CURSOR_HAND);
toolbar.setCursor(handCursor);
// Cursor needs to be explicitly disposed
toolbar.addDisposeListener(new DisposeListener()
{
public void widgetDisposed(DisposeEvent e)
{
if ((handCursor != null) && (handCursor.isDisposed() == false))
{
handCursor.dispose();
}
}
});
section.setTextClient(toolbar);
return toolBarManager;
}
}