/* # 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 /** * 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(NULL), _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 NULL * our reference to it. It will finish running in a detached state and finally * delete itself upon completion. */ void VncWindow::terminateVncThread() { if (_vncWorker == NULL) 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 = NULL; 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 == NULL) return; QSharedPointer 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 == NULL || remote->size() == _remoteSize)) return false; const QSize mySize = this->size(); const QSize remoteSize = remote == NULL ? _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 == NULL) 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 != NULL && !_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 == NULL || !_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); } }