package sk.stuba.fiit.perconik.activity.listeners.ui.text;
import java.util.LinkedList;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import sk.stuba.fiit.perconik.activity.listeners.ActivityListener;
import sk.stuba.fiit.perconik.activity.serializers.ui.selection.TextSelectionSerializer;
import sk.stuba.fiit.perconik.core.annotations.Version;
import sk.stuba.fiit.perconik.core.listeners.PartListener;
import sk.stuba.fiit.perconik.data.events.Event;
import sk.stuba.fiit.perconik.eclipse.jface.text.LineRegion;
import sk.stuba.fiit.perconik.eclipse.ui.Parts;
import sk.stuba.fiit.perconik.utilities.concurrent.TimeValue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static com.google.common.base.Strings.isNullOrEmpty;
import static sk.stuba.fiit.perconik.activity.listeners.ui.text.TextSelectionListener.Action.SELECT;
import static sk.stuba.fiit.perconik.activity.serializers.ConfigurableSerializer.StandardOption.TREE;
import static sk.stuba.fiit.perconik.activity.serializers.ui.Ui.dereferencePart;
import static sk.stuba.fiit.perconik.data.content.StructuredContents.key;
import static sk.stuba.fiit.perconik.utilities.concurrent.TimeValue.of;
/**
* TODO
*
* @author Pavol Zbell
* @since 1.0
*/
@Version("0.1.0.alpha")
public final class TextSelectionListener extends AbstractTextListener implements PartListener, sk.stuba.fiit.perconik.core.listeners.TextSelectionListener {
// TODO note that a text selection is generated on each part activation meaning that same
// selection events are generated when one switches-by-clicking for example between
// an editor and an outline; should it be fixed or left as is?
// TODO fails on shutdown if both this and text view listener are processing pending events
// TODO note that select must be initiated by user after startup to be sent on shutdown
static final TimeValue selectionEventPause = of(500L, MILLISECONDS);
static final TimeValue selectionEventWindow = of(10L, SECONDS);
private final TextSelectionEvents events;
public TextSelectionListener() {
this.events = new TextSelectionEvents(this);
}
enum Action implements ActivityListener.Action {
SELECT;
private final String name;
private final String path;
private Action() {
this.name = actionName("eclipse", "text", this);
this.path = actionPath(this.name);
}
public String getName() {
return this.name;
}
public String getPath() {
return this.path;
}
}
Event build(final long time, final Action action, final LinkedList<TextSelectionEvent> sequence, final IWorkbenchPart part, final ITextSelection selection, final LineRegion region) {
assert sequence.getLast().selection.equals(selection);
Event data = this.build(time, action, part, region);
data.put(key("sequence", "first", "timestamp"), sequence.getFirst().time);
data.put(key("sequence", "first", "raw"), new TextSelectionSerializer(TREE).serialize(sequence.getFirst().selection));
data.put(key("sequence", "last", "timestamp"), sequence.getLast().time);
data.put(key("sequence", "last", "raw"), new TextSelectionSerializer(TREE).serialize(sequence.getLast().selection));
data.put(key("sequence", "count"), sequence.size());
return data;
}
void process(final long time, final Action action, final LinkedList<TextSelectionEvent> sequence, final IWorkbenchPart part, final ITextSelection selection) {
IDocument document = Parts.getDocument(TextViewerSupport.getTextViewer(part));
LineRegion region = LineRegion.compute(document, selection.getOffset(), selection.getLength(), selection.getText()).normalize();
this.send(action.getPath(), this.intern(this.build(time, action, sequence, part, selection, region)));
}
static final class TextSelectionEvents extends ContinuousEvent<TextSelectionListener, TextSelectionEvent> {
TextSelectionEvents(final TextSelectionListener listener) {
super(listener, "text-selection", selectionEventPause, selectionEventWindow);
}
@Override
protected boolean accept(final LinkedList<TextSelectionEvent> sequence, final TextSelectionEvent event) {
IDocument document = Parts.getDocument(TextViewerSupport.getTextViewer(event.part));
if (document == null) {
if (this.log.isEnabled()) {
this.log.print("%s: document not found for %s -> ignore", this.identifier, event.part);
}
return false;
}
boolean empty = isNullOrEmpty(event.selection.getText());
if (sequence.isEmpty()) {
return !empty;
}
TextSelectionEvent last = sequence.getLast();
if (empty && last.part != event.part) {
return false;
}
if (last.contentEquals(event)) {
return false;
}
return true;
}
@Override
protected boolean continuous(final LinkedList<TextSelectionEvent> sequence, final TextSelectionEvent event) {
return sequence.getLast().isContinuousWith(event);
}
@Override
protected void process(final LinkedList<TextSelectionEvent> sequence) {
this.listener.handleSelectionEvents(sequence);
}
}
void handleSelectionEvents(final LinkedList<TextSelectionEvent> sequence) {
this.execute(new Runnable() {
public void run() {
TextSelectionEvent event = sequence.getLast();
process(event.time, SELECT, sequence, event.part, event.selection);
}
});
}
public void partOpened(final IWorkbenchPartReference reference) {
// ignore
}
public void partClosed(final IWorkbenchPartReference reference) {
// ignore
}
public void partActivated(final IWorkbenchPartReference reference) {
// ensures that a selection change is always generated on part activation,
// this primarily helps handling selection context switch in some cases,
// as a side effect it breaks event continuation
this.selectionChanged(reference);
}
public void partDeactivated(final IWorkbenchPartReference reference) {
// ensures that pending events are flushed on part deactivation,
// this primarily handles proper selection termination on shutdown,
// as a side effect it breaks event continuation
this.events.flush();
}
public void partVisible(final IWorkbenchPartReference reference) {
// ignore
}
public void partHidden(final IWorkbenchPartReference reference) {
// ignore
}
public void partBroughtToTop(final IWorkbenchPartReference reference) {
// ignore
}
public void partInputChanged(final IWorkbenchPartReference reference) {
// ignore
}
private void push(final long time, final IWorkbenchPart part, final ITextSelection selection) {
// eagerly filter out empty selections, no need to push an event
// only to be filtered during acceptance phase by such simple operation
if (selection.isEmpty()) {
if (this.log.isEnabled()) {
this.log.print("%s: selection empty -> ignore", "text-selection");
}
return;
}
this.events.push(new TextSelectionEvent(time, part, selection));
}
public void selectionChanged(final IWorkbenchPartReference reference) {
final long time = this.currentTime();
IWorkbenchPart part = dereferencePart(reference);
ITextViewer viewer = TextViewerSupport.getTextViewer(part);
if (viewer == null) {
if (this.log.isEnabled()) {
this.log.print("%s: viewer not found for %s -> ignore", "text-selection", part);
}
return;
}
ISelection selection = viewer.getSelectionProvider().getSelection();
if (!(selection instanceof ITextSelection)) {
if (this.log.isEnabled()) {
this.log.print("%s: selection not textual in %s of %s -> ignore", "text-selection", viewer, part);
}
return;
}
this.push(time, part, (ITextSelection) selection);
}
public void selectionChanged(final IWorkbenchPart part, final ITextSelection selection) {
final long time = this.currentTime();
this.push(time, part, selection);
}
}