/*******************************************************************************
* Copyright (c) 2014-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.editor.codemirror.client.minimap;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import elemental.client.Browser;
import elemental.css.CSSStyleDeclaration.Cursor;
import elemental.css.CSSStyleDeclaration.Position;
import elemental.dom.DocumentFragment;
import elemental.dom.Element;
import elemental.dom.Node;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.EventTarget;
import elemental.events.MouseEvent;
import elemental.html.ClientRect;
import elemental.html.DivElement;
import elemental.util.Mappable;
public class MinimapViewImpl implements MinimapView {
private static final String MARKER_HEIGHT = "3px";
private static final String DATASET_KEY_LINE = "line";
/**
* Minimal size of the minimap to react on clicks.
*/
private static final int MIN_MINIMAP_SIZE = 10;
/**
* The canvas element.
*/
private final Element visible;
/**
* Canvas element for offscreen rendering.
*/
private final DocumentFragment offscreen;
/**
* The action delegate.
*/
private Delegate delegate;
/**
* Tells if the visible element needs to be synchronized with the invisible one.
*/
private boolean changed;
/**
* The click listener on the marks.
*/
private final EventListener markClickListener;
public MinimapViewImpl(final Element element) {
this.visible = element;
this.offscreen = Browser.getDocument().createDocumentFragment();
this.visible.addEventListener("click", new EventListener() {
@Override
public void handleEvent(final Event evt) {
if (evt instanceof MouseEvent) {
final MouseEvent mouseEvt = (MouseEvent)evt;
final EventTarget target = mouseEvt.getTarget();
if (visible.equals(target)) {
handleClick(mouseEvt);
}
}
}
}, false);
this.markClickListener = new EventListener() {
@Override
public void handleEvent(final Event evt) {
if (evt instanceof MouseEvent) {
final MouseEvent mouseEvt = (MouseEvent)evt;
handleMarkClick(mouseEvt);
// don't let the event reach the clickListener on 'visible'
mouseEvt.stopPropagation();
}
}
};
}
@Override
public void setDelegate(final Delegate delegate) {
this.delegate = delegate;
}
@Override
public void addMark(final double relativePos, final String style, final int line) {
addMark(relativePos, style, line, null);
}
@Override
public void addMark(final double relativePos, final String style, final int line, final Integer level) {
changed();
final DivElement mark = Browser.getDocument().createDivElement();
mark.setClassName(style);
mark.getStyle().setPosition(Position.ABSOLUTE);
mark.getStyle().setTop(Double.toString(relativePos * 100) + "%");
mark.getStyle().setHeight(MARKER_HEIGHT); // could be proportional to the document size
mark.getStyle().setMarginTop("0");
mark.getStyle().setMarginBottom("0");
mark.getStyle().setWidth("100%");
mark.getStyle().setLeft("0");
mark.getStyle().setCursor(Cursor.POINTER);
if (level != null) {
mark.getStyle().setZIndex(level);
}
mark.getDataset().setAt(DATASET_KEY_LINE, line);
this.offscreen.appendChild(mark);
}
@Override
public void clearMarks() {
changed();
emptyNode(this.visible);
emptyNode(this.offscreen);
}
@Override
public void removeMarks(final int lineStart, final int lineEnd) {
// naive implementation
Node current = this.offscreen.getFirstChild();
while (current != null) {
final Integer line = getLine(current);
if (lineStart <= line && lineEnd >= line) {
this.offscreen.removeChild(current);
changed();
}
current = current.getNextSibling();
}
}
private static void emptyNode(final Node node) {
while (node.hasChildNodes()) {
node.removeChild(node.getLastChild());
}
}
private void changed() {
if (!this.changed) {
this.changed = true;
// set up deferred sync
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
sync();
MinimapViewImpl.this.changed = false;
}
});
}
}
private void sync() {
final Node copy = this.offscreen.cloneNode(true);
emptyNode(this.visible);
this.visible.appendChild(copy);
Node current = this.visible.getFirstChild();
while (current != null) {
current.addEventListener("click", this.markClickListener, false);
current = current.getNextSibling();
}
}
private void handleClick(final MouseEvent event) {
if (this.delegate != null) {
final int clickY = event.getClientY();
final ClientRect rect = visible.getBoundingClientRect();
final float top = rect.getTop();
final float bottom = rect.getBottom();
final float total = bottom - top;
if (total < MIN_MINIMAP_SIZE) {
return;
}
if (clickY < top || clickY > bottom) {
return;
}
final float offset = clickY - top;
final float position = offset / total;
delegate.handleClick(position);
}
}
private void handleMarkClick(final MouseEvent mouseEvt) {
if (this.delegate != null) {
final EventTarget target = mouseEvt.getCurrentTarget();
final Integer line = getLine(target);
if (line != null) {
this.delegate.handleMarkClick(line);
}
}
}
private static Integer getLine(final EventTarget node) {
if (node instanceof Element) {
final Element element = (Element)node;
final Mappable dataset = element.getDataset();
final String lineAsString = (String)(dataset.at(DATASET_KEY_LINE));
try {
int line = Integer.parseInt(lineAsString);
return line;
} catch (final NumberFormatException e) {
return null;
}
} else {
return null;
}
}
}