/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
* 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
* 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.event.*;
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.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.*;
public class DesktopWindow extends JFrame
{
static LogWriter vlog = new LogWriter("DesktopWindow");
public DesktopWindow(int w, int h, String name,
PixelFormat serverPF, CConn cc_)
{
cc = cc_;
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 windowLostFocus(WindowEvent e) {
cc.releaseDownKeys();
}
});
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();
}
});
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();
}
});
// 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();
}
}
});
}
// 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);
}
@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);
}
@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);
}
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();
}
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) {
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);
if (maximize.getValue())
setExtendedState(JFrame.MAXIMIZED_BOTH);
}
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;
// 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);
}
}
viewport.resize(0, 0, new_w, new_h);
// We might not resize the main window, so we need to manually call this
// to make sure the viewport is centered.
repositionViewport();
// 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 setCursor(int width, int height, Point hotspot,
byte[] data)
{
viewport.setCursor(width, height, hotspot, data);
}
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);
}
public boolean fullscreen_active()
{
return isUndecorated();
}
private void handleDesktopSize()
{
if (!desktopSize.getValue().equals("")) {
int width, height;
// An explicit size has been requested
if (desktopSize.getValue().split("x").length != 2)
return;
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 void handleResizeTimeout()
{
DesktopWindow self = (DesktopWindow)this;
assert(self != null);
self.remoteResize(self.w(), self.h());
}
private void remoteResize(int width, int height)
{
ScreenSet layout;
ListIterator<Screen> iter;
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.
layout = cc.cp.screenLayout;
// 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").
while (true) {
iter = layout.begin();
Screen screen = iter.next();
if (iter == layout.end())
break;
layout.remove_screen(screen.id);
}
}
// 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 {
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));
}
}
// 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;
}
cc.writer().writeSetDesktopSize(width, height, layout);
}
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());
}
}
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 boolean isMaximized()
{
int state = getExtendedState();
return ((state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
}
public Dimension getScreenSize() {
return getScreenBounds().getSize();
}
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;
}
public static Window getFullScreenWindow() {
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
for (GraphicsDevice gd : ge.getScreenDevices()) {
Window fullScreenWindow = gd.getFullScreenWindow();
if (fullScreenWindow != null)
return fullScreenWindow;
}
return null;
}
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);
}
}
public void handleOptions()
{
if (fullScreen.getValue() && !fullscreen_active())
fullscreen_on();
else if (!fullScreen.getValue() && fullscreen_active())
fullscreen_off();
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());
}
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);
}
repositionViewport();
lastScaleFactor = scaleString;
}
}
if (isVisible()) {
toFront();
requestFocus();
}
}
public void handleFullscreenTimeout()
{
DesktopWindow self = (DesktopWindow)this;
assert(self != null);
self.delayedFullscreen = false;
if (self.delayedDesktopSize) {
self.handleDesktopSize();
self.delayedDesktopSize = false;
}
}
private CConn cc;
private JScrollPane scroll;
public Viewport viewport;
private boolean firstUpdate;
private boolean delayedFullscreen;
private boolean delayedDesktopSize;
private boolean canDoLionFS;
private String lastScaleFactor;
private Rectangle lastBounds;
private int lastState;
private Timer timer;
}