view src/viewer_swing/java/com/glavsoft/viewer/Viewer.java @ 468:f8a88cdb857b

fix retina frame buffer position
author mir3636
date Fri, 29 Jul 2016 15:30:48 +0900
parents fd803266ade7
children 3332879d1bd0
line wrap: on
line source

// Copyright (C) 2010, 2011, 2012, 2013 GlavSoft LLC.
// All rights reserved.
//
//-------------------------------------------------------------------------
// This file is part of the TightVNC software.  Please visit our Web site:
//
//                       http://www.tightvnc.com/
//
// This program 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 program 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 program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//-------------------------------------------------------------------------
//

package com.glavsoft.viewer;

import com.glavsoft.rfb.protocol.ProtocolSettings;
import com.glavsoft.transport.Reader;
import com.glavsoft.transport.Writer;
import com.glavsoft.viewer.cli.Parser;
import com.glavsoft.viewer.swing.ConnectionParams;
import com.glavsoft.viewer.swing.ParametersHandler;
import com.glavsoft.viewer.swing.SwingViewerWindow;
import jp.ac.u_ryukyu.treevnc.CreateConnectionParam;
import jp.ac.u_ryukyu.treevnc.TreeRFBProto;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

@SuppressWarnings("serial")
public class Viewer extends JApplet implements Runnable, WindowListener , ViewerInterface {

    private Logger logger;
    public int paramsMask;
    public boolean allowAppletInteractiveConnections;

    public final ConnectionParams connectionParams;
    public String passwordFromParams;
    public boolean isSeparateFrame = true;
    public boolean isApplet = true;
    public final ProtocolSettings settings;
    public UiSettings uiSettings;
    private volatile boolean isAppletStopped = false;
    private ConnectionPresenter connectionPresenter;
    boolean isTreeVNC = false;
    public TreeRFBProto myRfb;
    public boolean noConnection;
    public int vncport = ConnectionParams.DEFAULT_RFB_PORT;
    private int fbWidth;
    private boolean showTree = false;
    public int width;
    public int height;
    public int fixingSizeWidth;
    public int fixingSizeHeight;

    public static void main(String[] args) {
        Parser parser = new Parser();
        ParametersHandler.completeParserOptions(parser);

        parser.parse(args);
        if (parser.isSet(ParametersHandler.ARG_HELP)) {
            printUsage(parser.optionsUsage());
            System.exit(0);
        }
        Viewer viewer = new Viewer(parser);
        SwingUtilities.invokeLater(viewer);
    }

    public static void printUsage(String additional) {
        System.out.println("Usage: java -jar (progfilename) [hostname [port_number]] [Options]¥n" +
                "    or¥n" +
                " java -jar (progfilename) [Options]¥n" +
                "    or¥n java -jar (progfilename) -help¥n    to view this help¥n¥n" +
                "Where Options are:¥n" + additional +
                "¥nOptions format: -optionName=optionValue. Ex. -host=localhost -port=5900 -viewonly=yes¥n" +
                "Both option name and option value are case insensitive.");
    }

    public Viewer() {
        logger = Logger.getLogger(getClass().getName());
        connectionParams = new ConnectionParams();
        settings = ProtocolSettings.getDefaultSettings();
        uiSettings = new UiSettings();
    }

    private Viewer(Parser parser) {
        this();
        setLoggingLevel(parser.isSet(ParametersHandler.ARG_VERBOSE) ? Level.FINE :
                parser.isSet(ParametersHandler.ARG_VERBOSE_MORE) ? Level.FINER :
                        Level.INFO);

        paramsMask = ParametersHandler.completeSettingsFromCLI(parser, connectionParams, settings, uiSettings);
        passwordFromParams = parser.getValueFor(ParametersHandler.ARG_PASSWORD);
        logger.info("TightVNC Viewer version " + ver());
        isApplet = false;
    }

    private void setLoggingLevel(Level levelToSet) {
        final Logger appLogger = Logger.getLogger("com.glavsoft");
        appLogger.setLevel(levelToSet);
        ConsoleHandler ch = null;
        for (Handler h : appLogger.getHandlers()) {
            if (h instanceof ConsoleHandler) {
                ch = (ConsoleHandler) h;
                break;
            }
        }
        if (null == ch) {
            ch = new ConsoleHandler();
            appLogger.addHandler(ch);
        }
        //        ch.setFormatter(new SimpleFormatter());
        ch.setLevel(levelToSet);
    }


    @Override
    public void windowClosing(WindowEvent e) {
        if (e != null && e.getComponent() != null) {
            final Window w = e.getWindow();
            if (w != null) {
                w.setVisible(false);
                w.dispose();
            }
        }
        closeApp();
    }

    /**
     * Closes App(lication) or stops App(let).
     */
    public void closeApp() {
        if (connectionPresenter != null) {
            connectionPresenter.cancelConnection();
            logger.info("Connections cancelled.");
        }
        if (isApplet) {
            if ( ! isAppletStopped) {
                logger.severe("Applet is stopped.");
                isAppletStopped  = true;
                repaint();
                stop();
            }
        } else {
            System.exit(0);
        }
    }

    @Override
    public void paint(Graphics g) {
        if ( ! isAppletStopped) {
            super.paint(g);
        } else {
            getContentPane().removeAll();
            g.clearRect(0, 0, getWidth(), getHeight());
            g.drawString("Disconnected", 10, 20);
        }
    }

    @Override
    public void destroy() {
        closeApp();
        super.destroy();
    }

    @Override
    public void init() {
        paramsMask = ParametersHandler.completeSettingsFromApplet(this, connectionParams, settings, uiSettings);
        isSeparateFrame = ParametersHandler.isSeparateFrame;
        passwordFromParams = getParameter(ParametersHandler.ARG_PASSWORD);
        isApplet = true;
        allowAppletInteractiveConnections = ParametersHandler.allowAppletInteractiveConnections;
        repaint();

        try {
            SwingUtilities.invokeAndWait(this);
        } catch (Exception e) {
            logger.severe(e.getMessage());
        }
    }

    @Override
    public void start() {
        super.start();
    }

    public boolean checkJsch() {
        try {
            Class.forName("com.jcraft.jsch.JSch");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    public void setNoConnection(boolean c){
        noConnection = c;
    }

    @Override
    public void run() {
        final boolean hasJsch = checkJsch();
        final boolean allowInteractive = allowAppletInteractiveConnections || ! isApplet;
        connectionPresenter = new ConnectionPresenter(hasJsch, allowInteractive);
        connectionPresenter.setNoConnection(noConnection);
        connectionPresenter.addModel("ConnectionParamsModel", connectionParams);
        connectionPresenter.startVNCConnection(this, false, null, null);
    }

    @Override
    public ConnectionPresenter getConnectionPresenter() {
        return connectionPresenter;
    }

    @Override
    public void setConnectionPresenter(ConnectionPresenter connectionPresenter) {
        this.connectionPresenter = connectionPresenter;
    }

    @Override
    public void windowOpened(WindowEvent e) { /* nop */ }
    @Override
    public void windowClosed(WindowEvent e) { /* nop */ }
    @Override
    public void windowIconified(WindowEvent e) { /* nop */ }
    @Override
    public void windowDeiconified(WindowEvent e) { /* nop */ }
    @Override
    public void windowActivated(WindowEvent e) { /* nop */ }
    @Override
    public void windowDeactivated(WindowEvent e) { /* nop */ }

    public static String ver() {
        final InputStream mfStream = Viewer.class.getClassLoader().getResourceAsStream(
                "META-INF/MANIFEST.MF");
        if (null == mfStream) {
            System.out.println("No Manifest file found.");
            return "-1";
        }
        try {
            Manifest mf = new Manifest();
            mf.read(mfStream);
            Attributes atts = mf.getMainAttributes();
            return atts.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
        } catch (IOException e) {
            return "-2";
        }
    }


    public void setSocket(Socket soc) { 
        connectionParams.setConnectionParam(soc.getInetAddress().getHostAddress(),soc.getPort());
    }

    public void setOpenPort(int parseInt) {
    }

    public void setTerminationType(boolean b) {
        myRfb.setTerminationType(b);
    }

    /**
     * Start client with new parent (including reconnection) 
     * @param port
     * @param hostname
     * @throws IOException
     */
    @Override
    public void connectToParenet(int port, String hostname) throws IOException {
        setTerminationType(false);
        closeApp();
        connectionParams.setConnectionParam(hostname, port);
        run();
    }

    public void setIsTreeVNC(boolean flag) {
        isTreeVNC = flag;
    }

    public TreeRFBProto getRfb() {
        return myRfb;
    }

    public boolean getCuiVersion() {
        return myRfb.getCuiVersion();
    }
    public void setCuiVersion(boolean flag) {
        myRfb.setCuiVersion(flag);
    }

    public void setVncport(int vncport) {
        this.vncport = vncport;
    }

    /**
     * start new VNC server receiver with
     * inherited clients
     * @param hostName
     * @param newVNCServerId
     * @param x
     * @param y
     * @param width
     * @param height
     * @param scale
     */
    @Override
    public void inhelitClients(String hostName, short newVNCServerId, int x, int y, int width, int height, int scale) {
        final ConnectionPresenter connectionPresenter = createNewConnectionPresenter(hostName, newVNCServerId, x, y, width, height, scale);
        isApplet = true;
        this.setNoConnection(false);
        final Viewer v = this;
        new Thread(new Runnable() {
            @Override
            public void run() {
                connectionPresenter.startVNCConnection(v, false, null, null);
            }
        }, "ServerChangeThread").start();
    }

    public void changeToDirectConnectedServer(String hostName, Reader is, Writer os, int x, int y, int width, int height, int scale) {
        final ConnectionPresenter connectionPresenter = createNewConnectionPresenter(hostName, (short) -1, x, y, width, height, scale);
        connectionPresenter.startVNCConnection(this, true, is, os);
    }

    private ConnectionPresenter createNewConnectionPresenter(String hostName, short newVNCServerId, int x, int y, int width, int height, int scale) {
        final boolean hasJsch = checkJsch();
        final boolean allowInteractive = allowAppletInteractiveConnections || ! isApplet;
        final ConnectionPresenter connectionPresenter = new ConnectionPresenter(hasJsch, allowInteractive);
        ConnectionParams connectionParams = new ConnectionParams();
        connectionParams.setConnectionParam(hostName, vncport);
        connectionPresenter.addModel("ConnectionParamsModel", connectionParams);
        connectionPresenter.setConnectionParams(connectionParams);
        connectionPresenter.setReconnectingId(newVNCServerId);
        connectionPresenter.setIsTreeVNC(true);
        connectionPresenter.setNoConnection(false);
        connectionPresenter.setX(x);
        connectionPresenter.setY(y);
        connectionPresenter.setSingleWidth(width);
        connectionPresenter.setSingleHeight(height);
        connectionPresenter.setRetinaScale(scale);
        // System.out.println("Sarver change accepted from id :" + newVNCServerId);
        return connectionPresenter;
    }

    /**
     * start TreeVNC viewer
     */
    public void startTreeViewer(String hostName, boolean cui, boolean addSerialNum) {
        TreeRFBProto rfb = new TreeRFBProto(false, this);
        rfb.setCuiVersion(cui);
        rfb.setAddSerialNum(addSerialNum);
        rfb.setHasViewer(true);
        rfb.createConnectionAndStart(this);
        CreateConnectionParam cp = new CreateConnectionParam(rfb);
        if (hostName!=null) {
            cp.setHostName(hostName);
        } else {
            cp.findTreeVncRoot();

            // selected "Start Display Mode" or "Start as TreeVNC Root" for start selection panel
            if (cp.isDisplayMode() || cp.isRootMode()) {
                myRfb = rfb;
                myRfb.setIsTreeManager(true);
                return;
            }
        }
        cp.sendWhereToConnect(this);
        isTreeVNC = true;
        myRfb =  rfb;
        settings.setViewOnly(true); // too avoid unnecessary upward traffic
        rfb.getAcceptThread().waitForShutdown();
    }

    public void proxyStart(String[] argv, String hostName, int width, int height, boolean showTree, boolean checkDelay, boolean addSerialNum, boolean fixingSize, boolean filterSingleDisplay, boolean hasViewer) {
        fbWidth = width;
        this.showTree = showTree;
        Parser parser = new Parser();
        ParametersHandler.completeParserOptions(parser);
        if (fbWidth == 0)
            parser.parse(argv);
        if (parser.isSet(ParametersHandler.ARG_HELP)) {
            printUsage(parser.optionsUsage());
            System.exit(0);
        }

        // myRfb is null if use command line option "-d" "-p"
        // myRfb not null if use start panel
        if (myRfb == null) {
            myRfb = new TreeRFBProto(true, this);
        }
        myRfb.setCuiVersion(!hasViewer);
        myRfb.setHasViewer(hasViewer);
        myRfb.setShowTree(showTree);
        myRfb.setCheckDelay(checkDelay);
        myRfb.setAddSerialNum(addSerialNum);
        myRfb.setFixingSize(fixingSize);
        if(fixingSize) {
            myRfb.fixingSizeWidth = fixingSizeWidth;
            myRfb.fixingSizeHeight = fixingSizeHeight;
        }
        if (myRfb.getAcceptThread() == null) {
            myRfb.createConnectionAndStart(this);
        } else {
            myRfb.startTreeRootFindThread();
        }
        setIsTreeVNC(true);
        if (hostName == null) {
            hostName = "localhost";
        }
        connectionParams.setConnectionParam(hostName, vncport);
        isApplet = true;
        settings.setViewOnly(true); // to avoid unnecessary upward traffic
        run();
    }

    public void initRoot(TreeRFBProto myRfbProto, String hostName) {
        setIsTreeVNC(true);
        connectionParams.setConnectionParam(hostName, vncport);
        isApplet = true;
        myRfbProto.createConnectionAndStart(this);
        run();
    }

    @Override
    public void setVisible(boolean b) {
        if(connectionPresenter == null) return;
        SwingViewerWindow v = connectionPresenter.getViewer();
        if (v != null) 
            v.setVisible(b);
    }

    @Override
    public Socket getVNCSocket() {
        return connectionPresenter.getSocket();
    }

    @Override
    public boolean getShowTree() {
        return showTree;
    }

    @Override
    public void setWidth(int w) {
        width = w;
    }

    @Override
    public void setHeight(int h) {
        height = h;
    }

    @Override
    public void setFixingSize(int width, int height) {
        this.fixingSizeWidth = width;
        this.fixingSizeHeight = height;
    }

    @Override
    public ArrayList<FbRectangle> getScreenRectangles() {
        // New screen server has one or more screens.
        // Screens are numbered in the order from left.
        // put screens in an ArrayList.
        ArrayList<Rectangle> rectangles = new ArrayList<Rectangle>();
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] gs = ge.getScreenDevices();

        for (GraphicsDevice gd : gs) {
            int screenNumber = 0;
            for (GraphicsConfiguration r : gd.getConfigurations()) {
                Rectangle rect = r.getBounds();
                //System.out.println("screen "+ screenNumber +":"+rect);
                rectangles.add(rect);
                screenNumber++;
            }
        }
        Point offset = getScreenOffset(rectangles);
        ArrayList<FbRectangle> fbRectangles = new ArrayList<FbRectangle>();
        int screenNumber = 0;
        for (Rectangle rect : rectangles) {
            FbRectangle fbrect = new FbRectangle(rect.x, rect.y, rect.width, rect.height, screenNumber, retinaScale(screenNumber));
            fbrect.setXfb(rect.x + offset.x);
            fbrect.setYfb(rect.y + offset.y);
            fbRectangles.add(fbrect);
            screenNumber++;
        }

        fbRectangles.sort(new Comparator<FbRectangle>() {
            @Override
            public int compare(FbRectangle t0, FbRectangle t1) {
                return t0.getX() < t1.getX() ? -1 : t0.getX() == t1.getX() ? 0 : 1;
            }
        });

        int retinaOffsetX = 0;
        FbRectangle fbRectanglesPrev = fbRectangles.get(0);
        for (int i = 1; i < fbRectangles.size(); i++) {
            FbRectangle fbRect = fbRectangles.get(i);
            if (fbRectanglesPrev.getRetinaScale() != 1) {
                if (fbRectanglesPrev.x + fbRectanglesPrev.width < fbRect.x) {
                    // previous screen is touch with this screen, fix retina scale offset
                    retinaOffsetX += fbRectanglesPrev.getWidth() * (fbRectanglesPrev.retinaScale - 1);
                }
            }
            fbRect.setXfb((int) (fbRect.getXfb() + retinaOffsetX));
            fbRectanglesPrev = fbRect;
        }

        fbRectangles.sort(new Comparator<FbRectangle>() {
            @Override
            public int compare(FbRectangle t0, FbRectangle t1) {
                return t0.getY() < t1.getY() ? -1 : t0.getY() == t1.getY() ? 0 : 1;
            }
        });

        int retinaOffsetY = 0;
        fbRectanglesPrev = fbRectangles.get(0);
        for (int i = 1; i < fbRectangles.size(); i++) {
            FbRectangle fbRect = fbRectangles.get(i);
            if (fbRectanglesPrev.getRetinaScale() != 1) {
                if (fbRectanglesPrev.y + fbRectanglesPrev.height < fbRect.y) {
                    // previous screen is touch with this screen, fix retina scale offset
                    retinaOffsetY += fbRectanglesPrev.getHeight() * (fbRectanglesPrev.retinaScale - 1);
                }
            }
            fbRect.setYfb((int) (fbRect.getYfb() + retinaOffsetY));
            fbRectanglesPrev = fbRect;
        }
        return fbRectangles;
    }

    public Point getScreenOffset(ArrayList<Rectangle> rectangles) {
        // position of screen may negative, but RFBscreen position is always positive
        // compute offset to make them positive
        int offsetx = 0;
        int offsety = 0;
        for (Rectangle rect : rectangles) {
            if (rect.x < offsetx) offsetx = rect.x;
            if (rect.y < offsety) offsety = rect.y;
        }
        return new Point(-offsetx,-offsety);
    }

    public int retinaScale(int shareScreenNumber) {
        int scale = 1;
        GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice[] devices = env.getScreenDevices();

        try {
            Field field = devices[shareScreenNumber].getClass().getDeclaredField("scale");

            if (field != null) {
                field.setAccessible(true);
                Object retinaScale = field.get(devices[shareScreenNumber]);

                if (retinaScale instanceof Integer) {
                    scale = (Integer) retinaScale;
                    return scale;
                }
            }
        } catch (Exception ignore) {}
        return scale;
    }

    @Override
    public void setFitScreen() {
        SwingViewerWindow v = connectionPresenter.getViewer();
        if (v != null) {
            v.fitScreen();
        }
    }

}