【04】Cockatrice界面构成

 

目录

简介

源码结构

[1]标签窗口

[2]配置

[3]国际化

[4]提示窗口

[5]二级窗口

源码分析

[1]调用MainWindow

[2]MainWindow类构成

public:

explicit MainWindow(QWidget *parent = nullptr);

void setConnectTo(QString url);

~MainWindow() override;

protected:

void closeEvent(QCloseEvent *event) override;

void changeEvent(QEvent *event) override;

QString extractInvalidUsernameMessage(QString &in);

private:

void setClientStatusTitle();

void retranslateUi();

void createActions();

void createMenus();

void createTrayIcon();

void createTrayActions();

int getNextCustomSetPrefix(QDir dataDir);

inline QString getCardUpdaterBinaryName();

public slots:

void actCheckCardUpdates();

void actCheckServerUpdates();

private slots:

void updateTabMenu(const QList &newMenuList);

void statusChanged(ClientStatus _status);

void processConnectionClosedEvent(const Event_ConnectionClosed &event);

void processServerShutdownEvent(const Event_ServerShutdown &event);

void serverTimeout();

void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList missingFeatures);

void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);

void activateError();

void socketError(const QString &errorStr);

void protocolVersionMismatch(int localVersion, int remoteVersion);

void userInfoReceived(const ServerInfo_User &userInfo);

void registerAccepted();

void registerAcceptedNeedsActivate();

void activateAccepted();

void localGameEnded();

void pixmapCacheSizeChanged(int newSizeInMBs);

void notifyUserAboutUpdate();

void actConnect();

void actDisconnect();

void actSinglePlayer();

void actWatchReplay();

void actDeckEditor();

void actFullScreen(bool checked);

void actRegister();

void actSettings();

void actExit();

void actForgotPasswordRequest();

void actAbout();

void actTips();

void actUpdate();

void actViewLog();

void forgotPasswordSuccess();

void forgotPasswordError();

void promptForgotPasswordReset();

void iconActivated(QSystemTrayIcon::ActivationReason reason);

void promptForgotPasswordChallenge();

void showWindowIfHidden();

void cardUpdateError(QProcess::ProcessError err);

void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);

void refreshShortcuts();

void cardDatabaseLoadingFailed();

void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);

void cardDatabaseAllNewSetsEnabled();

void actOpenCustomFolder();

void actOpenCustomsetsFolder();

void actAddCustomSet();

void actManageSets();

void actEditTokens();

void startupConfigCheck();

void alertForcedOracleRun(const QString &version, bool isUpdate);

private:

static const QString appName;

static const QStringList fileNameFilters;

QList tabMenus;

QMenu *cockatriceMenu;

QMenu *dbMenu;

QMenu *helpMenu;

QMenu *trayIconMenu;

QAction *aConnect;

QAction *aDisconnect;

QAction *aSinglePlayer;

QAction *aWatchReplay;

QAction *aDeckEditor;

QAction *aFullScreen;

QAction *aSettings;

QAction *aExit;

QAction *aAbout;

QAction *aTips;

QAction *aCheckCardUpdates;

QAction *aRegister;

QAction *aUpdate;

QAction *aViewLog;

QAction *closeAction;

QAction *aManageSets;

QAction *aEditTokens;

QAction *aOpenCustomFolder;

QAction *aOpenCustomsetsFolder;

QAction *aAddCustomSet;

TabSupervisor *tabSupervisor;

WndSets *wndSets;

RemoteClient *client;

QThread *clientThread;

LocalServer *localServer;

bool bHasActivated;

QMessageBox serverShutdownMessageBox;

QProcess *cardUpdateProcess;

DlgViewLog *logviewDialog;

DlgConnect *dlgConnect;

GameReplay *replay;

DlgTipOfTheDay *tip;

QUrl connectTo;


简介

上一篇我们梳理了Cockatrice的初始化流程,这一篇我们接着分析Cockatrice的主界面构成。

Cockatrice提供了MainWindow类用来支撑主界面框架,该类继承自QMainWindow。


源码结构

Cockatrice的界面构成可以提炼为以下几个关键词:

[1]标签窗口

MainWindow提供了菜单栏和一个基于TabWidget的中心窗口,可通过菜单按钮在TabWidget中打开指定Tab标签页。目前提供了以下Tab标签页,它们的基类都是TabSupervisor类:

  • TabServer - 服务器管理页面
  • TabReplays - 游戏回放页面,可能会同时打开多个回放,所以通过 QList<TabGame *> replayTabs; 管理
  • TabDeckStorage - 牌组管理页面
  • TabAdmin - 用户登录页面
  • TabUserLists - 用户列表页面
  • TabLog - 日志页面
  • TabRoom -  房间页面,可能会同时打开多个房间,所以通过 QMap<int, TabRoom *> roomTabs; 管理
  • TabGame - 游戏页面,可能会同时打开多个游戏,所以通过 QMap<int, TabGame *> gameTabs; 管理
  • TabDeckEditor - 卡组编辑页面,可能会同时编辑多个卡组,所以通过 QList<TabDeckEditor *> deckEditorTabs; 管理
  • TabMessage - 聊天页面,因为消息分栏需要提供多个窗口,所以通过 QMap<QString, TabMessage *> messageTabs; 管理

[2]配置

SettingsCache类用来实现配置管理,封装了包括配置文件读写,全局配置缓存,配置查询,配置热更新等信号槽函数。

[3]国际化

每个窗体类中都实现了retranslateUi()方法,该方法会重新加载当前界面文字显示语言。各个窗口中的retranslateUi()存在层级调用关系。

[4]提示窗口

许多操作都需要用户确认或给出提示信息,因此提供了许多基于 QMessageBox 的提示页面。

[5]二级窗口

一些配置或功能需要弹出二级窗口,都是基于 QDialog 实现的,目前提供了以下 Dialog:

  • DlgConnect - 连接配置窗口
  • DlgRegister - 用户注册窗口
  • DlgEditUser - 用户信息修改窗口
  • DlgEditPassword -  密码修改装口
  • DlgForgotPasswordReset - 重置密码窗口
  • DlgForgotPasswordRequest  - 重置密码请求窗口
  • DlgForgotPasswordChallenge - 重置密码请求验证窗口
  • DlgCreateGame  - 游戏创建窗口
  • DlgFilterGames - 游戏过滤窗口
  • DlgLoadDeckFromClipboard - 牌组导入窗口(从剪贴板)
  • DlgLoadRemoteDeck - 牌组导入窗口(从远端)
  • DlgSettings - 配置窗口
  • DlgCreateToken -  Token创建窗口
  • DlgEditTokens - Token修改窗口
  • DlgEditAvatar - 背景自定义窗口
  • DlgUpdate - 更新窗口
  • DlgViewLog - 日志查看窗口
  • DlgTipOfTheDay - 每日提示窗口

源码分析

[1]调用MainWindow

main()中调用MainWindow生成主界面并设置主窗口&状态栏图标。

int main(int argc, char *argv[])
{
    。。。

    MainWindow ui;---->创建MainWindow实例
    if (parser.isSet("connect")) {
        ui.setConnectTo(parser.value("connect"));---->最终用QUrl保存了要连接的服务器地址
    }
    qDebug("main(): MainWindow constructor finished");

    ui.setWindowIcon(QPixmap("theme:cockatrice"));---->设置程序主窗口&状态栏显示的图标

    。。。

    ui.show();---->显示窗口
    qDebug("main(): ui.show() finished");
    
    。。。
}

[2]MainWindow类构成

MainWindow类继承自QMainWindow,主要在QMainWindow的基础上添加了许多槽函数。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public slots:
    void actCheckCardUpdates();---->启动一个线程加载外部数据库"oracle.exe",用来更新卡牌状态
    void actCheckServerUpdates();---->从Git镜像地址更新官方服务器列表文件。
private slots:
    void updateTabMenu(const QList<QMenu *> &newMenuList);---->用newMenuList内容更新菜单栏
    void statusChanged(ClientStatus _status);---->切换程序运行状态
    void processConnectionClosedEvent(const Event_ConnectionClosed &event);---->处理连接关闭事件
    void processServerShutdownEvent(const Event_ServerShutdown &event);---->处理服务器断链事件
    void serverTimeout();---->服务器超时
    void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);---->登录错误
    void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);---->注册错误
    void activateError();---->激活失败
    void socketError(const QString &errorStr);---->
    void protocolVersionMismatch(int localVersion, int remoteVersion);---->protobuf版本不匹配
    void userInfoReceived(const ServerInfo_User &userInfo);---->用户信息接收
    void registerAccepted();---->注册通过
    void registerAcceptedNeedsActivate();---->注册通过后需要激活
    void activateAccepted();---->激活通过
    void localGameEnded();---->本地游戏结束
    void pixmapCacheSizeChanged(int newSizeInMBs);---->图片缓存大小改变
    void notifyUserAboutUpdate();---->通知用户当前版本需要更新
    void actConnect();---->与服务器建立连接
    void actDisconnect();---->与服务器断开连接
    void actSinglePlayer();---->启动一个本地游戏
    void actWatchReplay();---->游戏录像回放
    void actDeckEditor();---->卡组编辑
    void actFullScreen(bool checked);---->设置全屏
    void actRegister();---->向服务器注册
    void actSettings();---->打开配置界面
    void actExit();---->退出
    void actForgotPasswordRequest();---->忘记密码
    void actAbout();---->关于
    void actTips();---->每日提醒
    void actUpdate();---->版本升级
    void actViewLog();---->创建日志窗口
    void forgotPasswordSuccess();---->重置密码成功
    void forgotPasswordError();---->重置密码失败
    void promptForgotPasswordReset();---->重置密码后邮件认证
    void iconActivated(QSystemTrayIcon::ActivationReason reason);---->点击右下角系统托盘图标
    void promptForgotPasswordChallenge();---->密码提示
    void showWindowIfHidden();---->从最小化恢复主界面显示
 
    void cardUpdateError(QProcess::ProcessError err);---->外部进程调用出错(更新卡牌数据库失败)
    void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);---->外部进程调用成功(更新卡牌数据库成功)
    void refreshShortcuts();---->刷新快捷键
    void cardDatabaseLoadingFailed();---->读取数据库失败
    void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);---->数据库配置查询
    void cardDatabaseAllNewSetsEnabled();---->数据库配置
 
    void actOpenCustomFolder();---->打开客户目录
    void actOpenCustomsetsFolder();---->打开客户指定目录
    void actAddCustomSet();---->添加客户配置
 
    void actManageSets();---->写入配置
    void actEditTokens();---->Token配置
 
    void startupConfigCheck();---->启动配置检查
    void alertForcedOracleRun(const QString &version, bool isUpdate);---->Oracle运行警告
 
private:
    static const QString appName;
    static const QStringList fileNameFilters;
    void setClientStatusTitle();---->按照当前状态更新标题栏
    void retranslateUi();---->按当前语言文字更新UI
    void createActions();---->创建所有菜单项并绑定信号槽
    void createMenus();---->创建菜单栏并添加菜单项
 
    void createTrayIcon();---->创建系统托盘图标
    void createTrayActions();---->为系统托盘图标创建菜单项
    int getNextCustomSetPrefix(QDir dataDir);---->生成用户设置名(指定目录下按顺序+1)
    // TODO: add a preference item to choose updater name for other games
    inline QString getCardUpdaterBinaryName()---->返回数据库程序名
    {
        return "oracle";
    };
 
    QList<QMenu *> tabMenus;
    QMenu *cockatriceMenu, *dbMenu, *helpMenu, *trayIconMenu;
    QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aFullScreen, *aSettings, *aExit,
        *aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aUpdate, *aViewLog, *closeAction;
    QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet;
    TabSupervisor *tabSupervisor;
    WndSets *wndSets;
    RemoteClient *client;
    QThread *clientThread;
    LocalServer *localServer;
    bool bHasActivated;
    QMessageBox serverShutdownMessageBox;
    QProcess *cardUpdateProcess;
    DlgViewLog *logviewDialog;
    DlgConnect *dlgConnect;
    GameReplay *replay;
    DlgTipOfTheDay *tip;
    QUrl connectTo;
 
public:
    explicit MainWindow(QWidget *parent = nullptr);---->构造函数
    void setConnectTo(QString url)---->指定服务器地址
    {
        connectTo = QUrl(QString("cockatrice://%1").arg(url));
    }
    ~MainWindow() override;---->析构函数
 
protected:
    void closeEvent(QCloseEvent *event) override;---->重写QCloceEvent事件,修复QT原生问题
    void changeEvent(QEvent *event) override;---->重写QEvent事件,额外处理语言切换事件和QActive触发事件
    QString extractInvalidUsernameMessage(QString &in);---->错误用户名分析
};

public:

explicit MainWindow(QWidget *parent = nullptr);

构造函数,explicit表示这是一个显示构造函数(参数不会被隐式转换),parent = nullptr表示这是顶层函数,所有窗口的爸爸,干掉它后所有子窗口/控件都会按结构倒着依次被析构。

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), localServer(nullptr), bHasActivated(false), cardUpdateProcess(nullptr),
      logviewDialog(nullptr)
{
    connect(settingsCache, SIGNAL(pixmapCacheSizeChanged(int)), this, SLOT(pixmapCacheSizeChanged(int)));
    pixmapCacheSizeChanged(settingsCache->getPixmapCacheSize());

    client = new RemoteClient;
    connect(client, SIGNAL(connectionClosedEventReceived(const Event_ConnectionClosed &)), this,
            SLOT(processConnectionClosedEvent(const Event_ConnectionClosed &)));
    connect(client, SIGNAL(serverShutdownEventReceived(const Event_ServerShutdown &)), this,
            SLOT(processServerShutdownEvent(const Event_ServerShutdown &)));
    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(serverTimeout()), this, SLOT(serverTimeout()));
    connect(client, SIGNAL(statusChanged(ClientStatus)), this, SLOT(statusChanged(ClientStatus)));
    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(notifyUserAboutUpdate()), this, SLOT(notifyUserAboutUpdate()));
    connect(client, SIGNAL(registerAccepted()), this, SLOT(registerAccepted()));
    connect(client, SIGNAL(registerAcceptedNeedsActivate()), this, SLOT(registerAcceptedNeedsActivate()));
    connect(client, SIGNAL(registerError(Response::ResponseCode, QString, quint32)), this,
            SLOT(registerError(Response::ResponseCode, QString, quint32)));
    connect(client, SIGNAL(activateAccepted()), this, SLOT(activateAccepted()));
    connect(client, SIGNAL(activateError()), this, SLOT(activateError()));
    connect(client, SIGNAL(sigForgotPasswordSuccess()), this, SLOT(forgotPasswordSuccess()));
    connect(client, SIGNAL(sigForgotPasswordError()), this, SLOT(forgotPasswordError()));
    connect(client, SIGNAL(sigPromptForForgotPasswordReset()), this, SLOT(promptForgotPasswordReset()));
    connect(client, SIGNAL(sigPromptForForgotPasswordChallenge()), this, SLOT(promptForgotPasswordChallenge()));

    clientThread = new QThread(this);
    client->moveToThread(clientThread);
    clientThread->start();

    createActions();
    createMenus();

    tabSupervisor = new TabSupervisor(client);
    connect(tabSupervisor, SIGNAL(setMenu(QList<QMenu *>)), this, SLOT(updateTabMenu(QList<QMenu *>)));
    connect(tabSupervisor, SIGNAL(localGameEnded()), this, SLOT(localGameEnded()));
    connect(tabSupervisor, SIGNAL(showWindowIfHidden()), this, SLOT(showWindowIfHidden()));
    tabSupervisor->addDeckEditorTab(nullptr);

    setCentralWidget(tabSupervisor);

    retranslateUi();

    if (!restoreGeometry(settingsCache->getMainWindowGeometry())) {
        setWindowState(Qt::WindowMaximized);
    }
    aFullScreen->setChecked(static_cast<bool>(windowState() & Qt::WindowFullScreen));

    if (QSystemTrayIcon::isSystemTrayAvailable()) {
        createTrayActions();
        createTrayIcon();
    }

    connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
    refreshShortcuts();

    connect(db, SIGNAL(cardDatabaseLoadingFailed()), this, SLOT(cardDatabaseLoadingFailed()));
    connect(db, SIGNAL(cardDatabaseNewSetsFound(int, QStringList)), this,
            SLOT(cardDatabaseNewSetsFound(int, QStringList)));
    connect(db, SIGNAL(cardDatabaseAllNewSetsEnabled()), this, SLOT(cardDatabaseAllNewSetsEnabled()));

    tip = new DlgTipOfTheDay();

    // run startup check async
    QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
}

void setConnectTo(QString url);

给connectTo变量赋值,指定服务器地址。

void setConnectTo(QString url)
{
    connectTo = QUrl(QString("cockatrice://%1").arg(url));
}

~MainWindow() override;

析构函数,To be a good neighbour~

MainWindow::~MainWindow()
{
    if (tip != nullptr) {
        delete tip;
        tip = nullptr;
    }
    if (trayIcon) {
        trayIcon->hide();
        trayIcon->deleteLater();
    }

    client->deleteLater();
    clientThread->wait();
}

protected:

void closeEvent(QCloseEvent *event) override;

重写了QMainWindow的closeEvent,这是为了解决QT原生QCloseEvent事件会连续触发两次的问题。

void MainWindow::closeEvent(QCloseEvent *event)
{
    // workaround Qt bug where closeEvent gets called twice
    static bool bClosingDown = false;
    if (bClosingDown)
        return;
    bClosingDown = true;

    if (!tabSupervisor->closeRequest()) {
        event->ignore();
        bClosingDown = false;
        return;
    }
    tip->close();

    event->accept();
    settingsCache->setMainWindowGeometry(saveGeometry());
    tabSupervisor->deleteLater();
}

void changeEvent(QEvent *event) override;

重写了QMainWindow的changeEvent,在调用QMainWindow的changeEvent前额外处理了QEvent::LanguageChange和QEvent::ActivationChange事件。

void MainWindow::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::LanguageChange)
        retranslateUi();
    else if (event->type() == QEvent::ActivationChange) {
        if (isActiveWindow() && !bHasActivated) {
            bHasActivated = true;
            if (!connectTo.isEmpty()) {
                qDebug() << "Command line connect to " << connectTo;
                client->connectToServer(connectTo.host(), connectTo.port(), connectTo.userName(), connectTo.password());
            } else if (settingsCache->servers().getAutoConnect()) {
                qDebug() << "Attempting auto-connect...";
                DlgConnect dlg(this);
                client->connectToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()), dlg.getPlayerName(),
                                        dlg.getPassword());
            }
        }
    }

    QMainWindow::changeEvent(event);
}

QString extractInvalidUsernameMessage(QString &in);

该函数只应该在用户名无效时被调用。它会检测用户名无效的原因,并输出相关提示信息。

QString MainWindow::extractInvalidUsernameMessage(QString &in)
{
    QString out = tr("Invalid username.") + "<br/>";
    QStringList rules = in.split(QChar('|'));
    if (rules.size() == 7 || rules.size() == 9) {
        out += tr("Your username must respect these rules:") + "<ul>";

        out += "<li>" + tr("is %1 - %2 characters long").arg(rules.at(0)).arg(rules.at(1)) + "</li>";
        out += "<li>" + tr("can %1 contain lowercase characters").arg((rules.at(2).toInt() > 0) ? "" : tr("NOT")) +
               "</li>";
        out += "<li>" + tr("can %1 contain uppercase characters").arg((rules.at(3).toInt() > 0) ? "" : tr("NOT")) +
               "</li>";
        out +=
            "<li>" + tr("can %1 contain numeric characters").arg((rules.at(4).toInt() > 0) ? "" : tr("NOT")) + "</li>";

        if (rules.at(6).size() > 0)
            out += "<li>" + tr("can contain the following punctuation: %1").arg(rules.at(6).toHtmlEscaped()) + "</li>";

        out += "<li>" +
               tr("first character can %1 be a punctuation mark").arg((rules.at(5).toInt() > 0) ? "" : tr("NOT")) +
               "</li>";

        if (rules.size() == 9) {
            if (rules.at(7).size() > 0)
                out += "<li>" + tr("can not contain any of the following words: %1").arg(rules.at(7).toHtmlEscaped()) +
                       "</li>";

            if (rules.at(8).size() > 0)
                out += "<li>" +
                       tr("can not match any of the following expressions: %1").arg(rules.at(8).toHtmlEscaped()) +
                       "</li>";
        }

        out += "</ul>";
    } else {
        out += tr("You may only use A-Z, a-z, 0-9, _, ., and - in your username.");
    }

    return out;
}

private:

void setClientStatusTitle();

通过client->getStatus()获取当前程序运行状态,然后显示对应描述在程序标题栏。正常运行时显示程序名。

void MainWindow::setClientStatusTitle()
{
    switch (client->getStatus()) {
        case StatusConnecting:
            setWindowTitle(appName + " - " + tr("Connecting to %1...").arg(client->peerName()));
            break;
        case StatusRegistering:
            setWindowTitle(appName + " - " +
                           tr("Registering to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
            break;
        case StatusDisconnected:
            setWindowTitle(appName + " - " + tr("Disconnected"));
            break;
        case StatusLoggingIn:
            setWindowTitle(appName + " - " + tr("Connected, logging in at %1").arg(client->peerName()));
            break;
        case StatusLoggedIn:
            setWindowTitle(client->getUserName() + "@" + client->peerName());
            break;
        case StatusRequestingForgotPassword:
            setWindowTitle(
                appName + " - " +
                tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
            break;
        case StatusSubmitForgotPasswordChallenge:
            setWindowTitle(
                appName + " - " +
                tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
            break;
        case StatusSubmitForgotPasswordReset:
            setWindowTitle(
                appName + " - " +
                tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
            break;
        default:
            setWindowTitle(appName);
    }
}

void retranslateUi();

调用setClientStatusTitle()将程序运行状态更新到标题栏(主要更新显示的文字语言),更新所有Menu(菜单栏)/Active(菜单项)/Tab(标签)的显示文字语言。

void MainWindow::retranslateUi()
{
    setClientStatusTitle();

    aConnect->setText(tr("&Connect..."));
    aDisconnect->setText(tr("&Disconnect"));
    aSinglePlayer->setText(tr("Start &local game..."));
    aWatchReplay->setText(tr("&Watch replay..."));
    aDeckEditor->setText(tr("&Deck editor"));
    aFullScreen->setText(tr("&Full screen"));
    aRegister->setText(tr("&Register to server..."));
    aSettings->setText(tr("&Settings..."));
    aSettings->setIcon(QPixmap("theme:icons/settings"));
    aExit->setText(tr("&Exit"));

#if defined(__APPLE__) /* For OSX */
    cockatriceMenu->setTitle(tr("A&ctions"));
#else
    cockatriceMenu->setTitle(tr("&Cockatrice"));
#endif

    dbMenu->setTitle(tr("C&ard Database"));
    aOpenCustomFolder->setText(tr("Open custom image folder"));
    aOpenCustomsetsFolder->setText(tr("Open custom sets folder"));
    aAddCustomSet->setText(tr("Add custom sets/cards"));
    aManageSets->setText(tr("&Manage sets..."));
    aEditTokens->setText(tr("Edit custom &tokens..."));

    aAbout->setText(tr("&About Cockatrice"));
    aTips->setText(tr("&Tip of the Day"));
    aUpdate->setText(tr("Check for Client Updates"));
    aViewLog->setText(tr("View &debug log"));
    helpMenu->setTitle(tr("&Help"));
    aCheckCardUpdates->setText(tr("Check for card updates..."));
    tabSupervisor->retranslateUi();
}

void createActions();

创建所有菜单项,这里使用QT_TRANSLATE_NOOP()翻译字符串。

QT_TR_NOOP("xxx")用来一次翻译一个字符串,QT_TRANSLATE_NOOP("xxx", "xxx")用来一次翻译多个字符串。

void MainWindow::createActions()
{
    aConnect = new QAction(this);
    connect(aConnect, SIGNAL(triggered()), this, SLOT(actConnect()));
    aDisconnect = new QAction(this);
    aDisconnect->setEnabled(false);
    connect(aDisconnect, SIGNAL(triggered()), this, SLOT(actDisconnect()));
    aSinglePlayer = new QAction(this);
    connect(aSinglePlayer, SIGNAL(triggered()), this, SLOT(actSinglePlayer()));
    aWatchReplay = new QAction(this);
    connect(aWatchReplay, SIGNAL(triggered()), this, SLOT(actWatchReplay()));
    aDeckEditor = new QAction(this);
    connect(aDeckEditor, SIGNAL(triggered()), this, SLOT(actDeckEditor()));
    aFullScreen = new QAction(this);
    aFullScreen->setCheckable(true);
    connect(aFullScreen, SIGNAL(toggled(bool)), this, SLOT(actFullScreen(bool)));
    aRegister = new QAction(this);
    connect(aRegister, SIGNAL(triggered()), this, SLOT(actRegister()));
    aSettings = new QAction(this);
    connect(aSettings, SIGNAL(triggered()), this, SLOT(actSettings()));
    aExit = new QAction(this);
    connect(aExit, SIGNAL(triggered()), this, SLOT(actExit()));

    aAbout = new QAction(this);
    connect(aAbout, SIGNAL(triggered()), this, SLOT(actAbout()));
    aTips = new QAction(this);
    connect(aTips, SIGNAL(triggered()), this, SLOT(actTips()));
    aUpdate = new QAction(this);
    connect(aUpdate, SIGNAL(triggered()), this, SLOT(actUpdate()));
    aViewLog = new QAction(this);
    connect(aViewLog, SIGNAL(triggered()), this, SLOT(actViewLog()));

    aCheckCardUpdates = new QAction(this);
    connect(aCheckCardUpdates, SIGNAL(triggered()), this, SLOT(actCheckCardUpdates()));

    aOpenCustomsetsFolder = new QAction(QString(), this);
    connect(aOpenCustomsetsFolder, SIGNAL(triggered()), this, SLOT(actOpenCustomsetsFolder()));

    aOpenCustomFolder = new QAction(QString(), this);
    connect(aOpenCustomFolder, SIGNAL(triggered()), this, SLOT(actOpenCustomFolder()));

    aAddCustomSet = new QAction(QString(), this);
    connect(aAddCustomSet, SIGNAL(triggered()), this, SLOT(actAddCustomSet()));

    aManageSets = new QAction(QString(), this);
    connect(aManageSets, SIGNAL(triggered()), this, SLOT(actManageSets()));

    aEditTokens = new QAction(QString(), this);
    connect(aEditTokens, SIGNAL(triggered()), this, SLOT(actEditTokens()));

#if defined(__APPLE__) /* For OSX */
    aSettings->setMenuRole(QAction::PreferencesRole);
    aExit->setMenuRole(QAction::QuitRole);
    aAbout->setMenuRole(QAction::AboutRole);

    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Services"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide %1"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide Others"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Show All"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Preferences..."));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Quit %1"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "About %1"));
#endif
    // translate Qt's dialogs "default button text"; list taken from QPlatformTheme::defaultStandardButtonText()
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "OK"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save All"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Open"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&No"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Abort"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Retry"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Close"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Discard"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Help"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Apply"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Reset"));
    Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults"));
}

void createMenus();

创建所有菜单栏,将具体菜单项添加到对应菜单栏。

QT创建菜单栏需要三步:

1 - 分别创建菜单栏QMenuBar,菜单项QMenu,每个菜单项对应的动作QAction

2 - QMenuBar->addMenu(QMenu)将菜单项加入菜单栏

3 - QMenu->addAction(QAction)将动作绑定到菜单项

void MainWindow::createMenus()
{
    cockatriceMenu = menuBar()->addMenu(QString());
    cockatriceMenu->addAction(aConnect);
    cockatriceMenu->addAction(aDisconnect);
    cockatriceMenu->addAction(aRegister);
    cockatriceMenu->addSeparator();
    cockatriceMenu->addAction(aSinglePlayer);
    cockatriceMenu->addAction(aWatchReplay);
    cockatriceMenu->addSeparator();
    cockatriceMenu->addAction(aDeckEditor);
    cockatriceMenu->addSeparator();
    cockatriceMenu->addAction(aFullScreen);
    cockatriceMenu->addSeparator();
    cockatriceMenu->addAction(aSettings);
    cockatriceMenu->addAction(aCheckCardUpdates);
    cockatriceMenu->addSeparator();
    cockatriceMenu->addAction(aExit);

    dbMenu = menuBar()->addMenu(QString());
    dbMenu->addAction(aManageSets);
    dbMenu->addAction(aEditTokens);
    dbMenu->addSeparator();
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
    dbMenu->addAction(aOpenCustomFolder);
    dbMenu->addAction(aOpenCustomsetsFolder);
#endif
    dbMenu->addAction(aAddCustomSet);

    helpMenu = menuBar()->addMenu(QString());
    helpMenu->addAction(aAbout);
    helpMenu->addAction(aTips);
    helpMenu->addAction(aUpdate);
    helpMenu->addAction(aViewLog);
}

void createTrayIcon();

创建系统托盘图标(右下角状态栏)。

void MainWindow::createTrayIcon()
{
    trayIconMenu = new QMenu(this);
    trayIconMenu->addAction(closeAction);

    trayIcon = new QSystemTrayIcon(this);
    trayIcon->setContextMenu(trayIconMenu);
    trayIcon->setIcon(QPixmap("theme:cockatrice"));
    trayIcon->show();

    connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,
            SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
}

void createTrayActions();

为系统托盘图标创建菜单项,目前只做了退出程序。

void MainWindow::createTrayActions()
{
    closeAction = new QAction(tr("&Exit"), this);
    connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
}

int getNextCustomSetPrefix(QDir dataDir);

读取数据库目录下所有文件,形成列表并遍历检测出最大数字名,输出该数字+1作为新文件名。

遍历实现用到了很经典的C++风格遍历器写法。

int MainWindow::getNextCustomSetPrefix(QDir dataDir)
{
    QStringList files = dataDir.entryList();
    int maxIndex = 0;

    QStringList::const_iterator filesIterator;
    for (filesIterator = files.constBegin(); filesIterator != files.constEnd(); ++filesIterator) {
        int fileIndex = (*filesIterator).split(".").at(0).toInt();
        if (fileIndex > maxIndex)
            maxIndex = fileIndex;
    }

    return maxIndex + 1;
}

inline QString getCardUpdaterBinaryName();

返回调用的外部数据库程序名,目前使用的是oracle。

inline表明这是一个内联函数。内联函数一般建议放在头文件中定义,内联函数中不能直接递归(自己调用自己)或包含复杂控制语句(while/switch等),且具体效果与编译器相关。

inline QString getCardUpdaterBinaryName()
{
    return "oracle";
};

public slots:

void actCheckCardUpdates();

槽函数:启动一个线程加载外部数据库程序"oracle.exe"用来更新卡牌状态,这里数据库程序名是通过getCardUpdaterBinaryName()获取的。

/* CARD UPDATER */
void MainWindow::actCheckCardUpdates()
{
    if (cardUpdateProcess) {
        QMessageBox::information(this, tr("Information"), tr("A card database update is already running."));
        return;
    }

    cardUpdateProcess = new QProcess(this);
    connect(cardUpdateProcess, SIGNAL(error(QProcess::ProcessError)), this,
            SLOT(cardUpdateError(QProcess::ProcessError)));
    connect(cardUpdateProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this,
            SLOT(cardUpdateFinished(int, QProcess::ExitStatus)));

    // full "run the update" command; leave empty if not present
    QString updaterCmd;
    QString binaryName;
    QDir dir = QDir(QApplication::applicationDirPath());

#if defined(Q_OS_MAC)
    binaryName = getCardUpdaterBinaryName();

    // exit from the application bundle
    dir.cdUp();
    dir.cdUp();
    dir.cdUp();
    dir.cd(binaryName + ".app");
    dir.cd("Contents");
    dir.cd("MacOS");
#elif defined(Q_OS_WIN)
    binaryName = getCardUpdaterBinaryName() + ".exe";
#else
    binaryName = getCardUpdaterBinaryName();
#endif

    if (dir.exists(binaryName))
        updaterCmd = dir.absoluteFilePath(binaryName);

    if (updaterCmd.isEmpty()) {
        QMessageBox::warning(this, tr("Error"),
                             tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
        return;
    }

    cardUpdateProcess->start("\"" + updaterCmd + "\"");
}

void actCheckServerUpdates();

槽函数:从"https://cockatrice.github.io/public-servers.json"下载鸡蛇官方服务器地址列表。如果我们要自己搭服务器的话,需要改动这个URL或者在官方列表中加入自己的服务器地址。

void MainWindow::actCheckServerUpdates()
{
    auto hps = new HandlePublicServers(this);
    hps->downloadPublicServers();
    connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, [=]() { hps->deleteLater(); });
}

private slots:

void updateTabMenu(const QList<QMenu *> &newMenuList);

槽函数:用传入的newMenuList内容动态更新菜单栏。

void MainWindow::updateTabMenu(const QList<QMenu *> &newMenuList)
{
    for (auto &tabMenu : tabMenus)
        menuBar()->removeAction(tabMenu->menuAction());
    tabMenus = newMenuList;
    for (auto &tabMenu : tabMenus)
        menuBar()->insertMenu(helpMenu->menuAction(), tabMenu);
}

void statusChanged(ClientStatus _status);

槽函数:根据当前状态刷新标题栏显示,并决定菜单栏选项是否可用(不可用的选项会变成灰色,无法选中)。

void MainWindow::statusChanged(ClientStatus _status)
{
    setClientStatusTitle();
    switch (_status) {
        case StatusDisconnected:
            tabSupervisor->stop();
            aSinglePlayer->setEnabled(true);
            aConnect->setEnabled(true);
            aRegister->setEnabled(true);
            aDisconnect->setEnabled(false);
            break;
        case StatusLoggingIn:
            aSinglePlayer->setEnabled(false);
            aConnect->setEnabled(false);
            aRegister->setEnabled(false);
            aDisconnect->setEnabled(true);
            break;
        case StatusConnecting:
        case StatusRegistering:
        case StatusLoggedIn:
        default:
            break;
    }
}

void processConnectionClosedEvent(const Event_ConnectionClosed &event);

槽函数:调用的外部进程正常退出时,给出QMessageBox弹窗提示。

void MainWindow::processConnectionClosedEvent(const Event_ConnectionClosed &event)
{
    client->disconnectFromServer();
    QString reasonStr;
    switch (event.reason()) {
        case Event_ConnectionClosed::USER_LIMIT_REACHED:
            reasonStr = tr("The server has reached its maximum user capacity, please check back later.");
            break;
        case Event_ConnectionClosed::TOO_MANY_CONNECTIONS:
            reasonStr = tr("There are too many concurrent connections from your address.");
            break;
        case Event_ConnectionClosed::BANNED: {
            reasonStr = tr("Banned by moderator");
            if (event.has_end_time())
                reasonStr.append("\n" +
                                 tr("Expected end time: %1").arg(QDateTime::fromTime_t(event.end_time()).toString()));
            else
                reasonStr.append("\n" + tr("This ban lasts indefinitely."));
            if (event.has_reason_str())
                reasonStr.append("\n\n" + QString::fromStdString(event.reason_str()));
            break;
        }
        case Event_ConnectionClosed::SERVER_SHUTDOWN:
            reasonStr = tr("Scheduled server shutdown.");
            break;
        case Event_ConnectionClosed::USERNAMEINVALID:
            reasonStr = tr("Invalid username.");
            break;
        case Event_ConnectionClosed::LOGGEDINELSEWERE:
            reasonStr = tr("You have been logged out due to logging in at another location.");
            break;
        default:
            reasonStr = QString::fromStdString(event.reason_str());
    }
    QMessageBox::critical(this, tr("Connection closed"),
                          tr("The server has terminated your connection.\nReason: %1").arg(reasonStr));
}

void processServerShutdownEvent(const Event_ServerShutdown &event);

槽函数:调用的外部进程被强制中断时,给出QMessageBox弹窗提示。

void MainWindow::processServerShutdownEvent(const Event_ServerShutdown &event)
{
    serverShutdownMessageBox.setInformativeText(tr("The server is going to be restarted in %n minute(s).\nAll running "
                                                   "games will be lost.\nReason for shutdown: %1",
                                                   "", event.minutes())
                                                    .arg(QString::fromStdString(event.reason())));
    serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
    serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
    serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
    serverShutdownMessageBox.setVisible(true);
}

void serverTimeout();

槽函数:服务超时时,给出QMessageBox弹窗提示,并调用actConnect()尝试重新建立连接。

void MainWindow::serverTimeout()
{
    QMessageBox::critical(this, tr("Error"), tr("Server timeout"));
    actConnect();
}

void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);

槽函数:登陆失败时,给出QMessageBox弹窗提示,并调用actConnect()尝试重新建立连接。

void MainWindow::loginError(Response::ResponseCode r,
                            QString reasonStr,
                            quint32 endTime,
                            QList<QString> missingFeatures)
{
    switch (r) {
        case Response::RespClientUpdateRequired: {
            QString formattedMissingFeatures;
            formattedMissingFeatures = "Missing Features: ";
            for (int i = 0; i < missingFeatures.size(); ++i)
                formattedMissingFeatures.append(QString("\n     %1").arg(QChar(0x2022)) + " " +
                                                missingFeatures.value(i));

            QMessageBox msgBox;
            msgBox.setIcon(QMessageBox::Critical);
            msgBox.setWindowTitle(tr("Failed Login"));
            msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
                           "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
            msgBox.setDetailedText(formattedMissingFeatures);
            msgBox.exec();
            break;
        }
        case Response::RespWrongPassword:
            QMessageBox::critical(
                this, tr("Error"),
                tr("Incorrect username or password. Please check your authentication information and try again."));
            break;
        case Response::RespWouldOverwriteOldSession:
            QMessageBox::critical(this, tr("Error"),
                                  tr("There is already an active session using this user name.\nPlease close that "
                                     "session first and re-login."));
            break;
        case Response::RespUserIsBanned: {
            QString bannedStr;
            if (endTime)
                bannedStr = tr("You are banned until %1.").arg(QDateTime::fromTime_t(endTime).toString());
            else
                bannedStr = tr("You are banned indefinitely.");
            if (!reasonStr.isEmpty())
                bannedStr.append("\n\n" + reasonStr);

            QMessageBox::critical(this, tr("Error"), bannedStr);
            break;
        }
        case Response::RespUsernameInvalid: {
            QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
            break;
        }
        case Response::RespRegistrationRequired:
            if (QMessageBox::question(this, tr("Error"),
                                      tr("This server requires user registration. Do you want to register now?"),
                                      QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
                actRegister();
            }
            break;
        case Response::RespClientIdRequired:
            QMessageBox::critical(
                this, tr("Error"),
                tr("This server requires client IDs. Your client is either failing to generate an ID or you are "
                   "running a modified client.\nPlease close and reopen your client to try again."));
            break;
        case Response::RespContextError:
            QMessageBox::critical(this, tr("Error"),
                                  tr("An internal error has occurred, please close and reopen Cockatrice before trying "
                                     "again.\nIf the error persists, ensure you are running the latest version of the "
                                     "software and if needed contact the software developers."));
            break;
        case Response::RespAccountNotActivated: {
            bool ok = false;
            QString token = QInputDialog::getText(this, tr("Account activation"),
                                                  tr("Your account has not been activated yet.\nYou need to provide "
                                                     "the activation token received in the activation email."),
                                                  QLineEdit::Normal, QString(), &ok);
            if (ok && !token.isEmpty()) {
                client->activateToServer(token);
                return;
            }
            client->disconnectFromServer();
            break;
        }
        case Response::RespServerFull: {
            QMessageBox::critical(this, tr("Server Full"),
                                  tr("The server has reached its maximum user capacity, please check back later."));
            break;
        }
        default:
            QMessageBox::critical(this, tr("Error"),
                                  tr("Unknown login error: %1").arg(static_cast<int>(r)) +
                                      tr("\nThis usually means that your client version is out of date, and the server "
                                         "sent a reply your client doesn't understand."));
            break;
    }
    actConnect();
}

void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);

槽函数:注册失败时,给出QMessageBox弹窗提示,并调用actRegister()尝试重新建立连接。

void MainWindow::registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime)
{
    switch (r) {
        case Response::RespRegistrationDisabled:
            QMessageBox::critical(this, tr("Registration denied"),
                                  tr("Registration is currently disabled on this server"));
            break;
        case Response::RespUserAlreadyExists:
            QMessageBox::critical(this, tr("Registration denied"),
                                  tr("There is already an existing account with the same user name."));
            break;
        case Response::RespEmailRequiredToRegister:
            QMessageBox::critical(this, tr("Registration denied"),
                                  tr("It's mandatory to specify a valid email address when registering."));
            break;
        case Response::RespEmailBlackListed:
            QMessageBox::critical(
                this, tr("Registration denied"),
                tr("The email address provider used during registration has been blacklisted for use on this server."));
            break;
        case Response::RespTooManyRequests:
            QMessageBox::critical(
                this, tr("Registration denied"),
                tr("It appears you are attempting to register a new account on this server yet you already have an "
                   "account registered with the email provided. This server restricts the number of accounts a user "
                   "can register per address.  Please contact the server operator for further assistance or to obtain "
                   "your credential information."));
            break;
        case Response::RespPasswordTooShort:
            QMessageBox::critical(this, tr("Registration denied"), tr("Password too short."));
            break;
        case Response::RespUserIsBanned: {
            QString bannedStr;
            if (endTime)
                bannedStr = tr("You are banned until %1.").arg(QDateTime::fromTime_t(endTime).toString());
            else
                bannedStr = tr("You are banned indefinitely.");
            if (!reasonStr.isEmpty())
                bannedStr.append("\n\n" + reasonStr);

            QMessageBox::critical(this, tr("Error"), bannedStr);
            break;
        }
        case Response::RespUsernameInvalid: {
            QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
            break;
        }
        case Response::RespRegistrationFailed:
            QMessageBox::critical(this, tr("Error"), tr("Registration failed for a technical problem on the server."));
            break;
        default:
            QMessageBox::critical(this, tr("Error"),
                                  tr("Unknown registration error: %1").arg(static_cast<int>(r)) +
                                      tr("\nThis usually means that your client version is out of date, and the server "
                                         "sent a reply your client doesn't understand."));
    }
    actRegister();
}

void activateError();

槽函数:激活失败时,给出QMessageBox弹窗提示,并调用actConnect()尝试重新建立连接。

void MainWindow::activateError()
{
    QMessageBox::critical(this, tr("Error"), tr("Account activation failed"));
    client->disconnectFromServer();
    actConnect();
}

void socketError(const QString &errorStr);

槽函数:socket连接出错时,给出QMessageBox弹窗提示,并调用actConnect()尝试重新建立连接。

void MainWindow::socketError(const QString &errorStr)
{
    QMessageBox::critical(this, tr("Error"), tr("Socket error: %1").arg(errorStr));
    actConnect();
}

void protocolVersionMismatch(int localVersion, int remoteVersion);

槽函数:服务器与客户端版本校验失败时,给出QMessageBox弹窗提示。

由于使用了protobuf进行数据封装,因此需要服务器客户端间版本匹配~

void MainWindow::protocolVersionMismatch(int localVersion, int remoteVersion)
{
    if (localVersion > remoteVersion)
        QMessageBox::critical(this, tr("Error"),
                              tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
                                 "version or connect to a suitable server.\nLocal version is %1, remote version is %2.")
                                  .arg(localVersion)
                                  .arg(remoteVersion));
    else
        QMessageBox::critical(this, tr("Error"),
                              tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\nLocal "
                                 "version is %1, remote version is %2.")
                                  .arg(localVersion)
                                  .arg(remoteVersion));
}

void userInfoReceived(const ServerInfo_User &userInfo);

槽函数:用户信息保存,显示在tab页面上。

void MainWindow::userInfoReceived(const ServerInfo_User &info)
{
    tabSupervisor->start(info);
}

void registerAccepted();

槽函数:注册成功时,给出QMessageBox弹窗提示。

void MainWindow::registerAccepted()
{
    QMessageBox::information(this, tr("Success"), tr("Registration accepted.\nWill now login."));
}

void registerAcceptedNeedsActivate();

槽函数:注册通过后需要激活。

void MainWindow::registerAcceptedNeedsActivate()
{
    // nothing
}

void activateAccepted();

槽函数:激活通过后,给出QMessageBox弹窗提示。

void MainWindow::activateAccepted()
{
    QMessageBox::information(this, tr("Success"), tr("Account activation accepted.\nWill now login."));
}

void localGameEnded();

槽函数:游戏结束后断开连接,设置菜单栏按钮使能(允许开始别的游戏)。

void MainWindow::localGameEnded()
{
    delete localServer;
    localServer = nullptr;

    aConnect->setEnabled(true);
    aRegister->setEnabled(true);
    aSinglePlayer->setEnabled(true);
}

void pixmapCacheSizeChanged(int newSizeInMBs);

槽函数:设置QPixmap占用缓存的上限,QT默认值为10240KB。

void MainWindow::pixmapCacheSizeChanged(int newSizeInMBs)
{
    // qDebug() << "Setting pixmap cache size to " << value << " MBs";
    // translate MBs to KBs
    QPixmapCache::setCacheLimit(newSizeInMBs * 1024);
}

void notifyUserAboutUpdate();

槽函数:通知用户当前版本需要更新,并给出QMessageBox弹窗提示。

void MainWindow::notifyUserAboutUpdate()
{
    QMessageBox::information(
        this, tr("Information"),
        tr("This server supports additional features that your client doesn't have.\nThis is most likely not a "
           "problem, but this message might mean there is a new version of Cockatrice available or this server is "
           "running a custom or pre-release version.\n\nTo update your client, go to Help -> Check for Updates."));
}

void actConnect();

槽函数:与服务器建立连接,注意这里先创建了一个QDialog,之后调用dlgConnect->exec()设置dlgConnect为模态,然后尝试建立连接。

QDialog->exec()会将当前QDialog设置为模态,返回结果成功(QDialog::Accepted---->1),失败(QDialog::Rejected---->0)。

这里client是一个RemoteClient类型的实例,RemoteClient单独开一篇博文分析。

void MainWindow::actConnect()
{
    dlgConnect = new DlgConnect(this);
    connect(dlgConnect, SIGNAL(sigStartForgotPasswordRequest()), this, SLOT(actForgotPasswordRequest()));

    if (dlgConnect->exec()) {
        client->connectToServer(dlgConnect->getHost(), static_cast<unsigned int>(dlgConnect->getPort()),
                                dlgConnect->getPlayerName(), dlgConnect->getPassword());
    }
}

void actDisconnect();

槽函数:与服务器断开连接。

这里client是一个RemoteClient类型的实例,RemoteClient单独开一篇博文分析。

void MainWindow::actDisconnect()
{
    client->disconnectFromServer();
}

void actSinglePlayer();

槽函数:启动一个本地游戏,SOLO模式(打开一个新的Tab页)。

void MainWindow::actSinglePlayer()
{
    bool ok;
    int numberPlayers =
        QInputDialog::getInt(this, tr("Number of players"), tr("Please enter the number of players."), 1, 1, 8, 1, &ok);
    if (!ok)
        return;

    aConnect->setEnabled(false);
    aRegister->setEnabled(false);
    aSinglePlayer->setEnabled(false);

    localServer = new LocalServer(this);
    LocalServerInterface *mainLsi = localServer->newConnection();
    LocalClient *mainClient = new LocalClient(mainLsi, tr("Player %1").arg(1), settingsCache->getClientID(), this);
    QList<AbstractClient *> localClients;
    localClients.append(mainClient);

    for (int i = 0; i < numberPlayers - 1; ++i) {
        LocalServerInterface *slaveLsi = localServer->newConnection();
        LocalClient *slaveClient =
            new LocalClient(slaveLsi, tr("Player %1").arg(i + 2), settingsCache->getClientID(), this);
        localClients.append(slaveClient);
    }
    tabSupervisor->startLocal(localClients);

    Command_CreateGame createCommand;
    createCommand.set_max_players(static_cast<google::protobuf::uint32>(numberPlayers));
    mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0));
}

void actWatchReplay();

槽函数:播放Cockatrice支持的.cor格式录像(打开一个新的Tab页)。

void MainWindow::actWatchReplay()
{
    QFileDialog dlg(this, tr("Load replay"));
    dlg.setDirectory(settingsCache->getReplaysPath());
    dlg.setNameFilters(QStringList() << QObject::tr("Cockatrice replays (*.cor)"));
    if (!dlg.exec())
        return;

    QString fileName = dlg.selectedFiles().at(0);
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly))
        return;
    QByteArray buf = file.readAll();
    file.close();

    replay = new GameReplay;
    replay->ParseFromArray(buf.data(), buf.size());

    tabSupervisor->openReplay(replay);
}

void actDeckEditor();

槽函数:打开卡组编辑器(打开一个新的Tab页)。

void MainWindow::actDeckEditor()
{
    tabSupervisor->addDeckEditorTab(nullptr);
}

void actFullScreen(bool checked);

槽函数:设置全屏。

void MainWindow::actFullScreen(bool checked)
{
    if (checked)
        setWindowState(windowState() | Qt::WindowFullScreen);
    else
        setWindowState(windowState() & ~Qt::WindowFullScreen);
}

void actRegister();

槽函数:向服务器注册。

这里client是一个RemoteClient类型的实例,RemoteClient单独开一篇博文分析。

void MainWindow::actRegister()
{
    DlgRegister dlg(this);
    if (dlg.exec()) {
        client->registerToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()), dlg.getPlayerName(),
                                 dlg.getPassword(), dlg.getEmail(), dlg.getGender(), dlg.getCountry(),
                                 dlg.getRealName());
    }
}

void actSettings();

槽函数:打开配置窗口,代码实现同actConnect()。

QDialog->exec()会将当前QDialog设置为模态,返回结果成功(QDialog::Accepted---->1),失败(QDialog::Rejected---->0)。

void MainWindow::actSettings()
{
    DlgSettings dlg(this);
    dlg.exec();
}

void actExit();

槽函数:执行QWidget->close()关闭MainWindow窗口。

void MainWindow::actExit()
{
    close();
}

void actForgotPasswordRequest();

槽函数:忘记密码,,向服务器发送密码找回/重置请求,代码实现同actConnect()。

QDialog->exec()会将当前QDialog设置为模态,返回结果成功(QDialog::Accepted---->1),失败(QDialog::Rejected---->0)。

这里client是一个RemoteClient类型的实例,RemoteClient单独开一篇博文分析。

void MainWindow::actForgotPasswordRequest()
{
    DlgForgotPasswordRequest dlg(this);
    if (dlg.exec())
        client->requestForgotPasswordToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
                                              dlg.getPlayerName());
}

void actAbout();

槽函数:显示About关于信息,直接用HTML语言写了个页面内嵌到QMessageBox中。

void MainWindow::actAbout()
{
    QMessageBox mb(
        QMessageBox::NoIcon, tr("About Cockatrice"),
        QString("<font size=\"8\"><b>Cockatrice</b></font> (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")<br>" +
                tr("Version") + QString(" %1").arg(VERSION_STRING) + "<br><br><b><a href='" + GITHUB_PAGES_URL + "'>" +
                tr("Cockatrice Webpage") + "</a></b><br>" + "<br><b>" + tr("Project Manager:") +
                "</b><br>Zach Halpern<br><br>" + "<b>" + tr("Past Project Managers:") +
                "</b><br>Gavin Bisesi<br>Max-Wilhelm Bruker<br>Marcus Schütz<br><br>" + "<b>" + tr("Developers:") +
                "</b><br>" + "<a href='" + GITHUB_CONTRIBUTORS_URL + "'>" + tr("Our Developers") + "</a><br>" +
                "<a href='" + GITHUB_CONTRIBUTE_URL + "'>" + tr("Help Develop!") + "</a><br><br>" + "<b>" +
                tr("Translators:") + "</b><br>" + "<a href='" + GITHUB_TRANSIFEX_TRANSLATORS_URL + "'>" +
                tr("Our Translators") + "</a><br>" + "<a href='" + GITHUB_TRANSLATOR_FAQ_URL + "'>" +
                tr("Help Translate!") + "</a><br><br>" + "<b>" + tr("Support:") + "</b><br>" + "<a href='" +
                GITHUB_ISSUES_URL + "'>" + tr("Report an Issue") + "</a><br>" + "<a href='" +
                GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
                "'>" + tr("F.A.Q.") + "</a><br>"),
        QMessageBox::Ok, this);
    mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
    mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
    mb.exec();
}

void actTips();

槽函数:显示Tips每日提示。

void MainWindow::actTips()
{
    if (tip != nullptr) {
        delete tip;
        tip = nullptr;
    }
    tip = new DlgTipOfTheDay(this);
    if (tip->successfulInit) {
        tip->show();
    }
}

void actUpdate();

槽函数:打开升级窗口,代码实现同actConnect()。

QDialog->exec()会将当前QDialog设置为模态,返回结果成功(QDialog::Accepted---->1),失败(QDialog::Rejected---->0)。

void MainWindow::actUpdate()
{
    DlgUpdate dlg(this);
    dlg.exec();
}

void actViewLog();

槽函数:创建log窗口。

void MainWindow::actViewLog()
{
    if (logviewDialog == nullptr) {
        logviewDialog = new DlgViewLog(this);
    }

    logviewDialog->show();
    logviewDialog->raise();
    logviewDialog->activateWindow();
}

void forgotPasswordSuccess();

槽函数:重置密码成功,并给出QMessageBox弹窗提示。

void MainWindow::forgotPasswordSuccess()
{
    QMessageBox::information(
        this, tr("Forgot Password"),
        tr("Your password has been reset successfully, you can now log in using the new credentials."));
    settingsCache->servers().setFPHostName("");
    settingsCache->servers().setFPPort("");
    settingsCache->servers().setFPPlayerName("");
}

void forgotPasswordError();

槽函数:重置密码失败,并给出QMessageBox弹窗提示。

void MainWindow::forgotPasswordError()
{
    QMessageBox::warning(
        this, tr("Forgot Password"),
        tr("Failed to reset user account password, please contact the server operator to reset your password."));
    settingsCache->servers().setFPHostName("");
    settingsCache->servers().setFPPort("");
    settingsCache->servers().setFPPlayerName("");
}

void promptForgotPasswordReset();

槽函数:重置密码请求被受理后,服务器发送邮件到邮箱,并给出QMessageBox弹窗提示。

void MainWindow::promptForgotPasswordReset()
{
    QMessageBox::information(this, tr("Forgot Password"),
                             tr("Activation request received, please check your email for an activation token."));
    DlgForgotPasswordReset dlg(this);
    if (dlg.exec()) {
        client->submitForgotPasswordResetToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
                                                  dlg.getPlayerName(), dlg.getToken(), dlg.getPassword());
    }
}

void iconActivated(QSystemTrayIcon::ActivationReason reason);

槽函数:系统托盘点击事件。

QSystemTrayIcon为系统托盘图标(右下角的状态栏图标)。

void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
{
    if (reason == QSystemTrayIcon::DoubleClick) {
        if (windowState() != Qt::WindowMinimized && windowState() != Qt::WindowMinimized + Qt::WindowMaximized)
            showMinimized();
        else {
            showNormal();
            QApplication::setActiveWindow(this);
        }
    }
}

void promptForgotPasswordChallenge();

槽函数:密码提示。

void MainWindow::promptForgotPasswordChallenge()
{
    DlgForgotPasswordChallenge dlg(this);
    if (dlg.exec())
        client->submitForgotPasswordChallengeToServer(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
                                                      dlg.getPlayerName(), dlg.getEmail());
}

void showWindowIfHidden();

槽函数:从最小化恢复主界面显示。

void MainWindow::showWindowIfHidden()
{
    // keep the previous window state
    setWindowState(windowState() & ~Qt::WindowMinimized);
    show();
}

void cardUpdateError(QProcess::ProcessError err);

槽函数:外部进程调用出错,清理进程资源,并给出QMessageBox弹窗提示。

void MainWindow::cardUpdateError(QProcess::ProcessError err)
{
    QString error;
    switch (err) {
        case QProcess::FailedToStart:
            error = tr("failed to start.");
            break;
        case QProcess::Crashed:
            error = tr("crashed.");
            break;
        case QProcess::Timedout:
            error = tr("timed out.");
            break;
        case QProcess::WriteError:
            error = tr("write error.");
            break;
        case QProcess::ReadError:
            error = tr("read error.");
            break;
        case QProcess::UnknownError:
        default:
            error = tr("unknown error.");
            break;
    }

    cardUpdateProcess->deleteLater();
    cardUpdateProcess = nullptr;

    QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error: %1").arg(error));
}

void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);

槽函数:外部进程运行完成,释放进程对象。

void MainWindow::cardUpdateFinished(int, QProcess::ExitStatus)
{
    cardUpdateProcess->deleteLater();
    cardUpdateProcess = nullptr;

    QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
}

void refreshShortcuts();

槽函数:更新快捷键设定。

QHash<QString, ShortcutKey> shortCuts 中保存了当前所有快捷键。

void MainWindow::refreshShortcuts()
{
    aConnect->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aConnect"));
    aDisconnect->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aDisconnect"));
    aSinglePlayer->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aSinglePlayer"));
    aWatchReplay->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aWatchReplay"));
    aDeckEditor->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aDeckEditor"));
    aFullScreen->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aFullScreen"));
    aRegister->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aRegister"));
    aSettings->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aSettings"));
    aExit->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aExit"));
    aCheckCardUpdates->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aCheckCardUpdates"));
    aOpenCustomFolder->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aOpenCustomFolder"));
    aManageSets->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aManageSets"));
    aEditTokens->setShortcuts(settingsCache->shortcuts().getShortcut("MainWindow/aEditTokens"));
}

void cardDatabaseLoadingFailed();

槽函数:加载数据库失败时调用该函数,给出QMessageBox弹窗提示并提示用户选择加载/配置数据库。

void MainWindow::cardDatabaseLoadingFailed()
{
    QMessageBox msgBox;
    msgBox.setWindowTitle(tr("Card database"));
    msgBox.setIcon(QMessageBox::Question);
    msgBox.setText(tr("Cockatrice is unable to load the card database.\n"
                      "Do you want to update your card database now?\n"
                      "If unsure or first time user, choose \"Yes\""));

    QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
    msgBox.addButton(tr("No"), QMessageBox::NoRole);
    QPushButton *settingsButton = msgBox.addButton(tr("Open settings"), QMessageBox::ActionRole);
    msgBox.setDefaultButton(yesButton);

    msgBox.exec();

    if (msgBox.clickedButton() == yesButton) {
        actCheckCardUpdates();
    } else if (msgBox.clickedButton() == settingsButton) {
        actSettings();
    }
}

void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);

槽函数:显示所有未配置项。

void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
{
    QMessageBox msgBox;
    msgBox.setWindowTitle(tr("New sets found"));
    msgBox.setIcon(QMessageBox::Question);
    msgBox.setText(tr("%n new set(s) found in the card database\n"
                      "Set code(s): %1\n"
                      "Do you want to enable it/them?",
                      "", numUnknownSets)
                       .arg(unknownSetsNames.join(", ")));

    QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
    QPushButton *noButton = msgBox.addButton(tr("No"), QMessageBox::NoRole);
    QPushButton *settingsButton = msgBox.addButton(tr("View sets"), QMessageBox::ActionRole);
    msgBox.setDefaultButton(yesButton);

    msgBox.exec();

    if (msgBox.clickedButton() == yesButton) {
        db->enableAllUnknownSets();
        QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
    } else if (msgBox.clickedButton() == noButton) {
        db->markAllSetsAsKnown();
    } else if (msgBox.clickedButton() == settingsButton) {
        db->markAllSetsAsKnown();
        actManageSets();
    }
}

void cardDatabaseAllNewSetsEnabled();

槽函数:选中所有未配置项。

void MainWindow::cardDatabaseAllNewSetsEnabled()
{
    QMessageBox::information(
        this, tr("Welcome"),
        tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the card "
           "database have been enabled.\nRead more about changing the set order or disabling specific sets and "
           "consequent effects in the \"Manage Sets\" dialog."));
    actManageSets();
}

void actOpenCustomFolder();

槽函数:获取用户卡牌图片缓存绝对路径。

void MainWindow::actOpenCustomFolder()
{
    QString dir = settingsCache->getCustomPicsPath();
#if defined(Q_OS_MAC)
    QStringList scriptArgs;
    scriptArgs << QLatin1String("-e");
    scriptArgs << QString::fromLatin1(R"(tell application "Finder" to open POSIX file "%1")").arg(dir);
    scriptArgs << QLatin1String("-e");
    scriptArgs << QLatin1String("tell application \"Finder\" to activate");

    QProcess::execute("/usr/bin/osascript", scriptArgs);
#elif defined(Q_OS_WIN)
    QStringList args;
    args << QDir::toNativeSeparators(dir);
    QProcess::startDetached("explorer", args);
#endif
}

void actOpenCustomsetsFolder();

槽函数:获取用户卡牌数据库目录。

void MainWindow::actOpenCustomsetsFolder()
{
    QString dir = settingsCache->getCustomCardDatabasePath();

#if defined(Q_OS_MAC)
    QStringList scriptArgs;
    scriptArgs << QLatin1String("-e");
    scriptArgs << QString::fromLatin1(R"(tell application "Finder" to open POSIX file "%1")").arg(dir);
    scriptArgs << QLatin1String("-e");
    scriptArgs << QLatin1String("tell application \"Finder\" to activate");

    QProcess::execute("/usr/bin/osascript", scriptArgs);
#elif defined(Q_OS_WIN)
    QStringList args;
    args << QDir::toNativeSeparators(dir);
    QProcess::startDetached("explorer", args);
#endif
}

void actAddCustomSet();

槽函数:加载用户配置文件。

void MainWindow::actAddCustomSet()
{
    QFileDialog dialog(this, tr("Load sets/cards"), QDir::homePath());
    dialog.setNameFilters(MainWindow::fileNameFilters);
    if (!dialog.exec()) {
        return;
    }

    QString fullFilePath = dialog.selectedFiles().at(0);

    if (!QFile::exists(fullFilePath)) {
        QMessageBox::warning(this, tr("Load sets/cards"), tr("Selected file cannot be found."));
        return;
    }

    if (QFileInfo(fullFilePath).suffix() != "xml") // fileName = *.xml
    {
        QMessageBox::warning(this, tr("Load sets/cards"), tr("You can only import XML databases at this time."));
        return;
    }

    QDir dir = settingsCache->getCustomCardDatabasePath();
    int nextPrefix = getNextCustomSetPrefix(dir);

    bool res;

    QString fileName = QFileInfo(fullFilePath).fileName();
    if (fileName.compare("spoiler.xml", Qt::CaseInsensitive) == 0) {
        /*
         * If the file being added is "spoiler.xml"
         * then we'll want to overwrite the old version
         * and replace it with the new one
         */
        if (QFile::exists(dir.absolutePath() + "/spoiler.xml")) {
            QFile::remove(dir.absolutePath() + "/spoiler.xml");
        }

        res = QFile::copy(fullFilePath, dir.absolutePath() + "/spoiler.xml");
    } else {
        res = QFile::copy(fullFilePath, dir.absolutePath() + "/" + (nextPrefix > 9 ? "" : "0") +
                                            QString::number(nextPrefix) + "." + fileName);
    }

    if (res) {
        QMessageBox::information(
            this, tr("Load sets/cards"),
            tr("The new sets/cards have been added successfully.\nCockatrice will now reload the card database."));
        QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
    } else {
        QMessageBox::warning(this, tr("Load sets/cards"), tr("Sets/cards failed to import."));
    }
}

void actManageSets();

槽函数:新建配置设定窗口。

void MainWindow::actManageSets()
{
    wndSets = new WndSets(this);
    wndSets->show();
}

void actEditTokens();

槽函数:新建Token自定义窗口。

void MainWindow::actEditTokens()
{
    DlgEditTokens dlg(this);
    dlg.exec();
    db->saveCustomTokensToFile();
}

void startupConfigCheck();

槽函数:首次启动时需要进行配置。

void MainWindow::startupConfigCheck()
{
    if (settingsCache->getClientVersion() == CLIENT_INFO_NOT_SET) {
        // no config found, 99% new clean install
        qDebug() << "Startup: old client version empty, assuming first start after clean install";
        alertForcedOracleRun(VERSION_STRING, false);
        settingsCache->setClientVersion(VERSION_STRING);
    } else if (settingsCache->getClientVersion() != VERSION_STRING) {
        // config found, from another (presumably older) version
        qDebug() << "Startup: old client version" << settingsCache->getClientVersion()
                 << "differs, assuming first start after update";
        if (settingsCache->getNotifyAboutNewVersion()) {
            alertForcedOracleRun(VERSION_STRING, true);
        } else {
            QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
        }
        settingsCache->setClientVersion(VERSION_STRING);
    } else {
        // previous config from this version found
        qDebug() << "Startup: found config with current version";
        QtConcurrent::run(db, &CardDatabase::loadCardDatabases);

        // Run the tips dialog only on subsequent startups.
        // On the first run after an install/update the startup is already crowded enough
        if (tip->successfulInit && settingsCache->getShowTipsOnStartup() && tip->newTipsAvailable) {
            tip->raise();
            tip->show();
        }
    }
}

void alertForcedOracleRun(const QString &version, bool isUpdate);

槽函数:初次启动时需要初始化Oracle数据库,给出QMessageBox弹窗提示并提示用户选择加载/配置数据库。

void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
{
    if (isUpdate) {
        QMessageBox::information(this, tr("New Version"),
                                 tr("Congratulations on updating to Cockatrice %1!\n"
                                    "Oracle will now launch to update your card database.")
                                     .arg(version));
    } else {
        QMessageBox::information(this, tr("Cockatrice installed"),
                                 tr("Congratulations on installing Cockatrice %1!\n"
                                    "Oracle will now launch to install the initial card database.")
                                     .arg(version));
    }

    actCheckCardUpdates();
    actCheckServerUpdates();
}

private:

static const QString appName;

static const QStringList fileNameFilters;

QList<QMenu *> tabMenus;

QMenu *cockatriceMenu;

QMenu *dbMenu;

QMenu *helpMenu;

QMenu *trayIconMenu;

QAction *aConnect;

QAction *aDisconnect;

QAction *aSinglePlayer;

QAction *aWatchReplay;

QAction *aDeckEditor;

QAction *aFullScreen;

QAction *aSettings;

QAction *aExit;

QAction *aAbout;

QAction *aTips;

QAction *aCheckCardUpdates;

QAction *aRegister;

QAction *aUpdate;

QAction *aViewLog;

QAction *closeAction;

QAction *aManageSets;

QAction *aEditTokens;

QAction *aOpenCustomFolder;

QAction *aOpenCustomsetsFolder;

QAction *aAddCustomSet;

TabSupervisor *tabSupervisor;

WndSets *wndSets;

RemoteClient *client;

QThread *clientThread;

LocalServer *localServer;

bool bHasActivated;

QMessageBox serverShutdownMessageBox;

QProcess *cardUpdateProcess;

DlgViewLog *logviewDialog;

DlgConnect *dlgConnect;

GameReplay *replay;

DlgTipOfTheDay *tip;

QUrl connectTo;


下一篇我们来分析Cockatrice的网络通信实现RemoteClient。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值