/* # Copyright (c) 2009 - 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/ # ----------------------------------------------------------------------------- # mainWindow.cpp This is the Main class for the pvsManager. The GUI is constructed here. # ----------------------------------------------------------------------------- */ // Self #include "mainwindow.h" // QT stuff #include #include #include #include #include // Other custom UI elements #include "../serverapp/serverapp.h" #include "../clicklabel/clicklabel.h" #include "../sessionnamewindow/sessionnamewindow.h" #include "../connectionframe/connectionframe.h" #include "../helpwindow/helpwindow.h" #include "../reloadroomwindow/reloadroomwindow.h" // Network #include "../net/listenserver.h" #include "../net/client.h" #include "../net/discoverylistener.h" #include "../net/filedownloader.h" // Others #include "../../shared/settings.h" #include "../util/platform/screensaver.h" // Auto-generated ui class #include "ui_mainwindow.h" #include "ui_reloadroom.h" #include #include #define sStrTutorNdef MainWindow::tr("No tutor defined.") #define sStrTutorOffline MainWindow::tr("Tutor is offline.") #define sStrSourceNdef MainWindow::tr("Please select a projection source.") #define sStrSourceOffline MainWindow::tr("The projection source is offline.") #define sStrDestNdef MainWindow::tr("Please select a projection destination.") #define sStrDestOffline MainWindow::tr("The projection destination is offline.") #define sStrSourceDestSame MainWindow::tr("Selected projection target is tutor.") #define sStrClientOnline MainWindow::tr("Selected client is currently online.") #define sStrNoDestAv MainWindow::tr("No projection destination available.") using std::vector; using std::cout; using std::endl; /** * Initialize MainWindow and ListenServer. * @param ipListUrl * @param parent */ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), _tbIconSize(24), _tbArea(Qt::LeftToolBarArea), _lastClientCount(0) { qDebug() << "MainWindow(parent)"; _mode = Mode::Multicast; _streamingSource = 0; /* default value, these will be updated a room is loaded */ _tilesX = 10; _tilesY = 10; _virtCols = 0; _virtRows = 0; _sessionNameWindow = new SessionNameWindow(this); _reloadWindow = new ReloadRoomWindow(this); _reloadWindow->setWindowTitle(tr("Reload Room")); ui->setupUi(this); setWindowFlags(Qt::FramelessWindowHint); serverApp->setSessionName(); //conWin = new ConnectionWindow(ui->widget); //ui->VconWinLayout->addWidget(conWin); //conList = new ConnectionList(ui->ClWidget); //ui->ClientGLayout->addWidget(conList); // Show only session name label, not textbox _sessionNameLabel = new ClickLabel(this); ui->mainLayout->addWidget(_sessionNameLabel); ui->frmRoom->setStyleSheet("background-color:#444"); // toolbar and actions in pvsmgr ui->action_Exit->setStatusTip(tr("Exit")); ui->action_Lock->setStatusTip(tr("Lock or Unlock all Clients")); /* the label */ _examModeLabel = new QLabel("Klausur-\nModus"); _examModeLabel->setObjectName("examModeLabel"); _examModeLabel->setAlignment(Qt::AlignCenter); _examModeLabel->setFixedHeight(400); ui->toolBar->insertWidget(ui->action_TutorToStudent, _examModeLabel); _dropMarker = new QLabel(ui->frmRoom); _dropMarker->setStyleSheet("background-color: #448; border-radius: 2px;"); _dropMarker->hide(); _helpWindow = new HelpWindow(ui->toolBar->actions(), this); serverApp->setExam(false); // Close button in tool bar connect(ui->action_Exit, SIGNAL(triggered()), this, SLOT(onButtonExit())); connect(ui->action_TutorToAll, SIGNAL(triggered()), this, SLOT(onButtonTutorToAll())); connect(ui->action_StudentToTutor, SIGNAL(triggered()), this, SLOT(onButtonStudentToTutor())); connect(ui->action_StudentToTutorExclusive, SIGNAL(triggered()), this, SLOT(onButtonStudentToTutorExclusive())); connect(ui->action_TutorToStudent, SIGNAL(triggered()), this, SLOT(onButtonTutorToStudent())); connect(ui->action_StopProjection, SIGNAL(triggered()), this, SLOT(onButtonStopProjection())); connect(ui->action_SetAsTutor, SIGNAL(triggered()), this, SLOT(onButtonSetAsTutor())); connect(ui->action_SetAsTutor, SIGNAL(triggered()), this, SLOT(onButtonStopProjection())); connect(ui->action_Lock, SIGNAL(toggled(bool)), this, SLOT(onButtonLock(bool))); connect(ui->action_LockSingle, SIGNAL(triggered()), this, SLOT(onButtonLockSingle())); connect(ui->action_Help, SIGNAL(triggered()), this, SLOT(onButtonHelp())); connect(ui->actionReload_Room_Configuration, SIGNAL(triggered()), this, SLOT(onButtonReloadRoomConfig())); connect(ui->action_DeleteClient, SIGNAL(triggered()), this, SLOT(onDeleteClient())); /* Stuff for the button lock */ //Setup a timeout _buttonLockTimer = new QTimer(this); _buttonLockTimer->setSingleShot(true); _buttonLockTimer->setInterval(BUTTON_BLOCK_TIME); connect(_buttonLockTimer, SIGNAL(timeout()), this, SLOT(enableButtons())); // Define the locking buttons _lockingButtons << ui->action_DeleteClient << ui->action_StudentToTutor << ui->action_StudentToTutorExclusive << ui->action_SetAsTutor << ui->action_TutorToStudent << ui->action_LockSingle << ui->action_Lock << ui->action_TutorToAll << ui->action_StopProjection; // Clicking the session name label shows the edit field for it connect(_sessionNameLabel, SIGNAL(clicked()), this, SLOT(onSessionNameClick())); // Listen to updates to the session name through the session name window connect(_sessionNameWindow, SIGNAL(updateSessionName()), this, SLOT(onSessionNameUpdate())); setAttribute(Qt::WA_QuitOnClose); setUnifiedTitleAndToolBarOnMac(true); this->showMaximized(); // show the Mainwindow maximized _listenServer = new ListenServer(CLIENT_PORT); connect(_listenServer, SIGNAL(newClient(Client*)), this, SLOT(onClientConnected(Client*))); _discoveryListener = new DiscoveryListener(); // Finally this->onSessionNameUpdate(); // Just make lable visible. reloadCurrentRoom(); } /** this function determines if the number of clients in exam mode comprise * more than 50%. In that case the whole manager switches to exam mode, * disabling many features in the toolbar **/ void MainWindow::clientCountChanged() { int examClientCount = 0; int clientCount = 0; for (auto it = _clientFrames.begin(); it != _clientFrames.end(); ++it) { Client* c = (*it)->client(); if (c != nullptr) { bool b = c->isExamMode(); examClientCount += b ? 1 : 0; clientCount++; } } serverApp->setExam(examClientCount * 2 >= clientCount && clientCount > 0); bool e = serverApp->isExam(); qDebug() << "isExam is " << e; ui->action_TutorToAll->setVisible(!e); ui->action_StudentToTutor->setVisible(!e); ui->action_StudentToTutorExclusive->setVisible(!e); ui->action_TutorToStudent->setVisible(!e); ui->action_StopProjection->setVisible(!e); _examModeLabel->setVisible(e); _examModeLabel->setFixedHeight(e ? 400 : 0); if (_lastClientCount != clientCount) { // Client count actually changed if (clientCount == 0) { // Last client must have disconnected, enable screen saver again ScreenSaver::allowSaverAndStandby(true); } else if (_lastClientCount == 0) { // We didn't have a client before, but now we do, disable screen saver ScreenSaver::forceUnlockAndScreenOn(); ScreenSaver::allowSaverAndStandby(false); } // Remember client count _lastClientCount = clientCount; } // Also we might have to enable/disable buttons in the toolbar if the // client frame is currently selected updateContextButtonStates(); } MainWindow::~MainWindow() { _sessionNameLabel->deleteLater(); delete ui; } /** Squared euclidean distance (why is this not implemented in QPoint?) */ static int distance(QPoint a, QPoint b) { const int dx = a.x() - b.x(); const int dy = a.y() - b.y(); const int sum = dx * dx + dy * dy; return sum; } /** * \brief: find the closest available frame. * * Closest: smallest Euclidean distance from center to center * * 'Algorithm': ideally, go in circles around the preferred point until you find * a free spot. Lazy way: store all free positions in an array and find later * there the closest (<- that's what I do) * * @param preferred in Pixels! * @param toIgnore, ignore this connectionframe when considering free slots * @return the free slot */ QPoint MainWindow::closestFreeSlot(const QPoint preferredPixels, const ConnectionFrame* toIgnore) { const bool pickFirstOne = ( preferredPixels == QPoint(-1, -1) ); const QSize& clientSize = serverApp->getCurrentRoom()->clientSize; #define GRID(X,Y) (grid[((X) * _tilesY) + (Y)]) bool *grid = new bool[_tilesX * _tilesY]; memset(grid, 0, sizeof(bool) * size_t(_tilesX * _tilesY)); /* set everything to false */ /* fill grid */ for (auto it = _clientFrames.begin(); it != _clientFrames.end(); ++it) { if (*it == toIgnore) { continue; } const QPoint p = (*it)->getGridPosition(); for (int x = p.x(); x < p.x() + clientSize.width(); x++) { for (int y = p.y(); y < p.y() + clientSize.height(); y++) { GRID(x, y) = true; } } } QList freePositions; /* check all positions to see if they are available */ for (int x = 0; x <= _tilesX - clientSize.width(); x++) { for ( int y = 0; y <= _tilesY - clientSize.height(); y++) { /* check if (x,y) is free */ bool isFree = true; int dx, dy = 0; for (dx = 0; dx < clientSize.width(); dx++) { for (dy = 0; dy < clientSize.height(); dy++) { if (GRID(x + dx, y + dy)) { isFree = false; goto endLoop; // double-break } } } endLoop: ; if (isFree) { freePositions << QPoint(x, y); if (pickFirstOne) { goto endSearch; } } } } endSearch: ; #undef GRID delete[] grid; if (freePositions.isEmpty() && toIgnore != nullptr) { return toIgnore->getGridPosition(); } /* among all the free positions, find the closest */ int min_distance = 10000000; QPoint bestPosition = QPoint(0, 0); for (QPoint freePos : freePositions) { const QPoint freePosPx( freePos.x() * getTileWidthPx(), freePos.y() * getTileHeightPx() ); const int dist = distance(freePosPx, preferredPixels); if (dist < min_distance) { min_distance = dist; bestPosition = freePos; } } return bestPosition; } /* place frame in the closest available spot */ void MainWindow::placeFrameInFreeSlot(ConnectionFrame* frame, QPoint preferredPixels) { QPoint bestPosition = closestFreeSlot(preferredPixels, frame); frame->setGridPosition(bestPosition); } /** * Create new Frame. * Create new frame and add to current available frame list. * Also connect signals frameMoved() and clicked() with slots. * @return ConnectionFrame* */ ConnectionFrame* MainWindow::createFrame(const QString &computerId, const QPoint* gridPosition, bool fromRoomplan) { ConnectionFrame *cf = new ConnectionFrame(this, ui->frmRoom, fromRoomplan); if (gridPosition == nullptr) { placeFrameInFreeSlot(cf); } else { cf->setGridPosition(*gridPosition); } cf->setComputerId(computerId); _clientFrames.append(cf); cf->show(); connect(cf, SIGNAL(frameMoved(ConnectionFrame *)), this, SLOT(onFrameDropped(ConnectionFrame *))); connect(cf, SIGNAL(clicked(ConnectionFrame *)), this, SLOT(onFrameClicked(ConnectionFrame *))); connect(cf, SIGNAL(frameMoving(ConnectionFrame *)), this, SLOT(onFrameMoving(ConnectionFrame *))); return cf; } /** * Tells the new client the current situation in class room. * Set the specific parameters like projection source or lock screen. * @param client */ void MainWindow::tellClientCurrentSituation(Client* client) { // If clients are currently locked, tell this new client if (ui->action_Lock->isChecked() || _mode == Mode::LockedUnicast) client->lockScreen(true); if (_mode == Mode::Broadcast) { client->setDesiredProjectionSource(_streamingSource); if (_streamingSource != 0) { client->startVncClient(getClientFromId(_streamingSource)); } } } /** * Returns connected client which belongs to given id. * Iterating over ConnectionFrames and comparing id to given id. * @param id * @return Client with given id, if not nullptr. */ Client* MainWindow::getClientFromId(int id) { for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if ((*it)->client() != nullptr) { if ((*it)->client()->id() == id) return (*it)->client(); } } return nullptr; } /** * Return the Frame, which is currently beeing Tutor. * Iterating over all ConnectionFrames, and looking for flag _isTutor. * @return Frame with flag _isTutor = true, * else nullptr if no Tutor is available. */ ConnectionFrame* MainWindow::getTutorFrame() { for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if (((*it) != nullptr) && ((*it)->isTutor())) return (*it); } return nullptr; } /** * Return the Frame, which is currently selected by user. * Iterating over all ConnectionFrame and looking for flag _isSelected. * @return Frame with flag _isSelected = true, * else nullptr if no frame is selected. */ ConnectionFrame* MainWindow::getSelectedFrame() { for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if (((*it) != nullptr) && ((*it)->isSelected())) return (*it); } return nullptr; } /* * Overridden methods */ /** * Handle closeEvent. * If user calls closing MainWindow, close, else ignore. * @param e */ void MainWindow::closeEvent(QCloseEvent* e) { QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Question"), tr("Are you sure you want to exit?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret == QMessageBox::Yes) { QApplication::exit(0); } else { e->ignore(); } } /** * Change Event. * @param e */ void MainWindow::changeEvent(QEvent* e) { QMainWindow::changeEvent(e); } enum AspectStatus { GRID_OK, GRID_TOO_WIDE, GRID_TOO_TALL }; /** * check the difference in the aspect ratio between the frame size and the grid * size. The parameters in here are hand-adjusted. Feel free to make it more or * less sensitive. * */ AspectStatus checkAspectRatio(const QSize& frameSize, const QSize& gridSize) { float aspectRoom = float(gridSize.height()) / float(gridSize.width()); float aspectFrame = float(frameSize.height()) / float(frameSize.width()); if (aspectRoom / aspectFrame < 0.8f) { return GRID_TOO_WIDE; } if ( aspectFrame / aspectRoom < 0.8f) { return GRID_TOO_TALL; } return GRID_OK; } /** * Resize event. * @param e */ void MainWindow::resizeEvent(QResizeEvent* /* e */ ) { if (ui->frmRoom->size().width() < 100 || ui->frmRoom->size().height() < 100) { return; } const Room* room = serverApp->getCurrentRoom(); QSize newGridSize = room->gridSize; const QSize& clientSize = room->clientSize; // Check if any frames have been moved beyond the room dimensions for (auto it = _clientFrames.begin(); it != _clientFrames.end(); ++it) { QPoint bounds = (*it)->getGridPosition(); while (bounds.x() + clientSize.width() > newGridSize.width()) { newGridSize += QSize(1, 0); } while (bounds.y() + clientSize.height() > newGridSize.height()) { newGridSize += QSize(0, 1); } } /* do we have to add virtual columns or rows? */ AspectStatus status; do { status = checkAspectRatio(ui->frmRoom->size(), newGridSize); if (status == GRID_TOO_WIDE) { /* add row */ newGridSize += QSize(0, 1); } if (status == GRID_TOO_TALL) { /* add column */ newGridSize += QSize(1, 0); } } while (status != GRID_OK); this->_tilesX = newGridSize.width(); this->_tilesY = newGridSize.height(); const int maxX = _tilesX - clientSize.width(); const int maxY = _tilesY - clientSize.height(); /* Bring back frames which are now out of the screen */ for (auto it = _clientFrames.begin(); it != _clientFrames.end(); ++it) { const QPoint gp = (*it)->getGridPosition(); if ( gp.x() > maxX || gp.y() > maxY ) { placeFrameInFreeSlot(*it, (*it)->pos()); } } /* Resize all connection windows */ for (auto it = _clientFrames.begin(); it != _clientFrames.end(); ++it) { (*it)->updateGeometry(); } /* update background image label */ if (_backgroundImage != nullptr) { int w = ui->frmRoom->width() - 5; /* to make sure that we don't enlarge the window */ int h = ui->frmRoom->height() - 5; ui->imageLabel->hide(); ui->imageLabel->setScaledContents(true); ui->imageLabel->setPixmap(QPixmap::fromImage(*_backgroundImage).scaled(w, h, Qt::IgnoreAspectRatio)); ui->imageLabel->show(); } else { ui->imageLabel->clear(); } } void MainWindow::updateContextButtonStates() { if (_buttonLockTimer->isActive()) // If buttons are disabled for cooldown, don't do anything return; const ConnectionFrame *selected = getSelectedFrame(); const ConnectionFrame *tutor = getTutorFrame(); const bool somethingSelected = (selected != nullptr); const bool selectedOnline = (selected != nullptr && selected->client() != nullptr); const bool tutorOnline = (tutor != nullptr && tutor->client() != nullptr); // Deletion only for offline clients, tutor if anything is selected, locking only for online clients ui->action_DeleteClient->setEnabled(somethingSelected && !selectedOnline); ui->action_SetAsTutor->setEnabled(somethingSelected); ui->action_LockSingle->setEnabled(selectedOnline && selected != tutor); // Don't allow projection to self ui->action_StudentToTutorExclusive->setEnabled(selectedOnline && tutorOnline && selected != tutor); ui->action_StudentToTutor->setEnabled(selectedOnline && tutorOnline && selected != tutor); ui->action_TutorToStudent->setEnabled(selectedOnline && tutorOnline && selected != tutor); // Only allow tutor broadcast if they're online ui->action_TutorToAll->setEnabled(tutorOnline); } /** * Handle Mouse Release Event. * Check if click was inside the frame and if that is the case, set the selection of each connected * client to false. * @param e */ void MainWindow::mouseReleaseEvent(QMouseEvent* e) { // Try to figure out if the user clicked inside the "room" frame // No idea if there is a more elegant way to do this without // sub-classing that frame and overriding its mouseReleaseEvent const QPoint pos(ui->frmRoom->mapFrom(this, e->pos())); if (pos.x() < 0 || pos.y() < 0) return; const QSize frame(ui->frmRoom->size()); if (frame.width() > pos.x() && frame.height() > pos.y()) { if (getSelectedFrame() != nullptr) { getSelectedFrame()->setSelection(false); updateContextButtonStates(); } } } /** * @brief reset */ void MainWindow::reset(bool lock) { _mode = Mode::None; // Stop server (Clients get stopped on ACK) if (getClientFromId(_streamingSource) != nullptr) { getClientFromId(_streamingSource)->stopVncServer(); } // Unlock all clients for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if ((*it)->client() != nullptr) { (*it)->client()->lockScreen(lock); (*it)->client()->removeAttention(); } } } /* * Slots */ void MainWindow::onFrameMoving(ConnectionFrame* connectionFrame) { QPoint slot = closestFreeSlot(connectionFrame->pos(), connectionFrame); _dropMarker->setGeometry(slot.x() * getTileWidthPx(), slot.y() * getTileHeightPx(), connectionFrame->width(), connectionFrame->height()); if (!_dropMarker->isVisible()) { _dropMarker->lower(); ui->imageLabel->lower(); _dropMarker->show(); } } /** * Place frame to from user specified position. Should be called when a * connectionFrame is dropped during the "drag-n-drop". */ void MainWindow::onFrameDropped(ConnectionFrame* connectionFrame) { _dropMarker->hide(); // if (_tilesX <= 0 || _tilesY <= 0) return; const QPoint preferredPixels = connectionFrame->pos(); placeFrameInFreeSlot(connectionFrame, preferredPixels); //resizeEvent(nullptr); } /** * Mark given frame after it was clicked on. * @param frame */ void MainWindow::onFrameClicked(ConnectionFrame* frame) { _dropMarker->hide(); ConnectionFrame *current = getSelectedFrame(); // If same frame is clicked again,, do nothing if (current == frame) return; // If another frame has been selected, unselect it // Set the ui selected and set a new reference if (current != nullptr) { current->setSelection(false); } frame->setSelection(true); updateContextButtonStates(); } /** * Show session name, after it was clicked on. */ void MainWindow::onSessionNameClick() { _sessionNameWindow->show((serverApp->sessionName())); } /** * Update session name. */ void MainWindow::onSessionNameUpdate() { _sessionNameLabel->setText(tr("Session Name: %1 [click to edit]").arg(serverApp->sessionName())); bool haveAdditionalClient = false; for (QMutableListIterator it(_clientFrames); it.hasNext(); ) { if (!it.next()->isFromRoomplan()) { haveAdditionalClient = true; break; } } if (!haveAdditionalClient) return; // No additional client exists, don't ask about reset // Current layout contains additional clients (voa session name), ask to delete QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Question"), tr("Do you want to delete and disconnect any clients\n" "not belonging to the current room layout?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret != QMessageBox::Yes) return; // Stop all projections and clear all clients, which were connected to old sessionName. reset(); for (QMutableListIterator it(_clientFrames); it.hasNext(); ) { ConnectionFrame *cf = it.next(); if (!cf->isFromRoomplan()) { cf->hide(); cf->deleteLater(); it.remove(); } } } /** * @brief MainWindow::startVncServerIfNecessary * @param from */ void MainWindow::startVncServerIfNecessary(int from) { Client* os = getClientFromId(_streamingSource); Client* ns = getClientFromId(from); // if there is a server running which is not "from" stop it. if (os != nullptr && _streamingSource != from) os->stopVncServer(); // Set new streaming source _streamingSource = from; // If streaming source is already active avoid a restart if (ns != nullptr) { if (ns->isActiveVncServer()) { this->onVncServerStateChange(ns); } else { // Could not take shortcut, (re)start VNC server on source ns->startVncServer(); } ns->removeAttention(); } } void MainWindow::onButtonReloadRoomConfig() { connect(_reloadWindow->ui->buttonBox, SIGNAL(accepted()), this, SLOT(onReloadRoomOk())); connect(_reloadWindow->ui->buttonBox, SIGNAL(rejected()), this, SLOT(onReloadRoomCancel())); QList keyList = serverApp->getRooms().keys(); for (QList::iterator it = keyList.begin(); it != keyList.end() ; it++) { _reloadWindow->ui->roomList->addItem(*it); } _reloadWindow->show(); } void MainWindow::onReloadRoomCancel() { disconnect(_reloadWindow->ui->buttonBox, SIGNAL(accepted()), this, SLOT(onReloadRoomOk())); disconnect(_reloadWindow->ui->buttonBox, SIGNAL(rejected()), this, SLOT(onReloadRoomCancel())); _reloadWindow->ui->roomList->clear(); _reloadWindow->hide(); } void MainWindow::reloadCurrentRoom() { /* delete old image */ if (_backgroundImage != nullptr) {delete _backgroundImage; } _backgroundImage = nullptr; const Room *room = serverApp->getCurrentRoom(); if (room != nullptr) { /* set tiles */ _tilesX = room->gridSize.width(); _tilesY = room->gridSize.height(); /* place connection frames */ for (auto it = room->clientPositions.begin(); it != room->clientPositions.end(); ++it) { QString computerId = it.key(); QPoint pos = it.value(); ConnectionFrame *cf = createFrame(computerId, &pos, true); onFrameDropped(cf); if (computerId == room->tutorIP) { qDebug() << "set computer with id " << computerId << " as tutor per configuration"; if (getTutorFrame() != nullptr) { getTutorFrame()->setTutor(false); } cf->setTutor(true); } } /* load background image */ QString imgPath = room->imagePath; qDebug() << "imgPath is " << imgPath; if (imgPath != "") { qDebug() << "set background image path: " << imgPath; if (imgPath.endsWith("svg")) { /* render once with maximal screen size */ const QSize &s = QApplication::desktop()->screenGeometry().size(); // ui->frmRoom->geometry().size(); QSvgRenderer renderer(imgPath); _backgroundImage = new QImage(s, QImage::Format_ARGB32); _backgroundImage->fill(Qt::lightGray); /* background color */ QPainter painter(_backgroundImage); renderer.render(&painter); } else { _backgroundImage = new QImage(); _backgroundImage->load(imgPath); } } } /* and force a resize event (this scales the image) */ resizeEvent(nullptr); clientCountChanged(); } void MainWindow::onReloadRoomOk() { if (_reloadWindow->ui->roomList->currentItem() == nullptr) { QMessageBox::critical(this, "Warning", tr("No item selected, please select room!"), 0, 1); return; } QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Warning"), tr("Are you sure you want to reload the room?\n" "Note that all clients will be deleted."), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (ret == QMessageBox::Yes) { disconnect(_reloadWindow->ui->buttonBox, SIGNAL(accepted()), this, SLOT(onReloadRoomOk())); disconnect(_reloadWindow->ui->buttonBox, SIGNAL(rejected()), this, SLOT(onReloadRoomCancel())); // Delete all clients. for (QList::iterator it = _clientFrames.begin(); it != _clientFrames.end(); it++) { (*it)->hide(); (*it)->deleteLater(); } _clientFrames.clear(); // Load new room configuration. QString roomToReload = _reloadWindow->ui->roomList->currentItem()->data(0).toString(); serverApp->setCurrentRoom(roomToReload); reloadCurrentRoom(); _reloadWindow->ui->roomList->clear(); _reloadWindow->hide(); } } int MainWindow::getTileWidthPx() const { return ui->frmRoom->size().width() / _tilesX; } int MainWindow::getTileHeightPx() const { return ui->frmRoom->size().height() / _tilesY; } QRect MainWindow::calcFrameGeometry(ConnectionFrame* frame) const { const QPoint pos = frame->getGridPosition(); const QSize& clientSize = serverApp->getCurrentRoom()->clientSize; const int w = getTileWidthPx(); const int h = getTileHeightPx(); return QRect(pos.x() * w, pos.y() * h, w * clientSize.width(), h * clientSize.height()); } /** * Display popup which explains possible actions about the buttons. */ void MainWindow::onButtonHelp() { _helpWindow->show(); } /** * Handle projection from tutor to all. * Get the client who is tutor and set the projectionSource of all * clients, except the tutor ones, to false. */ void MainWindow::onButtonTutorToAll() { ui->action_Lock->setChecked(false); if (getTutorFrame() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrTutorNdef); else if (getTutorFrame()->client() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrTutorOffline); else if (_clientFrames.size() == 1) QMessageBox::critical(this, tr("Projection"), sStrNoDestAv); else { // If this mode is already active, reset everything if (_mode == Mode::Broadcast) { reset(); return; } // Set all clients as watchers of tutor. Except for the tutors desired source, which hase to be none for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) if ((*it)->client() != nullptr) (*it)->client()->setDesiredProjectionSource((*it)->client() == getTutorFrame()->client() ? NO_SOURCE : getTutorFrame()->client()->id()); disableButtons(); _mode = Mode::Broadcast; startVncServerIfNecessary(getTutorFrame()->client()->id()); } } /** * Handle the projection from Tutor to specific student. * Set the client who is tutor as from and the selected client as to. */ void MainWindow::onButtonTutorToStudent() { ui->action_Lock->setChecked(false); if (getSelectedFrame() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrDestNdef); else if (getTutorFrame() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrTutorNdef); else if (getSelectedFrame() == getTutorFrame()) QMessageBox::critical(this, tr("Projection"), sStrSourceDestSame); else if (getSelectedFrame()->client() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrDestOffline); else if (getTutorFrame()->client() == nullptr) QMessageBox::critical(this, tr("Projection"), sStrTutorOffline); else { auto sourceClient = getTutorFrame()->client(); auto destinationClient = getSelectedFrame()->client(); // If this is the first call in this mode clear the watchers if (_mode != Mode::Multicast) { for (auto c : _clientFrames) { if (c->client() != nullptr) { c->client()->stopVncClient(); } } } // If "to" already watches "from" stop it if (destinationClient->desiredProjectionSource() == sourceClient->id() ) { destinationClient->stopVncClient(); } else { destinationClient->setDesiredProjectionSource(sourceClient->id()); } disableButtons(); int numClients = 0; for (auto c : _clientFrames) { if (c->client() != nullptr && c->client()->desiredProjectionSource() == sourceClient->id()) { numClients++; } } if (numClients == 0) { _mode = Mode::None; sourceClient->stopVncServer(); } else { _mode = Mode::Multicast; startVncServerIfNecessary(sourceClient->id()); } } } /** * Handle projection from one student to tutor. */ void MainWindow::onButtonStudentToTutor() { this->vncOneOnOne(false); } /** * Handle projection from one student to tutor, lock everyone else. */ void MainWindow::onButtonStudentToTutorExclusive() { this->vncOneOnOne(true); } void MainWindow::vncOneOnOne(bool exclusive) { if (getSelectedFrame() == nullptr) { QMessageBox::critical(this, tr("Projection"), sStrSourceNdef); } else if (getTutorFrame() == nullptr) { QMessageBox::critical(this, tr("Projection"), sStrTutorNdef); } else if (getTutorFrame() == getSelectedFrame()) { QMessageBox::critical(this, tr("Projection"), sStrSourceDestSame); } else if (getSelectedFrame()->client() == nullptr) { QMessageBox::critical(this, tr("Projection"), sStrSourceOffline); } else if (getTutorFrame()->client() == nullptr) { QMessageBox::critical(this, tr("Projection"), sStrTutorOffline); } else { const bool wasLocked = ui->action_Lock->isChecked(); ui->action_Lock->setChecked(false); Client *source = getSelectedFrame()->client(); Client *dest = getTutorFrame()->client(); const Mode mode = exclusive ? Mode::LockedUnicast : Mode::Unicast; if (_mode == mode && source->id() == dest->desiredProjectionSource()) { // Button clicked again with same selection, treat this as reset reset(); return; } for (ConnectionFrame* f : _clientFrames) { if (f->client() == nullptr) // Disconnected frame continue; if (source != f->client() && dest != f->client()) { //Not source or destination f->client()->setDesiredProjectionSource(NO_SOURCE); if (_mode == Mode::Unicast) { // Already in unlocked unicast mode if (exclusive) { // Only change lock state (to locked) if switching to locked unicast f->client()->lockScreen(true); // (don't change otherwise, would reset individually locked clients) } } else if (wasLocked) { // Was in "lock all" mode, update lock mode for everyone f->client()->lockScreen(exclusive); } } } dest->lockScreen(false); source->lockScreen(false); dest->setDesiredProjectionSource(source->id()); source->setDesiredProjectionSource(NO_SOURCE); disableButtons(); _mode = mode; startVncServerIfNecessary(source->id()); } } /** * Handle Button StopProjection. * Set ProjectionSource of each client to false, stop the active VNC Server * and the active VNC Client(s) and unlock all screens. */ void MainWindow::onButtonStopProjection() { ui->action_Lock->setChecked(false); reset(); } /** * Handle button to lock or unlock screens of client(s). * If already locked, do nothing, else lock or unlock the clients, except the * tutor and the manager running machine. * @param checked */ void MainWindow::onButtonLock(bool checked) { // Stop all projections and lock if requested reset(checked); } void MainWindow::onButtonLockSingle() { // If no frame is selected, warning. if (getSelectedFrame() == nullptr) { QMessageBox::critical(this, tr("Selection"), tr("No client is selected.")); return; } Client *client = getSelectedFrame()->client(); // If frame of inactive client has been selected unselect it if (client == nullptr) { QMessageBox::critical(this, tr("Selection"), tr("The selected client is not connected.")); return; } else { // If selected client is locked, first unlock bool newState = !client->isLocked(); client->lockScreen(newState); if (!newState) { // If no more clients are locked, reset button for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if ((*it)->client() == nullptr) continue; if ((*it)->client()->isLocked()) return; } ui->action_Lock->setChecked(false); } } } /** * On button exit, close application. */ void MainWindow::onButtonExit() { this->close(); } /** * Handle button to set specific client as tutor. * Unset active tutor and set selected client, if not inactive, as new tutor. */ void MainWindow::onButtonSetAsTutor() { ConnectionFrame *selected = getSelectedFrame(); ConnectionFrame *tutor = getTutorFrame(); if (selected == nullptr) { QMessageBox::critical(this, tr("Selection"), tr("No client is selected.")); return; } // Unlock client about to become a tutor, just in case if (selected->client() != nullptr) { selected->client()->lockScreen(false); } // If same frame is already tutor, do nothing if (selected == tutor) return; // Else unset the old and set the new tutor if (tutor != nullptr) { tutor->setTutor(false); } selected->setTutor(true); updateContextButtonStates(); } /** * Handle from ListenServer signaled new client connection. * @param client */ void MainWindow::onClientConnected(Client* client) { connect(client, SIGNAL(authenticating(Client*, ClientLogin*)), this, SLOT(onClientAuthenticating(Client*, ClientLogin*))); connect(client, SIGNAL(authenticated(Client*)), this, SLOT(onClientAuthenticated(Client*))); } /** * Authenticate new Client client. * Check if incoming client ip already exists, * if yes --> return. * Check if same name of client already exist, * if yes --> name new client as name(x). * x = number of clients with the same name. * @param client * @param request */ void MainWindow::onClientAuthenticating(Client* client, ClientLogin* request) { disconnect(client, SIGNAL(authenticating(Client*, ClientLogin*)), this, SLOT(onClientAuthenticating(Client*, ClientLogin*))); /* copy examMode */ client->setExamMode(request->examMode); if (!request->accept) // Another receiver of that signal already rejected the client, so nothing to do return; bool inuse; QString check = request->name; int addnum = 1; do { inuse = false; for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { Client *c = (**it).client(); if (c == nullptr) continue; if (!c->isAuthed()) continue; if (c->ip() == request->ip) { request->accept = false; return; } if (c->name() == check) { inuse = true; check = request->name + " (" + QString::number(++addnum) + ")"; break; } } } while (inuse && addnum < 100); if (inuse) request->accept = false; else request->name = check; } /** * New client was authenticated, make several checks. * Check if new clients ip already exists, * if yes --> not necessary to create new clientFrame. * if not --> create a new one and adapt this one to current * situation in class room(projection / lock screen). * Check if avctiv tutor is available, * if not --> check if new client is possible tutor. * @param client */ void MainWindow::onClientAuthenticated(Client* client) { disconnect(client, SIGNAL(authenticated(Client*)), this, SLOT(onClientAuthenticated(Client*))); connect(client, SIGNAL(vncServerStateChange(Client*)), this, SLOT(onVncServerStateChange(Client*))); connect(client, SIGNAL(vncClientStateChange(Client*)), this, SLOT(onVncClientStateChange(Client*))); ConnectionFrame *existing = nullptr; ConnectionFrame *cf = nullptr; for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if ((*it)->computerId() == client->ip()) { existing = *it; } } // Clients ip already exists, but was not active. if (existing != nullptr) { cf = existing; } else { cf = createFrame(); } cf->assignClient(client); connect(client, SIGNAL(disconnected()), this, SLOT(clientCountChanged())); tellClientCurrentSituation(client); clientCountChanged(); } /** * Handle if VNC Server State has changed. * Check if VNC server has been started/stopped on some client and start VNC server/reset pending * VNC projection information. * @param client */ void MainWindow::onVncServerStateChange(Client* client) { if (client == getClientFromId(_streamingSource)) { enableButtons(); } if (client->isActiveVncServer()) { // apply the desired projection sources for (ConnectionFrame *frame : _clientFrames) { if (frame->client() == nullptr) // Ignore offline clients continue; if (frame->client()->desiredProjectionSource() == client->id()) { frame->client()->startVncClient(client); client->lockScreen(false); } else { frame->client()->lockScreen(frame->client()->desiredProjectionSource() == NO_SOURCE && _mode == Mode::LockedUnicast); } } // Dont forget to unlock the vnc server client->lockScreen(false); } else { // VNC server stopped on some client or failed to start - reset local pending projection information for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) { if ((*it)->client() != nullptr) { if ((*it)->client()->desiredProjectionSource() == client->id()) { (*it)->client()->setDesiredProjectionSource(NO_SOURCE); (*it)->client()->stopVncClient(); if (_mode == Mode::LockedUnicast) (*it)->client()->lockScreen(true); } } } // Dont forget to unlock the vnc server (if necesarry) client->lockScreen((client->desiredProjectionSource() == NO_SOURCE && _mode == Mode::LockedUnicast) || ui->action_Lock->isChecked()); // If this was the current source remember that there is no source anymore and reset mode if (client == getClientFromId(_streamingSource)) { _streamingSource = NO_SOURCE; _mode = Mode::None; } } } /** * Handle VNC client state change. * Check if any vnc client is in use, and if that is not the case, stop VNC Server. * @param client * @param lastProjectionSource */ void MainWindow::onVncClientStateChange(Client* client) { if (client != nullptr) { // VNC Client stopped -> remove from watchers if (!client->isActiveVncClient()) { // Only unset a desired Projection source if it has not changed meanwhile if (client->projectionSource() == client->desiredProjectionSource()) client->setDesiredProjectionSource(NO_SOURCE); /* * If nobody is watching the VNC server of the pvsclient that * stopped its vncclient stop it. (The _LAST_ vncserver, there * may be a new one) * This is necessary for the race condition when a server * is stopped and another started at the same time, since the new * server would be killed if all client disconnect before any of * the new connect. */ bool serverHasWatchers = false; for (QList::iterator it(_clientFrames.begin()); it != _clientFrames.end(); ++it) if ((*it)->client() != nullptr) if ((*it)->client()->desiredProjectionSource() == client->projectionSource()) { serverHasWatchers = true; break; } if ( !serverHasWatchers ) { Client* c = getClientFromId(client->projectionSource()); if (c != nullptr) c->stopVncServer(); } } } } /** * DisableButtons. */ void MainWindow::disableButtons() { _buttonLockTimer->start(); for (QAction *a : _lockingButtons) { a->setDisabled(true); } } /** * EnableButtons. */ void MainWindow::enableButtons() { _buttonLockTimer->stop(); for (QAction *a : _lockingButtons) { a->setEnabled(true); } updateContextButtonStates(); // In case user changed selection while buttons were disabled } void MainWindow::onDeleteClient() { // If no frame is selected, warning. ConnectionFrame* frame = getSelectedFrame(); if (frame == nullptr) { QMessageBox::critical(this, tr("Selection"), tr("No client is selected.")); return; } if (frame->client() != nullptr) { QMessageBox::critical(this, tr("Selection"), tr("This client is still connected.")); return; } QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Warning"), tr("Are you sure you want to delete the selected client?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret == QMessageBox::Yes) { frame->hide(); frame->deleteLater(); _clientFrames.removeAll(frame); clientCountChanged(); return; } }