Idle Client Timeout Functionality

Added the functionality client side to log users out of servers if they
are idle for more than 1 hour without joining either a game or room.
Sending a message (room/game/private) or performing a game action.
This commit is contained in:
woogerboy21 2016-10-09 13:55:07 -04:00
parent e4127fead3
commit 7af2f3f057
15 changed files with 70 additions and 3 deletions

View file

@ -30,12 +30,17 @@ RemoteClient::RemoteClient(QObject *parent)
timer->setInterval(keepalive * 1000); timer->setInterval(keepalive * 1000);
connect(timer, SIGNAL(timeout()), this, SLOT(ping())); connect(timer, SIGNAL(timeout()), this, SLOT(ping()));
int idlekeepalive = settingsCache->getIdleKeepAlive();
idleTimer = new QTimer(this);
idleTimer->setInterval(idlekeepalive * 1000);
connect(idleTimer, SIGNAL(timeout()), this, SLOT(doIdleTimeOut()));
connect(this, SIGNAL(resetIdleTimerClock()), idleTimer, SLOT(start()));
socket = new QTcpSocket(this); socket = new QTcpSocket(this);
socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
connect(socket, SIGNAL(connected()), this, SLOT(slotConnected())); connect(socket, SIGNAL(connected()), this, SLOT(slotConnected()));
connect(socket, SIGNAL(readyRead()), this, SLOT(readData())); connect(socket, SIGNAL(readyRead()), this, SLOT(readData()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(slotSocketError(QAbstractSocket::SocketError))); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(slotSocketError(QAbstractSocket::SocketError)));
connect(this, SIGNAL(serverIdentificationEventReceived(const Event_ServerIdentification &)), this, SLOT(processServerIdentificationEvent(const Event_ServerIdentification &))); connect(this, SIGNAL(serverIdentificationEventReceived(const Event_ServerIdentification &)), this, SLOT(processServerIdentificationEvent(const Event_ServerIdentification &)));
connect(this, SIGNAL(connectionClosedEventReceived(Event_ConnectionClosed)), this, SLOT(processConnectionClosedEvent(Event_ConnectionClosed))); connect(this, SIGNAL(connectionClosedEventReceived(Event_ConnectionClosed)), this, SLOT(processConnectionClosedEvent(Event_ConnectionClosed)));
connect(this, SIGNAL(sigConnectToServer(QString, unsigned int, QString, QString)), this, SLOT(doConnectToServer(QString, unsigned int, QString, QString))); connect(this, SIGNAL(sigConnectToServer(QString, unsigned int, QString, QString)), this, SLOT(doConnectToServer(QString, unsigned int, QString, QString)));
@ -61,6 +66,7 @@ void RemoteClient::slotConnected()
{ {
timeRunning = lastDataReceived = 0; timeRunning = lastDataReceived = 0;
timer->start(); timer->start();
idleTimer->start();
// dirty hack to be compatible with v14 server // dirty hack to be compatible with v14 server
sendCommandContainer(CommandContainer()); sendCommandContainer(CommandContainer());
@ -302,6 +308,7 @@ void RemoteClient::doActivateToServer(const QString &_token)
void RemoteClient::doDisconnectFromServer() void RemoteClient::doDisconnectFromServer()
{ {
timer->stop(); timer->stop();
idleTimer->stop();
messageInProgress = false; messageInProgress = false;
handshakeStarted = false; handshakeStarted = false;
@ -379,4 +386,15 @@ QString RemoteClient::getSrvClientID(const QString _hostname)
} }
QString uniqueServerClientID = QCryptographicHash::hash(srvClientID.toUtf8(), QCryptographicHash::Sha1).toHex().right(15); QString uniqueServerClientID = QCryptographicHash::hash(srvClientID.toUtf8(), QCryptographicHash::Sha1).toHex().right(15);
return uniqueServerClientID; return uniqueServerClientID;
}
void RemoteClient::doIdleTimeOut()
{
doDisconnectFromServer();
emit idleTimeout();
}
void RemoteClient::resetIdleTimer()
{
emit resetIdleTimerClock();
} }

View file

@ -11,6 +11,8 @@ class RemoteClient : public AbstractClient {
signals: signals:
void maxPingTime(int seconds, int maxSeconds); void maxPingTime(int seconds, int maxSeconds);
void serverTimeout(); void serverTimeout();
void idleTimeout();
void resetIdleTimerClock();
void loginError(Response::ResponseCode resp, QString reasonStr, quint32 endTime, QList<QString> missingFeatures); void loginError(Response::ResponseCode resp, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);
void registerError(Response::ResponseCode resp, QString reasonStr, quint32 endTime); void registerError(Response::ResponseCode resp, QString reasonStr, quint32 endTime);
void activateError(); void activateError();
@ -37,6 +39,7 @@ private slots:
void doLogin(); void doLogin();
void doDisconnectFromServer(); void doDisconnectFromServer();
void doActivateToServer(const QString &_token); void doActivateToServer(const QString &_token);
void doIdleTimeOut();
private: private:
static const int maxTimeout = 10; static const int maxTimeout = 10;
@ -48,6 +51,7 @@ private:
int messageLength; int messageLength;
QTimer *timer; QTimer *timer;
QTimer *idleTimer;
QTcpSocket *socket; QTcpSocket *socket;
QString lastHostname; QString lastHostname;
int lastPort; int lastPort;
@ -62,6 +66,7 @@ public:
void registerToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password, const QString &_email, const int _gender, const QString &_country, const QString &_realname); void registerToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password, const QString &_email, const int _gender, const QString &_country, const QString &_realname);
void activateToServer(const QString &_token); void activateToServer(const QString &_token);
void disconnectFromServer(); void disconnectFromServer();
void resetIdleTimer();
}; };
#endif #endif

View file

@ -162,6 +162,7 @@ SettingsCache::SettingsCache()
notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool(); notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool();
lang = settings->value("personal/lang").toString(); lang = settings->value("personal/lang").toString();
keepalive = settings->value("personal/keepalive", 5).toInt(); keepalive = settings->value("personal/keepalive", 5).toInt();
idlekeepalive = settings->value("personal/idlekeepalive", 36000).toInt();
deckPath = getSafeConfigPath("paths/decks", dataPath + "/decks/"); deckPath = getSafeConfigPath("paths/decks", dataPath + "/decks/");
replaysPath = getSafeConfigPath("paths/replays", dataPath + "/replays/"); replaysPath = getSafeConfigPath("paths/replays", dataPath + "/replays/");

View file

@ -104,7 +104,8 @@ private:
bool spectatorsNeedPassword; bool spectatorsNeedPassword;
bool spectatorsCanTalk; bool spectatorsCanTalk;
bool spectatorsCanSeeEverything; bool spectatorsCanSeeEverything;
int keepalive; int keepalive;
int idlekeepalive;
void translateLegacySettings(); void translateLegacySettings();
QString getSafeConfigPath(QString configEntry, QString defaultPath) const; QString getSafeConfigPath(QString configEntry, QString defaultPath) const;
QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const; QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const;
@ -180,6 +181,7 @@ public:
bool getSpectatorsCanSeeEverything() const { return spectatorsCanSeeEverything; } bool getSpectatorsCanSeeEverything() const { return spectatorsCanSeeEverything; }
bool getRememberGameSettings() const { return rememberGameSettings; } bool getRememberGameSettings() const { return rememberGameSettings; }
int getKeepAlive() const { return keepalive; } int getKeepAlive() const { return keepalive; }
int getIdleKeepAlive() const { return idlekeepalive; }
void setClientID(QString clientID); void setClientID(QString clientID);
QString getClientID() { return clientID; } QString getClientID() { return clientID; }
ShortcutsSettings& shortcuts() const { return *shortcutsSettings; } ShortcutsSettings& shortcuts() const { return *shortcutsSettings; }

View file

@ -547,6 +547,7 @@ void TabGame::replayFinished()
void TabGame::replayStartButtonClicked() void TabGame::replayStartButtonClicked()
{ {
emit notIdle();
replayStartButton->setEnabled(false); replayStartButton->setEnabled(false);
replayPauseButton->setEnabled(true); replayPauseButton->setEnabled(true);
replayFastForwardButton->setEnabled(true); replayFastForwardButton->setEnabled(true);
@ -556,6 +557,7 @@ void TabGame::replayStartButtonClicked()
void TabGame::replayPauseButtonClicked() void TabGame::replayPauseButtonClicked()
{ {
emit notIdle();
replayStartButton->setEnabled(true); replayStartButton->setEnabled(true);
replayPauseButton->setEnabled(false); replayPauseButton->setEnabled(false);
replayFastForwardButton->setEnabled(false); replayFastForwardButton->setEnabled(false);
@ -565,6 +567,7 @@ void TabGame::replayPauseButtonClicked()
void TabGame::replayFastForwardButtonToggled(bool checked) void TabGame::replayFastForwardButtonToggled(bool checked)
{ {
emit notIdle();
timelineWidget->setTimeScaleFactor(checked ? 10.0 : 1.0); timelineWidget->setTimeScaleFactor(checked ? 10.0 : 1.0);
} }
@ -594,6 +597,7 @@ void TabGame::actGameInfo()
void TabGame::actConcede() void TabGame::actConcede()
{ {
emit notIdle();
if (QMessageBox::question(this, tr("Concede"), tr("Are you sure you want to concede this game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) if (QMessageBox::question(this, tr("Concede"), tr("Are you sure you want to concede this game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
return; return;
@ -602,6 +606,7 @@ void TabGame::actConcede()
void TabGame::actLeaveGame() void TabGame::actLeaveGame()
{ {
emit notIdle();
if (!gameClosed) { if (!gameClosed) {
if (!spectator) if (!spectator)
if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
@ -625,6 +630,7 @@ void TabGame::actSay()
sendGameCommand(cmd); sendGameCommand(cmd);
sayEdit->clear(); sayEdit->clear();
} }
emit notIdle();
} }
void TabGame::actPhaseAction() void TabGame::actPhaseAction()
@ -778,6 +784,7 @@ AbstractClient *TabGame::getClientForPlayer(int playerId) const
void TabGame::sendGameCommand(PendingCommand *pend, int playerId) void TabGame::sendGameCommand(PendingCommand *pend, int playerId)
{ {
emit notIdle();
AbstractClient *client = getClientForPlayer(playerId); AbstractClient *client = getClientForPlayer(playerId);
if (!client) if (!client)
return; return;
@ -788,6 +795,7 @@ void TabGame::sendGameCommand(PendingCommand *pend, int playerId)
void TabGame::sendGameCommand(const google::protobuf::Message &command, int playerId) void TabGame::sendGameCommand(const google::protobuf::Message &command, int playerId)
{ {
emit notIdle();
AbstractClient *client = getClientForPlayer(playerId); AbstractClient *client = getClientForPlayer(playerId);
if (!client) if (!client)
return; return;

View file

@ -65,6 +65,7 @@ private:
bool state; bool state;
signals: signals:
void stateChanged(); void stateChanged();
void notIdle();
public: public:
ToggleButton(QWidget *parent = 0); ToggleButton(QWidget *parent = 0);
bool getState() const { return state; } bool getState() const { return state; }
@ -92,6 +93,7 @@ private slots:
void refreshShortcuts(); void refreshShortcuts();
signals: signals:
void newCardAdded(AbstractCardItem *card); void newCardAdded(AbstractCardItem *card);
void notIdle();
public: public:
DeckViewContainer(int _playerId, TabGame *parent); DeckViewContainer(int _playerId, TabGame *parent);
void retranslateUi(); void retranslateUi();
@ -193,6 +195,7 @@ signals:
void containerProcessingDone(); void containerProcessingDone();
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);
void openDeckEditor(const DeckLoader *deck); void openDeckEditor(const DeckLoader *deck);
void notIdle();
private slots: private slots:
void replayNextEvent(); void replayNextEvent();
void replayFinished(); void replayFinished();

View file

@ -99,6 +99,7 @@ void TabMessage::sendMessage()
client->sendCommand(pend); client->sendCommand(pend);
sayEdit->clear(); sayEdit->clear();
emit notIdle();
} }
void TabMessage::messageSent(const Response &response) void TabMessage::messageSent(const Response &response)

View file

@ -26,6 +26,7 @@ private:
signals: signals:
void talkClosing(TabMessage *tab); void talkClosing(TabMessage *tab);
void maximizeClient(); void maximizeClient();
void notIdle();
private slots: private slots:
void sendMessage(); void sendMessage();
void actLeave(); void actLeave();

View file

@ -24,7 +24,6 @@
#include "settingscache.h" #include "settingscache.h"
#include "main.h" #include "main.h"
#include "lineeditcompleter.h" #include "lineeditcompleter.h"
#include "get_pb_extension.h" #include "get_pb_extension.h"
#include "pb/room_commands.pb.h" #include "pb/room_commands.pb.h"
#include "pb/serverinfo_room.pb.h" #include "pb/serverinfo_room.pb.h"
@ -200,6 +199,7 @@ void TabRoom::sendMessage()
sendRoomCommand(pend); sendRoomCommand(pend);
sayEdit->clear(); sayEdit->clear();
} }
emit notIdle();
} }
void TabRoom::sayFinished(const Response &response) void TabRoom::sayFinished(const Response &response)

View file

@ -59,6 +59,7 @@ signals:
void roomClosing(TabRoom *tab); void roomClosing(TabRoom *tab);
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);
void maximizeClient(); void maximizeClient();
void notIdle();
private slots: private slots:
void sendMessage(); void sendMessage();
void sayFinished(const Response &response); void sayFinished(const Response &response);

View file

@ -342,10 +342,12 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *))); connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool))); connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *))); connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
connect(tab, SIGNAL(notIdle()), this, SLOT(resetIdleTimer()));
int tabIndex = myAddTab(tab); int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex); addCloseButtonToTab(tab, tabIndex);
gameTabs.insert(event.game_info().game_id(), tab); gameTabs.insert(event.game_info().game_id(), tab);
setCurrentWidget(tab); setCurrentWidget(tab);
emit idleTimerReset();
} }
void TabSupervisor::localGameJoined(const Event_GameJoined &event) void TabSupervisor::localGameJoined(const Event_GameJoined &event)
@ -383,11 +385,13 @@ void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow())); connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
connect(tab, SIGNAL(roomClosing(TabRoom *)), this, SLOT(roomLeft(TabRoom *))); connect(tab, SIGNAL(roomClosing(TabRoom *)), this, SLOT(roomLeft(TabRoom *)));
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool))); connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
connect(tab, SIGNAL(notIdle()), this, SLOT(resetIdleTimer()));
int tabIndex = myAddTab(tab); int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex); addCloseButtonToTab(tab, tabIndex);
roomTabs.insert(info.room_id(), tab); roomTabs.insert(info.room_id(), tab);
if (setCurrent) if (setCurrent)
setCurrentWidget(tab); setCurrentWidget(tab);
emit idleTimerReset();
} }
void TabSupervisor::roomLeft(TabRoom *tab) void TabSupervisor::roomLeft(TabRoom *tab)
@ -440,6 +444,7 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
tab = new TabMessage(this, client, *userInfo, otherUser); tab = new TabMessage(this, client, *userInfo, otherUser);
connect(tab, SIGNAL(talkClosing(TabMessage *)), this, SLOT(talkLeft(TabMessage *))); connect(tab, SIGNAL(talkClosing(TabMessage *)), this, SLOT(talkLeft(TabMessage *)));
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow())); connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
connect(tab, SIGNAL(notIdle()), this, SLOT(resetIdleTimer()));
int tabIndex = myAddTab(tab); int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex); addCloseButtonToTab(tab, tabIndex);
messageTabs.insert(receiverName, tab); messageTabs.insert(receiverName, tab);
@ -589,3 +594,7 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
} }
void TabSupervisor::resetIdleTimer()
{
emit idleTimerReset();
}

View file

@ -83,6 +83,7 @@ signals:
void localGameEnded(); void localGameEnded();
void adminLockChanged(bool lock); void adminLockChanged(bool lock);
void showWindowIfHidden(); void showWindowIfHidden();
void idleTimerReset();
public slots: public slots:
TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen); TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen);
void openReplay(GameReplay *replay); void openReplay(GameReplay *replay);
@ -108,6 +109,7 @@ private slots:
void processGameEventContainer(const GameEventContainer &cont); void processGameEventContainer(const GameEventContainer &cont);
void processUserMessageEvent(const Event_UserMessage &event); void processUserMessageEvent(const Event_UserMessage &event);
void processNotifyUserEvent(const Event_NotifyUser &event); void processNotifyUserEvent(const Event_NotifyUser &event);
void resetIdleTimer();
}; };
#endif #endif

View file

@ -321,6 +321,17 @@ void MainWindow::serverTimeout()
actConnect(); actConnect();
} }
void MainWindow::idleTimeout()
{
QMessageBox::critical(this, tr("Inactivity Timeout"), tr("You have been signed out due to inactivity."));
actConnect();
}
void MainWindow::idleTimerReset()
{
client->resetIdleTimer();
}
void MainWindow::loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures) void MainWindow::loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures)
{ {
switch (r) { switch (r) {
@ -639,6 +650,7 @@ MainWindow::MainWindow(QWidget *parent)
connect(client, SIGNAL(loginError(Response::ResponseCode, QString, quint32, QList<QString>)), this, SLOT(loginError(Response::ResponseCode, QString, quint32, QList<QString>))); connect(client, SIGNAL(loginError(Response::ResponseCode, QString, quint32, QList<QString>)), this, SLOT(loginError(Response::ResponseCode, QString, quint32, QList<QString>)));
connect(client, SIGNAL(socketError(const QString &)), this, SLOT(socketError(const QString &))); connect(client, SIGNAL(socketError(const QString &)), this, SLOT(socketError(const QString &)));
connect(client, SIGNAL(serverTimeout()), this, SLOT(serverTimeout())); connect(client, SIGNAL(serverTimeout()), this, SLOT(serverTimeout()));
connect(client, SIGNAL(idleTimeout()), this, SLOT(idleTimeout()));
connect(client, SIGNAL(statusChanged(ClientStatus)), this, SLOT(statusChanged(ClientStatus))); connect(client, SIGNAL(statusChanged(ClientStatus)), this, SLOT(statusChanged(ClientStatus)));
connect(client, SIGNAL(protocolVersionMismatch(int, int)), this, SLOT(protocolVersionMismatch(int, int))); connect(client, SIGNAL(protocolVersionMismatch(int, int)), this, SLOT(protocolVersionMismatch(int, int)));
connect(client, SIGNAL(userInfoChanged(const ServerInfo_User &)), this, SLOT(userInfoReceived(const ServerInfo_User &)), Qt::BlockingQueuedConnection); connect(client, SIGNAL(userInfoChanged(const ServerInfo_User &)), this, SLOT(userInfoReceived(const ServerInfo_User &)), Qt::BlockingQueuedConnection);
@ -660,6 +672,7 @@ MainWindow::MainWindow(QWidget *parent)
connect(tabSupervisor, SIGNAL(setMenu(QList<QMenu *>)), this, SLOT(updateTabMenu(QList<QMenu *>))); connect(tabSupervisor, SIGNAL(setMenu(QList<QMenu *>)), this, SLOT(updateTabMenu(QList<QMenu *>)));
connect(tabSupervisor, SIGNAL(localGameEnded()), this, SLOT(localGameEnded())); connect(tabSupervisor, SIGNAL(localGameEnded()), this, SLOT(localGameEnded()));
connect(tabSupervisor, SIGNAL(showWindowIfHidden()), this, SLOT(showWindowIfHidden())); connect(tabSupervisor, SIGNAL(showWindowIfHidden()), this, SLOT(showWindowIfHidden()));
connect(tabSupervisor, SIGNAL(idleTimerReset()), this, SLOT(idleTimerReset()));
tabSupervisor->addDeckEditorTab(0); tabSupervisor->addDeckEditorTab(0);
setCentralWidget(tabSupervisor); setCentralWidget(tabSupervisor);

View file

@ -46,6 +46,8 @@ private slots:
void processConnectionClosedEvent(const Event_ConnectionClosed &event); void processConnectionClosedEvent(const Event_ConnectionClosed &event);
void processServerShutdownEvent(const Event_ServerShutdown &event); void processServerShutdownEvent(const Event_ServerShutdown &event);
void serverTimeout(); void serverTimeout();
void idleTimerReset();
void idleTimeout();
void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures); void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);
void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime); void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);
void activateError(); void activateError();

View file

@ -20,6 +20,7 @@ void FeatureSet::initalizeFeatureList(QMap<QString, bool> &featureList) {
featureList.insert("room_chat_history", false); featureList.insert("room_chat_history", false);
featureList.insert("client_warnings", false); featureList.insert("client_warnings", false);
featureList.insert("mod_log_lookup", false); featureList.insert("mod_log_lookup", false);
featureList.insert("client_inactivetimeout", false);
} }
void FeatureSet::enableRequiredFeature(QMap<QString, bool> &featureList, QString featureName){ void FeatureSet::enableRequiredFeature(QMap<QString, bool> &featureList, QString featureName){