package ch.unifr.pai.twice.multipointer.client.widgets;
/*
* Copyright 2013 Oliver Schmid
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.HashMap;
import java.util.Map;
import ch.unifr.pai.twice.multipointer.client.MultiCursorController;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.Context2d.TextAlign;
import com.google.gwt.canvas.dom.client.Context2d.TextBaseline;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.Style.BorderStyle;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
public class MultiFocusTextBox extends Composite implements HasValue<String> {
private final int padding = 5;
private final int fontSize = 13;
private final Map<String, Cursor> cursors = new HashMap<String, Cursor>();
FlowPanel p = new FlowPanel();
private String value;
AbsolutePanel multiFocus = new AbsolutePanel();
private final int cursorSpeed = 700;
private final Context2d context;
private final TextBox textBox = new TextBox();
private final Canvas c;
private final Timer blinkTimer;
private boolean cursorsVisible;
private void showCursor() {
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
if (event.getTypeInt() == Event.ONMOUSEUP && !c.getElement().isOrHasChild(Element.as(event.getNativeEvent().getEventTarget()))) {
String uuid = MultiCursorController.getUUID(event.getNativeEvent());
Cursor c = cursors.get(uuid);
if (c != null) {
c.hide();
}
}
}
});
}
private void setStyle() {
getElement().getStyle().setBorderStyle(BorderStyle.SOLID);
getElement().getStyle().setBorderWidth(1, Unit.PX);
}
private InputElement replacedElement;
int currentWidth;
int currentHeight;
public void replaceTextInput(InputElement el) {
if (!el.isDisabled() && !el.getAttribute("multifocus").equals("true")) {
replacedElement = el;
RootPanel.get().add(this);
setValue(el.getValue());
currentWidth = el.getOffsetWidth();
currentHeight = el.getOffsetHeight();
this.getElement().getStyle().setPosition(Position.RELATIVE);
this.getElement().getStyle().setPadding(0, Unit.PX);
this.getElement().setId(el.getId());
refreshDisplay();
}
}
public void refreshDisplay() {
if (replacedElement != null) {
if (replacedElement.getParentElement() != null)
replacedElement.getParentElement().insertAfter(this.getElement(), replacedElement);
setStyle();
// c.getElement().setAttribute("style", replacedElement.getAttribute("style"));
// c.getElement().setAttribute("class", replacedElement.getAttribute("class"));
copyProperties(replacedElement, this.getElement(), "border", "padding", "margin", "outline", "verticalAlign");
this.setWidth(currentWidth + "px");
this.setHeight(currentHeight + "px");
replacedElement.setAttribute("multifocus", "true");
replacedElement.setAttribute("id", "");
replacedElement.getStyle().setDisplay(Display.NONE);
}
}
private void copyProperties(Element original, Element target, String... properties) {
for (String property : properties)
copyProperty(property, original, target);
}
private void copyProperty(String property, Element original, Element target) {
String value = original.getStyle().getProperty(property);
if (value != null && !value.isEmpty()) {
target.getStyle().setProperty(property, value);
}
}
public MultiFocusTextBox() {
blinkTimer = new Timer() {
@Override
public void run() {
for (Cursor c : cursors.values()) {
c.setVisible(cursorsVisible);
}
cursorsVisible = !cursorsVisible;
}
};
blinkTimer.scheduleRepeating(cursorSpeed);
p.getElement().getStyle().setDisplay(Display.INLINE_BLOCK);
c = Canvas.createIfSupported();
c.setCoordinateSpaceWidth(10000);
c.addStyleName("multiFocusWidget");
c.getElement().getStyle().setBorderWidth(0, Unit.PX);
c.getElement().getStyle().setProperty("outline", "none");
c.addKeyPressHandler(new KeyPressHandler() {
@Override
public void onKeyPress(KeyPressEvent event) {
processInput(MultiCursorController.getUUID(event.getNativeEvent()), event.getCharCode());
}
});
c.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
Cursor c = cursors.get(MultiCursorController.getUUID(event.getNativeEvent()));
if (c != null) {
switch (event.getNativeKeyCode()) {
case KeyCodes.KEY_LEFT:
c.setPosition(Math.max(0, c.position - 1));
scrollIfNecessary();
break;
case KeyCodes.KEY_RIGHT:
c.setPosition(Math.min(value.length(), c.position + 1));
scrollIfNecessary();
break;
case KeyCodes.KEY_UP:
c.setPosition(0);
scrollIfNecessary();
break;
case KeyCodes.KEY_DOWN:
c.setPosition(value != null ? value.length() : 0);
scrollIfNecessary();
break;
case KeyCodes.KEY_DELETE:
if (value != null && c.position < value.length()) {
setValue(value.substring(0, c.position) + value.substring(c.position + 1));
for (Cursor cursor : cursors.values()) {
if (c.position < cursor.getPosition()) {
cursor.setPosition(cursor.getPosition() - 1);
}
}
scrollIfNecessary();
}
break;
case KeyCodes.KEY_BACKSPACE:
if (value != null && c.position > 0 && c.position <= value.length()) {
setValue(value.substring(0, c.position - 1) + value.substring(c.position));
c.setPosition(c.position - 1);
for (Cursor cursor : cursors.values()) {
if (c.position < cursor.position) {
cursor.setPosition(cursor.getPosition() - 1);
}
}
scrollIfNecessary();
}
break;
}
}
}
});
c.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
repositionCursor(MultiCursorController.getUUID(event.getNativeEvent()), MultiCursorController.getColorNative(event.getNativeEvent()),
event.getRelativeX(c.getCanvasElement()), event.getRelativeY(c.getCanvasElement()));
}
});
multiFocus.insert(c, 0, 0, 0);
initWidget(multiFocus);
context = c.getContext2d();
context.setTextAlign(TextAlign.LEFT);
context.setTextBaseline(TextBaseline.TOP);
context.setFont("normal " + fontSize + "px sans-serif");
c.getElement().getStyle().setPadding(padding, Unit.PX);
setStyle();
// TODO Auto-generated constructor stub
// multiFocus.setVisible(false);
multiFocus.setWidth("161px");
multiFocus.setHeight("25px");
}
private void repositionCursor(String uuid, String color, int x, int y) {
Cursor c = getOrCreateCursor(uuid, color);
if (c != null) {
c.setPosition(findChar(x));
}
}
private int findChar(int x) {
StringBuilder b = new StringBuilder();
// remove padding space
x = x - padding;
if (value == null)
return 0;
for (char c : value.toCharArray()) {
b.append(c);
double textWidth = context.measureText(b.toString()).getWidth();
if (x - 5 < textWidth) {
double charWidth = context.measureText(String.valueOf(c)).getWidth();
if (textWidth - (charWidth / 2.0) > x)
return Math.max(b.length() - 1, 0);
else
return b.length();
}
}
return b.length();
}
private void registerCursor(String uuid, Cursor c) {
cursors.put(uuid, c);
}
protected class Cursor extends HTML {
int x;
int y;
final String uuid;
int position;
private Cursor(String color, String uuid) {
this.uuid = uuid;
setColor(color);
setWidth("1px");
setHeight("20px");
registerCursor(uuid, this);
}
private void setColor(String color) {
getElement().getStyle().setBackgroundColor(color);
}
private void hide() {
multiFocus.remove(this);
}
private void show() {
multiFocus.add(this);
multiFocus.setWidgetPosition(this, x + 5, y + 3);
showCursor();
}
public void setPosition(int position) {
for (Cursor c : cursors.values()) {
c.hide();
}
this.position = position;
this.x = (int) Math.max(0, context.measureText(value == null ? "" : position < value.length() ? value.substring(0, position) : value).getWidth());
for (Cursor c : cursors.values()) {
c.show();
}
}
public int getPosition() {
return position;
}
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override
public String getValue() {
return value;
}
protected Cursor getOrCreateCursor(String uuid, String color) {
Cursor c = cursors.get(uuid);
if (c == null) {
c = new Cursor(color, uuid);
registerCursor(uuid, c);
}
else {
c.setColor(color);
}
return c;
}
protected Map<String, Cursor> getCursors() {
return cursors;
}
private void processInput(String uuid, char c) {
Cursor origincursor = cursors.get(uuid);
int pos;
if (origincursor != null) {
pos = origincursor.position;
}
else {
pos = value != null ? value.length() : 0;
}
if (value == null)
setValue(String.valueOf(c));
else if (pos <= value.length()) {
setValue(value.substring(0, pos) + c + ((pos == value.length()) ? "" : value.substring(pos)));
}
for (Cursor cursor : cursors.values()) {
if (pos <= cursor.getPosition()) {
cursor.setPosition(cursor.getPosition() + 1);
}
}
scrollIfNecessary();
}
private void scrollIfNecessary() {
int lastCursor = 0;
for (Cursor cursor : cursors.values()) {
if (cursor.position > lastCursor)
lastCursor = cursor.position;
}
if (value != null) {
int width = (int) context.measureText(value.substring(0, lastCursor)).getWidth() + padding;
int fullwidth = (int) context.measureText(value).getWidth();
int relPosLeft = width - multiFocus.getElement().getScrollLeft();
if (relPosLeft < 0) {
multiFocus.getElement().setScrollLeft(width - padding);
}
else if (relPosLeft > multiFocus.getOffsetWidth() - padding) {
multiFocus.getElement().setScrollLeft(width - multiFocus.getOffsetWidth() + padding);
}
if (multiFocus.getElement().getScrollLeft() > 0 && (multiFocus.getElement().getScrollLeft() + multiFocus.getOffsetWidth()) > fullwidth) {
multiFocus.getElement().setScrollLeft(fullwidth - multiFocus.getOffsetWidth() + 2 * padding);
}
}
}
@Override
public void setValue(String value) {
this.value = value;
if (replacedElement != null)
replacedElement.setValue(value);
textBox.setValue(value);
context.clearRect(0, 0, c.getOffsetWidth(), c.getOffsetHeight());
context.setTextAlign(TextAlign.LEFT);
context.setTextBaseline(TextBaseline.TOP);
context.fillText(value, 0, 0);
}
@Override
public void setValue(String value, boolean fireEvents) {
setValue(value);
// TODO
}
}