From 985d0eb0657351c7bf01db3d1d30012f35c153de Mon Sep 17 00:00:00 2001 From: "Brian P. Hinz" Date: Sat, 10 Dec 2016 19:28:42 -0500 Subject: [PATCH] Complete rewrite of pixel buffer & decoder implementation. Adds multi-threading, more robust support for different pixel formats, and several new runtime options. --- java/com/tigervnc/rfb/CopyRectDecoder.java | 44 + java/com/tigervnc/rfb/Cursor.java | 9 +- java/com/tigervnc/rfb/DecodeManager.java | 386 +++++++ java/com/tigervnc/rfb/Decoder.java | 88 +- .../tigervnc/rfb/FullFramePixelBuffer.java | 54 + java/com/tigervnc/rfb/HextileDecoder.java | 168 ++- java/com/tigervnc/rfb/JpegDecompressor.java | 53 + java/com/tigervnc/rfb/ManagedPixelBuffer.java | 34 +- .../tigervnc/rfb/ModifiablePixelBuffer.java | 267 +++++ java/com/tigervnc/rfb/PixelBuffer.java | 143 +-- java/com/tigervnc/rfb/PixelFormat.java | 491 ++++++-- java/com/tigervnc/rfb/RREDecoder.java | 83 +- java/com/tigervnc/rfb/RawDecoder.java | 33 +- java/com/tigervnc/rfb/Region.java | 102 ++ java/com/tigervnc/rfb/TightDecoder.java | 630 +++++++--- java/com/tigervnc/rfb/ZRLEDecoder.java | 210 +++- .../com/tigervnc/vncviewer/BIPixelBuffer.java | 141 --- java/com/tigervnc/vncviewer/CConn.java | 445 +++----- .../com/tigervnc/vncviewer/DesktopWindow.java | 1013 +++++++++-------- java/com/tigervnc/vncviewer/Dialog.java | 6 - java/com/tigervnc/vncviewer/F8Menu.java | 43 +- .../tigervnc/vncviewer/JavaPixelBuffer.java | 59 + .../com/tigervnc/vncviewer/OptionsDialog.java | 147 ++- java/com/tigervnc/vncviewer/Parameters.java | 220 ++-- .../vncviewer/PlatformPixelBuffer.java | 88 +- java/com/tigervnc/vncviewer/Viewport.java | 558 ++++++--- java/com/tigervnc/vncviewer/VncViewer.java | 138 +-- 27 files changed, 3802 insertions(+), 1851 deletions(-) create mode 100644 java/com/tigervnc/rfb/CopyRectDecoder.java create mode 100644 java/com/tigervnc/rfb/DecodeManager.java create mode 100644 java/com/tigervnc/rfb/FullFramePixelBuffer.java create mode 100644 java/com/tigervnc/rfb/JpegDecompressor.java create mode 100644 java/com/tigervnc/rfb/ModifiablePixelBuffer.java create mode 100644 java/com/tigervnc/rfb/Region.java delete mode 100644 java/com/tigervnc/vncviewer/BIPixelBuffer.java create mode 100644 java/com/tigervnc/vncviewer/JavaPixelBuffer.java diff --git a/java/com/tigervnc/rfb/CopyRectDecoder.java b/java/com/tigervnc/rfb/CopyRectDecoder.java new file mode 100644 index 00000000..a4298fd5 --- /dev/null +++ b/java/com/tigervnc/rfb/CopyRectDecoder.java @@ -0,0 +1,44 @@ +/* Copyright 2014 Pierre Ossman for Cendio AB + * Copyright 2016 Brian P. Hinz + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +package com.tigervnc.rfb; + +import com.tigervnc.rdr.*; + +public class CopyRectDecoder extends Decoder { + + public CopyRectDecoder() { super(DecoderFlags.DecoderPlain); } + + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) + { + os.copyBytes(is, 4); + } + + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + MemInStream is = new MemInStream((byte[])buffer, 0, buflen); + int srcX = is.readU16(); + int srcY = is.readU16(); + pb.copyRect(r, new Point(r.tl.x-srcX, r.tl.y-srcY)); + } + +} diff --git a/java/com/tigervnc/rfb/Cursor.java b/java/com/tigervnc/rfb/Cursor.java index 78aa0fb2..05122ae5 100644 --- a/java/com/tigervnc/rfb/Cursor.java +++ b/java/com/tigervnc/rfb/Cursor.java @@ -20,11 +20,18 @@ package com.tigervnc.rfb; public class Cursor extends ManagedPixelBuffer { + public Cursor(PixelFormat pf, int w, int h) { + super(pf, w, h); + hotspot = new Point(0, 0); + } + public void setSize(int w, int h) { + int oldMaskLen = maskLen(); super.setSize(w, h); - if (mask == null || mask.length < maskLen()) + if (mask == null || maskLen() > oldMaskLen) mask = new byte[maskLen()]; } + public int maskLen() { return (width() + 7) / 8 * height(); } public Point hotspot; diff --git a/java/com/tigervnc/rfb/DecodeManager.java b/java/com/tigervnc/rfb/DecodeManager.java new file mode 100644 index 00000000..9e254ad2 --- /dev/null +++ b/java/com/tigervnc/rfb/DecodeManager.java @@ -0,0 +1,386 @@ +/* Copyright 2015 Pierre Ossman for Cendio AB + * Copyright 2016 Brian P. Hinz + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +package com.tigervnc.rfb; + +import java.lang.Runtime; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.locks.*; + +import com.tigervnc.rdr.*; +import com.tigervnc.rdr.Exception; + +import static com.tigervnc.rfb.Decoder.DecoderFlags.*; + +public class DecodeManager { + + static LogWriter vlog = new LogWriter("DecodeManager"); + + public DecodeManager(CConnection conn) { + int cpuCount; + + this.conn = conn; threadException = null; + decoders = new Decoder[Encodings.encodingMax+1]; + + queueMutex = new ReentrantLock(); + producerCond = queueMutex.newCondition(); + consumerCond = queueMutex.newCondition(); + + //cpuCount = 1; + cpuCount = Runtime.getRuntime().availableProcessors(); + if (cpuCount == 0) { + vlog.error("Unable to determine the number of CPU cores on this system"); + cpuCount = 1; + } else { + vlog.info("Detected "+cpuCount+" CPU core(s)"); + // No point creating more threads than this, they'll just end up + // wasting CPU fighting for locks + if (cpuCount > 4) + cpuCount = 4; + // The overhead of threading is small, but not small enough to + // ignore on single CPU systems + if (cpuCount == 1) + vlog.info("Decoding data on main thread"); + else + vlog.info("Creating "+cpuCount+" decoder thread(s)"); + } + + freeBuffers = new ArrayDeque(cpuCount*2); + workQueue = new ArrayDeque(cpuCount); + threads = new ArrayList(cpuCount); + while (cpuCount-- > 0) { + // Twice as many possible entries in the queue as there + // are worker threads to make sure they don't stall + try { + freeBuffers.addLast(new MemOutStream()); + freeBuffers.addLast(new MemOutStream()); + + threads.add(new DecodeThread(this)); + } catch (IllegalStateException e) { } + } + + } + + public void decodeRect(Rect r, int encoding, + ModifiablePixelBuffer pb) + { + Decoder decoder; + MemOutStream bufferStream; + + QueueEntry entry; + + assert(pb != null); + + if (!Decoder.supported(encoding)) { + vlog.error("Unknown encoding " + encoding); + throw new Exception("Unknown encoding"); + } + + if (decoders[encoding] == null) { + decoders[encoding] = Decoder.createDecoder(encoding); + if (decoders[encoding] == null) { + vlog.error("Unknown encoding " + encoding); + throw new Exception("Unknown encoding"); + } + } + + decoder = decoders[encoding]; + + // Fast path for single CPU machines to avoid the context + // switching overhead + if (threads.size() == 1) { + bufferStream = freeBuffers.getFirst(); + bufferStream.clear(); + decoder.readRect(r, conn.getInStream(), conn.cp, bufferStream); + decoder.decodeRect(r, (Object)bufferStream.data(), bufferStream.length(), + conn.cp, pb); + return; + } + + // Wait for an available memory buffer + queueMutex.lock(); + + while (freeBuffers.isEmpty()) + try { + producerCond.await(); + } catch (InterruptedException e) { } + + // Don't pop the buffer in case we throw an exception + // whilst reading + bufferStream = freeBuffers.getFirst(); + + queueMutex.unlock(); + + // First check if any thread has encountered a problem + throwThreadException(); + + // Read the rect + bufferStream.clear(); + decoder.readRect(r, conn.getInStream(), conn.cp, bufferStream); + + // Then try to put it on the queue + entry = new QueueEntry(); + + entry.active = false; + entry.rect = r; + entry.encoding = encoding; + entry.decoder = decoder; + entry.cp = conn.cp; + entry.pb = pb; + entry.bufferStream = bufferStream; + entry.affectedRegion = new Region(r); + + decoder.getAffectedRegion(r, bufferStream.data(), + bufferStream.length(), conn.cp, + entry.affectedRegion); + + // The workers add buffers to the end so it's safe to assume + // the front is still the same buffer + freeBuffers.removeFirst(); + + queueMutex.lock(); + + workQueue.addLast(entry); + + // We only put a single entry on the queue so waking a single + // thread is sufficient + consumerCond.signal(); + + queueMutex.unlock(); + } + + public void flush() + { + queueMutex.lock(); + + while (!workQueue.isEmpty()) + try { + producerCond.await(); + } catch (InterruptedException e) { } + + queueMutex.unlock(); + + throwThreadException(); + } + + private void setThreadException(Exception e) + { + //os::AutoMutex a(queueMutex); + queueMutex.lock(); + + if (threadException == null) + return; + + threadException = + new Exception("Exception on worker thread: "+e.getMessage()); + } + + private void throwThreadException() + { + //os::AutoMutex a(queueMutex); + queueMutex.lock(); + + if (threadException == null) + return; + + Exception e = new Exception(threadException.getMessage()); + + threadException = null; + + throw e; + } + + private class QueueEntry { + + public QueueEntry() { + } + public boolean active; + public Rect rect; + public int encoding; + public Decoder decoder; + public ConnParams cp; + public ModifiablePixelBuffer pb; + public MemOutStream bufferStream; + public Region affectedRegion; + } + + private class DecodeThread implements Runnable { + + public DecodeThread(DecodeManager manager) + { + this.manager = manager; + + stopRequested = false; + + (thread = new Thread(this)).start(); + } + + public void stop() + { + //os::AutoMutex a(manager.queueMutex); + manager.queueMutex.lock(); + + if (!thread.isAlive()) + return; + + stopRequested = true; + + // We can't wake just this thread, so wake everyone + manager.consumerCond.signalAll(); + } + + public void run() + { + manager.queueMutex.lock(); + while (!stopRequested) { + QueueEntry entry; + + // Look for an available entry in the work queue + entry = findEntry(); + if (entry == null) { + // Wait and try again + try { + manager.consumerCond.await(); + } catch (InterruptedException e) { } + continue; + } + + // This is ours now + entry.active = true; + + manager.queueMutex.unlock(); + + // Do the actual decoding + try { + entry.decoder.decodeRect(entry.rect, entry.bufferStream.data(), + entry.bufferStream.length(), + entry.cp, entry.pb); + } catch (com.tigervnc.rdr.Exception e) { + manager.setThreadException(e); + } catch(java.lang.Exception e) { + assert(false); + } + + manager.queueMutex.lock(); + + // Remove the entry from the queue and give back the memory buffer + manager.freeBuffers.add(entry.bufferStream); + manager.workQueue.remove(entry); + entry = null; + + // Wake the main thread in case it is waiting for a memory buffer + manager.producerCond.signal(); + // This rect might have been blocking multiple other rects, so + // wake up every worker thread + if (manager.workQueue.size() > 1) + manager.consumerCond.signalAll(); + } + + manager.queueMutex.unlock(); + } + + protected QueueEntry findEntry() + { + Iterator iter; + Region lockedRegion = new Region(); + + if (manager.workQueue.isEmpty()) + return null; + + if (!manager.workQueue.peek().active) + return manager.workQueue.peek(); + + for (iter = manager.workQueue.iterator(); iter.hasNext();) { + QueueEntry entry; + + Iterator iter2; + + entry = iter.next(); + + // Another thread working on this? + if (entry.active) { + lockedRegion.assign_union(entry.affectedRegion); + continue; + } + + // If this is an ordered decoder then make sure this is the first + // rectangle in the queue for that decoder + if ((entry.decoder.flags & DecoderOrdered) != 0) { + for (iter2 = manager.workQueue.iterator(); iter2.hasNext() && iter2 != iter;) { + if (entry.encoding == (iter2.next()).encoding) { + lockedRegion.assign_union(entry.affectedRegion); + continue; + } + } + } + + // For a partially ordered decoder we must ask the decoder for each + // pair of rectangles. + if ((entry.decoder.flags & DecoderPartiallyOrdered) != 0) { + for (iter2 = manager.workQueue.iterator(); iter2.hasNext() && iter2 != iter;) { + QueueEntry entry2 = iter2.next(); + if (entry.encoding != entry2.encoding) + continue; + if (entry.decoder.doRectsConflict(entry.rect, + entry.bufferStream.data(), + entry.bufferStream.length(), + entry2.rect, + entry2.bufferStream.data(), + entry2.bufferStream.length(), + entry.cp)) + lockedRegion.assign_union(entry.affectedRegion); + continue; + } + } + + // Check overlap with earlier rectangles + if (!lockedRegion.intersect(entry.affectedRegion).is_empty()) { + lockedRegion.assign_union(entry.affectedRegion); + continue; + } + + return entry; + + } + + return null; + } + + private DecodeManager manager; + private boolean stopRequested; + + private Thread thread; + + } + + private CConnection conn; + private Decoder[] decoders; + + private ArrayDeque freeBuffers; + private ArrayDeque workQueue; + + private ReentrantLock queueMutex; + private Condition producerCond; + private Condition consumerCond; + + private List threads; + private com.tigervnc.rdr.Exception threadException; + +} diff --git a/java/com/tigervnc/rfb/Decoder.java b/java/com/tigervnc/rfb/Decoder.java index f0ece0af..6bbed85e 100644 --- a/java/com/tigervnc/rfb/Decoder.java +++ b/java/com/tigervnc/rfb/Decoder.java @@ -18,34 +18,80 @@ package com.tigervnc.rfb; +import com.tigervnc.rdr.*; + abstract public class Decoder { - abstract public void readRect(Rect r, CMsgHandler handler); + public static class DecoderFlags { + // A constant for decoders that don't need anything special + public static int DecoderPlain = 0; + // All rects for this decoder must be handled in order + public static int DecoderOrdered = 1 << 0; + // Only some of the rects must be handled in order, + // see doesRectsConflict() + public static int DecoderPartiallyOrdered = 1 << 1; + }; + + public Decoder(int flags) + { + this.flags = flags; + } + + abstract public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os); + + abstract public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb); + + public void getAffectedRegion(Rect rect, Object buffer, + int buflen, ConnParams cp, + Region region) + { + region.reset(rect); + } + + public boolean doRectsConflict(Rect rectA, Object bufferA, + int buflenA, Rect rectB, + Object bufferB, int buflenB, + ConnParams cp) + { + return false; + } static public boolean supported(int encoding) { -/* - return encoding <= Encodings.encodingMax && createFns[encoding]; -*/ - return (encoding == Encodings.encodingRaw || - encoding == Encodings.encodingRRE || - encoding == Encodings.encodingHextile || - encoding == Encodings.encodingTight || - encoding == Encodings.encodingZRLE); + switch(encoding) { + case Encodings.encodingRaw: + case Encodings.encodingCopyRect: + case Encodings.encodingRRE: + case Encodings.encodingHextile: + case Encodings.encodingZRLE: + case Encodings.encodingTight: + return true; + default: + return false; + } } - static public Decoder createDecoder(int encoding, CMsgReader reader) { -/* - if (encoding <= Encodings.encodingMax && createFns[encoding]) - return (createFns[encoding])(reader); - return 0; -*/ + + static public Decoder createDecoder(int encoding) { switch(encoding) { - case Encodings.encodingRaw: return new RawDecoder(reader); - case Encodings.encodingRRE: return new RREDecoder(reader); - case Encodings.encodingHextile: return new HextileDecoder(reader); - case Encodings.encodingTight: return new TightDecoder(reader); - case Encodings.encodingZRLE: return new ZRLEDecoder(reader); + case Encodings.encodingRaw: + return new RawDecoder(); + case Encodings.encodingCopyRect: + return new CopyRectDecoder(); + case Encodings.encodingRRE: + return new RREDecoder(); + case Encodings.encodingHextile: + return new HextileDecoder(); + case Encodings.encodingZRLE: + return new ZRLEDecoder(); + case Encodings.encodingTight: + return new TightDecoder(); + default: + return null; } - return null; } + + public final int flags; } diff --git a/java/com/tigervnc/rfb/FullFramePixelBuffer.java b/java/com/tigervnc/rfb/FullFramePixelBuffer.java new file mode 100644 index 00000000..1c3b0958 --- /dev/null +++ b/java/com/tigervnc/rfb/FullFramePixelBuffer.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +package com.tigervnc.rfb; + +import java.awt.image.*; + +public class FullFramePixelBuffer extends ModifiablePixelBuffer { + + public FullFramePixelBuffer(PixelFormat pf, int w, int h, + WritableRaster data_) { + super(pf, w, h); + data = data_; + } + + protected FullFramePixelBuffer() {} + + public WritableRaster getBufferRW(Rect r) + { + return data.createWritableChild(r.tl.x, r.tl.y, r.width(), r.height(), + 0, 0, null); + } + + public void commitBufferRW(Rect r) + { + } + + public Raster getBuffer(Rect r) + { + Raster src = + data.createChild(r.tl.x, r.tl.y, r.width(), r.height(), 0, 0, null); + WritableRaster dst = + data.createCompatibleWritableRaster(r.width(), r.height()); + dst.setDataElements(0, 0, src); + return dst; + } + + protected WritableRaster data; +} diff --git a/java/com/tigervnc/rfb/HextileDecoder.java b/java/com/tigervnc/rfb/HextileDecoder.java index 94e91f70..b0744cad 100644 --- a/java/com/tigervnc/rfb/HextileDecoder.java +++ b/java/com/tigervnc/rfb/HextileDecoder.java @@ -18,22 +18,126 @@ package com.tigervnc.rfb; +import java.awt.image.*; +import java.nio.*; +import java.util.Arrays; + import com.tigervnc.rdr.*; public class HextileDecoder extends Decoder { - public HextileDecoder(CMsgReader reader_) { reader = reader_; } + public static final int hextileRaw = (1 << 0); + public static final int hextileBgSpecified = (1 << 1); + public static final int hextileFgSpecified = (1 << 2); + public static final int hextileAnySubrects = (1 << 3); + public static final int hextileSubrectsColoured = (1 << 4); + + public HextileDecoder() { super(DecoderFlags.DecoderPlain); } + + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) + { + Rect t = new Rect(); + int bytesPerPixel; + + bytesPerPixel = cp.pf().bpp/8; + + for (t.tl.y = r.tl.y; t.tl.y < r.br.y; t.tl.y += 16) { + + t.br.y = Math.min(r.br.y, t.tl.y + 16); + + for (t.tl.x = r.tl.x; t.tl.x < r.br.x; t.tl.x += 16) { + int tileType; + + t.br.x = Math.min(r.br.x, t.tl.x + 16); + + tileType = is.readU8() & 0xff; + os.writeU32(tileType); + + if ((tileType & hextileRaw) != 0) { + os.copyBytes(is, t.area() * bytesPerPixel); + continue; + } + + if ((tileType & hextileBgSpecified) != 0) + os.copyBytes(is, bytesPerPixel); + + if ((tileType & hextileFgSpecified) != 0) + os.copyBytes(is, bytesPerPixel); - public void readRect(Rect r, CMsgHandler handler) { - InStream is = reader.getInStream(); - int bytesPerPixel = handler.cp.pf().bpp / 8; - boolean bigEndian = handler.cp.pf().bigEndian; + if ((tileType & hextileAnySubrects) != 0) { + int nSubrects; - int[] buf = reader.getImageBuf(16 * 16 * 4); + nSubrects = is.readU8() & 0xff; + os.writeU32(nSubrects); + if ((tileType & hextileSubrectsColoured) != 0) + os.copyBytes(is, nSubrects * (bytesPerPixel + 2)); + else + os.copyBytes(is, nSubrects * 2); + } + } + } + } + + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + MemInStream is = new MemInStream((byte[])buffer, 0, buflen); + PixelFormat pf = cp.pf(); + switch (pf.bpp) { + case 8: hextileDecode8(r, is, pf, pb); break; + case 16: hextileDecode16(r, is, pf, pb); break; + case 32: hextileDecode32(r, is, pf, pb); break; + } + } + + private void hextileDecode8(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + HEXTILE_DECODE(r, is, pf, pb); + } + + private void hextileDecode16(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + HEXTILE_DECODE(r, is, pf, pb); + } + + private void hextileDecode32(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + HEXTILE_DECODE(r, is, pf, pb); + } + + private static ByteBuffer READ_PIXEL(InStream is, PixelFormat pf) { + ByteBuffer b = ByteBuffer.allocate(4); + switch (pf.bpp) { + case 8: + b.putInt(is.readOpaque8()); + return ByteBuffer.allocate(1).put(b.get(3)); + case 16: + b.putInt(is.readOpaque16()); + return ByteBuffer.allocate(2).put(b.array(), 2, 2); + case 32: + default: + b.putInt(is.readOpaque32()); + return b; + } + } + + private void HEXTILE_DECODE(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { Rect t = new Rect(); - int bg = 0; - int fg = 0; + ByteBuffer bg = ByteBuffer.allocate(pf.bpp/8); + ByteBuffer fg = ByteBuffer.allocate(pf.bpp/8); + ByteBuffer buf = ByteBuffer.allocate(16 * 16 * 4); for (t.tl.y = r.tl.y; t.tl.y < r.br.y; t.tl.y += 16) { @@ -43,59 +147,51 @@ public class HextileDecoder extends Decoder { t.br.x = Math.min(r.br.x, t.tl.x + 16); - int tileType = is.readU8(); + int tileType = is.readU32(); - if ((tileType & Hextile.raw) != 0) { - is.readPixels(buf, t.area(), bytesPerPixel, bigEndian); - handler.imageRect(t, buf); + if ((tileType & hextileRaw) != 0) { + is.readBytes(buf, t.area() * (pf.bpp/8)); + pb.imageRect(pf, t, buf.array()); continue; } - if ((tileType & Hextile.bgSpecified) != 0) - bg = is.readPixel(bytesPerPixel, bigEndian); + if ((tileType & hextileBgSpecified) != 0) + bg = READ_PIXEL(is, pf); int len = t.area(); - int ptr = 0; - while (len-- > 0) buf[ptr++] = bg; + ByteBuffer ptr = buf.duplicate(); + while (len-- > 0) ptr.put(bg.array()); - if ((tileType & Hextile.fgSpecified) != 0) - fg = is.readPixel(bytesPerPixel, bigEndian); + if ((tileType & hextileFgSpecified) != 0) + fg = READ_PIXEL(is, pf); - if ((tileType & Hextile.anySubrects) != 0) { - int nSubrects = is.readU8(); + if ((tileType & hextileAnySubrects) != 0) { + int nSubrects = is.readU32(); for (int i = 0; i < nSubrects; i++) { - if ((tileType & Hextile.subrectsColoured) != 0) - fg = is.readPixel(bytesPerPixel, bigEndian); + if ((tileType & hextileSubrectsColoured) != 0) + fg = READ_PIXEL(is, pf); int xy = is.readU8(); int wh = is.readU8(); -/* - Rect s = new Rect(); - s.tl.x = t.tl.x + ((xy >> 4) & 15); - s.tl.y = t.tl.y + (xy & 15); - s.br.x = s.tl.x + ((wh >> 4) & 15) + 1; - s.br.y = s.tl.y + (wh & 15) + 1; -*/ int x = ((xy >> 4) & 15); int y = (xy & 15); int w = ((wh >> 4) & 15) + 1; int h = (wh & 15) + 1; - ptr = y * t.width() + x; - int rowAdd = t.width() - w; + ptr = buf.duplicate(); + ptr.position((y * t.width() + x)*pf.bpp/8); + int rowAdd = (t.width() - w)*pf.bpp/8; while (h-- > 0) { len = w; - while (len-- > 0) buf[ptr++] = fg; - ptr += rowAdd; + while (len-- > 0) ptr.put(fg.array()); + ptr.position(ptr.position()+Math.min(rowAdd,ptr.remaining())); } } } - handler.imageRect(t, buf); + pb.imageRect(pf, t, buf.array()); } } } - - CMsgReader reader; } diff --git a/java/com/tigervnc/rfb/JpegDecompressor.java b/java/com/tigervnc/rfb/JpegDecompressor.java new file mode 100644 index 00000000..9137847c --- /dev/null +++ b/java/com/tigervnc/rfb/JpegDecompressor.java @@ -0,0 +1,53 @@ +/* Copyright (C) 2016 Brian P. Hinz + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ +package com.tigervnc.rfb; + +import java.awt.image.*; +import java.io.*; +import java.nio.ByteBuffer; +import javax.imageio.*; +import javax.imageio.stream.*; + +public class JpegDecompressor { + + public JpegDecompressor() {} + + public void decompress(ByteBuffer jpegBuf, int jpegBufLen, + WritableRaster buf, Rect r, PixelFormat pf) + { + + byte[] src = new byte[jpegBufLen]; + + jpegBuf.get(src); + try { + BufferedImage image = + ImageIO.read(new MemoryCacheImageInputStream(new ByteArrayInputStream(src))); + ColorModel cm = pf.getColorModel(); + if (cm.isCompatibleRaster(image.getRaster()) && + cm.isCompatibleSampleModel(image.getRaster().getSampleModel())) { + buf.setDataElements(0, 0, image.getRaster()); + } else { + ColorConvertOp converter = pf.getColorConvertOp(cm.getColorSpace()); + converter.filter(image.getRaster(), buf); + } + image.flush(); + } catch (IOException e) { + throw new Exception(e.getMessage()); + } + } +} diff --git a/java/com/tigervnc/rfb/ManagedPixelBuffer.java b/java/com/tigervnc/rfb/ManagedPixelBuffer.java index f947af71..6e14b92e 100644 --- a/java/com/tigervnc/rfb/ManagedPixelBuffer.java +++ b/java/com/tigervnc/rfb/ManagedPixelBuffer.java @@ -18,21 +18,37 @@ package com.tigervnc.rfb; -public class ManagedPixelBuffer extends PixelBuffer { - public void setSize(int w, int h) { - width_ = w; - height_ = h; +public class ManagedPixelBuffer extends FullFramePixelBuffer { + + public ManagedPixelBuffer() { + datasize = 0; checkDataSize(); } - public void setPF(PixelFormat pf) { - super.setPF(pf); + + public ManagedPixelBuffer(PixelFormat pf, int w, int h) + { + super(pf, w, h, null); + datasize = 0; checkDataSize(); } - public int dataLen() { return area(); } + public void setPF(PixelFormat pf) { + format = pf; checkDataSize(); + } + + public void setSize(int w, int h) { + width_ = w; height_ = h; checkDataSize(); + } final void checkDataSize() { - if (data == null || data.length < dataLen()) - data = new int[dataLen()]; + int new_datasize = width_ * height_; + if (datasize < new_datasize) { + vlog.debug("reallocating managed buffer ("+width_+"x"+height_+")"); + if (format != null) + data = PixelFormat.getColorModel(format).createCompatibleWritableRaster(width_, height_); + } } + + protected int datasize; + static LogWriter vlog = new LogWriter("ManagedPixelBuffer"); } diff --git a/java/com/tigervnc/rfb/ModifiablePixelBuffer.java b/java/com/tigervnc/rfb/ModifiablePixelBuffer.java new file mode 100644 index 00000000..bcc559d5 --- /dev/null +++ b/java/com/tigervnc/rfb/ModifiablePixelBuffer.java @@ -0,0 +1,267 @@ +/* Copyright 2016 Brian P. Hinz + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +// -=- Modifiable generic pixel buffer class + +package com.tigervnc.rfb; + +import java.awt.image.*; +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.lang.*; +import java.nio.*; +import java.util.*; + +import static java.awt.image.DataBuffer.*; + +public abstract class ModifiablePixelBuffer extends PixelBuffer +{ + + public ModifiablePixelBuffer(PixelFormat pf, int w, int h) + { + super(pf, w, h); + } + + protected ModifiablePixelBuffer() + { + } + + /////////////////////////////////////////////// + // Access to pixel data + // + + // Get a writeable pointer into the buffer + // Like getBuffer(), the pointer is to the top-left pixel of the + // specified Rect. + public abstract WritableRaster getBufferRW(Rect r); + // Commit the modified contents + // Ensures that the changes to the specified Rect is properly + // stored away and any temporary buffers are freed. The Rect given + // here needs to match the Rect given to the earlier call to + // getBufferRW(). + public abstract void commitBufferRW(Rect r); + + static LogWriter vlog = new LogWriter("ModifiablePixelBuffer"); + /////////////////////////////////////////////// + // Basic rendering operations + // These operations DO NOT clip to the pixelbuffer area, or trap overruns. + + // Fill a rectangle + public synchronized void fillRect(Rect r, byte[] pix) + { + WritableRaster buf; + int w, h; + + w = r.width(); + h = r.height(); + + if (h == 0 || w ==0) + return; + + buf = getBufferRW(r); + + ByteBuffer src = + ByteBuffer.allocate(r.area()*format.bpp/8).order(format.getByteOrder()); + for (int i=0; i < r.area(); i++) + src.put(pix); + Raster raster = format.rasterFromBuffer(r, (ByteBuffer)src.rewind()); + buf.setDataElements(0, 0, raster); + + commitBufferRW(r); + } + + // Copy pixel data to the buffer + public synchronized void imageRect(Rect r, byte[] pixels) + { + WritableRaster dest = getBufferRW(r); + + ByteBuffer src = ByteBuffer.wrap(pixels).order(format.getByteOrder()); + Raster raster = format.rasterFromBuffer(r, src); + dest.setDataElements(0, 0, raster); + + commitBufferRW(r); + } + + // Copy pixel data from one PixelBuffer location to another + public synchronized void copyRect(Rect rect, + Point move_by_delta) + { + Raster srcData; + WritableRaster dstData; + + Rect drect, srect; + + drect = new Rect(rect.tl, rect.br); + if (!drect.enclosed_by(getRect())) { + String msg = "Destination rect %dx%d at %d,%d exceeds framebuffer %dx%d"; + vlog.error(String.format(msg, drect.width(), drect.height(), + drect.tl.x, drect.tl.y, width_, height_)); + drect = drect.intersect(getRect()); + } + + if (drect.is_empty()) + return; + + srect = drect.translate(move_by_delta.negate()); + if (!srect.enclosed_by(getRect())) { + String msg = "Source rect %dx%d at %d,%d exceeds framebuffer %dx%d"; + vlog.error(String.format(msg, srect.width(), srect.height(), + srect.tl.x, srect.tl.y, width_, height_)); + srect = srect.intersect(getRect()); + // Need to readjust the destination now that the area has changed + drect = srect.translate(move_by_delta); + } + + if (srect.is_empty()) + return; + + srcData = getBuffer(srect); + dstData = getBufferRW(drect); + + dstData.setDataElements(0, 0, srcData); + + commitBufferRW(rect); + } + + // Copy pixel data to the buffer through a mask + // pixels is a pointer to the pixel to be copied to r.tl. + // maskPos specifies the pixel offset in the mask to start from. + // mask_ is a pointer to the mask bits at (0,0). + // pStride and mStride are the strides of the pixel and mask buffers. + public synchronized void maskRect(Rect r, + Object pixels, byte[] mask_) + { + Rect cr = getRect().intersect(r); + if (cr.is_empty()) return; + WritableRaster data = getBufferRW(cr); + + // FIXME + ColorModel cm = format.getColorModel(); + SampleModel sm = + cm.createCompatibleSampleModel(r.width(), r.height()); + DataBuffer db = null; + ByteBuffer src = + ByteBuffer.wrap((byte[])pixels).order(format.getByteOrder()); + Buffer dst; + switch (sm.getTransferType()) { + case TYPE_INT: + dst = IntBuffer.allocate(src.remaining()).put(src.asIntBuffer()); + db = new DataBufferInt(((IntBuffer)dst).array(), r.area()); + break; + case TYPE_BYTE: + db = new DataBufferByte(src.array(), r.area()); + break; + case TYPE_SHORT: + dst = ShortBuffer.allocate(src.remaining()).put(src.asShortBuffer()); + db = new DataBufferShort(((ShortBuffer)dst).array(), r.area()); + break; + } + assert(db != null); + Raster raster = + Raster.createRaster(sm, db, new java.awt.Point(0, 0)); + ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace()); + WritableRaster t = data.createCompatibleWritableRaster(); + converter.filter(raster, t); + + int w = cr.width(); + int h = cr.height(); + + Point offset = new Point(cr.tl.x-r.tl.x, cr.tl.y-r.tl.y); + + int maskBytesPerRow = (w + 7) / 8; + + for (int y = 0; y < h; y++) { + int cy = offset.y + y; + for (int x = 0; x < w; x++) { + int cx = offset.x + x; + int byte_ = cy * maskBytesPerRow + y / 8; + int bit = 7 - cx % 8; + + if ((mask_[byte_] & (1 << bit)) != 0) + data.setDataElements(x+cx, y+cy, t.getDataElements(x+cx, y+cy, null)); + } + } + + commitBufferRW(r); + } + + // pixel is the Pixel value to be used where mask_ is set + public synchronized void maskRect(Rect r, int pixel, byte[] mask) + { + // FIXME + } + + // Render in a specific format + // Does the exact same thing as the above methods, but the given + // pixel values are defined by the given PixelFormat. + public synchronized void fillRect(PixelFormat pf, Rect dest, byte[] pix) + { + WritableRaster dstBuffer = getBufferRW(dest); + + ColorModel cm = pf.getColorModel(); + if (cm.isCompatibleRaster(dstBuffer) && + cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) { + fillRect(dest, pix); + } else { + ByteBuffer src = + ByteBuffer.allocate(dest.area()*pf.bpp/8).order(pf.getByteOrder()); + for (int i=0; i < dest.area(); i++) + src.put(pix); + Raster raster = pf.rasterFromBuffer(dest, (ByteBuffer)src.rewind()); + ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace()); + converter.filter(raster, dstBuffer); + } + + commitBufferRW(dest); + } + + public synchronized void imageRect(PixelFormat pf, Rect dest, byte[] pixels) + { + WritableRaster dstBuffer = getBufferRW(dest); + + ColorModel cm = pf.getColorModel(); + if (cm.isCompatibleRaster(dstBuffer) && + cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) { + imageRect(dest, pixels); + } else { + ByteBuffer src = ByteBuffer.wrap(pixels).order(pf.getByteOrder()); + Raster raster = pf.rasterFromBuffer(dest, src); + ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace()); + converter.filter(raster, dstBuffer); + } + + commitBufferRW(dest); + } + + public synchronized void imageRect(PixelFormat pf, Rect dest, Raster pixels) + { + WritableRaster dstBuffer = getBufferRW(dest); + + ColorModel cm = pf.getColorModel(); + if (cm.isCompatibleRaster(dstBuffer) && + cm.isCompatibleSampleModel(dstBuffer.getSampleModel())) { + dstBuffer.setDataElements(0, 0, pixels); + } else { + ColorConvertOp converter = format.getColorConvertOp(cm.getColorSpace()); + converter.filter(pixels, dstBuffer); + } + + commitBufferRW(dest); + } + +} diff --git a/java/com/tigervnc/rfb/PixelBuffer.java b/java/com/tigervnc/rfb/PixelBuffer.java index a46667d3..1b7d2c1a 100644 --- a/java/com/tigervnc/rfb/PixelBuffer.java +++ b/java/com/tigervnc/rfb/PixelBuffer.java @@ -1,4 +1,5 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * Copyright 2016 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 @@ -16,117 +17,67 @@ * USA. */ -// -// PixelBuffer - note that this code is only written for the 8, 16, and 32 bpp cases at the -// moment. -// +// -=- Generic pixel buffer class package com.tigervnc.rfb; import java.awt.image.*; +import java.awt.Color; +import java.nio.*; +import java.util.concurrent.atomic.*; -public class PixelBuffer { +public abstract class PixelBuffer { - public PixelBuffer() { - setPF(new PixelFormat()); - } - - public void setPF(PixelFormat pf) { - if (!(pf.bpp == 32) && !(pf.bpp == 16) && !(pf.bpp == 8)) - throw new Exception("Internal error: bpp must be 8, 16, or 32 in PixelBuffer ("+pf.bpp+")"); + public PixelBuffer(PixelFormat pf, int w, int h) { format = pf; - switch (pf.depth) { - case 3: - // Fall-through to depth 8 - case 6: - // Fall-through to depth 8 - case 8: - if (!pf.trueColour) { - if (cm == null) - cm = new IndexColorModel(8, 256, new byte[256], new byte[256], new byte[256]); - break; - } - int rmask = pf.redMax << pf.redShift; - int gmask = pf.greenMax << pf.greenShift; - int bmask = pf.blueMax << pf.blueShift; - cm = new DirectColorModel(8, rmask, gmask, bmask); - break; - case 16: - cm = new DirectColorModel(32, 0xF800, 0x07C0, 0x003E); - break; - case 24: - cm = new DirectColorModel(32, (0xff << 16), (0xff << 8), 0xff); - break; - case 32: - cm = new DirectColorModel(32, (0xff << pf.redShift), - (0xff << pf.greenShift), (0xff << pf.blueShift)); - break; - default: - throw new Exception("Unsupported color depth ("+pf.depth+")"); - } + width_ = w; + height_= h; } - public PixelFormat getPF() { return format; } + protected PixelBuffer() { width_ = 0; height_ = 0; } + + // Get pixel format + public final PixelFormat getPF() { return format; } + + // Get width, height and number of pixels public final int width() { return width_; } public final int height() { return height_; } public final int area() { return width_ * height_; } - public void fillRect(int x, int y, int w, int h, int pix) { - for (int ry = y; ry < y + h; ry++) - for (int rx = x; rx < x + w; rx++) - data[ry * width_ + rx] = pix; - } - - public void imageRect(int x, int y, int w, int h, int[] pix) { - for (int j = 0; j < h; j++) - System.arraycopy(pix, (w * j), data, width_ * (y + j) + x, w); - } - - public void copyRect(int x, int y, int w, int h, int srcX, int srcY) { - int dest = (width_ * y) + x; - int src = (width_ * srcY) + srcX; - int inc = width_; - - if (y > srcY) { - src += (h-1) * inc; - dest += (h-1) * inc; - inc = -inc; - } - int destEnd = dest + h * inc; - - while (dest != destEnd) { - System.arraycopy(data, src, data, dest, w); - src += inc; - dest += inc; - } - } - - public void maskRect(int x, int y, int w, int h, int[] pix, byte[] mask) { - int maskBytesPerRow = (w + 7) / 8; - - for (int j = 0; j < h; j++) { - int cy = y + j; - - if (cy < 0 || cy >= height_) - continue; - - for (int i = 0; i < w; i++) { - int cx = x + i; - - if (cx < 0 || cx >= width_) - continue; - - int byte_ = j * maskBytesPerRow + i / 8; - int bit = 7 - i % 8; - - if ((mask[byte_] & (1 << bit)) != 0) - data[cy * width_ + cx] = pix[j * w + i]; - } - } + // Get rectangle encompassing this buffer + // Top-left of rectangle is either at (0,0), or the specified point. + public final Rect getRect() { return new Rect(0, 0, width_, height_); } + public final Rect getRect(Point pos) { + return new Rect(pos, pos.translate(new Point(width_, height_))); } - public int[] data; - public ColorModel cm; + /////////////////////////////////////////////// + // Access to pixel data + // + + // Get a pointer into the buffer + // The pointer is to the top-left pixel of the specified Rect. + public abstract Raster getBuffer(Rect r); + + // Get pixel data for a given part of the buffer + // Data is copied into the supplied buffer, with the specified + // stride. Try to avoid using this though as getBuffer() will in + // most cases avoid the extra memory copy. + //void getImage(void* imageBuf, const Rect& r, int stride=0) const; + // Get pixel data in a given format + // Works just the same as getImage(), but guaranteed to be in a + // specific format. + //void getImage(const PixelFormat& pf, void* imageBuf, + // const Rect& r, int stride=0) const; + + /////////////////////////////////////////////// + // Framebuffer update methods + // + + // Ensure that the specified rectangle of buffer is up to date. + // Overridden by derived classes implementing framebuffer access + // to copy the required display data into place. + //public abstract void grabRegion(Region& region) {} protected PixelFormat format; protected int width_, height_; diff --git a/java/com/tigervnc/rfb/PixelFormat.java b/java/com/tigervnc/rfb/PixelFormat.java index c4d68701..9a269992 100644 --- a/java/com/tigervnc/rfb/PixelFormat.java +++ b/java/com/tigervnc/rfb/PixelFormat.java @@ -25,40 +25,80 @@ package com.tigervnc.rfb; +import java.awt.color.*; +import java.awt.image.*; +import java.nio.*; +import java.util.*; + import com.tigervnc.rdr.*; -import java.awt.image.ColorModel; public class PixelFormat { - public PixelFormat(int b, int d, boolean e, boolean t) { - bpp = b; - depth = d; - bigEndian = e; - trueColour = t; - } public PixelFormat(int b, int d, boolean e, boolean t, - int rm, int gm, int bm, int rs, int gs, int bs) { - this(b, d, e, t); - redMax = rm; - greenMax = gm; - blueMax = bm; - redShift = rs; - greenShift = gs; - blueShift = bs; + int rm, int gm, int bm, int rs, int gs, int bs) + { + bpp = b; depth = d; trueColour = t; bigEndian = e; + redMax = rm; greenMax = gm; blueMax = bm; + redShift = rs; greenShift = gs; blueShift = bs; + converters = new HashMap(); + assert(isSane()); + + updateState(); + } + + public PixelFormat() + { + this(8, 8, false, true, 7, 7, 3, 0, 3, 6); + updateState(); } - public PixelFormat() { this(8,8,false,true,7,7,3,0,3,6); } - - public boolean equal(PixelFormat x) { - return (bpp == x.bpp && - depth == x.depth && - (bigEndian == x.bigEndian || bpp == 8) && - trueColour == x.trueColour && - (!trueColour || (redMax == x.redMax && - greenMax == x.greenMax && - blueMax == x.blueMax && - redShift == x.redShift && - greenShift == x.greenShift && - blueShift == x.blueShift))); + + public boolean equal(PixelFormat other) + { + if (bpp != other.bpp || depth != other.depth) + return false; + + if (redMax != other.redMax) + return false; + if (greenMax != other.greenMax) + return false; + if (blueMax != other.blueMax) + return false; + + // Endianness requires more care to determine compatibility + if (bigEndian == other.bigEndian || bpp == 8) { + if (redShift != other.redShift) + return false; + if (greenShift != other.greenShift) + return false; + if (blueShift != other.blueShift) + return false; + } else { + // Has to be the same byte for each channel + if (redShift/8 != (3 - other.redShift/8)) + return false; + if (greenShift/8 != (3 - other.greenShift/8)) + return false; + if (blueShift/8 != (3 - other.blueShift/8)) + return false; + + // And the same bit offset within the byte + if (redShift%8 != other.redShift%8) + return false; + if (greenShift%8 != other.greenShift%8) + return false; + if (blueShift%8 != other.blueShift%8) + return false; + + // And not cross a byte boundary + if (redShift/8 != (redShift + redBits - 1)/8) + return false; + if (greenShift/8 != (greenShift + greenBits - 1)/8) + return false; + if (blueShift/8 != (blueShift + blueBits - 1)/8) + return false; + } + + return true; } public void read(InStream is) { @@ -73,6 +113,23 @@ public class PixelFormat { greenShift = is.readU8(); blueShift = is.readU8(); is.skip(3); + + // We have no real support for colour maps. If the client + // wants one, then we force a 8-bit true colour format and + // pretend it's a colour map. + if (!trueColour) { + redMax = 7; + greenMax = 7; + blueMax = 3; + redShift = 0; + greenShift = 3; + blueShift = 6; + } + + if (!isSane()) + throw new Exception("invalid pixel format: "+print()); + + updateState(); } public void write(OutStream os) { @@ -89,6 +146,14 @@ public class PixelFormat { os.pad(3); } + public final boolean isBigEndian() { + return bigEndian; + } + + public final boolean isLittleEndian() { + return ! bigEndian; + } + public final boolean is888() { if(!trueColour) return false; @@ -139,53 +204,140 @@ public class PixelFormat { return 0; } - public void bufferFromRGB(int[] dst, int dstPtr, byte[] src, - int srcPtr, int pixels) { + public void bufferFromRGB(ByteBuffer dst, ByteBuffer src, int pixels) + { + bufferFromRGB(dst, src, pixels, pixels, 1); + } + + public void bufferFromRGB(ByteBuffer dst, ByteBuffer src, + int w, int stride, int h) + { if (is888()) { // Optimised common case - int r, g, b; + int r, g, b, x; + + if (bigEndian) { + r = dst.position() + (24 - redShift)/8; + g = dst.position() + (24 - greenShift)/8; + b = dst.position() + (24 - blueShift)/8; + x = dst.position() + (24 - (48 - redShift - greenShift - blueShift))/8; + } else { + r = dst.position() + redShift/8; + g = dst.position() + greenShift/8; + b = dst.position() + blueShift/8; + x = dst.position() + (48 - redShift - greenShift - blueShift)/8; + } - for (int i=srcPtr; i < pixels; i++) { - if (bigEndian) { - r = (src[3*i+0] & 0xff) << (24 - redShift); - g = (src[3*i+1] & 0xff) << (24 - greenShift); - b = (src[3*i+2] & 0xff) << (24 - blueShift); - dst[dstPtr+i] = r | g | b | 0xff; - } else { - r = (src[3*i+0] & 0xff) << redShift; - g = (src[3*i+1] & 0xff) << greenShift; - b = (src[3*i+2] & 0xff) << blueShift; - dst[dstPtr+i] = (0xff << 24) | r | g | b; + int dstPad = (stride - w) * 4; + while (h-- > 0) { + int w_ = w; + while (w_-- > 0) { + dst.put(r, src.get()); + dst.put(g, src.get()); + dst.put(b, src.get()); + dst.put(x, (byte)0); + r += 4; + g += 4; + b += 4; + x += 4; } + r += dstPad; + g += dstPad; + b += dstPad; + x += dstPad; } } else { // Generic code - int p, r, g, b; - int[] rgb = new int[4]; + int dstPad = (stride - w) * bpp/8; + while (h-- > 0) { + int w_ = w; + while (w_-- > 0) { + int p; + int r, g, b; - int i = srcPtr; int j = dstPtr; - while (i < pixels) { - r = src[i++] & 0xff; - g = src[i++] & 0xff; - b = src[i++] & 0xff; + r = src.get(); + g = src.get(); + b = src.get(); - //p = pixelFromRGB(r, g, b, cm); - p = ColorModel.getRGBdefault().getDataElement(new int[] {0xff, r, g, b}, 0); + p = pixelFromRGB(r, g, b, model); - bufferFromPixel(dst, j, p); - j += bpp/8; + bufferFromPixel(dst, p); + dst.position(dst.position() + bpp/8); + } + dst.position(dst.position() + dstPad); } } } - public void rgbFromBuffer(byte[] dst, int dstPtr, byte[] src, int srcPtr, int pixels, ColorModel cm) + public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src, int pixels) + { + rgbFromBuffer(dst, src, pixels, pixels, 1); + } + + public void rgbFromBuffer(ByteBuffer dst, ByteBuffer src, + int w, int stride, int h) + { + if (is888()) { + // Optimised common case + int r, g, b; + + if (bigEndian) { + r = src.position() + (24 - redShift)/8; + g = src.position() + (24 - greenShift)/8; + b = src.position() + (24 - blueShift)/8; + } else { + r = src.position() + redShift/8; + g = src.position() + greenShift/8; + b = src.position() + blueShift/8; + } + + int srcPad = (stride - w) * 4; + while (h-- > 0) { + int w_ = w; + while (w_-- > 0) { + dst.put(src.get(r)); + dst.put(src.get(g)); + dst.put(src.get(b)); + r += 4; + g += 4; + b += 4; + } + r += srcPad; + g += srcPad; + b += srcPad; + } + } else { + // Generic code + int srcPad = (stride - w) * bpp/8; + while (h-- > 0) { + int w_ = w; + while (w_-- > 0) { + int p; + byte r, g, b; + + p = pixelFromBuffer(src.duplicate()); + + r = (byte)getColorModel().getRed(p); + g = (byte)getColorModel().getGreen(p); + b = (byte)getColorModel().getBlue(p); + + dst.put(r); + dst.put(g); + dst.put(b); + src.position(src.position() + bpp/8); + } + src.reset().position(src.position() + srcPad).mark(); + } + } + } + + public void rgbFromPixels(byte[] dst, int dstPtr, int[] src, int srcPtr, int pixels, ColorModel cm) { int p; byte r, g, b; for (int i=0; i < pixels; i++) { - p = pixelFromBuffer(src, srcPtr); - srcPtr += bpp/8; + p = src[i]; dst[dstPtr++] = (byte)cm.getRed(p); dst[dstPtr++] = (byte)cm.getGreen(p); @@ -193,31 +345,29 @@ public class PixelFormat { } } - public int pixelFromBuffer(byte[] buffer, int bufferPtr) + public int pixelFromBuffer(ByteBuffer buffer) { int p; - p = 0; + p = 0xff000000; - if (bigEndian) { + if (!bigEndian) { switch (bpp) { case 32: - p = (buffer[0] & 0xff) << 24 | (buffer[1] & 0xff) << 16 | (buffer[2] & 0xff) << 8 | 0xff; - break; + p |= buffer.get() << 24; + p |= buffer.get() << 16; case 16: - p = (buffer[0] & 0xff) << 8 | (buffer[1] & 0xff); - break; + p |= buffer.get() << 8; case 8: - p = (buffer[0] & 0xff); - break; + p |= buffer.get(); } } else { - p = (buffer[0] & 0xff); + p |= buffer.get(0); if (bpp >= 16) { - p |= (buffer[1] & 0xff) << 8; + p |= buffer.get(1) << 8; if (bpp == 32) { - p |= (buffer[2] & 0xff) << 16; - p |= (buffer[3] & 0xff) << 24; + p |= buffer.get(2) << 16; + p |= buffer.get(3) << 24; } } } @@ -263,33 +413,212 @@ public class PixelFormat { return s.toString(); } - public void bufferFromPixel(int[] buffer, int bufPtr, int p) + private static int bits(int value) + { + int bits; + + bits = 16; + + if ((value & 0xff00) == 0) { + bits -= 8; + value <<= 8; + } + if ((value & 0xf000) == 0) { + bits -= 4; + value <<= 4; + } + if ((value & 0xc000) == 0) { + bits -= 2; + value <<= 2; + } + if ((value & 0x8000) == 0) { + bits -= 1; + value <<= 1; + } + + return bits; + } + + private void updateState() + { + int endianTest = 1; + + redBits = bits(redMax); + greenBits = bits(greenMax); + blueBits = bits(blueMax); + + maxBits = redBits; + if (greenBits > maxBits) + maxBits = greenBits; + if (blueBits > maxBits) + maxBits = blueBits; + + minBits = redBits; + if (greenBits < minBits) + minBits = greenBits; + if (blueBits < minBits) + minBits = blueBits; + + if ((((char)endianTest) == 0) != bigEndian) + endianMismatch = true; + else + endianMismatch = false; + + model = getColorModel(this); + } + + private boolean isSane() + { + int totalBits; + + if ((bpp != 8) && (bpp != 16) && (bpp != 32)) + return false; + if (depth > bpp) + return false; + + if (!trueColour && (depth != 8)) + return false; + + if ((redMax & (redMax + 1)) != 0) + return false; + if ((greenMax & (greenMax + 1)) != 0) + return false; + if ((blueMax & (blueMax + 1)) != 0) + return false; + + /* + * We don't allow individual channels > 8 bits in order to keep our + * conversions simple. + */ + if (redMax >= (1 << 8)) + return false; + if (greenMax >= (1 << 8)) + return false; + if (blueMax >= (1 << 8)) + return false; + + totalBits = bits(redMax) + bits(greenMax) + bits(blueMax); + if (totalBits > bpp) + return false; + + if (((redMax << redShift) & (greenMax << greenShift)) != 0) + return false; + if (((redMax << redShift) & (blueMax << blueShift)) != 0) + return false; + if (((greenMax << greenShift) & (blueMax << blueShift)) != 0) + return false; + + return true; + } + + public void bufferFromPixel(ByteBuffer buffer, int p) { if (bigEndian) { switch (bpp) { case 32: - buffer[bufPtr++] = (p >> 24) & 0xff; - buffer[bufPtr++] = (p >> 16) & 0xff; + buffer.put((byte)((p >> 24) & 0xff)); + buffer.put((byte)((p >> 16) & 0xff)); break; case 16: - buffer[bufPtr++] = (p >> 8) & 0xff; + buffer.put((byte)((p >> 8) & 0xff)); break; case 8: - buffer[bufPtr++] = (p >> 0) & 0xff; + buffer.put((byte)((p >> 0) & 0xff)); break; } } else { - buffer[0] = (p >> 0) & 0xff; + buffer.put(0, (byte)((p >> 0) & 0xff)); if (bpp >= 16) { - buffer[1] = (p >> 8) & 0xff; + buffer.put(1, (byte)((p >> 8) & 0xff)); if (bpp == 32) { - buffer[2] = (p >> 16) & 0xff; - buffer[3] = (p >> 24) & 0xff; + buffer.put(2, (byte)((p >> 16) & 0xff)); + buffer.put(3, (byte)((p >> 24) & 0xff)); } } } } + public ColorModel getColorModel() + { + return model; + } + + public static ColorModel getColorModel(PixelFormat pf) { + if (!(pf.bpp == 32) && !(pf.bpp == 16) && !(pf.bpp == 8)) + throw new Exception("Internal error: bpp must be 8, 16, or 32 in PixelBuffer ("+pf.bpp+")"); + ColorModel cm; + switch (pf.depth) { + case 3: + // Fall-through to depth 8 + case 6: + // Fall-through to depth 8 + case 8: + int rmask = pf.redMax << pf.redShift; + int gmask = pf.greenMax << pf.greenShift; + int bmask = pf.blueMax << pf.blueShift; + cm = new DirectColorModel(8, rmask, gmask, bmask); + break; + case 16: + cm = new DirectColorModel(32, 0xF800, 0x07C0, 0x003E); + break; + case 24: + cm = new DirectColorModel(32, (0xff << 16), (0xff << 8), 0xff); + break; + case 32: + cm = new DirectColorModel(32, (0xff << pf.redShift), + (0xff << pf.greenShift), (0xff << pf.blueShift)); + break; + default: + throw new Exception("Unsupported color depth ("+pf.depth+")"); + } + assert(cm != null); + return cm; + } + + public ColorConvertOp getColorConvertOp(ColorSpace src) + { + // The overhead associated with initializing ColorConvertOps is + // enough to justify maintaining a static lookup table. + if (converters.containsKey(src.getType())) + return converters.get(src.getType()); + ColorSpace dst = model.getColorSpace(); + converters.put(src.getType(), new ColorConvertOp(src, dst, null)); + return converters.get(src.getType()); + } + + public ByteOrder getByteOrder() + { + if (isBigEndian()) + return ByteOrder.BIG_ENDIAN; + else + return ByteOrder.LITTLE_ENDIAN; + } + + public Raster rasterFromBuffer(Rect r, ByteBuffer buf) + { + Buffer dst; + DataBuffer db = null; + + SampleModel sm = + model.createCompatibleSampleModel(r.width(), r.height()); + switch (sm.getTransferType()) { + case DataBuffer.TYPE_INT: + dst = IntBuffer.allocate(r.area()).put(buf.asIntBuffer()); + db = new DataBufferInt(((IntBuffer)dst).array(), r.area()); + break; + case DataBuffer.TYPE_BYTE: + db = new DataBufferByte(buf.array(), r.area()); + break; + case DataBuffer.TYPE_SHORT: + dst = ShortBuffer.allocate(r.area()).put(buf.asShortBuffer()); + db = new DataBufferShort(((ShortBuffer)dst).array(), r.area()); + break; + } + assert(db != null); + return Raster.createRaster(sm, db, new java.awt.Point(0, 0)); + } + + private static HashMap converters; public int bpp; public int depth; @@ -301,4 +630,10 @@ public class PixelFormat { public int redShift; public int greenShift; public int blueShift; + + protected int redBits, greenBits, blueBits; + protected int maxBits, minBits; + protected boolean endianMismatch; + + private ColorModel model; } diff --git a/java/com/tigervnc/rfb/RREDecoder.java b/java/com/tigervnc/rfb/RREDecoder.java index 487aa3d0..c73c7a94 100644 --- a/java/com/tigervnc/rfb/RREDecoder.java +++ b/java/com/tigervnc/rfb/RREDecoder.java @@ -1,4 +1,5 @@ /* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. + * Copyright 2016 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,30 +19,90 @@ package com.tigervnc.rfb; +import java.nio.*; + import com.tigervnc.rdr.*; public class RREDecoder extends Decoder { - public RREDecoder(CMsgReader reader_) { reader = reader_; } + public RREDecoder() { super(DecoderFlags.DecoderPlain); } + + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) + { + int numRects; + + numRects = is.readU32(); + os.writeU32(numRects); + + os.copyBytes(is, cp.pf().bpp/8 + numRects * (cp.pf().bpp/8 + 8)); + } - public void readRect(Rect r, CMsgHandler handler) { - InStream is = reader.getInStream(); - int bytesPerPixel = handler.cp.pf().bpp / 8; - boolean bigEndian = handler.cp.pf().bigEndian; + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + MemInStream is = new MemInStream((byte[])buffer, 0, buflen); + PixelFormat pf = cp.pf(); + switch (pf.bpp) { + case 8: rreDecode8 (r, is, pf, pb); break; + case 16: rreDecode16(r, is, pf, pb); break; + case 32: rreDecode32(r, is, pf, pb); break; + } + } + + private static ByteBuffer READ_PIXEL(InStream is, PixelFormat pf) { + ByteBuffer b = ByteBuffer.allocate(4); + switch (pf.bpp) { + case 8: + b.putInt(is.readOpaque8()); + return ByteBuffer.allocate(1).put(b.get(3)); + case 16: + b.putInt(is.readOpaque16()); + return ByteBuffer.allocate(2).put(b.array(), 2, 2); + case 32: + default: + b.putInt(is.readOpaque32()); + return b; + } + } + + private void RRE_DECODE(Rect r, InStream is, + PixelFormat pf, ModifiablePixelBuffer pb) + { int nSubrects = is.readU32(); - int bg = is.readPixel(bytesPerPixel, bigEndian); - handler.fillRect(r, bg); + byte[] bg = READ_PIXEL(is, pf).array(); + pb.fillRect(pf, r, bg); for (int i = 0; i < nSubrects; i++) { - int pix = is.readPixel(bytesPerPixel, bigEndian); + byte[] pix = READ_PIXEL(is, pf).array(); int x = is.readU16(); int y = is.readU16(); int w = is.readU16(); int h = is.readU16(); - handler.fillRect(new Rect(r.tl.x + x, r.tl.y + y, - r.tl.x + x + w, r.tl.y + y + h), pix); + pb.fillRect(pf, new Rect(r.tl.x+x, r.tl.y+y, r.tl.x+x+w, r.tl.y+y+h), pix); } } - CMsgReader reader; + private void rreDecode8(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + RRE_DECODE(r, is, pf, pb); + } + + private void rreDecode16(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + RRE_DECODE(r, is, pf, pb); + } + + private void rreDecode32(Rect r, InStream is, + PixelFormat pf, + ModifiablePixelBuffer pb) + { + RRE_DECODE(r, is, pf, pb); + } + } diff --git a/java/com/tigervnc/rfb/RawDecoder.java b/java/com/tigervnc/rfb/RawDecoder.java index b2219a24..71b79607 100644 --- a/java/com/tigervnc/rfb/RawDecoder.java +++ b/java/com/tigervnc/rfb/RawDecoder.java @@ -18,28 +18,25 @@ package com.tigervnc.rfb; +import com.tigervnc.rdr.*; + public class RawDecoder extends Decoder { - public RawDecoder(CMsgReader reader_) { reader = reader_; } + public RawDecoder() { super(DecoderFlags.DecoderPlain); } - public void readRect(Rect r, CMsgHandler handler) { - int x = r.tl.x; - int y = r.tl.y; - int w = r.width(); - int h = r.height(); - int[] imageBuf = new int[w*h]; - int nPixels = imageBuf.length; - int bytesPerRow = w * (reader.bpp() / 8); - while (h > 0) { - int nRows = nPixels / w; - if (nRows > h) nRows = h; - reader.getInStream().readPixels(imageBuf, nPixels, (reader.bpp() / 8), handler.cp.pf().bigEndian); - handler.imageRect(new Rect(x, y, x+w, y+nRows), imageBuf); - h -= nRows; - y += nRows; - } + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) + { + os.copyBytes(is, r.area() * cp.pf().bpp/8); } - CMsgReader reader; static LogWriter vlog = new LogWriter("RawDecoder"); + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + assert(buflen >= r.area() * cp.pf().bpp/8); + pb.imageRect(cp.pf(), r, (byte[])buffer); + } + } diff --git a/java/com/tigervnc/rfb/Region.java b/java/com/tigervnc/rfb/Region.java new file mode 100644 index 00000000..f7da91de --- /dev/null +++ b/java/com/tigervnc/rfb/Region.java @@ -0,0 +1,102 @@ +/* Copyright (C) 2016 Brian P. Hinz. 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.rfb; + +import java.awt.*; +import java.awt.geom.*; + +public class Region extends Area { + + // Create an empty region + public Region() { + super(); + } + + // Create a rectangular region + public Region(Rect r) { + super(new Rectangle(r.tl.x, r.tl.y, r.width(), r.height())); + } + + public Region(Region r) { + super(r); + //intersect(r); + } + + public void clear() { reset(); } + + public void reset(Rect r) { + if (r.is_empty()) { + clear(); + } else { + clear(); + assign_union(new Region(r)); + /* + xrgn.numRects = 1; + xrgn.rects[0].x1 = xrgn.extents.x1 = r.tl.x; + xrgn.rects[0].y1 = xrgn.extents.y1 = r.tl.y; + xrgn.rects[0].x2 = xrgn.extents.x2 = r.br.x; + xrgn.rects[0].y2 = xrgn.extents.y2 = r.br.y; + */ + } + } + + public void translate(Point delta) { + AffineTransform t = + AffineTransform.getTranslateInstance((double)delta.x, (double)delta.y); + transform(t); + } + + public void assign_intersect(Region r) { + intersect(r); + } + + public void assign_union(Region r) { + add(r); + } + + public void assign_subtract(Region r) { + subtract(r); + } + + public Region intersect(Region r) { + Region ret = new Region(this); + ((Area)ret).intersect(this); + return ret; + } + + public Region union(Region r) { + Region ret = new Region(r); + ((Area)ret).add(this); + return ret; + } + + public Region subtract(Region r) { + Region ret = new Region(this); + ((Area)ret).subtract(r); + return ret; + } + + public boolean is_empty() { return isEmpty(); } + + public Rect get_bounding_rect() { + Rectangle b = getBounds(); + return new Rect((int)b.getX(), (int)b.getY(), + (int)b.getWidth(), (int)b.getHeight()); + } +} diff --git a/java/com/tigervnc/rfb/TightDecoder.java b/java/com/tigervnc/rfb/TightDecoder.java index b644cdb4..aa468eb8 100644 --- a/java/com/tigervnc/rfb/TightDecoder.java +++ b/java/com/tigervnc/rfb/TightDecoder.java @@ -22,56 +22,181 @@ package com.tigervnc.rfb; import com.tigervnc.rdr.InStream; +import com.tigervnc.rdr.MemInStream; +import com.tigervnc.rdr.OutStream; import com.tigervnc.rdr.ZlibInStream; import java.util.ArrayList; import java.io.InputStream; import java.awt.image.*; import java.awt.*; +import java.math.BigInteger; +import java.io.*; +import java.nio.*; +import javax.imageio.*; +import javax.imageio.stream.*; public class TightDecoder extends Decoder { final static int TIGHT_MAX_WIDTH = 2048; + final static int TIGHT_MIN_TO_COMPRESS = 12; // Compression control - final static int rfbTightExplicitFilter = 0x04; - final static int rfbTightFill = 0x08; - final static int rfbTightJpeg = 0x09; - final static int rfbTightMaxSubencoding = 0x09; + final static int tightExplicitFilter = 0x04; + final static int tightFill = 0x08; + final static int tightJpeg = 0x09; + final static int tightMaxSubencoding = 0x09; // Filters to improve compression efficiency - final static int rfbTightFilterCopy = 0x00; - final static int rfbTightFilterPalette = 0x01; - final static int rfbTightFilterGradient = 0x02; - final static int rfbTightMinToCompress = 12; + final static int tightFilterCopy = 0x00; + final static int tightFilterPalette = 0x01; + final static int tightFilterGradient = 0x02; - final static Toolkit tk = Toolkit.getDefaultToolkit(); - - public TightDecoder(CMsgReader reader_) { - reader = reader_; + public TightDecoder() { + super(DecoderFlags.DecoderPartiallyOrdered); zis = new ZlibInStream[4]; for (int i = 0; i < 4; i++) zis[i] = new ZlibInStream(); } - public void readRect(Rect r, CMsgHandler handler) + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) { - InStream is = reader.getInStream(); - boolean cutZeros = false; - clientpf = handler.getPreferredPF(); - serverpf = handler.cp.pf(); - int bpp = serverpf.bpp; - cutZeros = false; - if (bpp == 32) { - if (serverpf.is888()) { - cutZeros = true; + int comp_ctl; + + comp_ctl = is.readU8(); + os.writeU8(comp_ctl); + + comp_ctl >>= 4; + + // "Fill" compression type. + if (comp_ctl == tightFill) { + if (cp.pf().is888()) + os.copyBytes(is, 3); + else + os.copyBytes(is, cp.pf().bpp/8); + return; + } + + // "JPEG" compression type. + if (comp_ctl == tightJpeg) { + int len; + + len = readCompact(is); + os.writeOpaque32(len); + os.copyBytes(is, len); + return; + } + + // Quit on unsupported compression type. + if (comp_ctl > tightMaxSubencoding) + throw new Exception("TightDecoder: bad subencoding value received"); + + // "Basic" compression type. + + int palSize = 0; + + if (r.width() > TIGHT_MAX_WIDTH) + throw new Exception("TightDecoder: too large rectangle ("+r.width()+" pixels)"); + + // Possible palette + if ((comp_ctl & tightExplicitFilter) != 0) { + int filterId; + + filterId = is.readU8() & 0xff; + os.writeU8(filterId); + + switch (filterId) { + case tightFilterPalette: + palSize = is.readU8() + 1; + os.writeU32(palSize - 1); + + if (cp.pf().is888()) + os.copyBytes(is, palSize * 3); + else + os.copyBytes(is, palSize * cp.pf().bpp/8); + break; + case tightFilterGradient: + if (cp.pf().bpp == 8) + throw new Exception("TightDecoder: invalid BPP for gradient filter"); + break; + case tightFilterCopy: + break; + default: + throw new Exception("TightDecoder: unknown filter code received"); } } - int comp_ctl = is.readU8(); + int rowSize, dataSize; + + if (palSize != 0) { + if (palSize <= 2) + rowSize = (r.width() + 7) / 8; + else + rowSize = r.width(); + } else if (cp.pf().is888()) { + rowSize = r.width() * 3; + } else { + rowSize = r.width() * cp.pf().bpp/8; + } - boolean bigEndian = handler.cp.pf().bigEndian; + dataSize = r.height() * rowSize; - // Flush zlib streams if we are told by the server to do so. + if (dataSize < TIGHT_MIN_TO_COMPRESS) { + os.copyBytes(is, dataSize); + } else { + int len; + + len = readCompact(is); + os.writeOpaque32(len); + os.copyBytes(is, len); + } + } + + public boolean doRectsConflict(Rect rectA, + Object bufferA, + int buflenA, + Rect rectB, + Object bufferB, + int buflenB, + ConnParams cp) + { + byte comp_ctl_a, comp_ctl_b; + + assert(buflenA >= 1); + assert(buflenB >= 1); + + comp_ctl_a = ((byte[])bufferA)[0]; + comp_ctl_b = ((byte[])bufferB)[0]; + + // Resets or use of zlib pose the same problem, so merge them + if ((comp_ctl_a & 0x80) == 0x00) + comp_ctl_a |= 1 << ((comp_ctl_a >> 4) & 0x03); + if ((comp_ctl_b & 0x80) == 0x00) + comp_ctl_b |= 1 << ((comp_ctl_b >> 4) & 0x03); + + if (((comp_ctl_a & 0x0f) & (comp_ctl_b & 0x0f)) != 0) + return true; + + return false; + } + + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + ByteBuffer bufptr; + PixelFormat pf = cp.pf(); + + int comp_ctl; + + bufptr = ByteBuffer.wrap((byte[])buffer); + + assert(buflen >= 1); + + comp_ctl = bufptr.get() & 0xff; + buflen -= 1; + + // Reset zlib streams if we are told by the server to do so. for (int i = 0; i < 4; i++) { if ((comp_ctl & 1) != 0) { zis[i].reset(); @@ -80,190 +205,216 @@ public class TightDecoder extends Decoder { } // "Fill" compression type. - if (comp_ctl == rfbTightFill) { - int[] pix = new int[1]; - if (cutZeros) { - byte[] bytebuf = new byte[3]; - is.readBytes(bytebuf, 0, 3); - serverpf.bufferFromRGB(pix, 0, bytebuf, 0, 1); + if (comp_ctl == tightFill) { + if (pf.is888()) { + ByteBuffer pix = ByteBuffer.allocate(4); + + assert(buflen >= 3); + + pf.bufferFromRGB(pix, bufptr, 1); + pb.fillRect(pf, r, pix.array()); } else { - pix[0] = is.readPixel(serverpf.bpp/8, serverpf.bigEndian); + assert(buflen >= pf.bpp/8); + byte[] pix = new byte[pf.bpp/8]; + bufptr.get(pix); + pb.fillRect(pf, r, pix); } - handler.fillRect(r, pix[0]); return; } // "JPEG" compression type. - if (comp_ctl == rfbTightJpeg) { - DECOMPRESS_JPEG_RECT(r, is, handler); + if (comp_ctl == tightJpeg) { + int len; + + WritableRaster buf; + + JpegDecompressor jd = new JpegDecompressor(); + + assert(buflen >= 4); + + len = bufptr.getInt(); + buflen -= 4; + + // We always use direct decoding with JPEG images + buf = pb.getBufferRW(r); + jd.decompress(bufptr, len, buf, r, pb.getPF()); + pb.commitBufferRW(r); return; } // Quit on unsupported compression type. - if (comp_ctl > rfbTightMaxSubencoding) { + if (comp_ctl > tightMaxSubencoding) throw new Exception("TightDecoder: bad subencoding value received"); - } // "Basic" compression type. int palSize = 0; - int[] palette = new int[256]; + ByteBuffer palette = ByteBuffer.allocate(256 * 4); boolean useGradient = false; - if ((comp_ctl & rfbTightExplicitFilter) != 0) { - int filterId = is.readU8(); + if ((comp_ctl & tightExplicitFilter) != 0) { + int filterId; + + assert(buflen >= 1); + + filterId = bufptr.get(); switch (filterId) { - case rfbTightFilterPalette: - palSize = is.readU8() + 1; - byte[] tightPalette; - if (cutZeros) { - tightPalette = new byte[256 * 3]; - is.readBytes(tightPalette, 0, palSize * 3); - serverpf.bufferFromRGB(palette, 0, tightPalette, 0, palSize); + case tightFilterPalette: + assert(buflen >= 1); + + palSize = bufptr.getInt() + 1; + buflen -= 4; + + if (pf.is888()) { + ByteBuffer tightPalette = ByteBuffer.allocate(palSize * 3); + + assert(buflen >= tightPalette.capacity()); + + bufptr.get(tightPalette.array(), 0, tightPalette.capacity()); + buflen -= tightPalette.capacity(); + + pf.bufferFromRGB(palette.duplicate(), tightPalette, palSize); } else { - is.readPixels(palette, palSize, serverpf.bpp/8, serverpf.bigEndian); + int len; + + len = palSize * pf.bpp/8; + + assert(buflen >= len); + + bufptr.get(palette.array(), 0, len); + buflen -= len; } break; - case rfbTightFilterGradient: + case tightFilterGradient: useGradient = true; break; - case rfbTightFilterCopy: + case tightFilterCopy: break; default: - throw new Exception("TightDecoder: unknown filter code recieved"); + assert(false); } } - int bppp = bpp; + // Determine if the data should be decompressed or just copied. + int rowSize, dataSize; + byte[] netbuf; + if (palSize != 0) { - bppp = (palSize <= 2) ? 1 : 8; - } else if (cutZeros) { - bppp = 24; + if (palSize <= 2) + rowSize = (r.width() + 7) / 8; + else + rowSize = r.width(); + } else if (pf.is888()) { + rowSize = r.width() * 3; + } else { + rowSize = r.width() * pf.bpp/8; } - // Determine if the data should be decompressed or just copied. - int rowSize = (r.width() * bppp + 7) / 8; - int dataSize = r.height() * rowSize; - int streamId = -1; - InStream input; - if (dataSize < rfbTightMinToCompress) { - input = is; + dataSize = r.height() * rowSize; + + if (dataSize < TIGHT_MIN_TO_COMPRESS) { + assert(buflen >= dataSize); } else { - int length = is.readCompactLength(); + int len; + int streamId; + MemInStream ms; + + assert(buflen >= 4); + + len = bufptr.getInt(); + buflen -= 4; + + assert(buflen >= len); + streamId = comp_ctl & 0x03; - zis[streamId].setUnderlying(is, length); - input = (ZlibInStream)zis[streamId]; - } + ms = new MemInStream(bufptr.array(), bufptr.position(), len); + zis[streamId].setUnderlying(ms, len); + + // Allocate netbuf and read in data + netbuf = new byte[dataSize]; - // Allocate netbuf and read in data - byte[] netbuf = new byte[dataSize]; - input.readBytes(netbuf, 0, dataSize); + zis[streamId].readBytes(netbuf, 0, dataSize); + zis[streamId].removeUnderlying(); + ms = null; + + bufptr = ByteBuffer.wrap(netbuf); + buflen = dataSize; + } + + ByteBuffer outbuf = ByteBuffer.allocate(r.area() * pf.bpp/8); int stride = r.width(); - int[] buf = reader.getImageBuf(r.area()); if (palSize == 0) { // Truecolor data. if (useGradient) { - if (bpp == 32 && cutZeros) { - FilterGradient24(netbuf, buf, stride, r); + if (pf.is888()) { + FilterGradient24(bufptr, pf, outbuf, stride, r); } else { - FilterGradient(netbuf, buf, stride, r); + switch (pf.bpp) { + case 8: + assert(false); + break; + case 16: + FilterGradient(bufptr, pf, outbuf, stride, r); + break; + case 32: + FilterGradient(bufptr, pf, outbuf, stride, r); + break; + } } } else { // Copy - int h = r.height(); - int ptr = 0; - int srcPtr = 0; + ByteBuffer ptr = (ByteBuffer)outbuf.duplicate().mark(); + ByteBuffer srcPtr = bufptr.duplicate(); int w = r.width(); - if (cutZeros) { - serverpf.bufferFromRGB(buf, ptr, netbuf, srcPtr, w*h); + int h = r.height(); + if (pf.is888()) { + while (h > 0) { + pf.bufferFromRGB(ptr.duplicate(), srcPtr.duplicate(), w); + ptr.position(ptr.position() + stride * pf.bpp/8); + srcPtr.position(srcPtr.position() + w * 3); + h--; + } } else { - int pixelSize = (bpp >= 24) ? 3 : bpp/8; while (h > 0) { - for (int i = 0; i < w; i++) { - if (bpp == 8) { - buf[ptr+i] = netbuf[srcPtr+i] & 0xff; - } else { - for (int j = pixelSize-1; j >= 0; j--) - buf[ptr+i] |= ((netbuf[srcPtr+i+j] & 0xff) << j*8); - } - } - ptr += stride; - srcPtr += w * pixelSize; + ptr.put(srcPtr.array(), srcPtr.position(), w * pf.bpp/8); + ptr.reset().position(ptr.position() + stride * pf.bpp/8).mark(); + srcPtr.position(srcPtr.position() + w * pf.bpp/8); h--; } } } } else { // Indexed color - int x, h = r.height(), w = r.width(), b, pad = stride - w; - int ptr = 0; - int srcPtr = 0, bits; - if (palSize <= 2) { - // 2-color palette - while (h > 0) { - for (x = 0; x < w / 8; x++) { - bits = netbuf[srcPtr++]; - for(b = 7; b >= 0; b--) { - buf[ptr++] = palette[bits >> b & 1]; - } - } - if (w % 8 != 0) { - bits = netbuf[srcPtr++]; - for (b = 7; b >= 8 - w % 8; b--) { - buf[ptr++] = palette[bits >> b & 1]; - } - } - ptr += pad; - h--; - } - } else { - // 256-color palette - while (h > 0) { - int endOfRow = ptr + w; - while (ptr < endOfRow) { - buf[ptr++] = palette[netbuf[srcPtr++] & 0xff]; - } - ptr += pad; - h--; - } + switch (pf.bpp) { + case 8: + FilterPalette8(palette, palSize, + bufptr, outbuf, stride, r); + break; + case 16: + FilterPalette16(palette.asShortBuffer(), palSize, + bufptr, outbuf.asShortBuffer(), stride, r); + break; + case 32: + FilterPalette32(palette.asIntBuffer(), palSize, + bufptr, outbuf.asIntBuffer(), stride, r); + break; } } - handler.imageRect(r, buf); - - if (streamId != -1) { - zis[streamId].reset(); - } - } + pb.imageRect(pf, r, outbuf.array()); - final private void DECOMPRESS_JPEG_RECT(Rect r, InStream is, CMsgHandler handler) - { - // Read length - int compressedLen = is.readCompactLength(); - if (compressedLen <= 0) - vlog.info("Incorrect data received from the server."); - - // Allocate netbuf and read in data - byte[] netbuf = new byte[compressedLen]; - is.readBytes(netbuf, 0, compressedLen); - - // Create an Image object from the JPEG data. - Image jpeg = tk.createImage(netbuf); - jpeg.setAccelerationPriority(1); - handler.imageRect(r, jpeg); - jpeg.flush(); } - final private void FilterGradient24(byte[] netbuf, int[] buf, int stride, - Rect r) + final private void FilterGradient24(ByteBuffer inbuf, + PixelFormat pf, ByteBuffer outbuf, + int stride, Rect r) { - int x, y, c; byte[] prevRow = new byte[TIGHT_MAX_WIDTH*3]; byte[] thisRow = new byte[TIGHT_MAX_WIDTH*3]; - byte[] pix = new byte[3]; + ByteBuffer pix = ByteBuffer.allocate(3); int[] est = new int[3]; // Set up shortcut variables @@ -273,38 +424,38 @@ public class TightDecoder extends Decoder { for (y = 0; y < rectHeight; y++) { /* First pixel in a row */ for (c = 0; c < 3; c++) { - pix[c] = (byte)(netbuf[y*rectWidth*3+c] + prevRow[c]); - thisRow[c] = pix[c]; + pix.put(c, (byte)(inbuf.get(y*rectWidth*3+c) + prevRow[c])); + thisRow[c] = pix.get(c); } - serverpf.bufferFromRGB(buf, y*stride, pix, 0, 1); + pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride), pix, 1); /* Remaining pixels of a row */ for (x = 1; x < rectWidth; x++) { for (c = 0; c < 3; c++) { - est[c] = (int)(prevRow[x*3+c] + pix[c] - prevRow[(x-1)*3+c]); - if (est[c] > 0xFF) { - est[c] = 0xFF; + est[c] = prevRow[x*3+c] + pix.get(c) - prevRow[(x-1)*3+c]; + if (est[c] > 0xff) { + est[c] = 0xff; } else if (est[c] < 0) { est[c] = 0; } - pix[c] = (byte)(netbuf[(y*rectWidth+x)*3+c] + est[c]); - thisRow[x*3+c] = pix[c]; + pix.put(c, (byte)(inbuf.get((y*rectWidth+x)*3+c) + est[c])); + thisRow[x*3+c] = pix.get(c); } - serverpf.bufferFromRGB(buf, y*stride+x, pix, 0, 1); + pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride+x), pix, 1); } System.arraycopy(thisRow, 0, prevRow, 0, prevRow.length); } } - final private void FilterGradient(byte[] netbuf, int[] buf, int stride, - Rect r) + final private void FilterGradient(ByteBuffer inbuf, + PixelFormat pf, ByteBuffer outbuf, + int stride, Rect r) { - int x, y, c; byte[] prevRow = new byte[TIGHT_MAX_WIDTH]; byte[] thisRow = new byte[TIGHT_MAX_WIDTH]; - byte[] pix = new byte[3]; + ByteBuffer pix = ByteBuffer.allocate(3); int[] est = new int[3]; // Set up shortcut variables @@ -313,19 +464,18 @@ public class TightDecoder extends Decoder { for (y = 0; y < rectHeight; y++) { /* First pixel in a row */ - // FIXME - //serverpf.rgbFromBuffer(pix, 0, netbuf, y*rectWidth, 1, cm); + pf.rgbFromBuffer(pix, (ByteBuffer)inbuf.position(y*rectWidth), 1); for (c = 0; c < 3; c++) - pix[c] += prevRow[c]; + pix.put(c, (byte)(pix.get(c) + prevRow[c])); - System.arraycopy(pix, 0, thisRow, 0, pix.length); + System.arraycopy(pix.array(), 0, thisRow, 0, pix.capacity()); - serverpf.bufferFromRGB(buf, y*stride, pix, 0, 1); + pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride), pix, 1); /* Remaining pixels of a row */ for (x = 1; x < rectWidth; x++) { for (c = 0; c < 3; c++) { - est[c] = (int)(prevRow[x*3+c] + pix[c] - prevRow[(x-1)*3+c]); + est[c] = prevRow[x*3+c] + pix.get(c) - prevRow[(x-1)*3+c]; if (est[c] > 0xff) { est[c] = 0xff; } else if (est[c] < 0) { @@ -333,24 +483,156 @@ public class TightDecoder extends Decoder { } } - // FIXME - //serverpf.rgbFromBuffer(pix, 0, netbuf, y*rectWidth+x, 1, cm); + pf.rgbFromBuffer(pix, (ByteBuffer)inbuf.position(y*rectWidth+x), 1); for (c = 0; c < 3; c++) - pix[c] += est[c]; + pix.put(c, (byte)(pix.get(c) + est[c])); - System.arraycopy(pix, 0, thisRow, x*3, pix.length); + System.arraycopy(pix.array(), 0, thisRow, x*3, pix.capacity()); - serverpf.bufferFromRGB(buf, y*stride+x, pix, 0, 1); + pf.bufferFromRGB((ByteBuffer)outbuf.position(y*stride+x), pix, 1); } System.arraycopy(thisRow, 0, prevRow, 0, prevRow.length); } } - private CMsgReader reader; + private void FilterPalette8(ByteBuffer palette, int palSize, + ByteBuffer inbuf, ByteBuffer outbuf, + int stride, Rect r) + { + // Indexed color + int x, h = r.height(), w = r.width(), b, pad = stride - w; + ByteBuffer ptr = outbuf.duplicate(); + byte bits; + ByteBuffer srcPtr = inbuf.duplicate(); + if (palSize <= 2) { + // 2-color palette + while (h > 0) { + for (x = 0; x < w / 8; x++) { + bits = srcPtr.get(); + for (b = 7; b >= 0; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + if (w % 8 != 0) { + bits = srcPtr.get(); + for (b = 7; b >= 8 - w % 8; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + ptr.position(ptr.position() + pad); + h--; + } + } else { + // 256-color palette + while (h > 0) { + int endOfRow = ptr.position() + w; + while (ptr.position() < endOfRow) { + ptr.put(palette.get(srcPtr.get())); + } + ptr.position(ptr.position() + pad); + h--; + } + } + } + + private void FilterPalette16(ShortBuffer palette, int palSize, + ByteBuffer inbuf, ShortBuffer outbuf, + int stride, Rect r) + { + // Indexed color + int x, h = r.height(), w = r.width(), b, pad = stride - w; + ShortBuffer ptr = outbuf.duplicate(); + byte bits; + ByteBuffer srcPtr = inbuf.duplicate(); + if (palSize <= 2) { + // 2-color palette + while (h > 0) { + for (x = 0; x < w / 8; x++) { + bits = srcPtr.get(); + for (b = 7; b >= 0; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + if (w % 8 != 0) { + bits = srcPtr.get(); + for (b = 7; b >= 8 - w % 8; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + ptr.position(ptr.position() + pad); + h--; + } + } else { + // 256-color palette + while (h > 0) { + int endOfRow = ptr.position() + w; + while (ptr.position() < endOfRow) { + ptr.put(palette.get(srcPtr.get())); + } + ptr.position(ptr.position() + pad); + h--; + } + } + } + + private void FilterPalette32(IntBuffer palette, int palSize, + ByteBuffer inbuf, IntBuffer outbuf, + int stride, Rect r) + { + // Indexed color + int x, h = r.height(), w = r.width(), b, pad = stride - w; + IntBuffer ptr = outbuf.duplicate(); + byte bits; + ByteBuffer srcPtr = inbuf.duplicate(); + if (palSize <= 2) { + // 2-color palette + while (h > 0) { + for (x = 0; x < w / 8; x++) { + bits = srcPtr.get(); + for (b = 7; b >= 0; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + if (w % 8 != 0) { + bits = srcPtr.get(); + for (b = 7; b >= 8 - w % 8; b--) { + ptr.put(palette.get(bits >> b & 1)); + } + } + ptr.position(ptr.position() + pad); + h--; + } + } else { + // 256-color palette + while (h > 0) { + int endOfRow = ptr.position() + w; + while (ptr.position() < endOfRow) { + ptr.put(palette.get(srcPtr.get() & 0xff)); + } + ptr.position(ptr.position() + pad); + h--; + } + } + } + + public final int readCompact(InStream is) { + byte b; + int result; + + b = (byte)is.readU8(); + result = (int)b & 0x7F; + if ((b & 0x80) != 0) { + b = (byte)is.readU8(); + result |= ((int)b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = (byte)is.readU8(); + result |= ((int)b & 0xFF) << 14; + } + } + return result; + } + private ZlibInStream[] zis; - private PixelFormat serverpf; - private PixelFormat clientpf; - static LogWriter vlog = new LogWriter("TightDecoder"); } diff --git a/java/com/tigervnc/rfb/ZRLEDecoder.java b/java/com/tigervnc/rfb/ZRLEDecoder.java index e706510f..c1f908ab 100644 --- a/java/com/tigervnc/rfb/ZRLEDecoder.java +++ b/java/com/tigervnc/rfb/ZRLEDecoder.java @@ -18,22 +18,143 @@ package com.tigervnc.rfb; +import java.awt.image.*; +import java.nio.*; +import java.util.*; + import com.tigervnc.rdr.*; public class ZRLEDecoder extends Decoder { - public ZRLEDecoder(CMsgReader reader_) { - reader = reader_; + private static int readOpaque24A(InStream is) + { + is.check(3); + ByteBuffer r = ByteBuffer.allocate(4); + r.put(0, (byte)is.readU8()); + r.put(1, (byte)is.readU8()); + r.put(2, (byte)is.readU8()); + return ((ByteBuffer)r.rewind()).getInt(); + } + + private static int readOpaque24B(InStream is) + { + is.check(3); + ByteBuffer r = ByteBuffer.allocate(4); + r.put(2, (byte)is.readU8()); + r.put(1, (byte)is.readU8()); + r.put(0, (byte)is.readU8()); + return ((ByteBuffer)r.rewind()).getInt(); + } + + public ZRLEDecoder() { + super(DecoderFlags.DecoderOrdered); zis = new ZlibInStream(); } - public void readRect(Rect r, CMsgHandler handler) { - InStream is = reader.getInStream(); - int[] buf = reader.getImageBuf(64 * 64 * 4); - int bpp = handler.cp.pf().bpp; - int bytesPerPixel = (bpp > 24 ? 3 : bpp / 8); - boolean bigEndian = handler.cp.pf().bigEndian; + public void readRect(Rect r, InStream is, + ConnParams cp, OutStream os) + { + int len; + len = is.readU32(); + os.writeU32(len); + os.copyBytes(is, len); + } + + public void decodeRect(Rect r, Object buffer, + int buflen, ConnParams cp, + ModifiablePixelBuffer pb) + { + MemInStream is = new MemInStream((byte[])buffer, 0, buflen); + PixelFormat pf = cp.pf(); + ByteBuffer buf = ByteBuffer.allocate(64 * 64 * 4); + switch (pf.bpp) { + case 8: zrleDecode8(r, is, zis, buf, pf, pb); break; + case 16: zrleDecode16(r, is, zis, buf, pf, pb); break; + case 32: + int maxPixel = pf.pixelFromRGB(-1, -1, -1, pf.getColorModel()); + boolean fitsInLS3Bytes = maxPixel < (1<<24); + boolean fitsInMS3Bytes = (maxPixel & 0xff) == 0; + + if ((fitsInLS3Bytes && pf.isLittleEndian()) || + (fitsInMS3Bytes && pf.isBigEndian())) + { + zrleDecode24A(r, is, zis, buf, pf, pb); + } + else if ((fitsInLS3Bytes && pf.isBigEndian()) || + (fitsInMS3Bytes && pf.isLittleEndian())) + { + zrleDecode24B(r, is, zis, buf, pf, pb); + } + else + { + zrleDecode32(r, is, zis, buf, pf, pb); + } + break; + } + } + + private static enum PIXEL_T { U8, U16, U24A, U24B, U32 }; + + private static ByteBuffer READ_PIXEL(InStream is, PIXEL_T type) { + ByteBuffer b = ByteBuffer.allocate(4); + switch (type) { + case U8: + b.putInt(is.readOpaque8()); + return (ByteBuffer)ByteBuffer.allocate(1).put(b.get(3)).rewind(); + case U16: + b.putInt(is.readOpaque16()); + return (ByteBuffer)ByteBuffer.allocate(2).put(b.array(), 2, 2).rewind(); + case U24A: + return (ByteBuffer)b.putInt(readOpaque24A(is)).rewind(); + case U24B: + return (ByteBuffer)b.putInt(readOpaque24B(is)).rewind(); + case U32: + default: + return (ByteBuffer)b.putInt(is.readOpaque32()).rewind(); + } + } + + private void zrleDecode8(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb) + { + ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U8); + } + + private void zrleDecode16(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb) + { + ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U16); + } + + private void zrleDecode24A(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb) + { + ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U24A); + } + + private void zrleDecode24B(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb) + { + ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U24B); + } + + private void zrleDecode32(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb) + { + ZRLE_DECODE(r, is, zis, buf, pf, pb, PIXEL_T.U32); + } + + private void ZRLE_DECODE(Rect r, InStream is, + ZlibInStream zis, ByteBuffer buf, + PixelFormat pf, ModifiablePixelBuffer pb, + PIXEL_T pix_t) + { int length = is.readU32(); zis.setUnderlying(is, length); Rect t = new Rect(); @@ -49,13 +170,16 @@ public class ZRLEDecoder extends Decoder { int mode = zis.readU8(); boolean rle = (mode & 128) != 0; int palSize = mode & 127; - int[] palette = new int[128]; + ByteBuffer palette = ByteBuffer.allocate(128 * pf.bpp/8); - zis.readPixels(palette, palSize, bytesPerPixel, bigEndian); + for (int i = 0; i < palSize; i++) { + palette.put(READ_PIXEL(zis, pix_t)); + } if (palSize == 1) { - int pix = palette[0]; - handler.fillRect(t, pix); + ByteBuffer pix = + ByteBuffer.allocate(pf.bpp/8).put(palette.array(), 0, pf.bpp/8); + pb.fillRect(pf, t, pix.array()); continue; } @@ -63,8 +187,17 @@ public class ZRLEDecoder extends Decoder { if (palSize == 0) { // raw - - zis.readPixels(buf, t.area(), bytesPerPixel, bigEndian); + switch (pix_t) { + case U24A: + case U24B: + ByteBuffer ptr = buf.duplicate(); + for (int iptr=0; iptr < t.area(); iptr++) { + ptr.put(READ_PIXEL(zis, pix_t)); + } + break; + default: + zis.readBytes(buf, t.area() * (pf.bpp/8)); + } } else { @@ -72,21 +205,21 @@ public class ZRLEDecoder extends Decoder { int bppp = ((palSize > 16) ? 8 : ((palSize > 4) ? 4 : ((palSize > 2) ? 2 : 1))); - int ptr = 0; + ByteBuffer ptr = buf.duplicate(); for (int i = 0; i < t.height(); i++) { - int eol = ptr + t.width(); + int eol = ptr.position() + t.width()*pf.bpp/8; int b = 0; int nbits = 0; - while (ptr < eol) { + while (ptr.position() < eol) { if (nbits == 0) { b = zis.readU8(); nbits = 8; } nbits -= bppp; int index = (b >> nbits) & ((1 << bppp) - 1) & 127; - buf[ptr++] = palette[index]; + ptr.put(palette.array(), index*pf.bpp/8, pf.bpp/8); } } } @@ -97,10 +230,10 @@ public class ZRLEDecoder extends Decoder { // plain RLE - int ptr = 0; - int end = ptr + t.area(); - while (ptr < end) { - int pix = zis.readPixel(bytesPerPixel, bigEndian); + ByteBuffer ptr = buf.duplicate(); + int end = ptr.position() + t.area()*pf.bpp/8; + while (ptr.position() < end) { + ByteBuffer pix = READ_PIXEL(zis, pix_t); int len = 1; int b; do { @@ -108,19 +241,21 @@ public class ZRLEDecoder extends Decoder { len += b; } while (b == 255); - if (!(len <= end - ptr)) - throw new Exception("ZRLEDecoder: assertion (len <= end - ptr)" - +" failed"); + if (end - ptr.position() < len*(pf.bpp/8)) { + System.err.println("ZRLE decode error\n"); + throw new Exception("ZRLE decode error"); + } + + while (len-- > 0) ptr.put(pix); - while (len-- > 0) buf[ptr++] = pix; } } else { // palette RLE - int ptr = 0; - int end = ptr + t.area(); - while (ptr < end) { + ByteBuffer ptr = buf.duplicate(); + int end = ptr.position() + t.area()*pf.bpp/8; + while (ptr.position() < end) { int index = zis.readU8(); int len = 1; if ((index & 128) != 0) { @@ -130,27 +265,26 @@ public class ZRLEDecoder extends Decoder { len += b; } while (b == 255); - if (!(len <= end - ptr)) - throw new Exception("ZRLEDecoder: assertion " - +"(len <= end - ptr) failed"); + if (end - ptr.position() < len*(pf.bpp/8)) { + System.err.println("ZRLE decode error\n"); + throw new Exception("ZRLE decode error"); + } } index &= 127; - int pix = palette[index]; + while (len-- > 0) ptr.put(palette.array(), index*pf.bpp/8, pf.bpp/8); - while (len-- > 0) buf[ptr++] = pix; } } } - handler.imageRect(t, buf); + pb.imageRect(pf, t, buf.array()); } } - zis.reset(); + zis.removeUnderlying(); } - CMsgReader reader; - ZlibInStream zis; + private ZlibInStream zis; } 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(); 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 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 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 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 callbacks = new HashMap(); + private static Map callbacks = new HashMap(); /* 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 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 iter : callbacks.entrySet()) { - Object obj = iter.getValue(); - Method cb = obj.getClass().getMethod(iter.getKey(), new Class[]{}); + for (Map.Entry 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 [:] or "+ - "::", - ""); - - /* - 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 [:] or ::", + ""); 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); -- 2.39.5