package ch.unifr.pai.twice.mousecontrol.client;
/*
* 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.Date;
import ch.unifr.pai.twice.authentication.client.Authentication;
import ch.unifr.pai.twice.module.client.TWICEAnnotations.Configurable;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Command;
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.Window;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.LayoutPanel;
import com.google.web.bindery.event.shared.HandlerRegistration;
/**
* A generic implementation of a remote mouse control component
*
* @author Oliver Schmid
*
*/
public abstract class TouchPadWidget extends LayoutPanel {
/**
* The interval in ms of updating messages
*/
@Configurable("Movement interval")
static int MOVEMENTUPDATEINTERVAL = 80;
/**
* The threshold of distance in pixels that has to be exceeded by a mouse movement to trigger an event
*/
@Configurable("Movement threshold")
static int MOVEMENTTHRESHOLD = 0;
/**
* The factor with which the movement shall be increased
*/
@Configurable("Movement factor")
static double MOVEFACTOR = 1.8;
/**
* The threshold of how long the mouse shall be pressed until the device switches to drag mode
*/
@Configurable("Mouse down threshold")
static int MOUSEDOWNTHRESHOLD = 300;
/**
* In which interval the client shall try to assign a cursor on the shared screen
*/
@Configurable("Look for cursor interval")
static int LOOKFORCURSORINTERVAL = 2000;
private String uuid;
private String host;
private Integer port;
private String currentColor;
private String screenDimension;
protected int screenWidth;
protected int screenHeight;
private boolean active = true;
private int currentX = -1;
private int currentY = -1;
private boolean running;
private boolean downLastAction = false;
private boolean doLog = false;
private StringBuilder log = new StringBuilder();
private String header;
private String[] availableClients;
private final Label label = new Label();
// private TextBox focusTextBox = new TextBox();
protected boolean dragging = false;
// private final static String controlServlet = "mouseManager";
private final static String controlServlet = "mouseManagerXBrowser";
// private final static String controlServlet = "mouseManagerXBrowserWS";
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
/**
* Add data to the log
*
* @param name
* @param attributes
* @param content
*/
public void addToLog(String name, String attributes, String content) {
log.append("<").append(name);
log.append(" time=\"").append(new Date().getTime()).append("\" uuid=\"").append(uuid).append("\" color=\"" + getColor() + "\"");
if (attributes != null) {
log.append(" ").append(attributes);
}
log.append(">");
if (content != null)
log.append(content);
log.append("</").append(name).append(">");
}
/**
* Clear the log
*
* @return the log state before clearance
*/
public String flushLog() {
String result = log.toString();
log = new StringBuilder();
return result;
}
/**
* @return true if the component is writing to the log
*/
public boolean isDoLog() {
return doLog;
}
/**
* define if the component shall write to the log
*
* @param doLog
*/
public void setDoLog(boolean doLog) {
this.doLog = doLog;
}
/**
* defines if drag is enabled
*/
protected boolean dragModeEnabled = true;
public TouchPadWidget(boolean handleFocus) {
super();
// add(focusTextBox);
add(label);
// focusTextBox.getElement().getStyle().setZIndex(-1);
// focusTextBox.getElement().getStyle().setVisibility(Visibility.HIDDEN);
// send(null);
}
/**
* @return true if drag mode is enabled, false otherwise
*/
public boolean isDragModeEnabled() {
return dragModeEnabled;
}
/**
* set if drag mode is enabled
*
* @param dragModeEnabled
*/
public void setDragModeEnabled(boolean dragModeEnabled) {
this.dragModeEnabled = dragModeEnabled;
}
/**
* timer which sends out movement events
*/
private final Timer movement = new Timer() {
@Override
public void run() {
int x = getX();
int y = getY();
if (currentX != x || currentY != y) {
move(x, y);
currentX = x;
currentY = y;
}
}
};
/**
* @return the id of the current available shared screen
*/
private String getCurrentClient() {
// TODO the user should select the client if there are multiple. For
// testing, we take the latest
return availableClients != null && availableClients.length > 0 ? availableClients[availableClients.length - 1] : null;
}
/**
* if no cursor is available on the shared screen, try to gather one with the given interval
*/
private void noCursorAvailable() {
setActive(false);
if (lookForCursor != null)
lookForCursor.schedule(LOOKFORCURSORINTERVAL);
}
/**
* If a cursor is assigned, start to fire events
*/
private void cursorAssigned() {
setActive(true);
running = true;
String updateInterval = Window.Location.getParameter("update");
if (updateInterval != null)
MOVEMENTUPDATEINTERVAL = Integer.parseInt(updateInterval);
movement.scheduleRepeating(MOVEMENTUPDATEINTERVAL);
keyboardHandler = Event.addNativePreviewHandler(keyboardPreviewHandler);
}
/**
* Handler for keyboard events and invocation of the key events on the shared screen
*/
protected NativePreviewHandler keyboardPreviewHandler = new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
switch (event.getTypeInt()) {
case Event.ONKEYDOWN:
// if (handleFocus)
// focusTextBox.setFocus(true);
send("a=kd&kc=" + event.getNativeEvent().getKeyCode() + "&cc=" + event.getNativeEvent().getCharCode());
break;
case Event.ONKEYUP:
// if (handleFocus)
// focusTextBox.setFocus(true);
send("a=ku&kc=" + event.getNativeEvent().getKeyCode() + "&cc=" + event.getNativeEvent().getCharCode());
break;
case Event.ONKEYPRESS:
// if (handleFocus)
// focusTextBox.setFocus(true);
send("a=kp&kc=" + event.getNativeEvent().getKeyCode() + "&cc=" + event.getNativeEvent().getCharCode());
break;
}
}
};
protected HandlerRegistration keyboardHandler;
private Timer lookForCursor;
/**
* Invoked if the last action has changed. Forces the stop or the start of dragging mode based on information originated on the server
*
* @param action
*/
protected void onActionChanged(String action) {
if (action != null) {
if (action.equals("startDrag"))
dragging = true;
else if (action.equals("endDrag"))
stopDragging();
}
}
/**
* stops the dragging
*/
protected void stopDragging() {
dragging = false;
}
/**
* starts the execution of the component
*/
public void start() {
if (!running) {
label.setText("looking for available remote-clients");
getAvailableClients(new Command() {
@Override
public void execute() {
label.setText((availableClients == null ? "0" : availableClients.length) + " clients found");
if (getCurrentClient() != null) {
label.setText("looking for cursor on client " + getCurrentClient());
lookForCursor = new Timer() {
@Override
public void run() {
try {
new RequestBuilder(RequestBuilder.GET, GWT.getHostPageBaseURL() + controlServlet + "?a=x"
+ (getCurrentClient() != null ? "&targetUUID=" + getCurrentClient() : "") + (uuid != null ? "&uuid=" + uuid : "")
+ (host != null ? "&host=" + host : "") + (port != null ? "&port=" + port : "")).sendRequest(null,
new RequestCallback() {
@Override
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() > 400)
onError(request, null);
label.setText("GOT DATA: " + response.getText());
String color = extractColor(response);
if (color == null || color.isEmpty() || color.equals("#null"))
color = null;
extractLastAction(response);
setScreenDimension(extractScreenDimensions(response));
if (color != null) {
setColor(color);
cursorAssigned();
}
else {
noCursorAvailable();
}
}
@Override
public void onError(Request request, Throwable exception) {
noCursorAvailable();
}
});
}
catch (RequestException e) {
noCursorAvailable();
}
}
};
lookForCursor.run();
}
}
});
}
}
/**
* stops the execution of the component (also interrupts the sending of events).
*/
public void stop() {
if (running) {
running = false;
movement.cancel();
}
}
/**
* Define the cursor id as well as the host and port for the target of the mouse control events. If not defined, the session id, localhost and the standard
* port will be used.
*
* @param uuid
* @param host
* @param port
*/
public void initialize(String uuid, String host, Integer port) {
this.uuid = uuid;
this.host = host;
this.port = port;
}
/**
* send a mouse down event to the mouse control servlet
*
* @param leftButton
*/
protected void down(boolean leftButton) {
send("a=d&b=" + (leftButton ? "l" : "r"));
downLastAction = true;
}
/**
* send a mouse up event to the mouse control servlet
*
* @param leftButton
*/
protected void up(boolean leftButton) {
send("a=u&b=" + (leftButton ? "l" : "r"));
downLastAction = false;
}
/**
* send a hide request to the mouse control servlet that lets the mouse pointer disappear on the shared screen
*/
protected void hide() {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
send("a=h");
}
});
}
/**
* @return the current x coordinate of the mouse pointer on the shared screen
*/
protected abstract int getX();
/**
* @return the current y coordinate of the mouse pointer on the shared screen
*/
protected abstract int getY();
/**
* Send a movement request to the mouse control servlet
*
* @param x
* @param y
*/
protected void move(int x, int y) {
if (screenDimension == null)
send(null);
else
send("a=m&x=" + x + "&y=" + y);
}
/**
* Delegator method to send a query to the mouse control servlet without callback
*
* @param query
*/
protected void send(String query) {
send(query, null);
}
/**
* Sends a request to the server to get the current status
*
* @param callback
*/
protected void getStatus(Command callback) {
send(null, callback);
}
private boolean noConnection;
/**
* Sends the given query to the mouse pointer controller servlet
*
* @param query
* @param callback
*/
protected void send(String query, final Command callback) {
try {
if (active) {
new RequestBuilder(RequestBuilder.GET, GWT.getHostPageBaseURL() + controlServlet + "?" + (query != null ? query : "a=x")
+ (getCurrentClient() != null ? "&targetUUID=" + getCurrentClient() : "") + (uuid != null ? "&uuid=" + uuid : "")
+ (host != null ? "&host=" + host : "") + (port != null ? "&port=" + port : "") + ("&user=" + Authentication.getUserName()))
.sendRequest(null, new RequestCallback() {
@Override
public void onResponseReceived(Request request, Response response) {
if (response.getStatusCode() > 400)
onError(request, null);
String color = extractColor(response);
if (response.getText().trim().isEmpty()) {
label.setText("No connection available");
noConnection = true;
}
else {
if (noConnection) {
label.setText("");
noConnection = false;
}
if (color == null || color.isEmpty() || color.equals("#null"))
color = null;
extractLastAction(response);
setColor(color);
setScreenDimension(extractScreenDimensions(response));
if (callback != null)
callback.execute();
}
}
@Override
public void onError(Request request, Throwable exception) {
setActive(false);
}
});
}
}
catch (Exception e) {
e.printStackTrace();
}
}
/**
* sets the mouse control active or inactive
*
* @param active
*/
private void setActive(boolean active) {
this.active = active;
if (!active) {
this.getElement().getStyle().setBackgroundColor("grey");
label.setText("Looking for available cursors... not found one so long.");
}
else {
label.setText("");
}
}
/**
* Further actions when the dimensions of the shared screen change
*/
protected abstract void updateScreenDimensions();
/**
* Adapts the widget to the given screen dimensions of the shared screen
*
* @param dimension
*/
private void setScreenDimension(String dimension) {
if (dimension != null && (screenDimension == null || !screenDimension.equals(dimension))) {
String[] values = dimension.split("x");
if (values.length == 2) {
screenWidth = Integer.parseInt(values[0]);
screenHeight = Integer.parseInt(values[1]);
updateScreenDimensions();
screenDimension = dimension;
}
}
}
/**
* @param resp
* @return the screen dimensions of the shared screen separated by a "x"
*/
private static String extractScreenDimensions(Response resp) {
return extractByRegex(resp, "[0-9]*x[0-9]*");
}
/**
* @param resp
* @return the color of the assigned the mouse pointer
*/
private static String extractColor(Response resp) {
return extractByRegex(resp, "#.{6}");
}
protected String lastAction = null;
/**
* @param response
* - the last action that has been applied
*/
private void extractLastAction(Response response) {
String[] split = response.getText().split("@");
if (split.length > 2) {
if (!split[2].equals(lastAction)) {
lastAction = split[2];
onActionChanged(lastAction);
GWT.log("Action changed: " + lastAction);
}
}
}
/**
* Helper method to extract values from the HTTP-response by the given regular expression
*
* @param response
* @param regex
* @return
*/
private static String extractByRegex(Response response, String regex) {
RegExp re = RegExp.compile(regex);
MatchResult result = re.exec(response.getText());
return result.getGroup(0);
}
/**
* Sets the color of the font based on the brightness of the background color either to black or to white
*
* @param color
*/
protected void setColor(String color) {
if (color != null) {
color = color.trim();
if (currentColor == null || !currentColor.equals(color)) {
currentColor = color;
this.getElement().getStyle().setBackgroundColor(color);
// this.getElement().getStyle().setColor(getInvertedColor(color));
this.getElement().getStyle().setColor(isColorBright(color) ? "#000000" : "#ffffff");
}
}
}
/**
* @param color
* @return true if the color brightness is above the average and false if not
*/
protected boolean isColorBright(String color) {
if (color != null) {
if (color.startsWith("#") && color.length() == 7) {
try {
Integer red = Integer.parseInt(color.substring(1, 3).toLowerCase(), 16);
Integer green = Integer.parseInt(color.substring(3, 5).toLowerCase(), 16);
Integer blue = Integer.parseInt(color.substring(5, 7).toLowerCase(), 16);
Double d = (red + green + blue) / 3.0;
// 128 is the average brightness (16²/2)
return d > 128;
}
catch (NumberFormatException e) {
return true;
}
}
}
return true;
}
/**
* Request the server for available shared devices
*
* @param callback
*/
private void getAvailableClients(final Command callback) {
try {
new RequestBuilder(RequestBuilder.GET, GWT.getHostPageBaseURL() + controlServlet + "?a=g").sendRequest(null, new RequestCallback() {
@Override
public void onResponseReceived(Request request, Response response) {
if (response.getText() != null && !response.getText().isEmpty()) {
availableClients = response.getText().split("\n");
}
if (callback != null)
callback.execute();
}
@Override
public void onError(Request request, Throwable exception) {
GWT.log("Available clients request", exception);
if (callback != null)
callback.execute();
}
});
}
catch (RequestException e) {
GWT.log("Request Exception", e);
if (callback != null)
callback.execute();
}
}
/**
* @return the color of the mouse pointer / background
*/
public String getColor() {
return currentColor;
}
/**
* @param color
* in HTML hexadecimal writing (e.g. "#000000");
* @return the inverted color
*/
protected String getInvertedColor(String color) {
if (color != null) {
if (color.startsWith("#")) {
String hexString = color.substring(1);
Integer i;
Integer maxHex;
try {
i = Integer.parseInt(hexString.toLowerCase(), 16);
maxHex = Integer.parseInt("ffffff", 16);
}
catch (NumberFormatException e) {
return "#000000";
}
String newHexString = Integer.toHexString(maxHex - i);
return "#" + newHexString;
}
}
return "#000000";
}
/**
* @return true if the widget shall be attached to the root panel or false if it takes care about the attachment by itself
*/
public boolean attachToRootPanel() {
return false;
}
}