summaryrefslogtreecommitdiffstats
path: root/src/client/vnc/vncwindow.cpp
blob: 79352ae130f4a084f1c77d537cb372d89ba4db63 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/*
 # 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(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<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 == 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);
	}
}