diff options
author | Brian P. Hinz <bphinz@users.sf.net> | 2016-12-10 19:28:42 -0500 |
---|---|---|
committer | Brian P. Hinz <bphinz@users.sf.net> | 2016-12-10 19:28:42 -0500 |
commit | 985d0eb0657351c7bf01db3d1d30012f35c153de (patch) | |
tree | bdc3fa1e21973dacbd41490b620198b0852ac97a /java/com/tigervnc/vncviewer | |
parent | 4efd8150bb2d0d116042d57af180dfd8a4ea11c3 (diff) | |
download | tigervnc-985d0eb0657351c7bf01db3d1d30012f35c153de.tar.gz tigervnc-985d0eb0657351c7bf01db3d1d30012f35c153de.zip |
Complete rewrite of pixel buffer & decoder implementation.
Adds multi-threading, more robust support for different pixel formats,
and several new runtime options.
Diffstat (limited to 'java/com/tigervnc/vncviewer')
-rw-r--r-- | java/com/tigervnc/vncviewer/BIPixelBuffer.java | 141 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/CConn.java | 445 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/DesktopWindow.java | 1013 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/Dialog.java | 6 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/F8Menu.java | 43 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/JavaPixelBuffer.java | 59 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/OptionsDialog.java | 147 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/Parameters.java | 220 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/PlatformPixelBuffer.java | 88 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/Viewport.java | 558 | ||||
-rw-r--r-- | java/com/tigervnc/vncviewer/VncViewer.java | 138 |
11 files changed, 1489 insertions, 1369 deletions
diff --git a/java/com/tigervnc/vncviewer/BIPixelBuffer.java b/java/com/tigervnc/vncviewer/BIPixelBuffer.java deleted file mode 100644 index 1634ebd1..00000000 --- a/java/com/tigervnc/vncviewer/BIPixelBuffer.java +++ /dev/null @@ -1,141 +0,0 @@ -/* Copyright (C) 2012 Brian P. Hinz - * Copyright (C) 2012 D. R. Commander. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, - * USA. - */ - -package com.tigervnc.vncviewer; - -import java.awt.*; -import java.awt.image.*; - -import com.tigervnc.rfb.*; -import com.tigervnc.rfb.Exception; - -public class BIPixelBuffer extends PlatformPixelBuffer implements ImageObserver -{ - public BIPixelBuffer(PixelFormat pf, int w, int h, DesktopWindow desktop_) { - super(pf, w, h, desktop_); - clip = new Rectangle(); - } - - public void setPF(PixelFormat pf) { - super.setPF(pf); - createImage(width(), height()); - } - - public void updateColourMap() { - super.updateColourMap(); - createImage(width_, height_); - } - - // resize() resizes the image, preserving the image data where possible. - public void resize(int w, int h) { - if (w == width() && h == height()) - return; - - width_ = w; - height_ = h; - createImage(w, h); - } - - private void createImage(int w, int h) { - if (w == 0 || h == 0) return; - WritableRaster wr; - if (cm instanceof IndexColorModel) - wr = ((IndexColorModel)cm).createCompatibleWritableRaster(w, h); - else - wr = ((DirectColorModel)cm).createCompatibleWritableRaster(w, h); - image = new BufferedImage(cm, wr, true, null); - db = wr.getDataBuffer(); - } - - public void fillRect(int x, int y, int w, int h, int pix) { - Graphics2D graphics = (Graphics2D)image.getGraphics(); - switch (format.depth) { - case 24: - graphics.setColor(new Color(pix)); - graphics.fillRect(x, y, w, h); - break; - default: - Color color = new Color((0xff << 24) | (cm.getRed(pix) << 16) | - (cm.getGreen(pix) << 8) | (cm.getBlue(pix))); - graphics.setColor(color); - graphics.fillRect(x, y, w, h); - break; - } - graphics.dispose(); - } - - public void imageRect(int x, int y, int w, int h, Object pix) { - if (pix instanceof Image) { - Image img = (Image)pix; - clip = new Rectangle(x, y, w, h); - synchronized(clip) { - tk.prepareImage(img, -1, -1, this); - try { - clip.wait(1000); - } catch (InterruptedException e) { - throw new Exception("Error decoding JPEG data"); - } - } - clip = null; - img.flush(); - } else { - if (image.getSampleModel().getTransferType() == DataBuffer.TYPE_BYTE) { - byte[] bytes = new byte[((int[])pix).length]; - for (int i = 0; i < bytes.length; i++) - bytes[i] = (byte)((int[])pix)[i]; - pix = bytes; - } - image.getSampleModel().setDataElements(x, y, w, h, pix, db); - } - } - - public void copyRect(int x, int y, int w, int h, int srcX, int srcY) { - Graphics2D graphics = (Graphics2D)image.getGraphics(); - graphics.copyArea(srcX, srcY, w, h, x - srcX, y - srcY); - graphics.dispose(); - } - - public Image getImage() { - return (Image)image; - } - - public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) { - if ((infoflags & (ALLBITS | ABORT)) == 0) { - return true; - } else { - if ((infoflags & ALLBITS) != 0) { - if (clip != null) { - synchronized(clip) { - Graphics2D graphics = (Graphics2D)image.getGraphics(); - graphics.drawImage(img, clip.x, clip.y, clip.width, clip.height, null); - graphics.dispose(); - clip.notify(); - } - } - } - return false; - } - } - - BufferedImage image; - DataBuffer db; - Rectangle clip; - - static LogWriter vlog = new LogWriter("BIPixelBuffer"); -} diff --git a/java/com/tigervnc/vncviewer/CConn.java b/java/com/tigervnc/vncviewer/CConn.java index d7134344..8a2303b0 100644 --- a/java/com/tigervnc/vncviewer/CConn.java +++ b/java/com/tigervnc/vncviewer/CConn.java @@ -63,16 +63,20 @@ import com.tigervnc.network.TcpSocket; import static com.tigervnc.vncviewer.Parameters.*; public class CConn extends CConnection implements - UserPasswdGetter, UserMsgBox, - FdInStreamBlockCallback, ActionListener { + UserPasswdGetter, FdInStreamBlockCallback, ActionListener { - public final PixelFormat getPreferredPF() { return fullColorPF; } + // 8 colours (1 bit per component) static final PixelFormat verylowColorPF = new PixelFormat(8, 3, false, true, 1, 1, 1, 2, 1, 0); + + // 64 colours (2 bits per component) static final PixelFormat lowColorPF = new PixelFormat(8, 6, false, true, 3, 3, 3, 4, 2, 0); + + // 256 colours (2-3 bits per component) static final PixelFormat mediumColorPF = - new PixelFormat(8, 8, false, false, 7, 7, 3, 0, 3, 6); + new PixelFormat(8, 8, false, true, 7, 7, 3, 5, 2, 0); + static final int KEY_LOC_SHIFT_R = 0; static final int KEY_LOC_SHIFT_L = 16; static final int SUPER_MASK = 1<<15; @@ -82,6 +86,7 @@ public class CConn extends CConnection implements public CConn(String vncServerName, Socket socket) { + serverHost = null; serverPort = 0; desktop = null; pendingPFChange = false; currentEncoding = Encodings.encodingTight; lastServerEncoding = -1; formatChange = false; encodingChange = false; @@ -93,13 +98,12 @@ public class CConn extends CConnection implements downKeySym = new HashMap<Integer, Integer>(); upg = this; - msg = this; int encNum = Encodings.encodingNum(preferredEncoding.getValue()); if (encNum != -1) currentEncoding = encNum; - cp.supportsLocalCursor = useLocalCursor.getValue(); + cp.supportsLocalCursor = true; cp.supportsDesktopResize = true; cp.supportsExtendedDesktopSize = true; @@ -107,12 +111,13 @@ public class CConn extends CConnection implements cp.supportsSetDesktopSize = false; cp.supportsClientRedirect = true; + if (customCompressLevel.getValue()) cp.compressLevel = compressLevel.getValue(); else cp.compressLevel = -1; - if (noJpeg.getValue()) + if (!noJpeg.getValue()) cp.qualityLevel = qualityLevel.getValue(); else cp.qualityLevel = -1; @@ -142,6 +147,7 @@ public class CConn extends CConnection implements vlog.info("connected to host "+Hostname.getHost(name)+" port "+Hostname.getPort(name)); } + // See callback below sock.inStream().setBlockCallback(this); setStreams(sock.inStream(), sock.outStream()); @@ -161,21 +167,37 @@ public class CConn extends CConnection implements requestNewUpdate(); } - public boolean showMsgBox(int flags, String title, String text) - { - //StringBuffer titleText = new StringBuffer("VNC Viewer: "+title); - return true; - } - - // deleteWindow() is called when the user closes the desktop or menu windows. + public String connectionInfo() { + String info = new String("Desktop name: %s%n"+ + "Host: %s:%d%n"+ + "Size: %dx%d%n"+ + "Pixel format: %s%n"+ + " (server default: %s)%n"+ + "Requested encoding: %s%n"+ + "Last used encoding: %s%n"+ + "Line speed estimate: %d kbit/s%n"+ + "Protocol version: %d.%d%n"+ + "Security method: %s [%s]%n"); + String infoText = + String.format(info, cp.name(), + sock.getPeerName(), sock.getPeerPort(), + cp.width, cp.height, + cp.pf().print(), + serverPF.print(), + Encodings.encodingName(currentEncoding), + Encodings.encodingName(lastServerEncoding), + sock.inStream().kbitsPerSecond(), + cp.majorVersion, cp.minorVersion, + Security.secTypeName(csecurity.getType()), + csecurity.description()); - void deleteWindow() { - if (viewport != null) - viewport.dispose(); - viewport = null; + return infoText; } - // blockCallback() is called when reading from the socket would block. + // The RFB core is not properly asynchronous, so it calls this callback + // whenever it needs to block to wait for more data. Since FLTK is + // monitoring the socket, we just make sure FLTK gets to run. + public void blockCallback() { try { synchronized(this) { @@ -238,12 +260,13 @@ public class CConn extends CConnection implements return true; } - // CConnection callback methods + ////////////////////// CConnection callback methods ////////////////////// // serverInit() is called when the serverInit message has been received. At // this point we create the desktop window and display it. We also tell the // server the pixel format and encodings to use and request the first update. - public void serverInit() { + public void serverInit() + { super.serverInit(); // If using AutoSelect with old servers, start in FullColor @@ -265,23 +288,22 @@ public class CConn extends CConnection implements // This initial update request is a bit of a corner case, so we need // to help out setting the correct format here. assert(pendingPFChange); - desktop.setServerPF(pendingPF); cp.setPF(pendingPF); pendingPFChange = false; - - recreateViewport(); } // setDesktopSize() is called when the desktop size changes (including when // it is set initially). - public void setDesktopSize(int w, int h) { + public void setDesktopSize(int w, int h) + { super.setDesktopSize(w, h); resizeFramebuffer(); } // setExtendedDesktopSize() is a more advanced version of setDesktopSize() public void setExtendedDesktopSize(int reason, int result, int w, int h, - ScreenSet layout) { + ScreenSet layout) + { super.setExtendedDesktopSize(reason, result, w, h, layout); if ((reason == screenTypes.reasonClient) && @@ -294,8 +316,8 @@ public class CConn extends CConnection implements } // clientRedirect() migrates the client to another host/port - public void clientRedirect(int port, String host, - String x509subject) { + public void clientRedirect(int port, String host, String x509subject) + { try { sock.close(); sock = new TcpSocket(host, port); @@ -311,10 +333,11 @@ public class CConn extends CConnection implements } // setName() is called when the desktop name changes - public void setName(String name) { + public void setName(String name) + { super.setName(name); - if (viewport != null) - viewport.setTitle(name+" - TigerVNC"); + if (desktop != null) + desktop.setName(name); } // framebufferUpdateStart() is called at the beginning of an update. @@ -323,12 +346,23 @@ public class CConn extends CConnection implements // one. public void framebufferUpdateStart() { + ModifiablePixelBuffer pb; + PlatformPixelBuffer ppb; + super.framebufferUpdateStart(); // Note: This might not be true if sync fences are supported pendingUpdate = false; requestNewUpdate(); + + // We might still be rendering the previous update + pb = getFramebuffer(); + assert(pb != null); + ppb = (PlatformPixelBuffer)pb; + assert(ppb != null); + + //FIXME } // framebufferUpdateEnd() is called at the end of an update. @@ -342,53 +376,17 @@ public class CConn extends CConnection implements desktop.updateWindow(); if (firstUpdate) { - int width, height; - // We need fences to make extra update requests and continuous // updates "safe". See fence() for the next step. if (cp.supportsFence) writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext, 0, null); - if (cp.supportsSetDesktopSize && - !desktopSize.getValue().isEmpty() && - desktopSize.getValue().split("x").length == 2) { - width = Integer.parseInt(desktopSize.getValue().split("x")[0]); - height = Integer.parseInt(desktopSize.getValue().split("x")[1]); - ScreenSet layout; - - layout = cp.screenLayout; - - if (layout.num_screens() == 0) - layout.add_screen(new Screen()); - else if (layout.num_screens() != 1) { - - while (true) { - Iterator<Screen> iter = layout.screens.iterator(); - Screen screen = (Screen)iter.next(); - - if (!iter.hasNext()) - break; - - layout.remove_screen(screen.id); - } - } - - Screen screen0 = (Screen)layout.screens.iterator().next(); - screen0.dimensions.tl.x = 0; - screen0.dimensions.tl.y = 0; - screen0.dimensions.br.x = width; - screen0.dimensions.br.y = height; - - writer().writeSetDesktopSize(width, height, layout); - } - firstUpdate = false; } // A format change has been scheduled and we are now past the update // with the old format. Time to active the new one. if (pendingPFChange) { - desktop.setServerPF(pendingPF); cp.setPF(pendingPF); pendingPFChange = false; } @@ -400,16 +398,19 @@ public class CConn extends CConnection implements // The rest of the callbacks are fairly self-explanatory... - public void setColourMapEntries(int firstColor, int nColors, int[] rgbs) { - desktop.setColourMapEntries(firstColor, nColors, rgbs); + public void setColourMapEntries(int firstColor, int nColors, int[] rgbs) + { + vlog.error("Invalid SetColourMapEntries from server!"); } - public void bell() { + public void bell() + { if (acceptBell.getValue()) desktop.getToolkit().beep(); } - public void serverCutText(String str, int len) { + public void serverCutText(String str, int len) + { StringSelection buffer; if (!acceptClipboard.getValue()) @@ -418,34 +419,21 @@ public class CConn extends CConnection implements ClipboardDialog.serverCutText(str); } - // We start timing on beginRect and stop timing on endRect, to - // avoid skewing the bandwidth estimation as a result of the server - // being slow or the network having high latency - public void beginRect(Rect r, int encoding) { + public void dataRect(Rect r, int encoding) + { sock.inStream().startTiming(); - if (encoding != Encodings.encodingCopyRect) { - lastServerEncoding = encoding; - } - } - public void endRect(Rect r, int encoding) { - sock.inStream().stopTiming(); - } - - public void fillRect(Rect r, int p) { - desktop.fillRect(r.tl.x, r.tl.y, r.width(), r.height(), p); - } + if (encoding != Encodings.encodingCopyRect) + lastServerEncoding = encoding; - public void imageRect(Rect r, Object p) { - desktop.imageRect(r.tl.x, r.tl.y, r.width(), r.height(), p); - } + super.dataRect(r, encoding); - public void copyRect(Rect r, int sx, int sy) { - desktop.copyRect(r.tl.x, r.tl.y, r.width(), r.height(), sx, sy); + sock.inStream().stopTiming(); } public void setCursor(int width, int height, Point hotspot, - int[] data, byte[] mask) { + byte[] data, byte[] mask) + { desktop.setCursor(width, height, hotspot, data, mask); } @@ -480,11 +468,11 @@ public class CConn extends CConnection implements pf.read(memStream); - desktop.setServerPF(pf); cp.setPF(pf); } } + ////////////////////// Internal methods ////////////////////// private void resizeFramebuffer() { if (desktop == null) @@ -493,82 +481,7 @@ public class CConn extends CConnection implements if (continuousUpdates) writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height); - if ((cp.width == 0) && (cp.height == 0)) - return; - if ((desktop.width() == cp.width) && (desktop.height() == cp.height)) - return; - - desktop.resize(); - if (!firstUpdate) - recreateViewport(); - } - - // recreateViewport() recreates our top-level window. This seems to be - // better than attempting to resize the existing window, at least with - // various X window managers. - - public void recreateViewport() { - if (embed.getValue()) { - desktop.setViewport(VncViewer.getViewport()); - Container viewer = - SwingUtilities.getAncestorOfClass(JApplet.class, desktop); - viewer.addFocusListener(new FocusAdapter() { - public void focusGained(FocusEvent e) { - Container c = - SwingUtilities.getAncestorOfClass(JApplet.class, desktop); - if (c != null && desktop.isAncestorOf(c)) - desktop.requestFocus(); - } - public void focusLost(FocusEvent e) { - releaseDownKeys(); - } - }); - viewer.validate(); - desktop.requestFocus(); - } else { - if (viewport != null) - viewport.dispose(); - viewport = new Viewport(cp.name(), this); - viewport.setUndecorated(fullScreen.getValue()); - desktop.setViewport(viewport.getViewport()); - reconfigureViewport(); - if ((cp.width > 0) && (cp.height > 0)) - viewport.setVisible(true); - desktop.requestFocusInWindow(); - } - } - - private void reconfigureViewport() { - Dimension dpySize = viewport.getScreenSize(); - int w = desktop.scaledWidth; - int h = desktop.scaledHeight; - if (fullScreen.getValue()) { - if (!fullScreenAllMonitors.getValue()) - viewport.setExtendedState(JFrame.MAXIMIZED_BOTH); - viewport.setBounds(viewport.getScreenBounds()); - if (!fullScreenAllMonitors.getValue()) - Viewport.setFullScreenWindow(viewport); - } else { - int wmDecorationWidth = viewport.getInsets().left + viewport.getInsets().right; - int wmDecorationHeight = viewport.getInsets().top + viewport.getInsets().bottom; - if (w + wmDecorationWidth >= dpySize.width) - w = dpySize.width - wmDecorationWidth; - if (h + wmDecorationHeight >= dpySize.height) - h = dpySize.height - wmDecorationHeight; - if (viewport.getExtendedState() == JFrame.MAXIMIZED_BOTH) { - w = viewport.getSize().width; - h = viewport.getSize().height; - int x = viewport.getLocation().x; - int y = viewport.getLocation().y; - viewport.setGeometry(x, y, w, h); - } else { - int x = (dpySize.width - w - wmDecorationWidth) / 2; - int y = (dpySize.height - h - wmDecorationHeight)/2; - viewport.setExtendedState(JFrame.NORMAL); - viewport.setGeometry(x, y, w, h); - } - Viewport.setFullScreenWindow(null); - } + desktop.resizeFramebuffer(cp.width, cp.height); } // autoSelectFormatAndEncoding() chooses the format and encoding appropriate @@ -586,11 +499,12 @@ public class CConn extends CConnection implements // Note: The system here is fairly arbitrary and should be replaced // with something more intelligent at the server end. // - private void autoSelectFormatAndEncoding() { + private void autoSelectFormatAndEncoding() + { long kbitsPerSecond = sock.inStream().kbitsPerSecond(); long timeWaited = sock.inStream().timeWaited(); boolean newFullColor = fullColor.getValue(); - int newQualityLevel = cp.qualityLevel; + int newQualityLevel = qualityLevel.getValue(); // Always use Tight if (currentEncoding != Encodings.encodingTight) { @@ -603,13 +517,13 @@ public class CConn extends CConnection implements return; // Select appropriate quality level - if (!cp.noJpeg) { + if (!noJpeg.getValue()) { if (kbitsPerSecond > 16000) newQualityLevel = 8; else newQualityLevel = 6; - if (newQualityLevel != cp.qualityLevel) { + if (newQualityLevel != qualityLevel.getValue()) { vlog.info("Throughput "+kbitsPerSecond+ " kbit/s - changing to quality "+newQualityLevel); cp.qualityLevel = newQualityLevel; @@ -637,7 +551,17 @@ public class CConn extends CConnection implements (newFullColor ? "enabled" : "disabled")); fullColor.setParam(newFullColor); formatChange = true; - forceNonincremental = true; + } + } + + // checkEncodings() sends a setEncodings message if one is needed. + private void checkEncodings() + { + if (encodingChange && (writer() != null)) { + vlog.info("Using " + Encodings.encodingName(currentEncoding) + + " encoding"); + writer().writeSetEncodings(currentEncoding, true); + encodingChange = false; } } @@ -699,84 +623,9 @@ public class CConn extends CConnection implements forceNonincremental = false; } - - //////////////////////////////////////////////////////////////////// - // The following methods are all called from the GUI thread - - // close() shuts down the socket, thus waking up the RFB thread. - public void close() { - if (closeListener != null) { - embed.setParam(true); - JFrame f = - (JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop); - if (f != null) - f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING)); - } - deleteWindow(); - shuttingDown = true; - try { - if (sock != null) - sock.shutdown(); - } catch (java.lang.Exception e) { - throw new Exception(e.getMessage()); - } - } - - void showInfo() { - Window fullScreenWindow = Viewport.getFullScreenWindow(); - if (fullScreenWindow != null) - Viewport.setFullScreenWindow(null); - String info = new String("Desktop name: %s%n"+ - "Host: %s:%d%n"+ - "Size: %dx%d%n"+ - "Pixel format: %s%n"+ - " (server default: %s)%n"+ - "Requested encoding: %s%n"+ - "Last used encoding: %s%n"+ - "Line speed estimate: %d kbit/s%n"+ - "Protocol version: %d.%d%n"+ - "Security method: %s [%s]%n"); - String msg = - String.format(info, cp.name(), - sock.getPeerName(), sock.getPeerPort(), - cp.width, cp.height, - desktop.getPF().print(), - serverPF.print(), - Encodings.encodingName(currentEncoding), - Encodings.encodingName(lastServerEncoding), - sock.inStream().kbitsPerSecond(), - cp.majorVersion, cp.minorVersion, - Security.secTypeName(csecurity.getType()), - csecurity.description()); - Object[] options = {"Close \u21B5"}; - JOptionPane op = - new JOptionPane(msg, JOptionPane.PLAIN_MESSAGE, - JOptionPane.DEFAULT_OPTION, null, options); - JDialog dlg = op.createDialog(desktop, "VNC connection info"); - dlg.setIconImage(VncViewer.frameIcon); - dlg.setAlwaysOnTop(true); - dlg.setVisible(true); - if (fullScreenWindow != null) - Viewport.setFullScreenWindow(fullScreenWindow); - } - - public void refresh() { - writer().writeFramebufferUpdateRequest(new Rect(0,0,cp.width,cp.height), false); - pendingUpdate = true; - } - - public synchronized int currentEncoding() { - return currentEncoding; - } - public void handleOptions() { - if (viewport != null && viewport.isVisible()) { - viewport.toFront(); - viewport.requestFocus(); - } - // Checking all the details of the current set of encodings is just // a pain. Assume something has changed, as resending the encoding // list is cheap. Avoid overriding what the auto logic has selected @@ -788,7 +637,7 @@ public class CConn extends CConnection implements this.currentEncoding = encNum; } - this.cp.supportsLocalCursor = useLocalCursor.getValue(); + this.cp.supportsLocalCursor = true; if (customCompressLevel.getValue()) this.cp.compressLevel = compressLevel.getValue(); @@ -829,19 +678,68 @@ public class CConn extends CConnection implements } - public void toggleFullScreen() { - if (embed.getValue()) - return; - fullScreen.setParam(!fullScreen.getValue()); - if (viewport != null) { - if (!viewport.lionFSSupported()) { - recreateViewport(); - } else { - viewport.toggleLionFS(); - } + //////////////////////////////////////////////////////////////////// + // The following methods are all called from the GUI thread + + // close() shuts down the socket, thus waking up the RFB thread. + public void close() { + if (closeListener != null) { + embed.setParam(true); + JFrame f = + (JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop); + if (f != null) + f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING)); + } + shuttingDown = true; + try { + if (sock != null) + sock.shutdown(); + } catch (java.lang.Exception e) { + throw new Exception(e.getMessage()); } } + void showInfo() { + Window fullScreenWindow = DesktopWindow.getFullScreenWindow(); + if (fullScreenWindow != null) + DesktopWindow.setFullScreenWindow(null); + String info = new String("Desktop name: %s%n"+ + "Host: %s:%d%n"+ + "Size: %dx%d%n"+ + "Pixel format: %s%n"+ + " (server default: %s)%n"+ + "Requested encoding: %s%n"+ + "Last used encoding: %s%n"+ + "Line speed estimate: %d kbit/s%n"+ + "Protocol version: %d.%d%n"+ + "Security method: %s [%s]%n"); + String msg = + String.format(info, cp.name(), + sock.getPeerName(), sock.getPeerPort(), + cp.width, cp.height, + cp.pf().print(), + serverPF.print(), + Encodings.encodingName(currentEncoding), + Encodings.encodingName(lastServerEncoding), + sock.inStream().kbitsPerSecond(), + cp.majorVersion, cp.minorVersion, + Security.secTypeName(csecurity.getType()), + csecurity.description()); + JOptionPane op = new JOptionPane(msg, JOptionPane.PLAIN_MESSAGE, + JOptionPane.DEFAULT_OPTION); + JDialog dlg = op.createDialog(desktop, "VNC connection info"); + dlg.setIconImage(VncViewer.frameIcon); + dlg.setAlwaysOnTop(true); + dlg.setVisible(true); + if (fullScreenWindow != null) + DesktopWindow.setFullScreenWindow(fullScreenWindow); + } + + public void refresh() { + writer().writeFramebufferUpdateRequest(new Rect(0,0,cp.width,cp.height), false); + pendingUpdate = true; + } + // writeClientCutText() is called from the clipboard dialog public void writeClientCutText(String str, int len) { if (state() != RFBSTATE_NORMAL || shuttingDown) @@ -860,7 +758,7 @@ public class CConn extends CConnection implements return; boolean down = (ev.getID() == KeyEvent.KEY_PRESSED); - + int keySym, keyCode = ev.getKeyCode(); // If neither the keyCode or keyChar are defined, then there's @@ -875,9 +773,9 @@ public class CConn extends CConnection implements if (iter == null) { // Note that dead keys will raise this sort of error falsely // See https://bugs.openjdk.java.net/browse/JDK-6534883 - vlog.error("Unexpected key release of keyCode "+keyCode); + vlog.debug("Unexpected key release of keyCode "+keyCode); String fmt = ev.paramString().replaceAll("%","%%"); - vlog.error(String.format(fmt.replaceAll(",","%n "))); + vlog.debug(String.format(fmt.replaceAll(",","%n "))); return; } @@ -961,15 +859,6 @@ public class CConn extends CConnection implements break; } - if (cp.width != desktop.scaledWidth || - cp.height != desktop.scaledHeight) { - int sx = (desktop.scaleWidthRatio == 1.00) ? - ev.getX() : (int)Math.floor(ev.getX() / desktop.scaleWidthRatio); - int sy = (desktop.scaleHeightRatio == 1.00) ? - ev.getY() : (int)Math.floor(ev.getY() / desktop.scaleHeightRatio); - ev.translatePoint(sx - ev.getX(), sy - ev.getY()); - } - writer().writePointerEvent(new Point(ev.getX(), ev.getY()), buttonMask); } @@ -1014,16 +903,6 @@ public class CConn extends CConnection implements //////////////////////////////////////////////////////////////////// // The following methods are called from both RFB and GUI threads - // checkEncodings() sends a setEncodings message if one is needed. - private void checkEncodings() { - if (encodingChange && (writer() != null)) { - vlog.info("Requesting " + Encodings.encodingName(currentEncoding) + - " encoding"); - writer().writeSetEncodings(currentEncoding, true); - encodingChange = false; - } - } - // the following never change so need no synchronization: // access to desktop by different threads is specified in DesktopWindow @@ -1031,7 +910,6 @@ public class CConn extends CConnection implements // the following need no synchronization: public static UserPasswdGetter upg; - public UserMsgBox msg; // shuttingDown is set by the GUI thread and only ever tested by the RFB // thread after the window has been destroyed. @@ -1072,7 +950,6 @@ public class CConn extends CConnection implements private boolean supportsSyncFence; - Viewport viewport; private HashMap<Integer, Integer> downKeySym; public ActionListener closeListener = null; diff --git a/java/com/tigervnc/vncviewer/DesktopWindow.java b/java/com/tigervnc/vncviewer/DesktopWindow.java index de2d2cd5..e76a0156 100644 --- a/java/com/tigervnc/vncviewer/DesktopWindow.java +++ b/java/com/tigervnc/vncviewer/DesktopWindow.java @@ -1,8 +1,6 @@ /* 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-2014 Brian P. Hinz + * Copyright (C) 2011-2016 Brian P. Hinz + * Copyright (C) 2012-2013 D. R. Commander. 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 @@ -20,584 +18,631 @@ * 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.event.*; -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.CharBuffer; +import java.lang.reflect.*; +import java.util.*; import javax.swing.*; +import javax.swing.Timer; +import javax.swing.border.*; import com.tigervnc.rfb.*; -import com.tigervnc.rfb.Cursor; import com.tigervnc.rfb.Point; +import java.lang.Exception; + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS; import static com.tigervnc.vncviewer.Parameters.*; -class DesktopWindow extends JPanel implements Runnable, MouseListener, - MouseMotionListener, MouseWheelListener, KeyListener { +public class DesktopWindow extends JFrame +{ - //////////////////////////////////////////////////////////////////// - // The following methods are all called from the RFB thread + static LogWriter vlog = new LogWriter("DesktopWindow"); - public DesktopWindow(int width, int height, String name, PixelFormat serverPF, - CConn cc_) { + public DesktopWindow(int w, int h, String name, + PixelFormat serverPF, CConn cc_) + { cc = cc_; - setSize(width, height); - setScaledSize(); - setOpaque(false); - if (cc.viewport != null) - cc.viewport.setName(name); - GraphicsEnvironment ge = - GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice gd = ge.getDefaultScreenDevice(); - GraphicsConfiguration gc = gd.getDefaultConfiguration(); - BufferCapabilities bufCaps = gc.getBufferCapabilities(); - ImageCapabilities imgCaps = gc.getImageCapabilities(); - if (bufCaps.isPageFlipping() || bufCaps.isMultiBufferAvailable() || - imgCaps.isAccelerated()) { - vlog.debug("GraphicsDevice supports HW acceleration."); - } else { - vlog.debug("GraphicsDevice does not support HW acceleration."); - } - im = new BIPixelBuffer(serverPF, width, height, this); - - cursor = new Cursor(); - cursorBacking = new ManagedPixelBuffer(); - Dimension bestSize = tk.getBestCursorSize(16, 16); - BufferedImage cursorImage; - cursorImage = new BufferedImage(bestSize.width, bestSize.height, - BufferedImage.TYPE_INT_ARGB); - java.awt.Point hotspot = new java.awt.Point(0,0); - nullCursor = tk.createCustomCursor(cursorImage, hotspot, "nullCursor"); - cursorImage.flush(); - if (!cc.cp.supportsLocalCursor && !bestSize.equals(new Dimension(0,0))) - setCursor(nullCursor); - addMouseListener(this); - addMouseWheelListener(this); - addMouseMotionListener(this); - addKeyListener(this); - addFocusListener(new FocusAdapter() { - public void focusGained(FocusEvent e) { - ClipboardDialog.clientCutText(); + firstUpdate = true; + delayedFullscreen = false; delayedDesktopSize = false; + + setFocusable(false); + setFocusTraversalKeysEnabled(false); + getToolkit().setDynamicLayout(false); + if (!VncViewer.os.startsWith("mac os x")) + setIconImage(VncViewer.frameIcon); + UIManager.getDefaults().put("ScrollPane.ancestorInputMap", + new UIDefaults.LazyInputMap(new Object[]{})); + scroll = new JScrollPane(new Viewport(w, h, serverPF, cc)); + viewport = (Viewport)scroll.getViewport().getView(); + scroll.setBorder(BorderFactory.createEmptyBorder(0,0,0,0)); + getContentPane().add(scroll); + + setName(name); + + lastScaleFactor = scalingFactor.getValue(); + if (VncViewer.os.startsWith("mac os x")) + if (!noLionFS.getValue()) + enableLionFS(); + + OptionsDialog.addCallback("handleOptions", this); + + addWindowFocusListener(new WindowAdapter() { + public void windowGainedFocus(WindowEvent e) { + if (isVisible()) + if (scroll.getViewport() != null) + scroll.getViewport().getView().requestFocusInWindow(); } - public void focusLost(FocusEvent e) { + public void windowLostFocus(WindowEvent e) { cc.releaseDownKeys(); } }); - setFocusTraversalKeysEnabled(false); - setFocusable(true); - OptionsDialog.addCallback("handleOptions", this); - } - public int width() { - return getWidth(); - } + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + cc.close(); + } + public void windowDeiconified(WindowEvent e) { + // ViewportBorder sometimes lost when window is shaded or de-iconified + repositionViewport(); + } + }); - public int height() { - return getHeight(); - } + addWindowStateListener(new WindowAdapter() { + public void windowStateChanged(WindowEvent e) { + int state = e.getNewState(); + if ((state & JFrame.MAXIMIZED_BOTH) != JFrame.MAXIMIZED_BOTH) { + Rectangle b = getGraphicsConfiguration().getBounds(); + if (!b.contains(getLocationOnScreen())) + setLocation((int)b.getX(), (int)b.getY()); + } + // ViewportBorder sometimes lost when restoring on Windows + repositionViewport(); + } + }); - public final PixelFormat getPF() { return im.getPF(); } + // Window resize events + timer = new Timer(500, new AbstractAction() { + public void actionPerformed(ActionEvent e) { + handleResizeTimeout(); + } + }); + timer.setRepeats(false); + addComponentListener(new ComponentAdapter() { + public void componentResized(ComponentEvent e) { + if (remoteResize.getValue()) { + if (timer.isRunning()) + timer.restart(); + else + // Try to get the remote size to match our window size, provided + // the following conditions are true: + // + // a) The user has this feature turned on + // b) The server supports it + // c) We're not still waiting for a chance to handle DesktopSize + // d) We're not still waiting for startup fullscreen to kick in + if (!firstUpdate && !delayedFullscreen && + remoteResize.getValue() && cc.cp.supportsSetDesktopSize) + timer.start(); + } else { + String scaleString = scalingFactor.getValue(); + if (!scaleString.matches("^[0-9]+$")) { + Dimension maxSize = getContentPane().getSize(); + if ((maxSize.width != viewport.scaledWidth) || + (maxSize.height != viewport.scaledHeight)) + viewport.setScaledSize(maxSize.width, maxSize.height); + if (!scaleString.equals("Auto")) { + if (!isMaximized() && !fullscreen_active()) { + int dx = getInsets().left + getInsets().right; + int dy = getInsets().top + getInsets().bottom; + setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy); + } + } + } + repositionViewport(); + } + } + }); - public void setViewport(JViewport viewport) { - setScaledSize(); - viewport.setView(this); - // pack() must be called on a JFrame before getInsets() - // will return anything other than 0. - if (viewport.getRootPane() != null) - if (getRootPane().getParent() instanceof JFrame) - ((JFrame)getRootPane().getParent()).pack(); - } + pack(); + if (embed.getValue()) { + scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); + scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED); + VncViewer.setupEmbeddedFrame(scroll); + } else { + if (fullScreen.getValue()) + fullscreen_on(); + else + setVisible(true); - // Methods called from the RFB thread - these need to be synchronized - // wherever they access data shared with the GUI thread. + if (maximize.getValue()) + setExtendedState(JFrame.MAXIMIZED_BOTH); + } - public void setCursor(int w, int h, Point hotspot, - int[] 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 (!useLocalCursor.getValue()) - return; + // Remove resize listener in order to prevent recursion when resizing + @Override + public void setSize(Dimension d) + { + ComponentListener[] listeners = getListeners(ComponentListener.class); + for (ComponentListener l : listeners) + removeComponentListener(l); + super.setSize(d); + for (ComponentListener l : listeners) + addComponentListener(l); + } - hideLocalCursor(); + @Override + public void setSize(int width, int height) + { + ComponentListener[] listeners = getListeners(ComponentListener.class); + for (ComponentListener l : listeners) + removeComponentListener(l); + super.setSize(width, height); + for (ComponentListener l : listeners) + addComponentListener(l); + } - cursor.hotspot = (hotspot != null) ? hotspot : new Point(0, 0); - cursor.setSize(w, h); - cursor.setPF(getPF()); + @Override + public void setBounds(Rectangle r) + { + ComponentListener[] listeners = getListeners(ComponentListener.class); + for (ComponentListener l : listeners) + removeComponentListener(l); + super.setBounds(r); + for (ComponentListener l : listeners) + addComponentListener(l); + } - cursorBacking.setSize(cursor.width(), cursor.height()); - cursorBacking.setPF(getPF()); + private void repositionViewport() + { + scroll.revalidate(); + Rectangle r = scroll.getViewportBorderBounds(); + int dx = r.width - viewport.scaledWidth; + int dy = r.height - viewport.scaledHeight; + int top = (int)Math.max(Math.floor(dy/2), 0); + int left = (int)Math.max(Math.floor(dx/2), 0); + int bottom = (int)Math.max(dy - top, 0); + int right = (int)Math.max(dx - left, 0); + Insets insets = new Insets(top, left, bottom, right); + scroll.setViewportBorder(new MatteBorder(insets, Color.BLACK)); + scroll.revalidate(); + } + + public PixelFormat getPreferredPF() + { + return viewport.getPreferredPF(); + } - cursor.data = new int[cursor.width() * cursor.height()]; - cursor.mask = new byte[cursor.maskLen()]; + public void setName(String name) + { + setTitle(name); + } + + // Copy the areas of the framebuffer that have been changed (damaged) + // to the displayed window. + + public void updateWindow() + { + if (firstUpdate) { + if (cc.cp.supportsSetDesktopSize && !desktopSize.getValue().equals("")) { + // Hack: Wait until we're in the proper mode and position until + // resizing things, otherwise we might send the wrong thing. + if (delayedFullscreen) + delayedDesktopSize = true; + else + handleDesktopSize(); + } + firstUpdate = false; + } + + viewport.updateWindow(); + } + + public void resizeFramebuffer(int new_w, int new_h) + { + if ((new_w == viewport.scaledWidth) && (new_h == viewport.scaledHeight)) + return; - int maskBytesPerRow = (w + 7) / 8; - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int byte_ = y * maskBytesPerRow + x / 8; - int bit = 7 - x % 8; - if ((mask[byte_] & (1 << bit)) > 0) { - cursor.data[y * cursor.width() + x] = (0xff << 24) | - (im.cm.getRed(data[y * w + x]) << 16) | - (im.cm.getGreen(data[y * w + x]) << 8) | - (im.cm.getBlue(data[y * w + x])); - } + // If we're letting the viewport match the window perfectly, then + // keep things that way for the new size, otherwise just keep things + // like they are. + int dx = getInsets().left + getInsets().right; + int dy = getInsets().top + getInsets().bottom; + if (!fullscreen_active()) { + if ((w() == viewport.scaledWidth) && (h() == viewport.scaledHeight)) + setSize(new_w+dx, new_h+dy); + else { + // Make sure the window isn't too big. We do this manually because + // we have to disable the window size restriction (and it isn't + // entirely trustworthy to begin with). + if ((w() > new_w) || (h() > new_h)) + setSize(Math.min(w(), new_w)+dx, Math.min(h(), new_h)+dy); } - System.arraycopy(mask, y * maskBytesPerRow, cursor.mask, - y * ((cursor.width() + 7) / 8), maskBytesPerRow); } - int cw = (int)Math.floor((float)cursor.width() * scaleWidthRatio); - int ch = (int)Math.floor((float)cursor.height() * scaleHeightRatio); - Dimension bestSize = tk.getBestCursorSize(cw, ch); - MemoryImageSource cursorSrc; - cursorSrc = new MemoryImageSource(cursor.width(), cursor.height(), - ColorModel.getRGBdefault(), - cursor.data, 0, cursor.width()); - Image srcImage = tk.createImage(cursorSrc); - BufferedImage cursorImage; - cursorImage = new BufferedImage(bestSize.width, bestSize.height, - BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = cursorImage.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_SPEED); - g2.drawImage(srcImage, 0, 0, (int)Math.min(cw, bestSize.width), - (int)Math.min(ch, bestSize.height), 0, 0, cursor.width(), - cursor.height(), null); - g2.dispose(); - srcImage.flush(); - - int x = (int)Math.floor((float)cursor.hotspot.x * scaleWidthRatio); - int y = (int)Math.floor((float)cursor.hotspot.y * scaleHeightRatio); - x = (int)Math.min(x, Math.max(bestSize.width - 1, 0)); - y = (int)Math.min(y, Math.max(bestSize.height - 1, 0)); - java.awt.Point hs = new java.awt.Point(x, y); - if (!bestSize.equals(new Dimension(0, 0))) - softCursor = tk.createCustomCursor(cursorImage, hs, "softCursor"); - cursorImage.flush(); - - if (softCursor != null) { - setCursor(softCursor); - cursorAvailable = false; - return; - } + viewport.resize(0, 0, new_w, new_h); - if (!cursorAvailable) { - cursorAvailable = true; - } + // We might not resize the main window, so we need to manually call this + // to make sure the viewport is centered. + repositionViewport(); - showLocalCursor(); + // repositionViewport() makes sure the scroll widget notices any changes + // in position, but it might be just the size that changes so we also + // need a poke here as well. + validate(); } - public void setServerPF(PixelFormat pf) { - im.setPF(pf); + public void setCursor(int width, int height, Point hotspot, + byte[] data, byte[] mask) + { + viewport.setCursor(width, height, hotspot, data, mask); } - public PixelFormat getPreferredPF() { - return im.getNativePF(); + public void fullscreen_on() + { + fullScreen.setParam(true); + lastState = getExtendedState(); + lastBounds = getBounds(); + dispose(); + // Screen bounds calculation affected by maximized window? + setExtendedState(JFrame.NORMAL); + setUndecorated(true); + setVisible(true); + setBounds(getScreenBounds()); + } + + public void fullscreen_off() + { + fullScreen.setParam(false); + dispose(); + setUndecorated(false); + setExtendedState(lastState); + setBounds(lastBounds); + setVisible(true); } - // 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. - - public synchronized void setColourMapEntries(int firstColour, int nColours, - int[] rgbs) { - im.setColourMapEntries(firstColour, nColours, rgbs); - if (nColours <= 256) { - im.updateColourMap(); - } else { - if (setColourMapEntriesTimerThread == null) { - setColourMapEntriesTimerThread = new Thread(this); - setColourMapEntriesTimerThread.start(); - } - } + public boolean fullscreen_active() + { + return isUndecorated(); } - // Update the actual window with the changed parts of the framebuffer. - public void updateWindow() { - Rect r = damage; - if (!r.is_empty()) { - if (cc.cp.width != scaledWidth || cc.cp.height != scaledHeight) { - int x = (int)Math.floor(r.tl.x * scaleWidthRatio); - int y = (int)Math.floor(r.tl.y * scaleHeightRatio); - // Need one extra pixel to account for rounding. - int width = (int)Math.ceil(r.width() * scaleWidthRatio) + 1; - int height = (int)Math.ceil(r.height() * scaleHeightRatio) + 1; - paintImmediately(x, y, width, height); - } else { - paintImmediately(r.tl.x, r.tl.y, r.width(), r.height()); - } - damage.clear(); - } - } + private void handleDesktopSize() + { + if (!desktopSize.getValue().equals("")) { + int width, height; - // resize() is called when the desktop has changed size - public void resize() { - int w = cc.cp.width; - int h = cc.cp.height; - hideLocalCursor(); - setSize(w, h); - setScaledSize(); - im.resize(w, h); - } + // An explicit size has been requested - public final 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); - damageRect(new Rect(x, y, x+w, y+h)); - showLocalCursor(); - } + if (desktopSize.getValue().split("x").length != 2) + return; - public final void imageRect(int x, int y, int w, int h, - Object pix) { - if (overlapsCursor(x, y, w, h)) hideLocalCursor(); - im.imageRect(x, y, w, h, pix); - damageRect(new Rect(x, y, x+w, y+h)); - showLocalCursor(); + width = Integer.parseInt(desktopSize.getValue().split("x")[0]); + height = Integer.parseInt(desktopSize.getValue().split("x")[1]); + remoteResize(width, height); + } else if (remoteResize.getValue()) { + // No explicit size, but remote resizing is on so make sure it + // matches whatever size the window ended up being + remoteResize(w(), h()); + } } - public final 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); - damageRect(new Rect(x, y, x+w, y+h)); - showLocalCursor(); - } + public void handleResizeTimeout() + { + DesktopWindow self = (DesktopWindow)this; + assert(self != null); - // 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); + self.remoteResize(self.w(), self.h()); } + private void remoteResize(int width, int height) + { + ScreenSet layout; + ListIterator<Screen> iter; - //////////////////////////////////////////////////////////////////// - // The following methods are all called from the GUI thread + if (!fullscreen_active() || (width > w()) || (height > h())) { + // In windowed mode (or the framebuffer is so large that we need + // to scroll) we just report a single virtual screen that covers + // the entire framebuffer. - void resetLocalCursor() { - if (cc.cp.supportsLocalCursor) { - if (softCursor != null) - setCursor(softCursor); - } else { - setCursor(nullCursor); - } - hideLocalCursor(); - cursorAvailable = false; - } + layout = cc.cp.screenLayout; - // - // Callback methods to determine geometry of our Component. - // + // Not sure why we have no screens, but adding a new one should be + // safe as there is nothing to conflict with... + if (layout.num_screens() == 0) + layout.add_screen(new Screen()); + else if (layout.num_screens() != 1) { + // More than one screen. Remove all but the first (which we + // assume is the "primary"). - public Dimension getPreferredSize() { - return new Dimension(scaledWidth, scaledHeight); - } + while (true) { + iter = layout.begin(); + Screen screen = iter.next(); - public Dimension getMinimumSize() { - return new Dimension(scaledWidth, scaledHeight); - } + if (iter == layout.end()) + break; - public Dimension getMaximumSize() { - return new Dimension(scaledWidth, scaledHeight); - } + layout.remove_screen(screen.id); + } + } - public void setScaledSize() { - String scaleString = scalingFactor.getValue(); - if (!scaleString.equalsIgnoreCase("Auto") && - !scaleString.equalsIgnoreCase("FixedRatio")) { - int scalingFactor = Integer.parseInt(scaleString); - scaledWidth = - (int)Math.floor((float)cc.cp.width * (float)scalingFactor/100.0); - scaledHeight = - (int)Math.floor((float)cc.cp.height * (float)scalingFactor/100.0); + // Resize the remaining single screen to the complete framebuffer + ((Screen)layout.begin().next()).dimensions.tl.x = 0; + ((Screen)layout.begin().next()).dimensions.tl.y = 0; + ((Screen)layout.begin().next()).dimensions.br.x = width; + ((Screen)layout.begin().next()).dimensions.br.y = height; } else { - if (cc.viewport == null) { - scaledWidth = cc.cp.width; - scaledHeight = cc.cp.height; - } else { - Dimension vpSize = cc.viewport.getSize(); - Insets vpInsets = cc.viewport.getInsets(); - Dimension availableSize = - new Dimension(vpSize.width - vpInsets.left - vpInsets.right, - vpSize.height - vpInsets.top - vpInsets.bottom); - if (availableSize.width == 0 || availableSize.height == 0) - availableSize = new Dimension(cc.cp.width, cc.cp.height); - if (scaleString.equalsIgnoreCase("FixedRatio")) { - float widthRatio = (float)availableSize.width / (float)cc.cp.width; - float heightRatio = (float)availableSize.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); - } else { - scaledWidth = availableSize.width; - scaledHeight = availableSize.height; + layout = new ScreenSet(); + int id; + int sx, sy, sw, sh; + Rect viewport_rect = new Rect(); + Rect screen_rect = new Rect(); + + // In full screen we report all screens that are fully covered. + + viewport_rect.setXYWH(x() + (w() - width)/2, y() + (h() - height)/2, + width, height); + + // If we can find a matching screen in the existing set, we use + // that, otherwise we create a brand new screen. + // + // FIXME: We should really track screens better so we can handle + // a resized one. + // + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + for (GraphicsDevice gd : ge.getScreenDevices()) { + for (GraphicsConfiguration gc : gd.getConfigurations()) { + Rectangle bounds = gc.getBounds(); + sx = bounds.x; + sy = bounds.y; + sw = bounds.width; + sh = bounds.height; + + // Check that the screen is fully inside the framebuffer + screen_rect.setXYWH(sx, sy, sw, sh); + if (!screen_rect.enclosed_by(viewport_rect)) + continue; + + // Adjust the coordinates so they are relative to our viewport + sx -= viewport_rect.tl.x; + sy -= viewport_rect.tl.y; + + // Look for perfectly matching existing screen... + for (iter = cc.cp.screenLayout.begin(); + iter != cc.cp.screenLayout.end(); iter.next()) { + Screen screen = iter.next(); iter.previous(); + if ((screen.dimensions.tl.x == sx) && + (screen.dimensions.tl.y == sy) && + (screen.dimensions.width() == sw) && + (screen.dimensions.height() == sh)) + break; + } + + // Found it? + if (iter != cc.cp.screenLayout.end()) { + layout.add_screen(iter.next()); + continue; + } + + // Need to add a new one, which means we need to find an unused id + Random rng = new Random(); + while (true) { + id = rng.nextInt(); + for (iter = cc.cp.screenLayout.begin(); + iter != cc.cp.screenLayout.end(); iter.next()) { + Screen screen = iter.next(); iter.previous(); + if (screen.id == id) + break; + } + + if (iter == cc.cp.screenLayout.end()) + break; + } + + layout.add_screen(new Screen(id, sx, sy, sw, sh, 0)); } + + // If the viewport doesn't match a physical screen, then we might + // end up with no screens in the layout. Add a fake one... + if (layout.num_screens() == 0) + layout.add_screen(new Screen(0, 0, 0, width, height, 0)); } } - scaleWidthRatio = (float)scaledWidth / (float)cc.cp.width; - scaleHeightRatio = (float)scaledHeight / (float)cc.cp.height; - } - 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(im.getImage(), 0, 0, scaledWidth, scaledHeight, null); - } else { - g2.drawImage(im.getImage(), 0, 0, null); + // Do we actually change anything? + if ((width == cc.cp.width) && + (height == cc.cp.height) && + (layout == cc.cp.screenLayout)) + return; + + String buffer; + vlog.debug(String.format("Requesting framebuffer resize from %dx%d to %dx%d", + cc.cp.width, cc.cp.height, width, height)); + layout.debug_print(); + + if (!layout.validate(width, height)) { + vlog.error("Invalid screen layout computed for resize request!"); + return; } - g2.dispose(); + + cc.writer().writeSetDesktopSize(width, height, layout); } - // 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(e); - // - If local cursor rendering is enabled then use it - if (cursorAvailable) { - // - Render the cursor! - if (e.getX() != cursorPosX || e.getY() != cursorPosY) { - hideLocalCursor(); - if (e.getX() >= 0 && e.getX() < im.width() && - e.getY() >= 0 && e.getY() < im.height()) { - cursorPosX = e.getX(); - cursorPosY = e.getY(); - showLocalCursor(); - } + boolean lionFSSupported() { return canDoLionFS; } + + private int x() { return getContentPane().getX(); } + private int y() { return getContentPane().getY(); } + private int w() { return getContentPane().getWidth(); } + private int h() { return getContentPane().getHeight(); } + + void enableLionFS() { + try { + String version = System.getProperty("os.version"); + int firstDot = version.indexOf('.'); + int lastDot = version.lastIndexOf('.'); + if (lastDot > firstDot && lastDot >= 0) { + version = version.substring(0, version.indexOf('.', firstDot + 1)); } + double v = Double.parseDouble(version); + if (v < 10.7) + throw new Exception("Operating system version is " + v); + + Class fsuClass = Class.forName("com.apple.eawt.FullScreenUtilities"); + Class argClasses[] = new Class[]{Window.class, Boolean.TYPE}; + Method setWindowCanFullScreen = + fsuClass.getMethod("setWindowCanFullScreen", argClasses); + setWindowCanFullScreen.invoke(fsuClass, this, true); + + canDoLionFS = true; + } catch (Exception e) { + vlog.debug("Could not enable OS X 10.7+ full-screen mode: " + + e.getMessage()); } - lastX = e.getX(); - lastY = e.getY(); } - 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(e); + + public void toggleLionFS() { + try { + Class appClass = Class.forName("com.apple.eawt.Application"); + Method getApplication = appClass.getMethod("getApplication", + (Class[])null); + Object app = getApplication.invoke(appClass); + Method requestToggleFullScreen = + appClass.getMethod("requestToggleFullScreen", Window.class); + requestToggleFullScreen.invoke(app, this); + } catch (Exception e) { + vlog.debug("Could not toggle OS X 10.7+ full-screen mode: " + + e.getMessage()); } - lastX = e.getX(); - lastY = e.getY(); - } - 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); + public boolean isMaximized() + { + int state = getExtendedState(); + return ((state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH); } - private static final Integer keyEventLock = 0; - - // Handle the key-typed event. - public void keyTyped(KeyEvent e) { } + public Dimension getScreenSize() { + return getScreenBounds().getSize(); + } - // Handle the key-released event. - public void keyReleased(KeyEvent e) { - synchronized(keyEventLock) { - cc.writeKeyEvent(e); + public Rectangle getScreenBounds() { + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + Rectangle r = new Rectangle(); + if (fullScreenAllMonitors.getValue()) { + for (GraphicsDevice gd : ge.getScreenDevices()) + for (GraphicsConfiguration gc : gd.getConfigurations()) + r = r.union(gc.getBounds()); + } else { + GraphicsConfiguration gc = getGraphicsConfiguration(); + r = gc.getBounds(); } + return r; } - // Handle the key-pressed event. - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == MenuKey.getMenuKeyCode()) { - int sx = (scaleWidthRatio == 1.00) ? - lastX : (int)Math.floor(lastX * scaleWidthRatio); - int sy = (scaleHeightRatio == 1.00) ? - lastY : (int)Math.floor(lastY * scaleHeightRatio); - java.awt.Point ev = new java.awt.Point(lastX, lastY); - ev.translate(sx - lastX, sy - lastY); - F8Menu menu = new F8Menu(cc); - menu.show(this, (int)ev.getX(), (int)ev.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: - cc.toggleFullScreen(); - return; - case KeyEvent.VK_H: - cc.refresh(); - return; - case KeyEvent.VK_I: - cc.showInfo(); - return; - case KeyEvent.VK_O: - OptionsDialog.showDialog(cc.viewport); - 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 static Window getFullScreenWindow() { + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + for (GraphicsDevice gd : ge.getScreenDevices()) { + Window fullScreenWindow = gd.getFullScreenWindow(); + if (fullScreenWindow != null) + return fullScreenWindow; } + return null; } - //////////////////////////////////////////////////////////////////// - // The following methods are called from both RFB and GUI threads - - // Note that mutex MUST be held when hideLocalCursor() and showLocalCursor() - // are called. - - private synchronized void hideLocalCursor() { - // - Blit the cursor backing store over the cursor - if (cursorVisible) { - cursorVisible = false; - im.imageRect(cursorBackingX, cursorBackingY, cursorBacking.width(), - cursorBacking.height(), cursorBacking.data); - damageRect(new Rect(cursorBackingX, cursorBackingY, - cursorBackingX+cursorBacking.width(), - cursorBackingY+cursorBacking.height())); + public static void setFullScreenWindow(Window fullScreenWindow) { + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + if (fullScreenAllMonitors.getValue()) { + for (GraphicsDevice gd : ge.getScreenDevices()) + gd.setFullScreenWindow(fullScreenWindow); + } else { + GraphicsDevice gd = ge.getDefaultScreenDevice(); + gd.setFullScreenWindow(fullScreenWindow); } } - private synchronized 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 = cursor.hotspot.x; - int cursorTop = cursor.hotspot.y; - int cursorRight = cursorLeft + cursor.width(); - int cursorBottom = cursorTop + cursor.height(); + public void handleOptions() + { - 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); + if (fullScreen.getValue() && !fullscreen_active()) + fullscreen_on(); + else if (!fullScreen.getValue() && fullscreen_active()) + fullscreen_off(); - cursorBackingX = x; - cursorBackingY = y; - cursorBacking.setSize(w, h); + if (remoteResize.getValue()) { + scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); + scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED); + remoteResize(w(), h()); + } else { + String scaleString = scalingFactor.getValue(); + if (!scaleString.equals(lastScaleFactor)) { + if (scaleString.matches("^[0-9]+$")) { + scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); + scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED); + viewport.setScaledSize(cc.cp.width, cc.cp.height); + } else { + scroll.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER); + scroll.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_NEVER); + viewport.setScaledSize(w(), h()); + } - for (int j = 0; j < h; j++) - System.arraycopy(im.data, (y + j) * im.width() + x, - cursorBacking.data, j * w, w); + if (isMaximized() || fullscreen_active()) { + repositionViewport(); + } else { + int dx = getInsets().left + getInsets().right; + int dy = getInsets().top + getInsets().bottom; + setSize(viewport.scaledWidth+dx, viewport.scaledHeight+dy); + } - im.maskRect(cursorLeft, cursorTop, cursor.width(), cursor.height(), - cursor.data, cursor.mask); - damageRect(new Rect(x, y, x+w, y+h)); + repositionViewport(); + lastScaleFactor = scaleString; + } } - } - void damageRect(Rect r) { - if (damage.is_empty()) { - damage.setXYWH(r.tl.x, r.tl.y, r.width(), r.height()); - } else { - r = damage.union_boundary(r); - damage.setXYWH(r.tl.x, r.tl.y, r.width(), r.height()); + if (isVisible()) { + toFront(); + requestFocus(); } } - // run() is executed by the setColourMapEntriesTimerThread - it sleeps for - // 100ms before actually updating the colourmap. - public synchronized void run() { - try { - Thread.sleep(100); - } catch(InterruptedException e) {} - im.updateColourMap(); - setColourMapEntriesTimerThread = null; - } - - public void handleOptions() + public void handleFullscreenTimeout() { - if (fullScreen.getValue() && Viewport.getFullScreenWindow() == null) - cc.toggleFullScreen(); - else if (!fullScreen.getValue() && Viewport.getFullScreenWindow() != null) - cc.toggleFullScreen(); - } - - // access to cc by different threads is specified in CConn - CConn cc; + DesktopWindow self = (DesktopWindow)this; - // access to the following must be synchronized: - PlatformPixelBuffer im; - Thread setColourMapEntriesTimerThread; + assert(self != null); - Cursor cursor; - boolean cursorVisible = false; // Is cursor currently rendered? - boolean cursorAvailable = false; // Is cursor available for rendering? - int cursorPosX, cursorPosY; - ManagedPixelBuffer cursorBacking; - int cursorBackingX, cursorBackingY; - java.awt.Cursor softCursor, nullCursor; - static Toolkit tk = Toolkit.getDefaultToolkit(); + self.delayedFullscreen = false; - public int scaledWidth = 0, scaledHeight = 0; - float scaleWidthRatio, scaleHeightRatio; + if (self.delayedDesktopSize) { + self.handleDesktopSize(); + self.delayedDesktopSize = false; + } + } - // the following are only ever accessed by the GUI thread: - int lastX, lastY; - Rect damage = new Rect(); + private CConn cc; + private JScrollPane scroll; + public Viewport viewport; - static LogWriter vlog = new LogWriter("DesktopWindow"); + private boolean firstUpdate; + private boolean delayedFullscreen; + private boolean delayedDesktopSize; + private boolean canDoLionFS; + private String lastScaleFactor; + private Rectangle lastBounds; + private int lastState; + private Timer timer; } + diff --git a/java/com/tigervnc/vncviewer/Dialog.java b/java/com/tigervnc/vncviewer/Dialog.java index 6204ba10..a2fb04fe 100644 --- a/java/com/tigervnc/vncviewer/Dialog.java +++ b/java/com/tigervnc/vncviewer/Dialog.java @@ -65,9 +65,6 @@ class Dialog extends JDialog implements ActionListener, int y = (dpySize.height - mySize.height) / 2; setLocation(x, y); } - fullScreenWindow = Viewport.getFullScreenWindow(); - if (fullScreenWindow != null) - Viewport.setFullScreenWindow(null); if (getModalityType() == ModalityType.APPLICATION_MODAL) setAlwaysOnTop(true); @@ -81,9 +78,6 @@ class Dialog extends JDialog implements ActionListener, public void endDialog() { setVisible(false); setAlwaysOnTop(false); - fullScreenWindow = Viewport.getFullScreenWindow(); - if (fullScreenWindow != null) - Viewport.setFullScreenWindow(fullScreenWindow); } // initDialog() can be overridden in a derived class. Typically it is used diff --git a/java/com/tigervnc/vncviewer/F8Menu.java b/java/com/tigervnc/vncviewer/F8Menu.java index d7f9e482..0c67305a 100644 --- a/java/com/tigervnc/vncviewer/F8Menu.java +++ b/java/com/tigervnc/vncviewer/F8Menu.java @@ -106,19 +106,24 @@ public class F8Menu extends JPopupMenu implements ActionListener { if (actionMatch(ev, exit)) { cc.close(); } else if (actionMatch(ev, fullScreenCheckbox)) { - cc.toggleFullScreen(); + if (fullScreenCheckbox.isSelected()) + cc.desktop.fullscreen_on(); + else + cc.desktop.fullscreen_off(); } else if (actionMatch(ev, restore)) { - if (fullScreen.getValue()) cc.toggleFullScreen(); - cc.viewport.setExtendedState(JFrame.NORMAL); + if (cc.desktop.fullscreen_active()) + cc.desktop.fullscreen_off(); + cc.desktop.setExtendedState(JFrame.NORMAL); } else if (actionMatch(ev, minimize)) { - if (fullScreen.getValue()) cc.toggleFullScreen(); - cc.viewport.setExtendedState(JFrame.ICONIFIED); + if (cc.desktop.fullscreen_active()) + cc.desktop.fullscreen_off(); + cc.desktop.setExtendedState(JFrame.ICONIFIED); } else if (actionMatch(ev, maximize)) { - if (fullScreen.getValue()) cc.toggleFullScreen(); - cc.viewport.setExtendedState(JFrame.MAXIMIZED_BOTH); + if (cc.desktop.fullscreen_active()) + cc.desktop.fullscreen_off(); + cc.desktop.setExtendedState(JFrame.MAXIMIZED_BOTH); } else if (actionMatch(ev, clipboard)) { - //ClipboardDialog dlg = new ClipboardDialog(cc); - ClipboardDialog.showDialog(cc.viewport); + ClipboardDialog.showDialog(cc.desktop); } else if (actionMatch(ev, f8)) { cc.writeKeyEvent(MenuKey.getMenuKeySym(), true); cc.writeKeyEvent(MenuKey.getMenuKeySym(), false); @@ -134,7 +139,7 @@ public class F8Menu extends JPopupMenu implements ActionListener { } else if (actionMatch(ev, newConn)) { VncViewer.newViewer(); } else if (actionMatch(ev, options)) { - OptionsDialog.showDialog(cc.viewport); + OptionsDialog.showDialog(cc.desktop); } else if (actionMatch(ev, save)) { String title = "Save the TigerVNC configuration to file"; File dflt = new File(FileUtils.getVncHomeDir().concat("default.tigervnc")); @@ -170,6 +175,24 @@ public class F8Menu extends JPopupMenu implements ActionListener { } } + public void show(Component invoker, int x, int y) { + // lightweight components can't show in FullScreen Exclusive mode + /* + Window fsw = DesktopWindow.getFullScreenWindow(); + GraphicsDevice gd = null; + if (fsw != null) { + gd = fsw.getGraphicsConfiguration().getDevice(); + if (gd.isFullScreenSupported()) + DesktopWindow.setFullScreenWindow(null); + } + */ + super.show(invoker, x, y); + /* + if (fsw != null && gd.isFullScreenSupported()) + DesktopWindow.setFullScreenWindow(fsw); + */ + } + CConn cc; JMenuItem restore, move, size, minimize, maximize; JMenuItem exit, clipboard, ctrlAltDel, refresh; diff --git a/java/com/tigervnc/vncviewer/JavaPixelBuffer.java b/java/com/tigervnc/vncviewer/JavaPixelBuffer.java new file mode 100644 index 00000000..b639673c --- /dev/null +++ b/java/com/tigervnc/vncviewer/JavaPixelBuffer.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2012-2016 Brian P. Hinz + * Copyright (C) 2012 D. R. Commander. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +package com.tigervnc.vncviewer; + +import java.awt.*; +import java.awt.image.*; +import java.nio.ByteOrder; + +import com.tigervnc.rfb.*; + +public class JavaPixelBuffer extends PlatformPixelBuffer +{ + + public JavaPixelBuffer(int w, int h) { + super(getPreferredPF(), w, h, + getPreferredPF().getColorModel().createCompatibleWritableRaster(w,h)); + } + + private static PixelFormat getPreferredPF() + { + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = ge.getDefaultScreenDevice(); + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + ColorModel cm = gc.getColorModel(); + int depth = ((cm.getPixelSize() > 24) ? 24 : cm.getPixelSize()); + int bpp = (depth > 16 ? 32 : (depth > 8 ? 16 : 8)); + ByteOrder byteOrder = ByteOrder.nativeOrder(); + boolean bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN ? true : false); + boolean trueColour = true; + int redShift = cm.getComponentSize()[0] + cm.getComponentSize()[1]; + int greenShift = cm.getComponentSize()[0]; + int blueShift = 0; + int redMask = ((int)Math.pow(2, cm.getComponentSize()[2]) - 1); + int greenMask = ((int)Math.pow(2, cm.getComponentSize()[1]) - 1); + int blueMmask = ((int)Math.pow(2, cm.getComponentSize()[0]) - 1); + return new PixelFormat(bpp, depth, bigEndian, trueColour, + redMask, greenMask, blueMmask, + redShift, greenShift, blueShift); + } + +} diff --git a/java/com/tigervnc/vncviewer/OptionsDialog.java b/java/com/tigervnc/vncviewer/OptionsDialog.java index db274911..a7c87784 100644 --- a/java/com/tigervnc/vncviewer/OptionsDialog.java +++ b/java/com/tigervnc/vncviewer/OptionsDialog.java @@ -94,7 +94,7 @@ class OptionsDialog extends Dialog { } } - private static Map<String, Object> callbacks = new HashMap<String, Object>(); + private static Map<Object, String> callbacks = new HashMap<Object, String>(); /* Compression */ JCheckBox autoselectCheckbox; @@ -140,13 +140,18 @@ class OptionsDialog extends Dialog { JCheckBox desktopSizeCheckbox; JTextField desktopWidthInput; JTextField desktopHeightInput; + + ButtonGroup sizingGroup; + JRadioButton remoteResizeButton; + JRadioButton remoteScaleButton; + JComboBox scalingFactorInput; + JCheckBox fullScreenCheckbox; JCheckBox fullScreenAllMonitorsCheckbox; - JComboBox scalingFactorInput; /* Misc. */ JCheckBox sharedCheckbox; - JCheckBox localCursorCheckbox; + JCheckBox dotWhenNoCursorCheckbox; JCheckBox acceptBellCheckbox; /* SSH */ @@ -190,9 +195,10 @@ class OptionsDialog extends Dialog { tabPane.addTab("SSH", createSshPanel()); tabPane.setBorder(BorderFactory.createEmptyBorder()); // Resize the tabPane if necessary to prevent scrolling - Insets tpi = - (Insets)UIManager.get("TabbedPane:TabbedPaneTabArea.contentMargins"); - int minWidth = tpi.left + tpi.right; + int minWidth = 0; + Object tpi = UIManager.get("TabbedPane:TabbedPaneTabArea.contentMargins"); + if (tpi != null) + minWidth += ((Insets)tpi).left + ((Insets)tpi).right; for (int i = 0; i < tabPane.getTabCount(); i++) minWidth += tabPane.getBoundsAt(i).width; int minHeight = tabPane.getPreferredSize().height; @@ -215,7 +221,7 @@ class OptionsDialog extends Dialog { }); JPanel buttonPane = new JPanel(new GridLayout(1, 5, 10, 10)); - buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5)); + buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5)); buttonPane.add(Box.createRigidArea(new Dimension())); buttonPane.add(Box.createRigidArea(new Dimension())); buttonPane.add(Box.createRigidArea(new Dimension())); @@ -240,12 +246,12 @@ class OptionsDialog extends Dialog { public static void addCallback(String cb, Object obj) { - callbacks.put(cb, obj); + callbacks.put(obj, cb); } - public static void removeCallback(String cb) + public static void removeCallback(Object obj) { - callbacks.remove(cb); + callbacks.remove(obj); } public void endDialog() { @@ -258,15 +264,18 @@ class OptionsDialog extends Dialog { fullScreenCheckbox.setEnabled(s); fullScreenAllMonitorsCheckbox.setEnabled(s); scalingFactorInput.setEnabled(s); + Enumeration<AbstractButton> e = sizingGroup.getElements(); + while (e.hasMoreElements()) + e.nextElement().setEnabled(s); } private void loadOptions() { /* Compression */ autoselectCheckbox.setSelected(autoSelect.getValue()); - + int encNum = Encodings.encodingNum(preferredEncoding.getValueStr()); - + switch (encNum) { case Encodings.encodingTight: tightButton.setSelected(true); @@ -281,7 +290,7 @@ class OptionsDialog extends Dialog { rawButton.setSelected(true); break; } - + if (fullColor.getValue()) fullcolorButton.setSelected(true); else { @@ -323,13 +332,13 @@ class OptionsDialog extends Dialog { encNoneCheckbox.setSelected(false); encTLSCheckbox.setSelected(false); encX509Checkbox.setSelected(false); - + authNoneCheckbox.setSelected(false); authVncCheckbox.setSelected(false); authPlainCheckbox.setSelected(false); authIdentCheckbox.setSelected(false); sendLocalUsernameCheckbox.setSelected(sendLocalUsername.getValue()); - + secTypes = security.GetEnabledSecTypes(); for (iter = secTypes.iterator(); iter.hasNext(); ) { switch ((Integer)iter.next()) { @@ -343,7 +352,7 @@ class OptionsDialog extends Dialog { break; } } - + secTypesExt = security.GetEnabledExtSecTypes(); for (iterExt = secTypesExt.iterator(); iterExt.hasNext(); ) { switch ((Integer)iterExt.next()) { @@ -404,14 +413,14 @@ class OptionsDialog extends Dialog { viewOnlyCheckbox.setSelected(viewOnly.getValue()); acceptClipboardCheckbox.setSelected(acceptClipboard.getValue()); sendClipboardCheckbox.setSelected(sendClipboard.getValue()); - + menuKeyChoice.setSelectedIndex(0); - + String menuKeyStr = menuKey.getValueStr(); for (int i = 0; i < menuKeyChoice.getItemCount(); i++) if (menuKeyStr.equals(menuKeyChoice.getItemAt(i))) menuKeyChoice.setSelectedIndex(i); - + /* Screen */ String width, height; @@ -427,6 +436,10 @@ class OptionsDialog extends Dialog { height = desktopSize.getValueStr().split("x")[1]; desktopHeightInput.setText(height); } + if (remoteResize.getValue()) + remoteResizeButton.setSelected(true); + else + remoteScaleButton.setSelected(true); fullScreenCheckbox.setSelected(fullScreen.getValue()); fullScreenAllMonitorsCheckbox.setSelected(fullScreenAllMonitors.getValue()); @@ -434,15 +447,17 @@ class OptionsDialog extends Dialog { String scaleStr = scalingFactor.getValueStr(); if (scaleStr.matches("^[0-9]+$")) scaleStr = scaleStr.concat("%"); + if (scaleStr.matches("^FixedRatio$")) + scaleStr = new String("Fixed Aspect Ratio"); for (int i = 0; i < scalingFactorInput.getItemCount(); i++) if (scaleStr.equals(scalingFactorInput.getItemAt(i))) scalingFactorInput.setSelectedIndex(i); handleDesktopSize(); - + /* Misc. */ sharedCheckbox.setSelected(shared.getValue()); - localCursorCheckbox.setSelected(useLocalCursor.getValue()); + dotWhenNoCursorCheckbox.setSelected(dotWhenNoCursor.getValue()); acceptBellCheckbox.setSelected(acceptBell.getValue()); /* SSH */ @@ -556,7 +571,7 @@ class OptionsDialog extends Dialog { File crlFile = new File(crlInput.getText()); if (crlFile.exists() && crlFile.canRead()) CSecurityTLS.X509CRL.setParam(crlFile.getAbsolutePath()); - + /* Input */ viewOnly.setParam(viewOnlyCheckbox.isSelected()); acceptClipboard.setParam(acceptClipboardCheckbox.isSelected()); @@ -576,18 +591,18 @@ class OptionsDialog extends Dialog { } else { desktopSize.setParam(""); } + remoteResize.setParam(remoteResizeButton.isSelected()); fullScreen.setParam(fullScreenCheckbox.isSelected()); fullScreenAllMonitors.setParam(fullScreenAllMonitorsCheckbox.isSelected()); String scaleStr = ((String)scalingFactorInput.getSelectedItem()).replace("%", ""); - if (scaleStr.equals("Fixed Aspect Ratio")) - scaleStr = "FixedRatio"; + scaleStr.replace("Fixed Aspect Ratio", "FixedRatio"); scalingFactor.setParam(scaleStr); /* Misc. */ shared.setParam(sharedCheckbox.isSelected()); - useLocalCursor.setParam(localCursorCheckbox.isSelected()); + dotWhenNoCursor.setParam(dotWhenNoCursorCheckbox.isSelected()); acceptBell.setParam(acceptBellCheckbox.isSelected()); /* SSH */ @@ -614,9 +629,11 @@ class OptionsDialog extends Dialog { sshKeyFile.setParam(sshKeyFileInput.getText()); try { - for (Map.Entry<String, Object> iter : callbacks.entrySet()) { - Object obj = iter.getValue(); - Method cb = obj.getClass().getMethod(iter.getKey(), new Class[]{}); + for (Map.Entry<Object, String> iter : callbacks.entrySet()) { + Object obj = iter.getKey(); + Method cb = obj.getClass().getMethod(iter.getValue(), new Class[]{}); + if (cb == null) + vlog.info(obj.getClass().getName()); cb.invoke(obj); } } catch (NoSuchMethodException e) { @@ -1015,6 +1032,9 @@ class OptionsDialog extends Dialog { private JPanel createScreenPanel() { JPanel ScreenPanel = new JPanel(new GridBagLayout()); ScreenPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); + + JPanel SizingPanel = new JPanel(new GridBagLayout()); + SizingPanel.setBorder(BorderFactory.createTitledBorder("Desktop Sizing")); desktopSizeCheckbox = new JCheckBox("Resize remote session on connect"); desktopSizeCheckbox.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { @@ -1028,16 +1048,28 @@ class OptionsDialog extends Dialog { desktopSizePanel.add(desktopWidthInput); desktopSizePanel.add(new JLabel(" x ")); desktopSizePanel.add(desktopHeightInput); - fullScreenCheckbox = new JCheckBox("Full-screen mode"); - fullScreenAllMonitorsCheckbox = - new JCheckBox("Enable full-screen mode over all monitors"); + sizingGroup = new ButtonGroup(); + remoteResizeButton = + new JRadioButton("Resize remote session to the local window"); + sizingGroup.add(remoteResizeButton); + remoteScaleButton = + new JRadioButton("Scale remote session to the local window"); + sizingGroup.add(remoteScaleButton); + remoteResizeButton.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + handleRemoteResize(); + } + }); JLabel scalingFactorLabel = new JLabel("Scaling Factor"); Object[] scalingFactors = { "Auto", "Fixed Aspect Ratio", "50%", "75%", "95%", "100%", "105%", "125%", "150%", "175%", "200%", "250%", "300%", "350%", "400%" }; scalingFactorInput = new MyJComboBox(scalingFactors); scalingFactorInput.setEditable(true); - ScreenPanel.add(desktopSizeCheckbox, + fullScreenCheckbox = new JCheckBox("Full-screen mode"); + fullScreenAllMonitorsCheckbox = + new JCheckBox("Enable full-screen mode over all monitors"); + SizingPanel.add(desktopSizeCheckbox, new GridBagConstraints(0, 0, REMAINDER, 1, LIGHT, LIGHT, @@ -1045,44 +1077,66 @@ class OptionsDialog extends Dialog { new Insets(0, 0, 0, 0), NONE, NONE)); int indent = getButtonLabelInset(desktopSizeCheckbox); - ScreenPanel.add(desktopSizePanel, + SizingPanel.add(desktopSizePanel, new GridBagConstraints(0, 1, REMAINDER, 1, LIGHT, LIGHT, LINE_START, NONE, new Insets(0, indent, 0, 0), NONE, NONE)); - ScreenPanel.add(fullScreenCheckbox, + SizingPanel.add(remoteResizeButton, new GridBagConstraints(0, 2, REMAINDER, 1, LIGHT, LIGHT, LINE_START, NONE, new Insets(0, 0, 4, 0), NONE, NONE)); - indent = getButtonLabelInset(fullScreenCheckbox); - ScreenPanel.add(fullScreenAllMonitorsCheckbox, + SizingPanel.add(remoteScaleButton, new GridBagConstraints(0, 3, REMAINDER, 1, LIGHT, LIGHT, LINE_START, NONE, - new Insets(0, indent, 4, 0), + new Insets(0, 0, 4, 0), NONE, NONE)); - ScreenPanel.add(scalingFactorLabel, + indent = getButtonLabelInset(remoteScaleButton); + SizingPanel.add(scalingFactorLabel, new GridBagConstraints(0, 4, 1, 1, LIGHT, LIGHT, LINE_START, NONE, - new Insets(0, 0, 4, 0), + new Insets(0, indent, 4, 0), NONE, NONE)); - ScreenPanel.add(scalingFactorInput, + SizingPanel.add(scalingFactorInput, new GridBagConstraints(1, 4, 1, 1, HEAVY, LIGHT, LINE_START, NONE, new Insets(0, 5, 4, 0), NONE, NONE)); + ScreenPanel.add(SizingPanel, + new GridBagConstraints(0, 0, + REMAINDER, 1, + LIGHT, LIGHT, + LINE_START, HORIZONTAL, + new Insets(0, 0, 4, 0), + NONE, NONE)); + ScreenPanel.add(fullScreenCheckbox, + new GridBagConstraints(0, 1, + REMAINDER, 1, + LIGHT, LIGHT, + LINE_START, NONE, + new Insets(0, 0, 4, 0), + NONE, NONE)); + indent = getButtonLabelInset(fullScreenCheckbox); + ScreenPanel.add(fullScreenAllMonitorsCheckbox, + new GridBagConstraints(0, 2, + REMAINDER, 1, + LIGHT, LIGHT, + LINE_START, NONE, + new Insets(0, indent, 4, 0), + NONE, NONE)); ScreenPanel.add(Box.createRigidArea(new Dimension(5, 0)), - new GridBagConstraints(0, 5, + new GridBagConstraints(0, 3, REMAINDER, REMAINDER, HEAVY, HEAVY, LINE_START, BOTH, @@ -1096,7 +1150,7 @@ class OptionsDialog extends Dialog { MiscPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); sharedCheckbox = new JCheckBox("Shared (don't disconnect other viewers)"); - localCursorCheckbox = new JCheckBox("Render cursor locally"); + dotWhenNoCursorCheckbox = new JCheckBox("Show dot when no cursor"); acceptBellCheckbox = new JCheckBox("Beep when requested by the server"); MiscPanel.add(sharedCheckbox, new GridBagConstraints(0, 0, @@ -1105,7 +1159,7 @@ class OptionsDialog extends Dialog { LINE_START, NONE, new Insets(0, 0, 4, 0), NONE, NONE)); - MiscPanel.add(localCursorCheckbox, + MiscPanel.add(dotWhenNoCursorCheckbox, new GridBagConstraints(0, 1, 1, 1, LIGHT, LIGHT, @@ -1472,6 +1526,11 @@ class OptionsDialog extends Dialog { desktopHeightInput.setEnabled(desktopSizeCheckbox.isSelected()); } + private void handleRemoteResize() + { + scalingFactorInput.setEnabled(!remoteResizeButton.isSelected()); + } + private void handleTunnel() { viaCheckbox.setEnabled(tunnelCheckbox.isSelected()); @@ -1533,6 +1592,8 @@ class OptionsDialog extends Dialog { desktopSizeCheckbox.setEnabled(false); desktopWidthInput.setEnabled(false); desktopHeightInput.setEnabled(false); + remoteResizeButton.setEnabled(false); + remoteScaleButton.setEnabled(false); fullScreenCheckbox.setEnabled(false); fullScreenAllMonitorsCheckbox.setEnabled(false); scalingFactorInput.setEnabled(false); diff --git a/java/com/tigervnc/vncviewer/Parameters.java b/java/com/tigervnc/vncviewer/Parameters.java index e6b91c33..50e26cba 100644 --- a/java/com/tigervnc/vncviewer/Parameters.java +++ b/java/com/tigervnc/vncviewer/Parameters.java @@ -31,168 +31,167 @@ import com.tigervnc.rfb.Exception; public class Parameters { - public static BoolParameter noLionFS = new BoolParameter("NoLionFS", - "On Mac systems, setting this parameter will force the use of the old "+ - "(pre-Lion) full-screen mode, even if the viewer is running on OS X 10.7 "+ - "Lion or later.", - false); + "On Mac systems, setting this parameter will force the use of the old "+ + "(pre-Lion) full-screen mode, even if the viewer is running on OS X 10.7 "+ + "Lion or later.", + false); public static BoolParameter embed = new BoolParameter("Embed", - "If the viewer is being run as an applet, display its output to " + - "an embedded frame in the browser window rather than to a dedicated " + - "window. Embed=1 implies FullScreen=0 and Scale=100.", - false); + "If the viewer is being run as an applet, display its output to " + + "an embedded frame in the browser window rather than to a dedicated " + + "window. Embed=1 implies FullScreen=0 and Scale=100.", + false); - public static BoolParameter useLocalCursor - = new BoolParameter("UseLocalCursor", - "Render the mouse cursor locally", - true); + public static BoolParameter dotWhenNoCursor + = new BoolParameter("DotWhenNoCursor", + "Show the dot cursor when the server sends an invisible cursor", + false); public static BoolParameter sendLocalUsername = new BoolParameter("SendLocalUsername", - "Send the local username for SecurityTypes "+ - "such as Plain rather than prompting", - true); + "Send the local username for SecurityTypes "+ + "such as Plain rather than prompting", + true); public static StringParameter passwordFile = new StringParameter("PasswordFile", - "Password file for VNC authentication", - ""); + "Password file for VNC authentication", + ""); public static AliasParameter passwd = new AliasParameter("passwd", - "Alias for PasswordFile", - passwordFile); + "Alias for PasswordFile", + passwordFile); public static BoolParameter autoSelect = new BoolParameter("AutoSelect", - "Auto select pixel format and encoding", - true); + "Auto select pixel format and encoding", + true); public static BoolParameter fullColor = new BoolParameter("FullColor", - "Use full color - otherwise 6-bit colour is "+ - "used until AutoSelect decides the link is "+ - "fast enough", - true); + "Use full color - otherwise 6-bit colour is used "+ + "until AutoSelect decides the link is fast enough", + true); public static AliasParameter fullColorAlias = new AliasParameter("FullColour", - "Alias for FullColor", - Parameters.fullColor); + "Alias for FullColor", + Parameters.fullColor); public static IntParameter lowColorLevel = new IntParameter("LowColorLevel", - "Color level to use on slow connections. "+ - "0 = Very Low (8 colors), 1 = Low (64 colors), "+ - "2 = Medium (256 colors)", - 2); + "Color level to use on slow connections. "+ + "0 = Very Low (8 colors), 1 = Low (64 colors), "+ + "2 = Medium (256 colors)", + 2); public static AliasParameter lowColorLevelAlias = new AliasParameter("LowColourLevel", - "Alias for LowColorLevel", - lowColorLevel); + "Alias for LowColorLevel", + lowColorLevel); public static StringParameter preferredEncoding = new StringParameter("PreferredEncoding", - "Preferred encoding to use (Tight, ZRLE, "+ - "hextile or raw) - implies AutoSelect=0", - "Tight"); + "Preferred encoding to use (Tight, ZRLE, "+ + "hextile or raw) - implies AutoSelect=0", + "Tight"); + + public static BoolParameter remoteResize + = new BoolParameter("RemoteResize", + "Dynamically resize the remote desktop size as "+ + "the size of the local client window changes. "+ + "(Does not work with all servers)", + true); public static BoolParameter viewOnly = new BoolParameter("ViewOnly", - "Don't send any mouse or keyboard events to "+ - "the server", - false); + "Don't send any mouse or keyboard events to the server", + false); public static BoolParameter shared = new BoolParameter("Shared", - "Don't disconnect other viewers upon "+ - "connection - share the desktop instead", - false); + "Don't disconnect other viewers upon "+ + "connection - share the desktop instead", + false); + + public static BoolParameter maximize + = new BoolParameter("Maximize", + "Maximize viewer window", + false); public static BoolParameter fullScreen = new BoolParameter("FullScreen", - "Full Screen Mode", - false); + "Full Screen Mode", + false); public static BoolParameter fullScreenAllMonitors = new BoolParameter("FullScreenAllMonitors", - "Enable full screen over all monitors", - true); + "Enable full screen over all monitors", + true); public static BoolParameter acceptClipboard = new BoolParameter("AcceptClipboard", - "Accept clipboard changes from the server", - true); + "Accept clipboard changes from the server", + true); public static BoolParameter sendClipboard = new BoolParameter("SendClipboard", - "Send clipboard changes to the server", - true); + "Send clipboard changes to the server", + true); public static IntParameter maxCutText = new IntParameter("MaxCutText", - "Maximum permitted length of an outgoing clipboard update", - 262144); + "Maximum permitted length of an outgoing clipboard update", + 262144); public static StringParameter menuKey = new StringParameter("MenuKey", - "The key which brings up the popup menu", - "F8"); + "The key which brings up the popup menu", + "F8"); public static StringParameter desktopSize = new StringParameter("DesktopSize", - "Reconfigure desktop size on the server on "+ - "connect (if possible)", ""); + "Reconfigure desktop size on the server on connect (if possible)", + ""); public static BoolParameter listenMode = new BoolParameter("listen", - "Listen for connections from VNC servers", - false); + "Listen for connections from VNC servers", + false); public static StringParameter scalingFactor = new StringParameter("ScalingFactor", - "Reduce or enlarge the remote desktop image. "+ - "The value is interpreted as a scaling factor "+ - "in percent. If the parameter is set to "+ - "\"Auto\", then automatic scaling is "+ - "performed. Auto-scaling tries to choose a "+ - "scaling factor in such a way that the whole "+ - "remote desktop will fit on the local screen. "+ - "If the parameter is set to \"FixedRatio\", "+ - "then automatic scaling is performed, but the "+ - "original aspect ratio is preserved.", - "100"); + "Reduce or enlarge the remote desktop image. "+ + "The value is interpreted as a scaling factor "+ + "in percent. If the parameter is set to "+ + "\"Auto\", then automatic scaling is "+ + "performed. Auto-scaling tries to choose a "+ + "scaling factor in such a way that the whole "+ + "remote desktop will fit on the local screen. "+ + "If the parameter is set to \"FixedRatio\", "+ + "then automatic scaling is performed, but the "+ + "original aspect ratio is preserved.", + "100"); public static BoolParameter alwaysShowServerDialog = new BoolParameter("AlwaysShowServerDialog", - "Always show the server dialog even if a server "+ - "has been specified in an applet parameter or on "+ - "the command line", - false); + "Always show the server dialog even if a server has been "+ + "specified in an applet parameter or on the command line", + false); public static StringParameter vncServerName = new StringParameter("Server", - "The VNC server <host>[:<dpyNum>] or "+ - "<host>::<port>", - ""); - - /* - public static IntParameter vncServerPort - = new IntParameter("Port", - "The VNC server's port number, assuming it is on "+ - "the host from which the applet was downloaded", - 0); - */ + "The VNC server <host>[:<dpyNum>] or <host>::<port>", + ""); public static BoolParameter acceptBell = new BoolParameter("AcceptBell", - "Produce a system beep when requested to by the server.", - true); + "Produce a system beep when requested to by the server.", + true); public static StringParameter via = new StringParameter("Via", @@ -271,28 +270,26 @@ public class Parameters { public static BoolParameter customCompressLevel = new BoolParameter("CustomCompressLevel", - "Use custom compression level. "+ - "Default if CompressLevel is specified.", - false); + "Use custom compression level. Default if CompressLevel is specified.", + false); public static IntParameter compressLevel = new IntParameter("CompressLevel", - "Use specified compression level "+ - "0 = Low, 6 = High", - 1); + "Use specified compression level. 0 = Low, 6 = High", + 1); public static BoolParameter noJpeg = new BoolParameter("NoJPEG", - "Disable lossy JPEG compression in Tight encoding.", - false); + "Disable lossy JPEG compression in Tight encoding.", + false); public static IntParameter qualityLevel = new IntParameter("QualityLevel", - "JPEG quality level. "+ - "0 = Low, 9 = High", - 8); + "JPEG quality level. 0 = Low, 9 = High", + 8); - private static final String IDENTIFIER_STRING = "TigerVNC Configuration file Version 1.0"; + private static final String IDENTIFIER_STRING + = "TigerVNC Configuration file Version 1.0"; static VoidParameter[] parameterArray = { CSecurityTLS.X509CA, @@ -306,16 +303,17 @@ public class Parameters { compressLevel, noJpeg, qualityLevel, + maximize, fullScreen, fullScreenAllMonitors, desktopSize, + remoteResize, viewOnly, shared, acceptClipboard, sendClipboard, menuKey, noLionFS, - useLocalCursor, sendLocalUsername, maxCutText, scalingFactor, @@ -333,7 +331,7 @@ public class Parameters { static LogWriter vlog = new LogWriter("Parameters"); public static void saveViewerParameters(String filename, String servername) { - + // Write to the registry or a predefined file if no filename was specified. String filepath; if (filename == null || filename.isEmpty()) { @@ -349,13 +347,13 @@ public class Parameters { } else { filepath = filename; } - + /* Write parameters to file */ File f = new File(filepath); if (f.exists() && !f.canWrite()) throw new Exception(String.format("Failed to write configuration file,"+ "can't open %s", filepath)); - + PrintWriter pw = null; try { pw = new PrintWriter(f, "UTF-8"); @@ -365,12 +363,12 @@ public class Parameters { pw.println(IDENTIFIER_STRING); pw.println(""); - + if (servername != null && !servername.isEmpty()) { pw.println(String.format("ServerName=%s\n", servername)); updateConnHistory(servername); } - + for (int i = 0; i < parameterArray.length; i++) { if (parameterArray[i] instanceof StringParameter) { //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName())) @@ -432,7 +430,7 @@ public class Parameters { int lineNr = 0; while (line != null) { - + // Read the next line try { line = reader.readLine(); @@ -449,7 +447,7 @@ public class Parameters { if(line.equals(IDENTIFIER_STRING)) continue; else - throw new Exception(String.format(new String("Configuration file %s is in an invalid format"), filename)); + throw new Exception(String.format("Configuration file %s is in an invalid format", filename)); } // Skip empty lines and comments @@ -551,13 +549,13 @@ public class Parameters { } public static String loadFromReg() { - + String hKey = "global"; - + String servername = UserPreferences.get(hKey, "ServerName"); if (servername == null) servername = ""; - + for (int i = 0; i < parameterArray.length; i++) { if (parameterArray[i] instanceof StringParameter) { if (UserPreferences.get(hKey, parameterArray[i].getName()) != null) { @@ -582,7 +580,7 @@ public class Parameters { parameterArray[i].getName())); } } - + return servername; } diff --git a/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java b/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java index 8fc2760b..564eb8eb 100644 --- a/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java +++ b/java/com/tigervnc/vncviewer/PlatformPixelBuffer.java @@ -24,84 +24,38 @@ import java.awt.image.*; import java.nio.ByteOrder; import com.tigervnc.rfb.*; +import com.tigervnc.rfb.Point; -abstract public class PlatformPixelBuffer extends PixelBuffer +public class PlatformPixelBuffer extends FullFramePixelBuffer { - public PlatformPixelBuffer(PixelFormat pf, int w, int h, DesktopWindow desktop_) { - desktop = desktop_; - PixelFormat nativePF = getNativePF(); - if (nativePF.depth > pf.depth) { - setPF(pf); - } else { - setPF(nativePF); - } - resize(w, h); + public PlatformPixelBuffer(PixelFormat pf, + int w, int h, + WritableRaster data) + { + super(pf, w, h, data); + damage = new Rect(0, 0, w, h); } - // resize() resizes the image, preserving the image data where possible. - abstract public void resize(int w, int h); - - public PixelFormat getNativePF() { - PixelFormat pf; - cm = tk.getColorModel(); - if (cm.getColorSpace().getType() == java.awt.color.ColorSpace.TYPE_RGB) { - int depth = ((cm.getPixelSize() > 24) ? 24 : cm.getPixelSize()); - int bpp = (depth > 16 ? 32 : (depth > 8 ? 16 : 8)); - ByteOrder byteOrder = ByteOrder.nativeOrder(); - boolean bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN ? true : false); - boolean trueColour = (depth > 8 ? true : false); - int redShift = cm.getComponentSize()[0] + cm.getComponentSize()[1]; - int greenShift = cm.getComponentSize()[0]; - int blueShift = 0; - pf = new PixelFormat(bpp, depth, bigEndian, trueColour, - (depth > 8 ? 0xff : 0), - (depth > 8 ? 0xff : 0), - (depth > 8 ? 0xff : 0), - (depth > 8 ? redShift : 0), - (depth > 8 ? greenShift : 0), - (depth > 8 ? blueShift : 0)); - } else { - pf = new PixelFormat(8, 8, false, false, 7, 7, 3, 0, 3, 6); + public void commitBufferRW(Rect r) + { + super.commitBufferRW(r); + synchronized(damage) { + Rect n = damage.union_boundary(r); + damage.setXYWH(n.tl.x, n.tl.y, n.width(), n.height()); } - vlog.debug("Native pixel format is "+pf.print()); - return pf; } - abstract public void imageRect(int x, int y, int w, int h, Object pix); + public Rect getDamage() { + Rect r = new Rect(); - // setColourMapEntries() changes some of the entries in the colourmap. - // However these settings won't take effect until updateColourMap() is - // called. This is because getting java to recalculate its internal - // translation table and redraw the screen is expensive. - - public void setColourMapEntries(int firstColour, int nColours_, - int[] rgbs) { - nColours = nColours_; - reds = new byte[nColours]; - blues = new byte[nColours]; - greens = new byte[nColours]; - for (int i = 0; i < nColours; i++) { - reds[firstColour+i] = (byte)(rgbs[i*3] >> 8); - greens[firstColour+i] = (byte)(rgbs[i*3+1] >> 8); - blues[firstColour+i] = (byte)(rgbs[i*3+2] >> 8); + synchronized(damage) { + r.setXYWH(damage.tl.x, damage.tl.y, damage.width(), damage.height()); + damage.clear(); } - } - public void updateColourMap() { - cm = new IndexColorModel(8, nColours, reds, greens, blues); + return r; } - protected static Toolkit tk = Toolkit.getDefaultToolkit(); - - abstract public Image getImage(); - - protected Image image; - - int nColours; - byte[] reds; - byte[] greens; - byte[] blues; + protected Rect damage; - DesktopWindow desktop; - static LogWriter vlog = new LogWriter("PlatformPixelBuffer"); } diff --git a/java/com/tigervnc/vncviewer/Viewport.java b/java/com/tigervnc/vncviewer/Viewport.java index 3a5fb54c..bf07d2d5 100644 --- a/java/com/tigervnc/vncviewer/Viewport.java +++ b/java/com/tigervnc/vncviewer/Viewport.java @@ -1,6 +1,8 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. - * Copyright (C) 2011-2015 Brian P. Hinz - * Copyright (C) 2012-2013 D. R. Commander. 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-2014 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 @@ -18,201 +20,443 @@ * USA. */ -package com.tigervnc.vncviewer; +// +// 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.Dimension; -import java.awt.Event; -import java.awt.GraphicsConfiguration; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.Image; -import java.awt.Insets; -import java.awt.Window; -import java.lang.reflect.*; +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 com.tigervnc.rfb.*; -import java.lang.Exception; -import java.awt.Rectangle; +import javax.imageio.*; +import java.io.*; -import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; -import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; -import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; -import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; +import com.tigervnc.rfb.*; +import com.tigervnc.rfb.Cursor; +import com.tigervnc.rfb.Point; import static com.tigervnc.vncviewer.Parameters.*; -public class Viewport extends JFrame -{ - public Viewport(String name, CConn cc_) { +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_; - setTitle(name+" - TigerVNC"); - setFocusable(false); - setFocusTraversalKeysEnabled(false); - if (!VncViewer.os.startsWith("mac os x")) - setIconImage(VncViewer.frameIcon); - UIManager.getDefaults().put("ScrollPane.ancestorInputMap", - new UIDefaults.LazyInputMap(new Object[]{})); - sp = new JScrollPane(); - sp.getViewport().setBackground(Color.BLACK); - sp.setBorder(BorderFactory.createEmptyBorder(0,0,0,0)); - getContentPane().add(sp); - if (VncViewer.os.startsWith("mac os x")) { - if (!noLionFS.getValue()) - enableLionFS(); - } - addWindowFocusListener(new WindowAdapter() { - public void windowGainedFocus(WindowEvent e) { - if (isVisible()) - sp.getViewport().getView().requestFocusInWindow(); + 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 windowLostFocus(WindowEvent e) { + public void focusLost(FocusEvent e) { cc.releaseDownKeys(); } }); - addWindowListener(new WindowAdapter() { - public void windowClosing(WindowEvent e) { - cc.close(); + 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)createImage(frameBuffer.width(), frameBuffer.height()); + image.getRaster().setDataElements(r.tl.x, r.tl.y, frameBuffer.getBuffer(r)); + 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()); } - }); - addComponentListener(new ComponentAdapter() { - public void componentResized(ComponentEvent e) { - String scaleString = scalingFactor.getValue(); - if (scaleString.equalsIgnoreCase("Auto") || - scaleString.equalsIgnoreCase("FixedRatio")) { - if ((sp.getSize().width != cc.desktop.scaledWidth) || - (sp.getSize().height != cc.desktop.scaledHeight)) { - cc.desktop.setScaledSize(); - sp.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER); - sp.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_NEVER); - sp.validate(); - if (getExtendedState() != JFrame.MAXIMIZED_BOTH && - !fullScreen.getValue()) { - sp.setSize(new Dimension(cc.desktop.scaledWidth, - cc.desktop.scaledHeight)); - int w = cc.desktop.scaledWidth + getInsets().left + - getInsets().right; - int h = cc.desktop.scaledHeight + getInsets().top + - getInsets().bottom; - if (scaleString.equalsIgnoreCase("FixedRatio")) - setSize(w, h); - } + } + } + + 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, byte[] mask) + { + + int mask_len = ((width+7)/8) * height; + int i; + + for (i = 0; i < mask_len; i++) + if ((mask[i] & 0xff) != 0) break; + + if ((i == mask_len) && 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 { + ByteBuffer buffer = ByteBuffer.allocate(width*height*4); + ByteBuffer in, o, m; + int m_width; + + PixelFormat pf; + + pf = cc.cp.pf(); + + in = (ByteBuffer)ByteBuffer.wrap(data).mark(); + o = (ByteBuffer)buffer.duplicate().mark(); + m = ByteBuffer.wrap(mask); + m_width = (width+7)/8; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // NOTE: BufferedImage needs ARGB, rather than RGBA + if ((m.get((m_width*y)+(x/8)) & 0x80>>(x%8)) != 0) + o.put((byte)255); + else + o.put((byte)0); + + pf.rgbFromBuffer(o, in.duplicate(), 1); + + o.position(o.reset().position() + 4).mark(); + in.position(in.position() + pf.bpp/8); } - } else { - sp.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); - sp.setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_AS_NEEDED); - sp.validate(); - } - if (cc.desktop.cursor != null) { - Cursor cursor = cc.desktop.cursor; - cc.setCursor(cursor.width(),cursor.height(),cursor.hotspot, - cursor.data, cursor.mask); } + + IntBuffer rgb = + IntBuffer.allocate(width*height).put(buffer.asIntBuffer()); + cursor = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + cursor.setRGB(0, 0, width, height, rgb.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 setName(String name) { - setTitle(name + "- TigerVNC"); + 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 = null; + } + setScaledSize(w, h); } - boolean lionFSSupported() { return canDoLionFS; } + private PlatformPixelBuffer createFramebuffer(PixelFormat pf, int w, int h) + { + PlatformPixelBuffer fb; - void enableLionFS() { - try { - String version = System.getProperty("os.version"); - int firstDot = version.indexOf('.'); - int lastDot = version.lastIndexOf('.'); - if (lastDot > firstDot && lastDot >= 0) { - version = version.substring(0, version.indexOf('.', firstDot + 1)); - } - double v = Double.parseDouble(version); - if (v < 10.7) - throw new Exception("Operating system version is " + v); - - Class fsuClass = Class.forName("com.apple.eawt.FullScreenUtilities"); - Class argClasses[] = new Class[]{Window.class, Boolean.TYPE}; - Method setWindowCanFullScreen = - fsuClass.getMethod("setWindowCanFullScreen", argClasses); - setWindowCanFullScreen.invoke(fsuClass, this, true); - - canDoLionFS = true; - } catch (Exception e) { - vlog.debug("Could not enable OS X 10.7+ full-screen mode: " + - e.getMessage()); - } + fb = new JavaPixelBuffer(w, h); + + return fb; + } + + // + // Callback methods to determine geometry of our Component. + // + + public Dimension getPreferredSize() { + return new Dimension(scaledWidth, scaledHeight); } - public void toggleLionFS() { - try { - Class appClass = Class.forName("com.apple.eawt.Application"); - Method getApplication = appClass.getMethod("getApplication", - (Class[])null); - Object app = getApplication.invoke(appClass); - Method requestToggleFullScreen = - appClass.getMethod("requestToggleFullScreen", Window.class); - requestToggleFullScreen.invoke(app, this); - } catch (Exception e) { - vlog.debug("Could not toggle OS X 10.7+ full-screen mode: " + - e.getMessage()); + 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(); } - public JViewport getViewport() { - return sp.getViewport(); + // 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); } - public void setGeometry(int x, int y, int w, int h) { - pack(); - if (!fullScreen.getValue()) - setLocation(x, y); + // 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) {} - public Dimension getScreenSize() { - return getScreenBounds().getSize(); + // MouseWheel callback function + private void mouseWheelCB(MouseWheelEvent e) { + if (!viewOnly.getValue()) + cc.writeWheelEvent(e); } - public Rectangle getScreenBounds() { - GraphicsEnvironment ge = - GraphicsEnvironment.getLocalGraphicsEnvironment(); - Rectangle r = new Rectangle(); - setMaximizedBounds(null); - if (fullScreenAllMonitors.getValue()) { - for (GraphicsDevice gd : ge.getScreenDevices()) - for (GraphicsConfiguration gc : gd.getConfigurations()) - r = r.union(gc.getBounds()); - Rectangle mb = new Rectangle(r); - mb.grow(getInsets().left, getInsets().bottom); - setMaximizedBounds(mb); + 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 { - GraphicsDevice gd = ge.getDefaultScreenDevice(); - GraphicsConfiguration gc = gd.getDefaultConfiguration(); - r = gc.getBounds(); + 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; } - return r; + if (scaledWidth != getWidth() || scaledHeight != getHeight()) + setSize(new Dimension(scaledWidth, scaledHeight)); } - public static Window getFullScreenWindow() { - GraphicsEnvironment ge = - GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice gd = ge.getDefaultScreenDevice(); - Window fullScreenWindow = gd.getFullScreenWindow(); - return fullScreenWindow; + 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 static void setFullScreenWindow(Window fullScreenWindow) { - GraphicsEnvironment ge = - GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice gd = ge.getDefaultScreenDevice(); - if (gd.isFullScreenSupported()) - gd.setFullScreenWindow(fullScreenWindow); + 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(); + } + */ } - CConn cc; - JScrollPane sp; - boolean canDoLionFS; - static LogWriter vlog = new LogWriter("Viewport"); -} + // 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(); + +} diff --git a/java/com/tigervnc/vncviewer/VncViewer.java b/java/com/tigervnc/vncviewer/VncViewer.java index a3daef31..f5b31775 100644 --- a/java/com/tigervnc/vncviewer/VncViewer.java +++ b/java/com/tigervnc/vncviewer/VncViewer.java @@ -47,6 +47,7 @@ import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.*; import javax.swing.*; +import javax.swing.border.*; import javax.swing.plaf.FontUIResource; import javax.swing.SwingUtilities; import javax.swing.UIManager.*; @@ -60,10 +61,11 @@ import static com.tigervnc.vncviewer.Parameters.*; public class VncViewer extends javax.swing.JApplet implements Runnable, ActionListener { - public static final String aboutText = new String("TigerVNC Java Viewer v%s (%s)%n"+ - "Built on %s at %s%n"+ - "Copyright (C) 1999-2016 TigerVNC Team and many others (see README.txt)%n"+ - "See http://www.tigervnc.org for information on TigerVNC."); + public static final String aboutText = + new String("TigerVNC Java Viewer v%s (%s)%n"+ + "Built on %s at %s%n"+ + "Copyright (C) 1999-2016 TigerVNC Team and many others (see README.txt)%n"+ + "See http://www.tigervnc.org for information on TigerVNC."); public static String version = null; public static String build = null; @@ -79,6 +81,7 @@ public class VncViewer extends javax.swing.JApplet VncViewer.class.getResourceAsStream("timestamp"); public static final String os = System.getProperty("os.name").toLowerCase(); + private static VncViewer applet; public static void setLookAndFeel() { try { @@ -140,8 +143,8 @@ public class VncViewer extends javax.swing.JApplet } public VncViewer() { - //this(new String[0]); - embed.setParam(true); + // Only called in applet mode + this(new String[0]); } public VncViewer(String[] argv) { @@ -311,7 +314,7 @@ public class VncViewer extends javax.swing.JApplet public void appletDragStarted() { embed.setParam(false); - cc.recreateViewport(); + //cc.recreateViewport(); JFrame f = (JFrame)JOptionPane.getFrameForComponent(this); // The default JFrame created by the drag event will be // visible briefly between appletDragStarted and Finished. @@ -333,20 +336,69 @@ public class VncViewer extends javax.swing.JApplet cc.setCloseListener(null); } - public void init() { - vlog.debug("init called"); - Container parent = getParent(); - while (!parent.isFocusCycleRoot()) { - parent = parent.getParent(); + public static void setupEmbeddedFrame(JScrollPane sp) { + InputMap im = sp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); + int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK; + if (im != null) { + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, ctrlAltShiftMask), + "unitScrollUp"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, ctrlAltShiftMask), + "unitScrollDown"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, ctrlAltShiftMask), + "unitScrollLeft"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, ctrlAltShiftMask), + "unitScrollRight"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, ctrlAltShiftMask), + "scrollUp"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, ctrlAltShiftMask), + "scrollDown"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, ctrlAltShiftMask), + "scrollLeft"); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, ctrlAltShiftMask), + "scrollRight"); } - ((Frame)parent).setModalExclusionType(null); - parent.setFocusable(false); - parent.setFocusTraversalKeysEnabled(false); + applet.getContentPane().removeAll(); + applet.getContentPane().add(sp); + applet.validate(); + } + + public void init() { + // Called right after zero-arg constructor in applet mode setLookAndFeel(); setBackground(Color.white); + applet = this; + String servername = loadAppletParameters(applet); + vncServerName.setParam(servername); + alwaysShowServerDialog.setParam(false); + if (embed.getValue()) { + fullScreen.setParam(false); + remoteResize.setParam(false); + maximize.setParam(false); + scalingFactor.setParam("100"); + } + setFocusTraversalKeysEnabled(false); + addFocusListener(new FocusAdapter() { + public void focusGained(FocusEvent e) { + if (cc != null && cc.desktop != null) + cc.desktop.viewport.requestFocusInWindow(); + } + }); + Frame frame = (Frame)getFocusCycleRootAncestor(); + frame.setFocusTraversalKeysEnabled(false); + frame.addWindowListener(new WindowAdapter() { + // Transfer focus to scrollpane when browser receives it + public void windowActivated(WindowEvent e) { + if (cc != null && cc.desktop != null) + cc.desktop.viewport.requestFocusInWindow(); + } + public void windowDeactivated(WindowEvent e) { + if (cc != null) + cc.releaseDownKeys(); + } + }); } - private void getTimestamp() { + private static void getTimestamp() { if (version == null || build == null) { try { Manifest manifest = new Manifest(timestamp); @@ -369,9 +421,9 @@ public class VncViewer extends javax.swing.JApplet pkgTime = attributes.getValue("Package-Time"); } catch (java.lang.Exception e) { } - Window fullScreenWindow = Viewport.getFullScreenWindow(); + Window fullScreenWindow = DesktopWindow.getFullScreenWindow(); if (fullScreenWindow != null) - Viewport.setFullScreenWindow(null); + DesktopWindow.setFullScreenWindow(null); String msg = String.format(VncViewer.aboutText, VncViewer.version, VncViewer.build, VncViewer.buildDate, VncViewer.buildTime); @@ -384,20 +436,10 @@ public class VncViewer extends javax.swing.JApplet dlg.setAlwaysOnTop(true); dlg.setVisible(true); if (fullScreenWindow != null) - Viewport.setFullScreenWindow(fullScreenWindow); + DesktopWindow.setFullScreenWindow(fullScreenWindow); } public void start() { - vlog.debug("start called"); - getTimestamp(); - if (embed.getValue()) { - setupEmbeddedFrame(); - alwaysShowServerDialog.setParam(false); - String servername = loadAppletParameters(this); - vncServerName.setParam(servername); - fullScreen.setParam(false); - scalingFactor.setParam("100"); - } thread = new Thread(this); thread.start(); } @@ -409,41 +451,6 @@ public class VncViewer extends javax.swing.JApplet System.exit(n); } - private void setupEmbeddedFrame() { - UIManager.getDefaults().put("ScrollPane.ancestorInputMap", - new UIDefaults.LazyInputMap(new Object[]{})); - sp = new JScrollPane(); - sp.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); - sp.getViewport().setBackground(Color.BLACK); - InputMap im = sp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - int ctrlAltShiftMask = Event.SHIFT_MASK | Event.CTRL_MASK | Event.ALT_MASK; - if (im != null) { - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, ctrlAltShiftMask), - "unitScrollUp"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, ctrlAltShiftMask), - "unitScrollDown"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, ctrlAltShiftMask), - "unitScrollLeft"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, ctrlAltShiftMask), - "unitScrollRight"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, ctrlAltShiftMask), - "scrollUp"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, ctrlAltShiftMask), - "scrollDown"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, ctrlAltShiftMask), - "scrollLeft"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, ctrlAltShiftMask), - "scrollRight"); - } - sp.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - sp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - add(sp); - } - - public static JViewport getViewport() { - return sp.getViewport(); - } - // If "Reconnect" button is pressed public void actionPerformed(ActionEvent e) { getContentPane().removeAll(); @@ -524,7 +531,7 @@ public class VncViewer extends javax.swing.JApplet if (cc == null || !cc.shuttingDown) { reportException(e); if (cc != null) - cc.deleteWindow(); + cc.close(); } else if (embed.getValue()) { reportException(new java.lang.Exception("Connection closed")); exit(0); @@ -534,7 +541,6 @@ public class VncViewer extends javax.swing.JApplet } public static CConn cc; - private static JScrollPane sp; public static StringParameter config = new StringParameter("Config", "Specifies a configuration file to load.", null); |