/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
//
// DesktopWindow is an AWT Canvas representing a VNC desktop.
//
// Methods on DesktopWindow are called from both the GUI thread and the thread
// which processes incoming RFB messages ("the RFB thread"). This means we
// need to be careful with synchronization here.
//
package vncviewer;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
@SuppressWarnings({"unchecked", "deprecation", "serial", "fallthrough"}) class DesktopWindow extends Canvas implements Runnable {
////////////////////////////////////////////////////////////////////
// The following methods are all called from the RFB thread
public DesktopWindow(rfb.PixelFormat serverPF, CConn cc_) {
cc = cc_;
setSize(cc.cp.width, cc.cp.height);
im = new PixelBufferImage(cc.cp.width, cc.cp.height, this);
cursor = new rfb.Cursor();
cursorBacking = new rfb.ManagedPixelBuffer();
}
// initGraphics() is needed because for some reason you can't call
// getGraphics() on a newly-created awt Component. It is called when the
// DesktopWindow has actually been made visible so that getGraphics() ought
// to work.
public void initGraphics() { graphics = getGraphics(); }
final public rfb.PixelFormat getPF() { return im.getPF(); }
// Methods called from the RFB thread - these need to be synchronized
// wherever they access data shared with the GUI thread.
synchronized public void setCursor(int hotspotX, int hotspotY, int w, int h,
byte[] data, byte[] mask) {
// strictly we should use a mutex around this test since useLocalCursor
// might be being altered by the GUI thread. However it's only a single
// boolean and it doesn't matter if we get the wrong value anyway.
if (!cc.viewer.useLocalCursor.getValue()) return;
if (!cursorAvailable) {
//XDefineCursor(dpy, win(), noCursor);
cursorAvailable = true;
}
hideLocalCursor();
cursor.hotspotX = hotspotX;
cursor.hotspotY = hotspotY;
cursor.setSize(w, h);
cursor.setPF(getPF());
System.arraycopy(data, 0, cursor.data, 0, cursor.dataLen());
System.arraycopy(mask, 0, cursor.mask, 0, cursor.maskLen());
cursorBacking.setSize(w, h);
cursorBacking.setPF(getPF());
showLocalCursor();
}
// setColourMapEntries() changes some of the entries in the colourmap.
// Unfortunately these messages are often sent one at a time, so we delay the
// settings taking effect unless the whole colourmap has changed. This is
// because getting java to recalculate its internal translation table and
// redraw the screen is expensive.
synchronized public void setColourMapEntries(int firstColour, int nColours,
int[] rgbs) {
im.setColourMapEntries(firstColour, nColours, rgbs);
if (nColours == 256) {
im.updateColourMap();
im.put(0, 0, im.width(), im.height(), graphics);
} else {
if (setColourMapEntriesTimerThread == null) {
setColourMapEntriesTimerThread = new Thread(this);
setColourMapEntriesTimerThread.start();
}
}
}
// resize() is called when the desktop has changed size
synchronized public void resize() {
vlog.debug("DesktopWindow.resize() called");
int w = cc.cp.width;
int h = cc.cp.height;
hideLocalCursor();
setSize(w, h);
im.resize(w, h, this);
}
final void drawInvalidRect() {
if (!invalidRect) return;
int x = invalidLeft;
int w = invalidRight - x;
int y = invalidTop;
int h = invalidBottom - y;
invalidRect = false;
synchronized (this) {
im.put(x, y, w, h, graphics);
}
}
final void invalidate(int x, int y, int w, int h) {
if (invalidRect) {
if (x < invalidLeft) invalidLeft = x;
if (x + w > invalidRight) invalidRight = x + w;
if (y < invalidTop) invalidTop = y;
if (y + h > invalidBottom) invalidBottom = y + h;
} else {
invalidLeft = x;
invalidRight = x + w;
invalidTop = y;
invalidBottom = y + h;
invalidRect = true;
}
if ((invalidRight - invalidLeft) * (invalidBottom - invalidTop) > 100000)
drawInvalidRect();
}
public void beginRect(int x, int y, int w, int h, int encoding) {
invalidRect = false;
}
public void endRect(int x, int y, int w, int h, int encoding) {
drawInvalidRect();
}
synchronized final public void fillRect(int x, int y, int w, int h, int pix)
{
if (overlapsCursor(x, y, w, h)) hideLocalCursor();
im.fillRect(x, y, w, h, pix);
invalidate(x, y, w, h);
showLocalCursor();
}
synchronized final public void imageRect(int x, int y, int w, int h,
byte[] pix, int offset) {
if (overlapsCursor(x, y, w, h)) hideLocalCursor();
im.imageRect(x, y, w, h, pix, offset);
invalidate(x, y, w, h);
showLocalCursor();
}
synchronized final public void copyRect(int x, int y, int w, int h,
int srcX, int srcY) {
if (overlapsCursor(x, y, w, h) || overlapsCursor(srcX, srcY, w, h))
hideLocalCursor();
im.copyRect(x, y, w, h, srcX, srcY);
if (cc.viewer.fastCopyRect.getValue()) {
graphics.setClip(0, 0, im.width(), im.height());
graphics.copyArea(srcX, srcY, w, h, x-srcX, y-srcY);
} else {
invalidate(x, y, w, h);
}
}
// mutex MUST be held when overlapsCursor() is called
final boolean overlapsCursor(int x, int y, int w, int h) {
return (x < cursorBackingX + cursorBacking.width() &&
y < cursorBackingY + cursorBacking.height() &&
x+w > cursorBackingX && y+h > cursorBackingY);
}
////////////////////////////////////////////////////////////////////
// The following methods are all called from the GUI thread
synchronized void resetLocalCursor() {
hideLocalCursor();
//XDefineCursor(dpy, win(), dotCursor);
cursorAvailable = false;
}
synchronized public Dimension getPreferredSize() {
return new Dimension(im.width(), im.height());
}
synchronized public Dimension getMinimumSize() {
return new Dimension(im.width(), im.height());
}
public void update(Graphics g) {
System.err.println("update called");
}
synchronized public void paint(Graphics g) {
g.drawImage(im.image, 0, 0, null);
}
String oldContents = "";
synchronized public void checkClipboard() {
if (ClipboardDialog.systemClipboard != null &&
cc.viewer.sendClipboard.getValue()) {
Transferable t = ClipboardDialog.systemClipboard.getContents(this);
if ((t != null) && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
String newContents = (String) t.getTransferData(DataFlavor.stringFlavor);
if (newContents != null && !newContents.equals(oldContents)) {
cc.writeClientCutText(newContents);
oldContents = newContents;
cc.clipboardDialog.setContents(newContents);
}
} catch (Exception e) {
System.out.println("Exception getting clipboard data: " + e.getMessage());
}
}
}
}
// handleEvent(). Called by the GUI thread and calls on to CConn as
// appropriate. CConn is responsible for synchronizing the writing of key
// and pointer events with other protocol messages.
public boolean handleEvent(Event event) {
switch (event.id) {
case Event.GOT_FOCUS:
checkClipboard();
break;
case Event.MOUSE_MOVE:
case Event.MOUSE_DRAG:
if (!cc.viewer.viewOnly.getValue())
cc.writePointerEvent(event);
// - If local cursor rendering is enabled then use it
synchronized (this) {
if (cursorAvailable) {
// - Render the cursor!
if (event.x != cursorPosX || event.y != cursorPosY) {
hideLocalCursor();
if (event.x >= 0 && event.x < im.width() &&
event.y >= 0 && event.y < im.height()) {
cursorPosX = event.x;
cursorPosY = event.y;
showLocalCursor();
}
}
}
}
lastX = event.x;
lastY = event.y;
break;
case Event.MOUSE_DOWN:
case Event.MOUSE_UP:
if (!cc.viewer.viewOnly.getValue())
cc.writePointerEvent(event);
lastX = event.x;
lastY = event.y;
break;
case Event.KEY_ACTION:
if (event.key == Event.F8) {
cc.showMenu(lastX, lastY);
break;
}
// drop through
case Event.KEY_PRESS:
// The AWT's release events are not useful since they don't correspond to
// the press [ Try pressing shift, pressing another key, releasing shift,
// then releasing the other key - you'll find that you get an AWT press
// event and a release event, but the key fields will be different.
// Without intimate knowledge of the keyboard layout being used, there's
// no way you can correlate the two events. ]
if (!cc.viewer.viewOnly.getValue())
cc.writeKeyEvent(event);
break;
}
return super.handleEvent(event);
}
////////////////////////////////////////////////////////////////////
// The following methods are called from both RFB and GUI threads
// Note that mutex MUST be held when hideLocalCursor() and showLocalCursor()
// are called.
private void hideLocalCursor() {
// - Blit the cursor backing store over the cursor
if (cursorVisible) {
cursorVisible = false;
im.imageRect(cursorBackingX, cursorBackingY, cursorBacking.width(),
cursorBacking.height(), cursorBacking.data, 0);
im.put(cursorBackingX, cursorBackingY, cursorBacking.width(),
cursorBacking.height(), graphics);
}
}
private void showLocalCursor() {
if (cursorAvailable && !cursorVisible) {
if (!im.getPF().equal(cursor.getPF()) ||
cursor.width() == 0 || cursor.height() == 0) {
vlog.debug("attempting to render invalid local cursor");
cursorAvailable = false;
return;
}
cursorVisible = true;
int cursorLeft = cursorPosX - cursor.hotspotX;
int cursorTop = cursorPosY - cursor.hotspotY;
int cursorRight = cursorLeft + cursor.width();
int cursorBottom = cursorTop + cursor.height();
int x = (cursorLeft >= 0 ? cursorLeft : 0);
int y = (cursorTop >= 0 ? cursorTop : 0);
int w = ((cursorRight < im.width() ? cursorRight : im.width()) - x);
int h = ((cursorBottom < im.height() ? cursorBottom : im.height()) - y);
cursorBackingX = x;
cursorBackingY = y;
cursorBacking.setSize(w, h);
for (int j = 0; j < h; j++)
System.arraycopy(im.data, (y+j) * im.width() + x,
cursorBacking.data, j*w, w);
im.maskRect(cursorLeft, cursorTop, cursor.width(), cursor.height(),
cursor.data, cursor.mask);
im.put(x, y, w, h, graphics);
}
}
// run() is executed by the setColourMapEntriesTimerThread - it sleeps for
// 100ms before actually updating the colourmap.
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
synchronized (this) {
im.updateColourMap();
im.put(0, 0, im.width(), im.height(), graphics);
setColourMapEntriesTimerThread = null;
}
}
// access to cc by different threads is specified in CConn
CConn cc;
// access to the following must be synchronized:
PixelBufferImage im;
Graphics graphics;
Thread setColourMapEntriesTimerThread;
rfb.Cursor cursor;
boolean cursorVisible; // Is cursor currently rendered?
boolean cursorAvailable; // Is cursor available for rendering?
int cursorPosX, cursorPosY;
rfb.ManagedPixelBuffer cursorBacking;
int cursorBackingX, cursorBackingY;
// the following are only ever accessed by the RFB thread:
boolean invalidRect;
int invalidLeft, invalidRight, invalidTop, invalidBottom;
// the following are only ever accessed by the GUI thread:
int lastX, lastY;
static rfb.LogWriter vlog = new rfb.LogWriter("DesktopWindow");
}