/*
# Copyright (c) 2009, 2010 - OpenSLX Project, Computer Center University of
# Freiburg
#
# This program is free software distributed under the GPL version 2.
# See http://openslx.org/COPYING
#
# If you have any feedback please consult http://openslx.org/feedback and
# send your suggestions, praise, or complaints to feedback@openslx.org
#
# General information about OpenSLX can be found at http://openslx.org/
# -----------------------------------------------------------------------------
# clientVNCViewer.cpp
# - connetct to vnc server and show remote screen (window/full)
# -----------------------------------------------------------------------------
*/
#include "vncwindow.h"
#include "vncthread.h"
#include "../clientapp/clientapp.h"
#include <QTimer>
/**
* Calc greatest common divisor.
*
* @param a one number
* @param b another number
* @return greatest common divisor of a and b
*/
static int gcd(int a, int b)
{
if (b == 0)
return a;
return gcd(b, a % b);
}
VncWindow::VncWindow(QWidget *parent) :
QWidget(parent), _srcStepX(1), _srcStepY(1), _dstStepX(1), _dstStepY(1),
_vncWorker(nullptr), _viewOnly(true), _multiScreen(false), _clientId(0), _redrawTimer(0), _tcpTimeoutTimer(0)
{
QTimer *upper = new QTimer(this);
connect(upper, SIGNAL(timeout()), this, SLOT(timer_moveToTop()));
upper->start(1111);
}
VncWindow::~VncWindow()
{
//
}
////////////////////////////////////////////////////////////////////////////////
// Private
/**
* Terminates the vnc worker thread and stops all related timers.
* The thread will be signalled to stop, but we don't wait for it
* to actually terminate. All signals of the thread are blocked, and we nullptr
* our reference to it. It will finish running in a detached state and finally
* delete itself upon completion.
*/
void VncWindow::terminateVncThread()
{
if (_vncWorker == nullptr)
return;
disconnect(_vncWorker, SIGNAL(projectionStopped()), this, SLOT(onProjectionStopped()));
disconnect(_vncWorker, SIGNAL(projectionStarted()), this, SLOT(onProjectionStarted()));
disconnect(_vncWorker, SIGNAL(imageUpdated(const int, const int, const int, const int)), this,
SLOT(onUpdateImage(const int, const int, const int, const int)));
_vncWorker->stop();
_vncWorker = nullptr;
if (_redrawTimer != 0) {
killTimer(_redrawTimer);
_redrawTimer = 0;
}
if (_tcpTimeoutTimer != 0) {
killTimer(_tcpTimeoutTimer);
_tcpTimeoutTimer = 0;
}
}
void VncWindow::deleteVncThread()
{
qDebug() << "Deleting thread" << QObject::sender();
delete QObject::sender();
}
/**
* Draws given part of the current VNC frame buffer to the window.
*
* @param x X offset of the region to draw
* @param y Y offset of the region to draw
* @param w width of the region to draw
* @param h height of the region to draw
*/
void VncWindow::draw(const int x, const int y, const int w, const int h)
{
if (_vncWorker == nullptr)
return;
QSharedPointer<QImage> buffer = _vncWorker->getFrameBuffer();
if (buffer.isNull())
return;
this->calcScaling(buffer.data());
QPainter painter(this);
if (_dstStepX > 1 || _dstStepY > 1) {
// Scaling is required as vnc server and client are using different resolutions
// Calc section offsets first
const int startX = x / _dstStepX;
const int startY = y / _dstStepY;
const int endX = (x + w - 1) / _dstStepX + 1;
const int endY = (y + h - 1) / _dstStepY + 1;
// Now pixel offsets for source
const int dstX = startX * _dstStepX;
const int dstY = startY * _dstStepY;
const int dstW = endX * _dstStepX - dstX;
const int dstH = endY * _dstStepY - dstY;
// Pixel offsets for destination
const int srcX = startX * _srcStepX;
const int srcY = startY * _srcStepY;
const int srcW = endX * _srcStepX - srcX;
const int srcH = endY * _srcStepY - srcY;
// Rescale
QImage scaled(
buffer->copy(srcX, srcY, srcW, srcH).scaled(dstW, dstH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
painter.drawImage(dstX, dstY, scaled, 0, 0, dstW, dstH);
} else {
// Same resolution, nothing to do
painter.drawImage(x, y, *buffer, x, y, w, h);
}
//painter.drawRect(x,y,w,h); // for debugging updated area
}
/**
* When using image scaling, calc matching pixel borders for both
* resolutions to prevent artifacts through bilinear scaling.
* If you simply round to the nearest number when scaling, you would
* slightly stretch or shrink the image area, which leads to strange
* sub-pixel-movements of parts of the image when being updated.
* The values calculated here are used to determine a larger area around
* the area that should actually be updated, that will not cause any
* artifacts when scaling down/up.
*
* @return whether scaling factors changed
*/
bool VncWindow::calcScaling(const QImage *remote)
{
if (this->size() == _desiredSize &&
(remote == nullptr || remote->size() == _remoteSize))
return false;
const QSize mySize = this->size();
const QSize remoteSize = remote == nullptr ? _remoteSize : remote->size();
if (mySize.isEmpty())
return false;
if (remoteSize.isEmpty())
return false;
_remoteSize = remoteSize;
_desiredSize = mySize;
const int gcdX = gcd(_desiredSize.width(), _remoteSize.width());
const int gcdY = gcd(_desiredSize.height(), _remoteSize.height());
_srcStepX = _remoteSize.width() / gcdX;
_srcStepY = _remoteSize.height() / gcdY;
_dstStepX = _desiredSize.width() / gcdX;
_dstStepY = _desiredSize.height() / gcdY;
qDebug() << "Scaling updated to" << _remoteSize << "->" << _desiredSize;
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Public
/**
* Show the VNC client window and connect to the given VNC server.
* Any currently active VNC client connection is signaled to terminate,
* and a new VNC client worker thread is created for the new connection.
*
* @param host IP address of VNC server to connect to
* @param port Port of VNC server to connect to
* @param passwd (view only) password of VNC server to connect to
* @param ro currently unused
* @param fullscreen display VNC image in fullscreen mode
* @param caption caption of window (only visible if not running in fullscreen mode)
* @param clientId the ID of the client we're connecting to (echoed back to server, not used locally)
*/
void VncWindow::open(const QString& host, int port, const QString& passwd, bool /* ro */ , bool fullscreen, const QString& caption, const int clientId, const QByteArray& rawThumb)
{
this->terminateVncThread();
_clientId = clientId;
_vncWorker = new VncThread(host, port, passwd, 1);
connect(_vncWorker, SIGNAL(projectionStopped()), this, SLOT(onProjectionStopped()), Qt::QueuedConnection);
connect(_vncWorker, SIGNAL(projectionStarted()), this, SLOT(onProjectionStarted()), Qt::QueuedConnection);
connect(_vncWorker, SIGNAL(imageUpdated(const int, const int, const int, const int)), this,
SLOT(onUpdateImage(const int, const int, const int, const int)),
Qt::QueuedConnection);
connect(_vncWorker, SIGNAL(finished()), this, SLOT(deleteVncThread()), Qt::QueuedConnection);
setWindowTitle(caption);
setAttribute(Qt::WA_OpaquePaintEvent);
_remoteThumb.loadFromData(rawThumb);
QSize size;
if (fullscreen) {
setWindowFlags(Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool); // | Qt::X11BypassWindowManagerHint); <- better, but window won't get any keyboard input
// Show projection on rightmost screen
QDesktopWidget *desktop = QApplication::desktop();
int ns = desktop->numScreens();
QRect best;
for (int i = 0; i < ns; ++i) {
QRect r = desktop->screenGeometry(i);
if (best.isNull() || r.left() > best.left()) {
best = r;
}
}
_multiScreen = ns > 1;
qDebug() << "Spawning at" << best;
size = best.size();
show();
setGeometry(best);
raise();
if (!_multiScreen) {
activateWindow();
}
move(best.topLeft());
} else {
setWindowFlags(Qt::Tool | Qt::WindowMaximizeButtonHint);
size = QSize(800, 600);
resize(size);
showNormal();
}
this->update();
_tcpTimeoutTimer = startTimer(10000);
_vncWorker->start(QThread::LowPriority);
}
/**
* Called by Qt if the window is requested to be closed. This can either happen
* through code (this->close()), or a keypress (Alt-F4). As long as
* Qt::WA_DeleteOnClose is not set the QWidget gets just hidden and not quit.
* See http://qt-project.org/doc/qt-4.8/qcloseevent.html#details for more
* details.
*
* @param e close event data
*/
void VncWindow::closeEvent(QCloseEvent * /* e */ )
{
qDebug("Closing VNC viewer window.");
this->terminateVncThread();
emit running(false, _clientId);
}
////////////////////////////////////////////////////////////////////////////////
// Slots
/**
* Triggered by imageUpdate signal from the _vncWorker thread telling
* that the given region of the image changed. Simply repaints the
* given area of the window.
* Since these are client coordinates we might have to do
* some scaling.
*
* @param x X offset of the region to update
* @param y Y offset of the region to update
* @param w width of the region to update
* @param h height of the region to update
*/
void VncWindow::onUpdateImage(const int x, const int y, const int w, const int h)
{
if (_vncWorker == nullptr)
return;
if (!this->calcScaling(_vncWorker->getFrameBuffer().data()) && !_remoteSize.isEmpty()) {
if (_srcStepX > 1 || _srcStepY > 1) {
this->update((x * _desiredSize.width()) / _remoteSize.width() - 2,
(y * _desiredSize.height()) / _remoteSize.height() - 2,
(w * _desiredSize.width()) / _remoteSize.width() + 4,
(h * _desiredSize.height()) / _remoteSize.height() + 4);
} else {
this->update(x, y, w, h);
}
} else {
// Changed, redraw everything
this->update();
}
}
/**
* Triggered by _vncWorker after successfully connecting to the VNC server.
* This emits a signal that will eventually lead to the ServerConnection
* telling the server that we're now watching another client.
*/
void VncWindow::onProjectionStarted()
{
_remoteThumb = QPixmap();
if (_tcpTimeoutTimer != 0) {
killTimer(_tcpTimeoutTimer);
_tcpTimeoutTimer = 0;
}
emit running(true, _clientId);
_redrawTimer = startTimer(500);
}
/**
* Triggered by _vncWorker when the connection to the VNC server is lost.
* We'll terminate the thread (detached) and close the window. A signal
* telling the server that we're not watching a VNC stream anymore will
* eventually be emited by the closeEvent.
*/
void VncWindow::onProjectionStopped()
{
this->terminateVncThread();
this->close();
}
void VncWindow::timer_moveToTop()
{
if (this->isHidden())
return;
if (!_multiScreen) {
activateWindow();
}
raise();
}
////////////////////////////////////////////////////////////////////////////////
// Protected
/**
* Called when a Qt timer fires.
*
* redrawTimer: Redraw whole viewer window.
*
* tcpTimeoutTimer: Check if we're connected, close window if not.
*
* @param event the timer event
*/
void VncWindow::timerEvent(QTimerEvent *event)
{
if (event->timerId() == _redrawTimer) {
killTimer(_redrawTimer);
_redrawTimer = 0;
this->update();
} else if (event->timerId() == _tcpTimeoutTimer) {
killTimer(_tcpTimeoutTimer);
_tcpTimeoutTimer = 0;
if (_vncWorker != nullptr && !_vncWorker->isConnected()) {
this->close();
}
} else
killTimer(event->timerId());
}
/**
* Called by Qt when a part of the window should be redrawn. This can either
* be caused by Qt (or the underlying graphical subsystem), or by
* an explicit call to QWidget::repaint()
*
* @param event the paint event, containing data like location and
* size area that should be repainted
*/
void VncWindow::paintEvent(QPaintEvent *event)
{
if (_vncWorker == nullptr || !_vncWorker->isConnected()) {
QPainter painter(this);
if (!_remoteThumb.isNull() && _remoteThumb.height() > 0) {
painter.drawPixmap(0, 0, this->width(), this->height(), _remoteThumb);
} else {
painter.fillRect(event->rect(), QColor(60, 63, 66));
}
QFontInfo fi = painter.fontInfo();
painter.setPen(QColor(200, 100, 10));
painter.setFont(QFont(fi.family(), 28, fi.weight(), fi.italic()));
painter.drawText(this->contentsRect(), Qt::AlignCenter, tr("Connecting..."));
} else {
const QRect &r = event->rect();
this->draw(r.left(), r.top(), r.width(), r.height());
}
event->accept();
}
/**
* Called when user releases a pressed key and the window has focus
*/
void VncWindow::keyReleaseEvent(QKeyEvent* event)
{
if (event->modifiers() == 0 && event->key() == Qt::Key_Escape && clientApp->isConnectedToLocalManager()) {
this->close();
} else {
QWidget::keyReleaseEvent(event);
}
}