/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
* Copyright (C) 2006 Constantin Kaplinsky. All Rights Reserved.
* Copyright (C) 2009 Paul Donohue. All Rights Reserved.
* Copyright (C) 2010, 2012-2013 D. R. Commander. All Rights Reserved.
* Copyright (C) 2011-2017 Brian P. Hinz
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* 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 com.tigervnc.vncviewer;
import java.awt.*;
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.Clipboard;
import java.io.BufferedReader;
import java.nio.*;
import javax.swing.*;
import javax.imageio.*;
import java.io.*;
import com.tigervnc.rfb.*;
import com.tigervnc.rfb.Cursor;
import com.tigervnc.rfb.Point;
import static com.tigervnc.vncviewer.Parameters.*;
class Viewport extends JPanel implements MouseListener,
MouseMotionListener, MouseWheelListener, KeyListener {
static LogWriter vlog = new LogWriter("Viewport");
public Viewport(int w, int h, PixelFormat serverPF, CConn cc_)
{
cc = cc_;
setScaledSize(cc.cp.width, cc.cp.height);
frameBuffer = createFramebuffer(serverPF, w, h);
assert(frameBuffer != null);
setBackground(Color.BLACK);
cc.setFramebuffer(frameBuffer);
OptionsDialog.addCallback("handleOptions", this);
addMouseListener(this);
addMouseWheelListener(this);
addMouseMotionListener(this);
addKeyListener(this);
addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent e) {
ClipboardDialog.clientCutText();
}
public void focusLost(FocusEvent e) {
cc.releaseDownKeys();
}
});
setFocusTraversalKeysEnabled(false);
setFocusable(true);
// Send a fake pointer event so that the server will stop rendering
// a server-side cursor. Ideally we'd like to send the actual pointer
// position, but we can't really tell when the window manager is done
// placing us so we don't have a good time for that.
cc.writer().writePointerEvent(new Point(w/2, h/2), 0);
}
// Most efficient format (from Viewport's point of view)
public PixelFormat getPreferredPF()
{
return frameBuffer.getPF();
}
// Copy the areas of the framebuffer that have been changed (damaged)
// to the displayed window.
public void updateWindow() {
Rect r = frameBuffer.getDamage();
if (!r.is_empty()) {
if (image == null)
image = (BufferedImage)frameBuffer.getImage();
if (cc.cp.width != scaledWidth ||
cc.cp.height != scaledHeight) {
AffineTransform t = new AffineTransform();
t.scale((double)scaleRatioX, (double)scaleRatioY);
Rectangle s = new Rectangle(r.tl.x, r.tl.y, r.width(), r.height());
s = t.createTransformedShape(s).getBounds();
paintImmediately(s.x, s.y, s.width, s.height);
} else {
paintImmediately(r.tl.x, r.tl.y, r.width(), r.height());
}
}
}
static final int[] dotcursor_xpm = {
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
0x00000000, 0xff000000, 0xff000000, 0xff000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
};
public void setCursor(int width, int height, Point hotspot,
byte[] data)
{
int i;
for (i = 0; i < width*height; i++)
if (data[i*4 + 3] != 0) break;
if ((i == width*height) && dotWhenNoCursor.getValue()) {
vlog.debug("cursor is empty - using dot");
cursor = new BufferedImage(5, 5, BufferedImage.TYPE_INT_ARGB_PRE);
cursor.setRGB(0, 0, 5, 5, dotcursor_xpm, 0, 5);
cursorHotspot.x = cursorHotspot.y = 3;
} else {
if ((width == 0) || (height == 0)) {
cursor = new BufferedImage(tk.getBestCursorSize(0, 0).width,
tk.getBestCursorSize(0, 0).height,
BufferedImage.TYPE_INT_ARGB_PRE);
cursorHotspot.x = cursorHotspot.y = 0;
} else {
IntBuffer buffer = IntBuffer.allocate(width*height);
buffer.put(ByteBuffer.wrap(data).asIntBuffer());
cursor =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
cursor.setRGB(0, 0, width, height, buffer.array(), 0, width);
cursorHotspot = hotspot;
}
}
int cw = (int)Math.floor((float)cursor.getWidth() * scaleRatioX);
int ch = (int)Math.floor((float)cursor.getHeight() * scaleRatioY);
int x = (int)Math.floor((float)cursorHotspot.x * scaleRatioX);
int y = (int)Math.floor((float)cursorHotspot.y * scaleRatioY);
java.awt.Cursor softCursor;
Dimension cs = tk.getBestCursorSize(cw, ch);
if (cs.width != cw && cs.height != ch) {
cw = Math.min(cw, cs.width);
ch = Math.min(ch, cs.height);
x = (int)Math.min(x, Math.max(cs.width - 1, 0));
y = (int)Math.min(y, Math.max(cs.height - 1, 0));
BufferedImage scaledImage =
new BufferedImage(cs.width, cs.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = scaledImage.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.drawImage(cursor,
0, 0, cw, ch,
0, 0, cursor.getWidth(), cursor.getHeight(), null);
g2.dispose();
java.awt.Point hs = new java.awt.Point(x, y);
softCursor = tk.createCustomCursor(scaledImage, hs, "softCursor");
scaledImage.flush();
} else {
java.awt.Point hs = new java.awt.Point(x, y);
softCursor = tk.createCustomCursor(cursor, hs, "softCursor");
}
cursor.flush();
setCursor(softCursor);
}
public void resize(int x, int y, int w, int h) {
if ((w != frameBuffer.width()) || (h != frameBuffer.height())) {
vlog.debug("Resizing framebuffer from "+frameBuffer.width()+"x"+
frameBuffer.height()+" to "+w+"x"+h);
frameBuffer = createFramebuffer(frameBuffer.getPF(), w, h);
assert(frameBuffer != null);
cc.setFramebuffer(frameBuffer);
image = (BufferedImage)frameBuffer.getImage();
}
setScaledSize(w, h);
}
private PlatformPixelBuffer createFramebuffer(PixelFormat pf, int w, int h)
{
PlatformPixelBuffer fb;
fb = new JavaPixelBuffer(w, h);
return fb;
}
//
// Callback methods to determine geometry of our Component.
//
public Dimension getPreferredSize() {
return new Dimension(scaledWidth, scaledHeight);
}
public Dimension getMinimumSize() {
return new Dimension(scaledWidth, scaledHeight);
}
public Dimension getMaximumSize() {
return new Dimension(scaledWidth, scaledHeight);
}
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
if (cc.cp.width != scaledWidth ||
cc.cp.height != scaledHeight) {
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
} else {
g2.drawImage(image, 0, 0, null);
}
g2.dispose();
}
// Mouse-Motion callback function
private void mouseMotionCB(MouseEvent e) {
if (!viewOnly.getValue() &&
e.getX() >= 0 && e.getX() <= scaledWidth &&
e.getY() >= 0 && e.getY() <= scaledHeight)
cc.writePointerEvent(translateMouseEvent(e));
}
public void mouseDragged(MouseEvent e) { mouseMotionCB(e); }
public void mouseMoved(MouseEvent e) { mouseMotionCB(e); }
// Mouse callback function
private void mouseCB(MouseEvent e) {
if (!viewOnly.getValue())
if ((e.getID() == MouseEvent.MOUSE_RELEASED) ||
(e.getX() >= 0 && e.getX() <= scaledWidth &&
e.getY() >= 0 && e.getY() <= scaledHeight))
cc.writePointerEvent(translateMouseEvent(e));
}
public void mouseReleased(MouseEvent e) { mouseCB(e); }
public void mousePressed(MouseEvent e) { mouseCB(e); }
public void mouseClicked(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {
if (embed.getValue())
requestFocus();
}
public void mouseExited(MouseEvent e) {}
// MouseWheel callback function
private void mouseWheelCB(MouseWheelEvent e) {
if (!viewOnly.getValue())
cc.writeWheelEvent(e);
}
public void mouseWheelMoved(MouseWheelEvent e) {
mouseWheelCB(e);
}
private static final Integer keyEventLock = 0;
// Handle the key-typed event.
public void keyTyped(KeyEvent e) { }
// Handle the key-released event.
public void keyReleased(KeyEvent e) {
synchronized(keyEventLock) {
cc.writeKeyEvent(e);
}
}
// Handle the key-pressed event.
public void keyPressed(KeyEvent e)
{
if (e.getKeyCode() == MenuKey.getMenuKeyCode()) {
java.awt.Point pt = e.getComponent().getMousePosition();
if (pt != null) {
F8Menu menu = new F8Menu(cc);
menu.show(e.getComponent(), (int)pt.getX(), (int)pt.getY());
}
return;
}
int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK;
if ((e.getModifiers() & ctrlAltShiftMask) == ctrlAltShiftMask) {
switch (e.getKeyCode()) {
case KeyEvent.VK_A:
VncViewer.showAbout(this);
return;
case KeyEvent.VK_F:
if (cc.desktop.fullscreen_active())
cc.desktop.fullscreen_on();
else
cc.desktop.fullscreen_off();
return;
case KeyEvent.VK_H:
cc.refresh();
return;
case KeyEvent.VK_I:
cc.showInfo();
return;
case KeyEvent.VK_O:
OptionsDialog.showDialog(this);
return;
case KeyEvent.VK_W:
VncViewer.newViewer();
return;
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
return;
}
}
if ((e.getModifiers() & Event.META_MASK) == Event.META_MASK) {
switch (e.getKeyCode()) {
case KeyEvent.VK_COMMA:
case KeyEvent.VK_N:
case KeyEvent.VK_W:
case KeyEvent.VK_I:
case KeyEvent.VK_R:
case KeyEvent.VK_L:
case KeyEvent.VK_F:
case KeyEvent.VK_Z:
case KeyEvent.VK_T:
return;
}
}
synchronized(keyEventLock) {
cc.writeKeyEvent(e);
}
}
public void setScaledSize(int width, int height)
{
assert(width != 0 && height != 0);
String scaleString = scalingFactor.getValue();
if (remoteResize.getValue()) {
scaledWidth = width;
scaledHeight = height;
scaleRatioX = 1.00f;
scaleRatioY = 1.00f;
} else {
if (scaleString.matches("^[0-9]+$")) {
int scalingFactor = Integer.parseInt(scaleString);
scaledWidth =
(int)Math.floor((float)width * (float)scalingFactor/100.0);
scaledHeight =
(int)Math.floor((float)height * (float)scalingFactor/100.0);
} else if (scaleString.equalsIgnoreCase("Auto")) {
scaledWidth = width;
scaledHeight = height;
} else {
float widthRatio = (float)width / (float)cc.cp.width;
float heightRatio = (float)height / (float)cc.cp.height;
float ratio = Math.min(widthRatio, heightRatio);
scaledWidth = (int)Math.floor(cc.cp.width * ratio);
scaledHeight = (int)Math.floor(cc.cp.height * ratio);
}
scaleRatioX = (float)scaledWidth / (float)cc.cp.width;
scaleRatioY = (float)scaledHeight / (float)cc.cp.height;
}
if (scaledWidth != getWidth() || scaledHeight != getHeight())
setSize(new Dimension(scaledWidth, scaledHeight));
}
private MouseEvent translateMouseEvent(MouseEvent e)
{
if (cc.cp.width != scaledWidth ||
cc.cp.height != scaledHeight) {
int sx = (scaleRatioX == 1.00) ?
e.getX() : (int)Math.floor(e.getX() / scaleRatioX);
int sy = (scaleRatioY == 1.00) ?
e.getY() : (int)Math.floor(e.getY() / scaleRatioY);
e.translatePoint(sx - e.getX(), sy - e.getY());
}
return e;
}
public void handleOptions()
{
/*
setScaledSize(cc.cp.width, cc.cp.height);
if (!oldSize.equals(new Dimension(scaledWidth, scaledHeight))) {
// Re-layout the DesktopWindow when the scaled size changes.
// Ideally we'd do this with a ComponentListener, but unfortunately
// sometimes a spurious resize event is triggered on the viewport
// when the DesktopWindow is manually resized via the drag handles.
if (cc.desktop != null && cc.desktop.isVisible()) {
JScrollPane scroll = (JScrollPane)((JViewport)getParent()).getParent();
scroll.setViewportBorder(BorderFactory.createEmptyBorder(0,0,0,0));
cc.desktop.pack();
}
*/
}
// access to cc by different threads is specified in CConn
private CConn cc;
private BufferedImage image;
// access to the following must be synchronized:
public PlatformPixelBuffer frameBuffer;
static Toolkit tk = Toolkit.getDefaultToolkit();
public int scaledWidth = 0, scaledHeight = 0;
float scaleRatioX, scaleRatioY;
BufferedImage cursor;
Point cursorHotspot = new Point();
}