package sk.stuba.fiit.perconik.activity.listeners.ui.text; import java.util.LinkedList; import com.google.common.base.Stopwatch; import org.eclipse.jface.text.ITextViewer; import org.eclipse.ui.IWorkbench; 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.text.LineRegionSerializer; import sk.stuba.fiit.perconik.core.annotations.Version; import sk.stuba.fiit.perconik.core.listeners.PartListener; import sk.stuba.fiit.perconik.core.listeners.ViewportListener; import sk.stuba.fiit.perconik.core.listeners.WorkbenchListener; import sk.stuba.fiit.perconik.data.events.Event; import sk.stuba.fiit.perconik.eclipse.jface.text.LineRegion; import sk.stuba.fiit.perconik.eclipse.swt.widgets.DisplayTask; 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 com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static sk.stuba.fiit.perconik.activity.listeners.ui.WorkbenchListener.isStartupProcessed; import static sk.stuba.fiit.perconik.activity.listeners.ui.text.TextViewListener.Action.VIEW; 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.eclipse.ui.Parts.activePartReferenceSupplier; 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 TextViewListener extends AbstractTextListener implements PartListener, ViewportListener, WorkbenchListener { // TODO fails on shutdown if both this and text selection listener are processing pending events // TODO note that a view event is generated on each part activation meaning that same view events // are generated when one switches-by-clicking on an editor and outline, should the view // be generated only if the part was hidden before or should it be generated always - as it // is now - indicating programmer's interest in that part (editor) // TODO note that event generated while workbench.isClosing do not have part.viewer field since // the viewer is already disposed // TODO note that this listener does not handle viewport changes in consoles static final TimeValue generateActivePartViewEventTimeout = of(2000, MILLISECONDS); static final TimeValue viewEventPause = of(250, MILLISECONDS); static final TimeValue viewEventWindow = of(1000, MILLISECONDS); private final TextViewEvents events; public TextViewListener() { this.events = new TextViewEvents(this); } enum Action implements ActivityListener.Action { VIEW; 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<TextViewEvent> sequence, final IWorkbenchPart part, final LineRegion region) { Event data = super.build(time, action, part, region); data.put(key("sequence", "first", "timestamp"), sequence.getFirst().time); data.put(key("sequence", "first", "raw"), new LineRegionSerializer().serialize(sequence.getFirst().region)); data.put(key("sequence", "last", "timestamp"), sequence.getLast().time); data.put(key("sequence", "last", "raw"), new LineRegionSerializer().serialize(sequence.getLast().region)); data.put(key("sequence", "count"), sequence.size()); return data; } void process(final long time, final Action action, final LinkedList<TextViewEvent> sequence, final ITextViewer viewer) { IWorkbenchPart part = Parts.forTextViewer(viewer); LineRegion region = sequence.getLast().region; this.send(action.getPath(), this.intern(this.build(time, action, sequence, part, region))); } static final class TextViewEvents extends ContinuousEvent<TextViewListener, TextViewEvent> { protected TextViewEvents(final TextViewListener listener) { super(listener, "text-view", viewEventPause, viewEventWindow); } @Override protected boolean accept(final LinkedList<TextViewEvent> sequence, final TextViewEvent event) { return event.verticalOffset >= 0; } @Override protected boolean continuous(final LinkedList<TextViewEvent> sequence, final TextViewEvent event) { return sequence.getFirst().isContinuousWith(event); } @Override protected void process(final LinkedList<TextViewEvent> sequence) { this.listener.handleViewEvents(sequence); } } void generateActivePartViewEvent() { // generates active part view event only after an active part is available, // and workbench startup event is processed or a certain amount of time passed final IWorkbenchPartReference reference = this.execute(DisplayTask.of(activePartReferenceSupplier())); final Log log = this.log; this.execute(new Runnable() { public void run() { Stopwatch stopwatch = getTimeContext().createStopwatch().start(); TimeValue timeout = generateActivePartViewEventTimeout.convert(MILLISECONDS); if (log.isEnabled()) { log.print("%s: generate active part view event -> wait (timeout %s)", "text-view", timeout); } boolean done = false; long delta = 0L; while (!(done = isStartupProcessed()) && (delta = stopwatch.elapsed(timeout.unit())) < timeout.duration()) { sleepUninterruptibly(20, MILLISECONDS); } if (log.isEnabled()) { log.print("%s: generate active part view event -> process (startup %s, elapse %s)", "text-view", done, timeout.duration(delta)); } viewportChanged(reference); } }); } void handleViewEvents(final LinkedList<TextViewEvent> sequence) { this.execute(new Runnable() { public void run() { TextViewEvent event = sequence.getLast(); process(event.time, VIEW, sequence, event.viewer); } }); } private LineRegion region(final ITextViewer viewer) { return this.execute(new DisplayTask<LineRegion>() { public LineRegion call() { int top = viewer.getTopIndex(); int bottom = viewer.getBottomIndex(); return LineRegion.between(viewer.getDocument(), top, bottom).normalize(); } }); } private int verticalOffset(final ITextViewer viewer) { return this.execute(new DisplayTask<Integer>() { public Integer call() { return viewer.getTextWidget().getTopPixel(); } }); } public void partOpened(final IWorkbenchPartReference reference) { // ignore } public void partClosed(final IWorkbenchPartReference reference) { // ignore } public void partActivated(final IWorkbenchPartReference reference) { // ensures that a viewport change is always generated on part activation, // this catches every user-to-part view, as a side effect it breaks event continuation this.viewportChanged(reference); } public void partDeactivated(final IWorkbenchPartReference reference) { // ensures that pending events are flushed on part deactivation, this primarily handles // proper user-to-part view 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 } public void viewportChanged(final IWorkbenchPartReference reference) { final long time = this.currentTime(); IWorkbenchPart part = dereferencePart(reference); ITextViewer viewer = Parts.getTextViewer(part); if (viewer == null) { if (this.log.isEnabled()) { this.log.print("%s: viewer not found for %s -> ignore", "text-view", part); } return; } LineRegion region = this.region(viewer); int verticalOffset = this.verticalOffset(viewer); this.events.push(new TextViewEvent(time, viewer, region, verticalOffset)); } public void viewportChanged(final ITextViewer viewer, final int verticalOffset) { final long time = this.currentTime(); LineRegion region = this.region(viewer); this.events.push(new TextViewEvent(time, viewer, region, verticalOffset)); } public void postStartup(final IWorkbench workbench) { // ensures that a user-to-part view is initiated on active part // right after the workbench starts, i.e. this listener is registered this.generateActivePartViewEvent(); } public boolean preShutdown(final IWorkbench workbench, final boolean forced) { // ignore return true; } public void postShutdown(final IWorkbench workbench) { // ignore } }