/*
* Carrot2 project.
*
* Copyright (C) 2002-2016, 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.vis.aduna;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.*;
import java.util.Map;
import javax.swing.*;
import org.apache.commons.lang.StringUtils;
import org.carrot2.core.*;
import org.carrot2.core.Cluster;
import org.carrot2.workbench.core.helpers.DisposeBin;
import org.carrot2.workbench.core.helpers.PostponableJob;
import org.carrot2.workbench.core.ui.*;
import org.carrot2.workbench.core.ui.actions.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.*;
import org.eclipse.swt.SWT;
import org.eclipse.swt.awt.SWT_AWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.progress.UIJob;
import biz.aduna.map.cluster.*;
import org.carrot2.shaded.guava.common.collect.Lists;
import org.carrot2.shaded.guava.common.collect.Maps;
/**
* A single {@link AdunaClusterMapViewPage} page embeds Aduna's Swing component with
* visualization of clusters.
*/
final class AdunaClusterMapViewPage extends Page
{
/** */
private final int REFRESH_DELAY = 500;
/**
* Classification root.
*/
private DefaultClassification root;
/**
* A map of the most recently shown {@link Cluster}s.
*/
private Map<Integer, DefaultClassification> clusterMap = Maps.newHashMap();
/**
* A map of the most recently shown {@link Document}s.
*/
private Map<String, DefaultObject> documentMap = Maps.newHashMap();
/**
* UI job for applying selection to the cluster map component.
*/
private PostponableJob selectionJob = new PostponableJob(new UIJob(
"Aduna ClusterMap (selection)...")
{
private IStructuredSelection currentlyDisplayed = null;
@Override
public IStatus runInUIThread(IProgressMonitor monitor)
{
final VisualizationMode mode = VisualizationMode
.valueOf(AdunaActivator.plugin.getPreferenceStore().getString(
PreferenceConstants.VISUALIZATION_MODE));
if (root != null)
{
final IStructuredSelection toBeDisplayed;
final IStructuredSelection currentSelection = getSelected();
switch (mode)
{
case SHOW_ALL_CLUSTERS:
toBeDisplayed = getAll();
break;
case SHOW_FIRST_LEVEL_CLUSTERS:
toBeDisplayed = getFirstLevel();
break;
case SHOW_SELECTED_CLUSTERS:
toBeDisplayed = currentSelection;
break;
default:
throw new RuntimeException("Unhanded case: " + mode);
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (!toBeDisplayed.equals(currentlyDisplayed))
{
mapMediator.visualize(selectionToClassification(toBeDisplayed));
currentlyDisplayed = toBeDisplayed;
}
mapMediator.select(selectionToClassification(currentSelection));
}
});
}
return Status.OK_STATUS;
}
/*
*
*/
private java.util.List<Classification> selectionToClassification(
IStructuredSelection s)
{
final IAdapterManager mgr = Platform.getAdapterManager();
final java.util.List<Classification> selected = Lists.newArrayList();
for (Object o : s.toList())
{
if (o != null && o instanceof Classification)
{
selected.add((Classification) o);
}
else
{
final Cluster c = (Cluster) mgr.getAdapter(o, Cluster.class);
if (c != null)
{
final Classification object = clusterMap.get(c.getId());
if (object != null) selected.add(object);
}
}
}
return selected;
}
/**
* Return the currently selected clusters.
*/
private IStructuredSelection getSelected()
{
final ISelectionProvider sProvider = editor.getSite().getSelectionProvider();
final ISelection selection = sProvider.getSelection();
return (IStructuredSelection) selection;
}
/**
* Return the first level of clusters as the selection.
*/
private IStructuredSelection getFirstLevel()
{
if (root == null)
{
return StructuredSelection.EMPTY;
}
else
{
return new StructuredSelection(root.getChildren().toArray());
}
}
/**
* Return All clusters as the selection.
*/
@SuppressWarnings("unchecked")
protected IStructuredSelection getAll()
{
if (root == null)
{
return StructuredSelection.EMPTY;
}
else
{
final java.util.List<Classification> clusters = Lists.newArrayList();
final java.util.List<Classification> left = Lists.newLinkedList();
left.add(root);
while (!left.isEmpty())
{
final Classification c = left.remove(0);
clusters.add(c);
left.addAll(c.getChildren());
}
return new StructuredSelection(clusters);
}
}
});
/**
* Refresh the entire structure of clusters.
*/
private PostponableJob refreshJob = new PostponableJob(new UIJob(
"Aduna ClusterMap (full refresh)...")
{
public IStatus runInUIThread(IProgressMonitor monitor)
{
final ProcessingResult result = editor.getSearchResult()
.getProcessingResult();
if (result != null)
{
root = new DefaultClassification("All clusters");
clusterMap = Maps.newHashMap();
documentMap = Maps.newHashMap();
toClassification(root, result.getClusters());
SwingUtilities.invokeLater(new Runnable() {
public void run() {
mapMediator.setClassificationTree(root);
}
});
selectionJob.reschedule(0);
}
return Status.OK_STATUS;
}
private void toClassification(DefaultClassification parent,
java.util.List<Cluster> clusters)
{
for (Cluster cluster : clusters)
{
if (clusterMap.containsKey(cluster.getId())) continue;
final DefaultClassification cc = new DefaultClassification(cluster.getLabel(), parent);
clusterMap.put(cluster.getId(), cc);
for (Document d : cluster.getAllDocuments())
{
if (!documentMap.containsKey(d.getStringId()))
{
String dt = (String) (String) d.getField(Document.TITLE);
String title = "[" + d.getStringId() + "]";
if (!StringUtils.isEmpty(dt))
{
title = title + " " + dt;
}
documentMap.put(d.getStringId(), new DefaultObject(title));
}
cc.add(documentMap.get(d.getStringId()));
}
toClassification(cc, cluster.getSubclusters());
}
}
});
/*
* Sync with search result updated event.
*/
private final SearchResultListenerAdapter editorSyncListener = new SearchResultListenerAdapter()
{
public void processingResultUpdated(ProcessingResult result)
{
refreshJob.reschedule(REFRESH_DELAY);
}
};
/**
* Editor selection listener.
*/
private final ISelectionChangedListener selectionListener = new ISelectionChangedListener()
{
/* */
public void selectionChanged(SelectionChangedEvent event)
{
selectionJob.reschedule(REFRESH_DELAY);
}
};
/*
*
*/
private SearchEditor editor;
/**
* SWT's composite inside which Aduna is embedded (AWT/Swing).
*/
private Composite scrollable;
/**
* Resource disposal.
*/
private DisposeBin disposeBin = new DisposeBin();
/**
* Aduna's GUI mediator component.
*/
private ClusterMapMediator mapMediator;
/**
* @see VisualizationMode
*/
private IPropertyChangeListener viewModeListener = new PropertyChangeListenerAdapter(
PreferenceConstants.VISUALIZATION_MODE)
{
protected void propertyChangeFiltered(PropertyChangeEvent event)
{
selectionJob.reschedule(REFRESH_DELAY);
}
};
/**
* A composite with embedded AWT stuff.
*/
private Composite embedded;
/*
*
*/
public AdunaClusterMapViewPage(SearchEditor editor)
{
this.editor = editor;
}
@Override
public void init(IPageSite pageSite)
{
super.init(pageSite);
pageSite.getActionBars().getToolBarManager().add(
new ExportImageAction(new IImageStreamProvider()
{
public void save(OutputStream os) throws IOException
{
mapMediator.getClusterMap().exportPngImage(os);
}
}));
}
/*
*
*/
@Override
public void createControl(Composite parent)
{
createAdunaControl(parent);
disposeBin.add(scrollable);
/*
* Add listeners.
*/
disposeBin.registerPropertyChangeListener(AdunaActivator.plugin
.getPreferenceStore(), viewModeListener);
/*
* Add a listener to the editor to update the view after new clusters are
* available.
*/
if (editor.getSearchResult().getProcessingResult() != null)
{
refreshJob.reschedule(REFRESH_DELAY);
}
editor.getSearchResult().addListener(editorSyncListener);
editor.getSite().getSelectionProvider()
.addSelectionChangedListener(selectionListener);
}
/*
*
*/
private void createAdunaControl(Composite parent)
{
/*
* If <code>true</code>, try some dirty hacks to avoid flicker on Windows.
*/
final boolean windowsFlickerHack = true;
if (windowsFlickerHack)
{
System.setProperty("sun.awt.noerasebackground", "true");
}
this.scrollable = new Composite(parent,
SWT.H_SCROLL | SWT.V_SCROLL);
scrollable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
final GridLayout layout = new GridLayout();
layout.marginBottom = 0;
layout.marginLeft = 0;
layout.marginRight= 0;
layout.marginTop = 0;
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
layout.marginHeight = 0;
layout.marginWidth = 0;
scrollable.setLayout(layout);
embedded = new Composite(scrollable, SWT.NO_BACKGROUND | SWT.EMBEDDED);
embedded.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
final Frame frame = SWT_AWT.new_Frame(embedded);
frame.setLayout(new java.awt.BorderLayout());
// LINGO-446: flicker fix; see "Creating a Root Pane Container" in http://www.eclipse.org/articles/article.php?file=Article-Swing-SWT-Integration/index.html
final JApplet applet = new JApplet();
frame.add(applet);
applet.setLayout(new java.awt.BorderLayout());
final JScrollPane scrollPanel = new JScrollPane(
JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPanel.setDoubleBuffered(true);
scrollPanel.setBorder(BorderFactory.createEmptyBorder());
applet.getContentPane().add(scrollPanel, java.awt.BorderLayout.CENTER);
final ClusterMapFactory factory = ClusterMapFactory.createFactory();
final ClusterMap clusterMap = factory.createClusterMap();
final ClusterMapMediator mapMediator = factory.createMediator(clusterMap);
this.mapMediator = mapMediator;
final ClusterGraphPanel graphPanel = mapMediator.getGraphPanel();
graphPanel.setDoubleBuffered(true);
scrollPanel.setViewportView(graphPanel);
scrollable.addControlListener(new ControlAdapter()
{
@Override
public void controlResized(ControlEvent e)
{
updateScrollBars();
}
});
final SelectionAdapter adapter = new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
ScrollBar hbar = scrollable.getHorizontalBar();
ScrollBar vbar = scrollable.getVerticalBar();
final java.awt.Rectangle viewport = new java.awt.Rectangle(
hbar.getSelection(),
vbar.getSelection(),
hbar.getThumb(),
vbar.getThumb());
SwingUtilities.invokeLater(new Runnable() {
public void run()
{
graphPanel.scrollRectToVisible(viewport);
}
});
}
};
scrollable.getVerticalBar().addSelectionListener(adapter);
scrollable.getHorizontalBar().addSelectionListener(adapter);
final Runnable updateScrollBarsAsync = new Runnable() {
public void run() {
updateScrollBars();
}
};
graphPanel.addComponentListener(new ComponentAdapter()
{
@Override
public void componentShown(ComponentEvent e)
{
graphPanelSize = graphPanel.getPreferredSize();
Display.getDefault().asyncExec(updateScrollBarsAsync);
}
@Override
public void componentResized(ComponentEvent e)
{
graphPanelSize = graphPanel.getPreferredSize();
Display.getDefault().asyncExec(updateScrollBarsAsync);
}
});
}
/**
* The latest size of Aduna's graph panel. Passed between Swing and SWT, so volatile.
*/
private volatile Dimension graphPanelSize;
/*
*
*/
protected void updateScrollBars()
{
if (Display.findDisplay(Thread.currentThread()) == null)
throw new IllegalStateException("Not an SWT thread: " + Thread.currentThread());
if (graphPanelSize == null)
return;
org.eclipse.swt.graphics.Rectangle swtScrollableArea = scrollable.getClientArea();
int width = Math.max(graphPanelSize.width, 0);
int viewportWidth = Math.max(swtScrollableArea.width, 0);
updateScrollBar(scrollable.getHorizontalBar(), width, viewportWidth);
int height = Math.max(graphPanelSize.height, 0);
int viewportHeight = Math.max(swtScrollableArea.height, 0);
updateScrollBar(scrollable.getVerticalBar(), height, viewportHeight);
}
private static void updateScrollBar(ScrollBar sbar, int value, int viewportValue)
{
int selection = sbar.getSelection();
int minimum = 0;
int maximum = value;
int thumb = Math.min(viewportValue, value);
int increment = /* SharedScrolledComposite.V_SCROLL_INCREMENT */ 64;
int pageIncrement = Math.max(thumb - 5 * thumb / 100, 5);
sbar.setValues(selection, minimum, maximum, thumb, increment, pageIncrement);
}
/*
*
*/
@Override
public Control getControl()
{
return scrollable;
}
/*
*
*/
@Override
public void dispose()
{
editor.getSearchResult().removeListener(editorSyncListener);
editor.getSite().getSelectionProvider().removeSelectionChangedListener(selectionListener);
disposeBin.dispose();
super.dispose();
}
/*
*
*/
@Override
public void setFocus()
{
// Ignore.
}
}