//-----------------------------------------------------------------------------
/** @file pentobi/MainWindow.cpp
    @author Markus Enzenberger
    @copyright GNU General Public License version 3 or later */
//-----------------------------------------------------------------------------

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "MainWindow.h"

#include <algorithm>
#include <fstream>
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QDesktopWidget>
#include <QFileDialog>
#include <QImageWriter>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSettings>
#include <QSplitter>
#include <QStandardPaths>
#include <QStatusBar>
#include <QToolBar>
#include <QToolButton>
#include <QtConcurrentRun>
#include "AnalyzeGameWindow.h"
#include "AnalyzeSpeedDialog.h"
#include "RatingDialog.h"
#include "ShowMessage.h"
#include "Util.h"
#include "libboardgame_sgf/SgfUtil.h"
#include "libboardgame_sgf/TreeReader.h"
#include "libboardgame_util/Assert.h"
#include "libpentobi_base/TreeUtil.h"
#include "libpentobi_base/PentobiTreeWriter.h"
#include "libpentobi_gui/ComputerColorDialog.h"
#include "libpentobi_gui/GameInfoDialog.h"
#include "libpentobi_gui/GuiBoard.h"
#include "libpentobi_gui/GuiBoardUtil.h"
#include "libpentobi_gui/HelpWindow.h"
#include "libpentobi_gui/InitialRatingDialog.h"
#include "libpentobi_gui/LeaveFullscreenButton.h"
#include "libpentobi_gui/OrientationDisplay.h"
#include "libpentobi_gui/PieceSelector.h"
#include "libpentobi_gui/SameHeightLayout.h"
#include "libpentobi_gui/ScoreDisplay.h"
#include "libpentobi_gui/Util.h"

using Util::getPlayerString;
using libboardgame_sgf::InvalidTree;
using libboardgame_sgf::TreeReader;
using libboardgame_sgf::util::back_to_main_variation;
using libboardgame_sgf::util::beginning_of_branch;
using libboardgame_sgf::util::find_next_comment;
using libboardgame_sgf::util::get_last_node;
using libboardgame_sgf::util::get_move_annotation;
using libboardgame_sgf::util::get_variation_string;
using libboardgame_sgf::util::has_comment;
using libboardgame_sgf::util::has_earlier_variation;
using libboardgame_sgf::util::is_main_variation;
using libboardgame_util::clear_abort;
using libboardgame_util::get_abort;
using libboardgame_util::set_abort;
using libboardgame_util::trim_right;
using libboardgame_util::ArrayList;
using libpentobi_base::BoardType;
using libpentobi_base::MoveInfo;
using libpentobi_base::MoveInfoExt;
using libpentobi_base::PieceInfo;
using libpentobi_base::PentobiTree;
using libpentobi_base::PentobiTreeWriter;
using libpentobi_base::tree_util::get_move_number;
using libpentobi_base::tree_util::get_moves_left;
using libpentobi_base::tree_util::get_position_info;
using libpentobi_mcts::Search;

//-----------------------------------------------------------------------------

namespace {

QToolButton* createOBoxToolButton(QAction* action)
{
    auto button = new QToolButton;
    button->setDefaultAction(action);
    button->setAutoRaise(true);
    button->setFocusPolicy(Qt::NoFocus);
    return button;
}

/** Return auto-save file name as a native path name. */
QString getAutoSaveFile()
{
    return Util::getDataDir() + QDir::separator() + "autosave.blksgf";
}

/** Determine the current color at the current node in the game.
    - If the color was explicitely set with a setup property, it will be used.
    - Otherwise, if the current node has children all with moves of the same
      color, this color will be used. This is because some trees like search
      dumps contain pass moves, so using the effective color to play is not
      appropriate there.
    - Otherwise, the effective color to play will be used. (see
      Game::get_effective_to_play())  */
Color getCurrentColor(const Game& game)
{
    auto& tree = game.get_tree();
    auto node = &game.get_current();
    Color c;
    while (node && ! tree.has_move(*node))
    {
        if (libpentobi_base::node_util::get_player(*node, c))
            return c;
        node = node->get_parent_or_null();
    }
    bool all_same_color = true;
    bool is_first = true;
    for (auto& i : game.get_current().get_children())
    {
        if (! tree.has_move(i))
            continue;
        if (is_first)
        {
            c = tree.get_move(i).color;
            is_first = false;
            continue;
        }
        if (tree.get_move(i).color != c)
        {
            all_same_color = false;
            break;
        }
    }
    if (! is_first && all_same_color)
        return c;
    return game.get_effective_to_play();
}

bool hasCurrentVariationOtherMoves(const PentobiTree& tree,
                                   const SgfNode& current)
{
    auto node = current.get_parent_or_null();
    while (node)
    {
        if (! tree.get_move(*node).is_null())
            return true;
        node = node->get_parent_or_null();
    }
    node = current.get_first_child_or_null();
    while (node)
    {
        if (! tree.get_move(*node).is_null())
            return true;
        node = node->get_first_child_or_null();
    }
    return false;
}

void initToolBarText(QToolBar* toolBar)
{
    QSettings settings;
    auto toolBarText = settings.value("toolbar_text", "system").toString();
    if (toolBarText == "no_text")
        toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
    else if (toolBarText == "beside_icons")
        toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    else if (toolBarText == "below_icons")
        toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
    else if (toolBarText == "text_only")
        toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
    else
        toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
}

void setIcon(QAction* action, const QString& name)
{
    QIcon icon(QString(":/pentobi/icons/%1.png").arg(name));
    QString file16 = QString(":/pentobi/icons/%1-16.png").arg(name);
    if (QFile::exists(file16))
        icon.addFile(file16, QSize(16, 16));
    action->setIcon(icon);
}

/** Simple heuristic that prefers moves with more piece points, more attach
    points and less adjacent points.
    Used for sorting the list used in Find Move. */
float getHeuristic(const Board& bd, Move mv)
{
    auto& piece_info = bd.get_piece_info(bd.get_move_info(mv).get_piece());
    auto& info_ext = bd.get_move_info_ext(mv);
    return static_cast<float>((1000 * piece_info.get_score_points()
                               + 10 * info_ext.size_attach_points
                               - info_ext.size_adj_points));
}

} // namespace

//-----------------------------------------------------------------------------

MainWindow::MainWindow(Variant variant, const QString& initialFile,
                       const QString& helpDir, unsigned maxLevel,
                       const QString& booksDir, bool noBook,
                       unsigned nuThreads)
    : m_game(variant),
      m_bd(m_game.get_board()),
      m_helpDir(helpDir)
{
    if (maxLevel > Player::max_supported_level)
        maxLevel = Player::max_supported_level;
    if (maxLevel < 1)
        maxLevel = 1;
    m_maxLevel = maxLevel;
    Util::initDataDir();
    QSettings settings;
    m_history.reset(new RatingHistory(variant));
    createActions();
    restoreLevel(variant);
    setCentralWidget(createCentralWidget());
    initPieceSelectors();
    m_moveNumber = new QLabel;
    statusBar()->addPermanentWidget(m_moveNumber);
    m_setupModeLabel = new QLabel(tr("Setup mode"));
    statusBar()->addWidget(m_setupModeLabel);
    m_setupModeLabel->hide();
    m_ratedGameLabelText = new QLabel(tr("Rated game"));
    statusBar()->addWidget(m_ratedGameLabelText);
    m_ratedGameLabelText->hide();
    initGame();
    m_player.reset(new Player(variant, maxLevel,
                              booksDir.toLocal8Bit().constData(), nuThreads));
    m_player->get_search().set_callback(bind(&MainWindow::searchCallback,
                                             this, placeholders::_1,
                                             placeholders::_2));
    m_player->set_use_book(! noBook);
    createToolBar();
    connect(&m_genMoveWatcher, SIGNAL(finished()),
            SLOT(genMoveFinished()));
    connect(m_guiBoard, SIGNAL(play(Color, Move)),
            SLOT(placePiece(Color, Move)));
    connect(m_guiBoard, SIGNAL(pointClicked(Point)),
            SLOT(pointClicked(Point)));
    connect(m_actionMovePieceLeft, SIGNAL(triggered()),
            m_guiBoard, SLOT(movePieceLeft()));
    connect(m_actionMovePieceRight, SIGNAL(triggered()),
            m_guiBoard, SLOT(movePieceRight()));
    connect(m_actionMovePieceUp, SIGNAL(triggered()),
            m_guiBoard, SLOT(movePieceUp()));
    connect(m_actionMovePieceDown, SIGNAL(triggered()),
            m_guiBoard, SLOT(movePieceDown()));
    connect(m_actionPlacePiece, SIGNAL(triggered()),
            m_guiBoard, SLOT(placePiece()));
    createMenu();
    qApp->installEventFilter(this);
    updateRecentFiles();
    auto marking = settings.value("move_marking", "last_dot").toString();
    if (marking == "all_number")
        m_actionMoveMarkingAllNumber->setChecked(true);
    else if (marking == "last_dot")
        m_actionMoveMarkingLastDot->setChecked(true);
    else if (marking == "last_number")
        m_actionMoveMarkingLastNumber->setChecked(true);
    else
        m_actionMoveMarkingNone->setChecked(true);
    auto coordinates = settings.value("coordinates", false).toBool();
    m_guiBoard->setCoordinates(coordinates);
    m_actionCoordinates->setChecked(coordinates);
    auto showToolbar = settings.value("toolbar", true).toBool();
    findChild<QToolBar*>()->setVisible(showToolbar);
    m_menuToolBarText->setEnabled(showToolbar);
    m_actionShowToolbar->setChecked(showToolbar);
    auto showVariations = settings.value("show_variations", true).toBool();
    m_actionShowVariations->setChecked(showVariations);
    initVariantActions();
    QIcon icon;
    icon.addFile(":/pentobi/icons/pentobi.png");
    icon.addFile(":/pentobi/icons/pentobi-16.png");
    icon.addFile(":/pentobi/icons/pentobi-32.png");
    setWindowIcon(icon);

    bool centerOnScreen = false;
    QRect screenGeometry = QApplication::desktop()->screenGeometry();
    if (restoreGeometry(settings.value("geometry").toByteArray()))
    {
        if (! screenGeometry.contains(geometry()))
        {
            if (width() > screenGeometry.width()
                    || height() > screenGeometry.height())
                adjustSize();
            centerOnScreen = true;
        }
    }
    else
    {
        adjustSize();
        centerOnScreen = true;
    }
    if (centerOnScreen)
    {
        int x = (screenGeometry.width() - width()) / 2;
        int y = (screenGeometry.height() - height()) / 2;
        move(x, y);
    }

    auto showComment = settings.value("show_comment", false).toBool();
    m_comment->setVisible(showComment);
    if (showComment)
        m_splitter->restoreState(
                               settings.value("splitter_state").toByteArray());
    m_actionShowComment->setChecked(showComment);
    updateWindow(true);
    clearFile();
    if (! initialFile.isEmpty())
    {
        if (open(initialFile))
            rememberFile(initialFile);
    }
    else
    {
        QString autoSaveFile = getAutoSaveFile();
        if (QFile(autoSaveFile).exists())
        {
            open(autoSaveFile, true);
            m_isAutoSaveLoaded = true;
            deleteAutoSaveFile();
            m_gameFinished = m_bd.is_game_over();
            updateWindow(true);
            if (settings.value("autosave_rated", false).toBool())
                QMetaObject::invokeMethod(this, "continueRatedGame",
                                          Qt::QueuedConnection);
        }
    }
}

MainWindow::~MainWindow()
{
}

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Pentobi"),
                       "<style type=\"text/css\">"
                       ":link { text-decoration: none; }"
                       "</style>"
                       "<h2>" + tr("Pentobi") + "</h2>"
                       "<p>" + tr("Version %1").arg(getVersion()) + "</p>"
                       "<p>" +
                       tr("Computer opponent for the board game Blokus.")
                       + "<br>" +
                       tr("&copy; 2011&ndash;%1 Markus Enzenberger").arg(2015) +
                       + "<br>" +
                       "<a href=\"http://pentobi.sourceforge.net\">http://pentobi.sourceforge.net</a>"
                       "</p>");
}

void MainWindow::analyzeGame()
{
    if (! is_main_variation(m_game.get_current()))
    {
        showInfo(tr("Game analysis is only possible in the main variation."));
        return;
    }
    AnalyzeSpeedDialog dialog(this, tr("Analyze Game"));
    if (! dialog.exec())
        return;
    int speed = dialog.getSpeedValue();
    cancelThread();
    if (m_analyzeGameWindow)
        delete m_analyzeGameWindow;
    m_analyzeGameWindow = new AnalyzeGameWindow(this);
    // Make sure all action shortcuts work when the analyze dialog has the
    // focus apart from m_actionLeaveFullscreen because the Esc key is used
    // to close the dialog
    m_analyzeGameWindow->addActions(actions());
    m_analyzeGameWindow->removeAction(m_actionLeaveFullscreen);
    m_analyzeGameWindow->show();
    m_isAnalyzeRunning = true;
    connect(m_analyzeGameWindow->analyzeGameWidget, SIGNAL(finished()),
            SLOT(analyzeGameFinished()));
    connect(m_analyzeGameWindow->analyzeGameWidget,
            SIGNAL(gotoPosition(Variant,const vector<ColorMove>&)),
            SLOT(gotoPosition(Variant,const vector<ColorMove>&)));
    size_t nuSimulations;
    switch (speed)
    {
    case 0:
        nuSimulations = 5000;
        break;
    case 1:
        nuSimulations = 20000;
        break;
    default:
        nuSimulations = 80000;
    }
    m_analyzeGameWindow->analyzeGameWidget->start(
                m_game, m_player->get_search(), nuSimulations);
}

void MainWindow::analyzeGameFinished()
{
    m_analyzeGameWindow->analyzeGameWidget->setCurrentPosition(
                m_game, m_game.get_current());
    m_isAnalyzeRunning = false;
}

/** Call to Player::genmove() that runs in a different thread. */
MainWindow::GenMoveResult MainWindow::asyncGenMove(Color c, int genMoveId,
                                                   bool playSingleMove)
{
    QElapsedTimer timer;
    timer.start();
    GenMoveResult result;
    result.playSingleMove = playSingleMove;
    result.color = c;
    result.genMoveId = genMoveId;
    result.move = m_player->genmove(m_bd, c);
    auto elapsed = timer.elapsed();
    // Enforce minimum thinking time of 0.8 sec
    if (elapsed < 800 && ! m_noDelay)
    {
        // Use std::thread because QThread::sleep() is protected in Qt4
        chrono::milliseconds duration(800 - elapsed);
        this_thread::sleep_for(duration);
    }
    return result;
}

void MainWindow::badMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_bad_move();
    updateWindow(false);
}

void MainWindow::backward()
{
    gotoNode(m_game.get_current().get_parent_or_null());
}

void MainWindow::backToMainVariation()
{
    gotoNode(back_to_main_variation(m_game.get_current()));
}

void MainWindow::beginning()
{
    gotoNode(m_game.get_root());
}

void MainWindow::beginningOfBranch()
{
    gotoNode(beginning_of_branch(m_game.get_current()));
}

void MainWindow::cancelThread()
{
    if (m_isAnalyzeRunning)
    {
        // This should never happen because AnalyzeGameWindow protects the
        // parent with a modal progress dialog while it is running. However,
        // due to bugs in Unity 2D (tested with Ubuntu 11.04 and 11.10), the
        // global menu can still trigger menu item events.
        m_analyzeGameWindow->analyzeGameWidget->cancel();
    }
    if (! m_isGenMoveRunning)
        return;
    // After waitForFinished() returns, we can be sure that the move generation
    // is no longer running, but we will still receive the finished event.
    // Increasing m_genMoveId will make genMoveFinished() ignore the event.
    ++m_genMoveId;
    set_abort();
    m_genMoveWatcher.waitForFinished();
    m_isGenMoveRunning = false;
    clearStatus();
    setCursor(QCursor(Qt::ArrowCursor));
    m_actionInterrupt->setEnabled(false);
    m_actionNextPiece->setEnabled(true);
    m_actionPlay->setEnabled(true);
    m_actionPlaySingleMove->setEnabled(true);
    m_actionPreviousPiece->setEnabled(true);
}

void MainWindow::checkComputerMove()
{
    if (! m_autoPlay || ! isComputerToPlay() || m_bd.is_game_over())
        m_lastComputerMovesBegin = 0;
    else if (! m_isGenMoveRunning)
        genMove();
}

bool MainWindow::checkSave()
{
    if (! m_file.isEmpty())
    {
        if (! m_game.is_modified())
            return true;
        QMessageBox msgBox(this);
        initQuestion(msgBox, tr("The file has been modified."),
                     tr("Do you want to save your changes?"));
        // Don't use QMessageBox::Discard because on some platforms it uses the
        // text "Close without saving" which implies that the window would be
        // closed
        auto discardButton =
            msgBox.addButton(tr("&Don't Save"), QMessageBox::DestructiveRole);
        auto saveButton = msgBox.addButton(QMessageBox::Save);
        auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
        msgBox.setDefaultButton(cancelButton);
        msgBox.exec();
        auto result = msgBox.clickedButton();
        if (result == saveButton)
        {
            save();
            return true;
        }
        return result == discardButton;
    }
    // Don't ask if game should be saved if it was finished because the user
    // might only want to play and never save games.
    if (m_game.get_root().has_children() && ! m_gameFinished)
    {
        QMessageBox msgBox(this);
        initQuestion(msgBox, tr("The current game is not finished."),
                     tr("Do you want to abort the game?"));
        auto abortGameButton =
            msgBox.addButton(tr("&Abort Game"), QMessageBox::DestructiveRole);
        auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
        msgBox.setDefaultButton(cancelButton);
        msgBox.exec();
        if (msgBox.clickedButton() != abortGameButton)
            return false;
        return true;
    }
    return true;
}

bool MainWindow::checkQuit()
{
    if (! m_file.isEmpty() && m_game.is_modified())
    {
        QMessageBox msgBox(this);
        initQuestion(msgBox, tr("The file has been modified."),
                     tr("Do you want to save your changes?"));
        auto discardButton = msgBox.addButton(QMessageBox::Discard);
        auto saveButton = msgBox.addButton(QMessageBox::Save);
        auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
        msgBox.setDefaultButton(cancelButton);
        msgBox.exec();
        auto result = msgBox.clickedButton();
        if (result == saveButton)
        {
            save();
            return true;
        }
        return result == discardButton;
    }
    cancelThread();
    QSettings settings;
    if (m_file.isEmpty() && ! m_gameFinished
            && (m_game.is_modified() || m_isAutoSaveLoaded))
    {
        writeGame(getAutoSaveFile().toLocal8Bit().constData());
        settings.setValue("autosave_rated", m_isRated);
        if (m_isRated)
            settings.setValue("autosave_rated_color",
                              m_ratedGameColor.to_int());
    }
    if (! isFullScreen())
        settings.setValue("geometry", saveGeometry());
    if (m_comment->isVisible())
        settings.setValue("splitter_state", m_splitter->saveState());
    return true;
}

void MainWindow::clearFile()
{
    setFile("");
}

void MainWindow::clearPiece()
{
    m_actionRotateClockwise->setEnabled(false);
    m_actionRotateAnticlockwise->setEnabled(false);
    m_actionFlipHorizontally->setEnabled(false);
    m_actionFlipVertically->setEnabled(false);
    m_actionClearPiece->setEnabled(false);
    m_guiBoard->clearPiece();
    m_orientationDisplay->clearPiece();
}

void MainWindow::clearStatus()
{
    statusBar()->clearMessage();
}

void MainWindow::closeEvent(QCloseEvent* event)
{
    if (checkQuit())
        event->accept();
    else
        event->ignore();
}

void MainWindow::commentChanged()
{
    if (m_ignoreCommentTextChanged)
        return;
    QString comment = m_comment->toPlainText();
    if (comment.isEmpty())
        m_game.set_comment("");
    else
    {
        string charset = m_game.get_root().get_property("CA", "");
        string value = Util::convertSgfValueFromQString(comment, charset);
        value = trim_right(value);
        m_game.set_comment(value);
    }
    updateWindowModified();
}

void MainWindow::computerColors()
{
    ColorMap<bool> oldComputerColors = m_computerColors;
    ComputerColorDialog dialog(this, m_bd.get_variant(), m_computerColors);
    dialog.exec();
    auto nu_colors = m_bd.get_nu_nonalt_colors();

    bool computerNone = true;
    for (Color c : Color::Range(nu_colors))
        if (m_computerColors[c])
        {
            computerNone = false;
            break;
        }
    QSettings settings;
    settings.setValue("computer_color_none", computerNone);

    // Enable auto play only if any color has changed because that means that
    // the user probably wants to continue playing, otherwise the user could
    // have only opened the dialog to check the current settings
    for (Color c : Color::Range(nu_colors))
        if (m_computerColors[c] != oldComputerColors[c])
        {
            m_autoPlay = true;
            break;
        }

    checkComputerMove();
}

bool MainWindow::computerPlaysAll() const
{
    for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
        if (! m_computerColors[c])
            return false;
    return true;
}

void MainWindow::continueRatedGame()
{
    auto nuColors = m_bd.get_nu_colors();
    QSettings settings;
    auto color =
            static_cast<Color::IntType>(
                settings.value("autosave_rated_color", 0).toUInt());
    if (color >= nuColors)
        return;
    m_ratedGameColor = Color(color);
    m_computerColors.fill(true);
    for (Color c : Color::Range(nuColors))
        if (m_bd.is_same_player(c, m_ratedGameColor))
            m_computerColors[c] = false;
    setRated(true);
    updateWindow(false);
    showInfo(tr("Continuing unfinished rated game."),
             tr("You play %1 in this game.")
             .arg(getPlayerString(m_bd.get_variant(), m_ratedGameColor)));
    m_autoPlay = true;
    checkComputerMove();
}

void MainWindow::coordinates(bool checked)
{
    m_guiBoard->setCoordinates(checked);
    QSettings settings;
    settings.setValue("coordinates", checked);
}

QAction* MainWindow::createAction(const QString& text)
{
    auto action = new QAction(text, this);
    // Add all actions also to main window. if an action is only added to
    // the menu bar, shortcuts won,t work in fullscreen mode because the menu
    // is not visible in fullscreen mode
    addAction(action);
    return action;
}

void MainWindow::createActions()
{
    auto groupVariant = new QActionGroup(this);
    auto groupLevel = new QActionGroup(this);
    auto groupMoveMarking = new QActionGroup(this);
    auto groupMoveAnnotation = new QActionGroup(this);
    auto groupToolBarText = new QActionGroup(this);

    m_actionAbout = createAction(tr("&About Pentobi"));
    connect(m_actionAbout, SIGNAL(triggered()), SLOT(about()));

    m_actionAnalyzeGame = createAction(tr("&Analyze Game..."));
    connect(m_actionAnalyzeGame, SIGNAL(triggered()), SLOT(analyzeGame()));

    m_actionBackward = createAction(tr("B&ackward"));
    m_actionBackward->setToolTip(tr("Go one move backward"));
    m_actionBackward->setPriority(QAction::LowPriority);
    setIcon(m_actionBackward, "pentobi-backward");
    m_actionBackward->setShortcut(QKeySequence::MoveToPreviousWord);
    connect(m_actionBackward, SIGNAL(triggered()), SLOT(backward()));

    m_actionBackToMainVariation = createAction(tr("Back to &Main Variation"));
    m_actionBackToMainVariation->setShortcut(QString("Ctrl+M"));
    connect(m_actionBackToMainVariation, SIGNAL(triggered()),
            SLOT(backToMainVariation()));

    m_actionBadMove = createAction(tr("&Bad"));
    m_actionBadMove->setActionGroup(groupMoveAnnotation);
    m_actionBadMove->setCheckable(true);
    connect(m_actionBadMove, SIGNAL(triggered(bool)), SLOT(badMove(bool)));

    m_actionBeginning = createAction(tr("&Beginning"));
    m_actionBeginning->setToolTip(tr("Go to beginning of game"));
    m_actionBeginning->setPriority(QAction::LowPriority);
    setIcon(m_actionBeginning, "pentobi-beginning");
    m_actionBeginning->setShortcut(QKeySequence::MoveToStartOfDocument);
    connect(m_actionBeginning, SIGNAL(triggered()), SLOT(beginning()));

    m_actionBeginningOfBranch = createAction(tr("Beginning of Bran&ch"));
    m_actionBeginningOfBranch->setShortcut(QString("Ctrl+B"));
    connect(m_actionBeginningOfBranch, SIGNAL(triggered()),
            SLOT(beginningOfBranch()));

    m_actionClearPiece = createAction(tr("Clear Piece"));
    setIcon(m_actionClearPiece, "pentobi-piece-clear");
    m_actionClearPiece->setShortcut(QString("0"));
    connect(m_actionClearPiece, SIGNAL(triggered()), SLOT(clearPiece()));

    m_actionComputerColors = createAction(tr("&Computer Colors"));
    m_actionComputerColors->setShortcut(QString("Ctrl+U"));
    m_actionComputerColors->setToolTip(
                                  tr("Set the colors played by the computer"));
    setIcon(m_actionComputerColors, "pentobi-computer-colors");
    connect(m_actionComputerColors, SIGNAL(triggered()),
            SLOT(computerColors()));

    m_actionCoordinates = createAction(tr("C&oordinates"));
    m_actionCoordinates->setCheckable(true);
    connect(m_actionCoordinates, SIGNAL(triggered(bool)),
            SLOT(coordinates(bool)));

    m_actionDeleteAllVariations = createAction(tr("&Delete All Variations"));
    connect(m_actionDeleteAllVariations, SIGNAL(triggered()),
            SLOT(deleteAllVariations()));

    m_actionDoubtfulMove = createAction(tr("&Doubtful"));
    m_actionDoubtfulMove->setActionGroup(groupMoveAnnotation);
    m_actionDoubtfulMove->setCheckable(true);
    connect(m_actionDoubtfulMove, SIGNAL(triggered(bool)),
            SLOT(doubtfulMove(bool)));

    m_actionEnd = createAction(tr("&End"));
    m_actionEnd->setToolTip(tr("Go to end of moves"));
    m_actionEnd->setPriority(QAction::LowPriority);
    m_actionEnd->setShortcut(QKeySequence::MoveToEndOfDocument);
    setIcon(m_actionEnd, "pentobi-end");
    connect(m_actionEnd, SIGNAL(triggered()), SLOT(end()));

    m_actionExportAsciiArt = createAction(tr("&ASCII Art"));
    connect(m_actionExportAsciiArt, SIGNAL(triggered()),
            SLOT(exportAsciiArt()));

    m_actionExportImage = createAction(tr("I&mage"));
    connect(m_actionExportImage, SIGNAL(triggered()), SLOT(exportImage()));

    m_actionFindMove = createAction(tr("&Find Move"));
    m_actionFindMove->setShortcut(QString("F6"));
    connect(m_actionFindMove, SIGNAL(triggered()), SLOT(findMove()));

    m_actionFindNextComment = createAction(tr("Find Next &Comment"));
    m_actionFindNextComment->setShortcut(QKeySequence::FindNext);
    connect(m_actionFindNextComment, SIGNAL(triggered()),
            SLOT(findNextComment()));

    m_actionFlipHorizontally = createAction(tr("Flip Horizontally"));
    setIcon(m_actionFlipHorizontally, "pentobi-flip-horizontal");
    connect(m_actionFlipHorizontally, SIGNAL(triggered()),
            SLOT(flipHorizontally()));

    m_actionFlipVertically = createAction(tr("Flip Vertically"));
    setIcon(m_actionFlipVertically, "pentobi-flip-vertical");

    m_actionForward = createAction(tr("&Forward"));
    m_actionForward->setToolTip(tr("Go one move forward"));
    m_actionForward->setPriority(QAction::LowPriority);
    m_actionForward->setShortcut(QKeySequence::MoveToNextWord);
    setIcon(m_actionForward, "pentobi-forward");
    connect(m_actionForward, SIGNAL(triggered()), SLOT(forward()));

    m_actionFullscreen = createAction(tr("&Fullscreen"));
    // Don't use QKeySequence::Fullscreen, it is Ctrl-F11 on Linux but that
    // doesn't work in Xfce
    m_actionFullscreen->setShortcut(QString("F11"));
    connect(m_actionFullscreen, SIGNAL(triggered()), SLOT(fullscreen()));

    m_actionGameInfo = createAction(tr("Ga&me Info"));
    m_actionGameInfo->setShortcut(QString("Ctrl+I"));
    connect(m_actionGameInfo, SIGNAL(triggered()), SLOT(gameInfo()));

    m_actionGoodMove = createAction(tr("&Good"));
    m_actionGoodMove->setActionGroup(groupMoveAnnotation);
    m_actionGoodMove->setCheckable(true);
    connect(m_actionGoodMove, SIGNAL(triggered(bool)), SLOT(goodMove(bool)));

    m_actionGotoMove = createAction(tr("&Go to Move..."));
    m_actionGotoMove->setShortcut(QString("Ctrl+G"));
    connect(m_actionGotoMove, SIGNAL(triggered()), SLOT(gotoMove()));

    m_actionHelp = createAction(tr("Pentobi &Help"));
    m_actionHelp->setShortcut(QKeySequence::HelpContents);
    connect(m_actionHelp, SIGNAL(triggered()), SLOT(help()));

    m_actionInterestingMove = createAction(tr("I&nteresting"));
    m_actionInterestingMove->setActionGroup(groupMoveAnnotation);
    m_actionInterestingMove->setCheckable(true);
    connect(m_actionInterestingMove, SIGNAL(triggered(bool)),
            SLOT(interestingMove(bool)));

    m_actionInterrupt = createAction(tr("St&op"));
    m_actionInterrupt->setEnabled(false);
    connect(m_actionInterrupt, SIGNAL(triggered()), SLOT(interrupt()));

    m_actionInterruptPlay = createAction();
    m_actionInterruptPlay->setShortcut(QString("Shift+Esc"));
    connect(m_actionInterruptPlay, SIGNAL(triggered()), SLOT(interruptPlay()));

    m_actionKeepOnlyPosition = createAction(tr("&Keep Only Position"));
    connect(m_actionKeepOnlyPosition, SIGNAL(triggered()),
            SLOT(keepOnlyPosition()));

    m_actionKeepOnlySubtree = createAction(tr("Keep Only &Subtree"));
    connect(m_actionKeepOnlySubtree, SIGNAL(triggered()),
            SLOT(keepOnlySubtree()));

    m_actionLeaveFullscreen = createAction(tr("Leave Fullscreen"));
    m_actionLeaveFullscreen->setShortcut(QString("Esc"));
    connect(m_actionLeaveFullscreen, SIGNAL(triggered()),
            SLOT(leaveFullscreen()));

    m_actionMakeMainVariation = createAction(tr("M&ake Main Variation"));
    connect(m_actionMakeMainVariation, SIGNAL(triggered()),
            SLOT(makeMainVariation()));

    m_actionMoveDownVariation = createAction(tr("Move Variation D&own"));
    connect(m_actionMoveDownVariation, SIGNAL(triggered()),
            SLOT(moveDownVariation()));

    m_actionMoveUpVariation = createAction(tr("Move Variation &Up"));
    connect(m_actionMoveUpVariation, SIGNAL(triggered()),
            SLOT(moveUpVariation()));

    static_assert(Player::max_supported_level <= 9, "");
    QString levelText[Player::max_supported_level] =
        { tr("&1"), tr("&2"), tr("&3"), tr("&4"), tr("&5"), tr("&6"),
          tr("&7"), tr("&8"), tr("&9") };
    m_actionLevel.reserve(m_maxLevel);
    for (unsigned i = 0; i < m_maxLevel; ++i)
        m_actionLevel.push_back(createLevelAction(groupLevel, i + 1,
                                                  levelText[i]));
    connect(m_actionFlipVertically, SIGNAL(triggered()),
            SLOT(flipVertically()));

    m_actionMoveMarkingAllNumber = createAction(tr("&All with Number"));
    m_actionMoveMarkingAllNumber->setActionGroup(groupMoveMarking);
    m_actionMoveMarkingAllNumber->setCheckable(true);
    connect(m_actionMoveMarkingAllNumber, SIGNAL(triggered(bool)),
            SLOT(setMoveMarkingAllNumber(bool)));

    m_actionMoveMarkingLastDot = createAction(tr("Last with &Dot"));
    m_actionMoveMarkingLastDot->setActionGroup(groupMoveMarking);
    m_actionMoveMarkingLastDot->setCheckable(true);
    m_actionMoveMarkingLastDot->setChecked(true);
    connect(m_actionMoveMarkingLastDot, SIGNAL(triggered(bool)),
            SLOT(setMoveMarkingLastDot(bool)));

    m_actionMoveMarkingLastNumber = createAction(tr("&Last with Number"));
    m_actionMoveMarkingLastNumber->setActionGroup(groupMoveMarking);
    m_actionMoveMarkingLastNumber->setCheckable(true);
    m_actionMoveMarkingLastNumber->setChecked(true);
    connect(m_actionMoveMarkingLastNumber, SIGNAL(triggered(bool)),
            SLOT(setMoveMarkingLastNumber(bool)));

    m_actionMoveMarkingNone = createAction(tr("&None", "move numbers"));
    m_actionMoveMarkingNone->setActionGroup(groupMoveMarking);
    m_actionMoveMarkingNone->setCheckable(true);
    connect(m_actionMoveMarkingNone, SIGNAL(triggered(bool)),
            SLOT(setMoveMarkingNone(bool)));

    m_actionMovePieceLeft = createAction();
    m_actionMovePieceLeft->setShortcut(QKeySequence::MoveToPreviousChar);

    m_actionMovePieceRight = createAction();
    m_actionMovePieceRight->setShortcut(QKeySequence::MoveToNextChar);

    m_actionMovePieceUp = createAction();
    m_actionMovePieceUp->setShortcut(QKeySequence::MoveToPreviousLine);

    m_actionMovePieceDown = createAction();
    m_actionMovePieceDown->setShortcut(QKeySequence::MoveToNextLine);

    m_actionNextPiece = createAction(tr("Next Piece"));
    setIcon(m_actionNextPiece, "pentobi-next-piece");
    m_actionNextPiece->setShortcut(QString("+"));
    connect(m_actionNextPiece, SIGNAL(triggered()), SLOT(nextPiece()));

    m_actionNextTransform = createAction();
    m_actionNextTransform->setShortcut(QString("Space"));
    connect(m_actionNextTransform, SIGNAL(triggered()), SLOT(nextTransform()));

    m_actionNextVariation = createAction(tr("&Next Variation"));
    m_actionNextVariation->setToolTip(tr("Go to next variation"));
    m_actionNextVariation->setPriority(QAction::LowPriority);
    m_actionNextVariation->setShortcut(QKeySequence::MoveToNextPage);
    setIcon(m_actionNextVariation, "pentobi-next-variation");
    connect(m_actionNextVariation, SIGNAL(triggered()), SLOT(nextVariation()));

    m_actionNew = createAction(tr("&New"));
    m_actionNew->setShortcut(QKeySequence::New);
    m_actionNew->setToolTip(tr("Start a new game"));
    setIcon(m_actionNew, "pentobi-newgame");
    connect(m_actionNew, SIGNAL(triggered()), SLOT(newGame()));

    m_actionNoMoveAnnotation = createAction(tr("N&one", "move annotation"));
    m_actionNoMoveAnnotation->setActionGroup(groupMoveAnnotation);
    m_actionNoMoveAnnotation->setCheckable(true);
    connect(m_actionNoMoveAnnotation, SIGNAL(triggered(bool)),
            SLOT(noMoveAnnotation(bool)));

    m_actionOpen = createAction(tr("&Open..."));
    m_actionOpen->setShortcut(QKeySequence::Open);
    connect(m_actionOpen, SIGNAL(triggered()), SLOT(open()));
    m_actionPlacePiece = createAction();
    m_actionPlacePiece->setShortcut(QString("Return"));

    m_actionPlay = createAction(tr("&Play"));
    m_actionPlay->setShortcut(QString("Ctrl+L"));
    setIcon(m_actionPlay, "pentobi-play");
    connect(m_actionPlay, SIGNAL(triggered()), SLOT(play()));

    m_actionPlaySingleMove = createAction(tr("Play &Single Move"));
    m_actionPlaySingleMove->setShortcut(QString("Ctrl+Shift+L"));
    connect(m_actionPlaySingleMove, SIGNAL(triggered()),
            SLOT(playSingleMove()));

    m_actionPreviousPiece = createAction(tr("Previous Piece"));
    setIcon(m_actionPreviousPiece, "pentobi-previous-piece");
    m_actionPreviousPiece->setShortcut(QString("-"));
    connect(m_actionPreviousPiece, SIGNAL(triggered()),
            SLOT(previousPiece()));

    m_actionPreviousTransform = createAction();
    m_actionPreviousTransform->setShortcut(QString("Shift+Space"));
    connect(m_actionPreviousTransform, SIGNAL(triggered()),
            SLOT(previousTransform()));

    m_actionPreviousVariation = createAction(tr("&Previous Variation"));
    m_actionPreviousVariation->setToolTip(tr("Go to previous variation"));
    m_actionPreviousVariation->setPriority(QAction::LowPriority);
    m_actionPreviousVariation->setShortcut(QKeySequence::MoveToPreviousPage);
    setIcon(m_actionPreviousVariation, "pentobi-previous-variation");
    connect(m_actionPreviousVariation, SIGNAL(triggered()),
            SLOT(previousVariation()));

    m_actionRatedGame = createAction(tr("&Rated Game"));
    m_actionRatedGame->setToolTip(tr("Start a rated game"));
    m_actionRatedGame->setShortcut(QString("Ctrl+Shift+N"));
    setIcon(m_actionRatedGame, "pentobi-rated-game");
    connect(m_actionRatedGame, SIGNAL(triggered()), SLOT(ratedGame()));

    for (auto& action : m_actionRecentFile)
    {
         action = createAction();
         action->setVisible(false);
         connect(action, SIGNAL(triggered()), SLOT(openRecentFile()));
    }

    m_actionRotateAnticlockwise = createAction(tr("Rotate Anticlockwise"));
    setIcon(m_actionRotateAnticlockwise, "pentobi-rotate-left");
    connect(m_actionRotateAnticlockwise, SIGNAL(triggered()),
            SLOT(rotateAnticlockwise()));

    m_actionRotateClockwise = createAction(tr("Rotate Clockwise"));
    setIcon(m_actionRotateClockwise, "pentobi-rotate-right");
    connect(m_actionRotateClockwise, SIGNAL(triggered()),
            SLOT(rotateClockwise()));

    m_actionQuit = createAction(tr("&Quit"));
    // QKeySequence::Quit does not work with Qt4 on Xfce
    if (QKeySequence(QKeySequence::Quit).toString().isEmpty())
        m_actionQuit->setShortcut(QString("Ctrl+Q"));
    else
        m_actionQuit->setShortcut(QKeySequence::Quit);
    connect(m_actionQuit, SIGNAL(triggered()), SLOT(close()));

    m_actionSave = createAction(tr("&Save"));
    m_actionSave->setShortcut(QKeySequence::Save);
    connect(m_actionSave, SIGNAL(triggered()), SLOT(save()));

    m_actionSaveAs = createAction(tr("Save &As..."));
    // QKeySequence::SaveAs does not work with Qt4 on Xfce
    if (QKeySequence(QKeySequence::SaveAs).toString().isEmpty())
        m_actionSaveAs->setShortcut(QString("Ctrl+Shift+S"));
    else
        m_actionSaveAs->setShortcut(QKeySequence::SaveAs);
    connect(m_actionSaveAs, SIGNAL(triggered()), SLOT(saveAs()));

    m_actionSelectNextColor = createAction(tr("Select Next &Color"));
    m_actionSelectNextColor->setShortcut(QString("Ctrl+C"));
    connect(m_actionSelectNextColor, SIGNAL(triggered()),
            SLOT(selectNextColor()));

    for (auto name : { "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L",
                       "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" })
    {
        auto action = createAction();
        action->setData(name);
        action->setShortcut(QString(name));
        connect(action, SIGNAL(triggered()), SLOT(selectNamedPiece()));
    }

    m_actionSetupMode = createAction(tr("S&etup Mode"));
    m_actionSetupMode->setCheckable(true);
    connect(m_actionSetupMode, SIGNAL(triggered(bool)), SLOT(setupMode(bool)));

    m_actionShowComment = createAction(tr("&Comment"));
    m_actionShowComment->setCheckable(true);
    m_actionShowComment->setShortcut(QString("Ctrl+T"));
    connect(m_actionShowComment, SIGNAL(triggered(bool)),
            SLOT(showComment(bool)));

    m_actionRating = createAction(tr("&Rating"));
    m_actionRating->setShortcut(QString("F7"));
    connect(m_actionRating, SIGNAL(triggered()), SLOT(showRating()));

    m_actionToolBarNoText = createAction(tr("&No Text"));
    m_actionToolBarNoText->setActionGroup(groupToolBarText);
    m_actionToolBarNoText->setCheckable(true);
    connect(m_actionToolBarNoText, SIGNAL(triggered(bool)),
            SLOT(toolBarNoText(bool)));

    m_actionToolBarTextBesideIcons = createAction(tr("Text &Beside Icons"));
    m_actionToolBarTextBesideIcons->setActionGroup(groupToolBarText);
    m_actionToolBarTextBesideIcons->setCheckable(true);
    connect(m_actionToolBarTextBesideIcons, SIGNAL(triggered(bool)),
            SLOT(toolBarTextBesideIcons(bool)));

    m_actionToolBarTextBelowIcons = createAction(tr("Text Bel&ow Icons"));
    m_actionToolBarTextBelowIcons->setActionGroup(groupToolBarText);
    m_actionToolBarTextBelowIcons->setCheckable(true);
    connect(m_actionToolBarTextBelowIcons, SIGNAL(triggered(bool)),
            SLOT(toolBarTextBelowIcons(bool)));

    m_actionToolBarTextOnly = createAction(tr("&Text Only"));
    m_actionToolBarTextOnly->setActionGroup(groupToolBarText);
    m_actionToolBarTextOnly->setCheckable(true);
    connect(m_actionToolBarTextOnly, SIGNAL(triggered(bool)),
            SLOT(toolBarTextOnly(bool)));

    m_actionToolBarTextSystem = createAction(tr("&System Default"));
    m_actionToolBarTextSystem->setActionGroup(groupToolBarText);
    m_actionToolBarTextSystem->setCheckable(true);
    connect(m_actionToolBarTextSystem, SIGNAL(triggered(bool)),
            SLOT(toolBarTextSystem(bool)));

    m_actionTruncate = createAction(tr("&Truncate"));
    connect(m_actionTruncate, SIGNAL(triggered()), SLOT(truncate()));

    m_actionTruncateChildren = createAction(tr("Truncate C&hildren"));
    connect(m_actionTruncateChildren, SIGNAL(triggered()),
             SLOT(truncateChildren()));

    m_actionShowToolbar = createAction(tr("&Toolbar"));
    m_actionShowToolbar->setCheckable(true);
    connect(m_actionShowToolbar, SIGNAL(triggered(bool)),
            SLOT(showToolbar(bool)));

    m_actionShowVariations = createAction(tr("Show &Variations"));
    m_actionShowVariations->setCheckable(true);
    connect(m_actionShowVariations, SIGNAL(triggered(bool)),
            SLOT(showVariations(bool)));

    m_actionUndo = createAction(tr("&Undo Move"));
    setIcon(m_actionUndo, "pentobi-undo");
    connect(m_actionUndo, SIGNAL(triggered()), SLOT(undo()));

    m_actionVariantClassic2 = createAction(tr("Classic (&2 Players)"));
    m_actionVariantClassic2->setActionGroup(groupVariant);
    m_actionVariantClassic2->setCheckable(true);
    connect(m_actionVariantClassic2, SIGNAL(triggered(bool)),
            SLOT(variantClassic2(bool)));

    m_actionVariantClassic3 = createAction(tr("Classic (&3 Players)"));
    m_actionVariantClassic3->setActionGroup(groupVariant);
    m_actionVariantClassic3->setCheckable(true);
    connect(m_actionVariantClassic3, SIGNAL(triggered(bool)),
            SLOT(variantClassic3(bool)));

    m_actionVariantClassic = createAction(tr("Classic (&4 Players)"));
    m_actionVariantClassic->setActionGroup(groupVariant);
    m_actionVariantClassic->setCheckable(true);
    connect(m_actionVariantClassic, SIGNAL(triggered(bool)),
            SLOT(variantClassic(bool)));

    m_actionVariantDuo = createAction(tr("&Duo"));
    m_actionVariantDuo->setActionGroup(groupVariant);
    m_actionVariantDuo->setCheckable(true);
    connect(m_actionVariantDuo, SIGNAL(triggered(bool)),
            SLOT(variantDuo(bool)));

    m_actionVariantJunior = createAction(tr("&Junior"));
    m_actionVariantJunior->setActionGroup(groupVariant);
    m_actionVariantJunior->setCheckable(true);
    connect(m_actionVariantJunior, SIGNAL(triggered(bool)),
            SLOT(variantJunior(bool)));

    m_actionVariantNexos2 = createAction(tr("Nexos (&2 Players)"));
    m_actionVariantNexos2->setActionGroup(groupVariant);
    m_actionVariantNexos2->setCheckable(true);
    connect(m_actionVariantNexos2, SIGNAL(triggered(bool)),
            SLOT(variantNexos2(bool)));

    m_actionVariantNexos = createAction(tr("Nexos (&4 Players)"));
    m_actionVariantNexos->setActionGroup(groupVariant);
    m_actionVariantNexos->setCheckable(true);
    connect(m_actionVariantNexos, SIGNAL(triggered(bool)),
            SLOT(variantNexos(bool)));

    m_actionVariantTrigon2 = createAction(tr("Trigon (&2 Players)"));
    m_actionVariantTrigon2->setActionGroup(groupVariant);
    m_actionVariantTrigon2->setCheckable(true);
    connect(m_actionVariantTrigon2, SIGNAL(triggered(bool)),
            SLOT(variantTrigon2(bool)));

    m_actionVariantTrigon3 = createAction(tr("Trigon (&3 Players)"));
    m_actionVariantTrigon3->setActionGroup(groupVariant);
    m_actionVariantTrigon3->setCheckable(true);
    connect(m_actionVariantTrigon3, SIGNAL(triggered(bool)),
            SLOT(variantTrigon3(bool)));

    m_actionVariantTrigon = createAction(tr("Trigon (&4 Players)"));
    m_actionVariantTrigon->setActionGroup(groupVariant);
    m_actionVariantTrigon->setCheckable(true);
    connect(m_actionVariantTrigon, SIGNAL(triggered(bool)),
            SLOT(variantTrigon(bool)));

    m_actionVeryBadMove = createAction(tr("V&ery Bad"));
    m_actionVeryBadMove->setActionGroup(groupMoveAnnotation);
    m_actionVeryBadMove->setCheckable(true);
    connect(m_actionVeryBadMove, SIGNAL(triggered(bool)),
            SLOT(veryBadMove(bool)));

    m_actionVeryGoodMove = createAction(tr("&Very Good"));
    m_actionVeryGoodMove->setActionGroup(groupMoveAnnotation);
    m_actionVeryGoodMove->setCheckable(true);
    connect(m_actionVeryGoodMove, SIGNAL(triggered(bool)),
            SLOT(veryGoodMove(bool)));
}

QWidget* MainWindow::createCentralWidget()
{
    auto widget = new QWidget;
    // We add spacing around and between the two panels using streches (such
    // that the spacing grows with the window size)
    auto outerLayout = new QVBoxLayout;
    widget->setLayout(outerLayout);
    auto innerLayout = new QHBoxLayout;
    outerLayout->addStretch(1);
    outerLayout->addLayout(innerLayout, 100);
    outerLayout->addStretch(1);
    innerLayout->addStretch(1);
    innerLayout->addWidget(createLeftPanel(), 60);
    innerLayout->addStretch(1);
    innerLayout->addLayout(createRightPanel(), 40);
    innerLayout->addStretch(1);
    // The central widget doesn't do anything with the focus right now, but we
    // allow it to receive the focus such that the user can switch away the
    // focus from the comment field and its blinking cursor.
    widget->setFocusPolicy(Qt::StrongFocus);
    return widget;
}

QWidget* MainWindow::createLeftPanel()
{
    m_splitter = new QSplitter(Qt::Vertical);
    m_guiBoard = new GuiBoard(nullptr, m_bd);
    m_splitter->addWidget(m_guiBoard);
    m_comment = new QPlainTextEdit;
    m_comment->setTabChangesFocus(true);
    connect(m_comment, SIGNAL(textChanged()), SLOT(commentChanged()));
    m_splitter->addWidget(m_comment);
    m_splitter->setStretchFactor(0, 85);
    m_splitter->setStretchFactor(1, 15);
    m_splitter->setCollapsible(0, false);
    m_splitter->setCollapsible(1, false);
    return m_splitter;
}

QAction* MainWindow::createLevelAction(QActionGroup* group, unsigned level,
                                       const QString& text)
{
    auto action = createAction(text);
    action->setCheckable(true);
    action->setActionGroup(group);
    action->setData(level);
    connect(action, SIGNAL(triggered(bool)), SLOT(levelTriggered(bool)));
    return action;
}

void MainWindow::createMenu()
{
    auto menuGame = menuBar()->addMenu(tr("&Game"));
    menuGame->addAction(m_actionNew);
    menuGame->addAction(m_actionRatedGame);
    menuGame->addSeparator();
    m_menuVariant = menuGame->addMenu(tr("Game &Variant"));
    auto menuClassic = m_menuVariant->addMenu(tr("&Classic"));
    menuClassic->addAction(m_actionVariantClassic2);
    menuClassic->addAction(m_actionVariantClassic3);
    menuClassic->addAction(m_actionVariantClassic);
    m_menuVariant->addAction(m_actionVariantDuo);
    m_menuVariant->addAction(m_actionVariantJunior);
    auto menuTrigon = m_menuVariant->addMenu(tr("&Trigon"));
    menuTrigon->addAction(m_actionVariantTrigon2);
    menuTrigon->addAction(m_actionVariantTrigon3);
    menuTrigon->addAction(m_actionVariantTrigon);
    auto menuNexos = m_menuVariant->addMenu(tr("&Nexos"));
    menuNexos->addAction(m_actionVariantNexos2);
    menuNexos->addAction(m_actionVariantNexos);
    menuGame->addAction(m_actionGameInfo);
    menuGame->addSeparator();
    menuGame->addAction(m_actionUndo);
    menuGame->addAction(m_actionFindMove);
    menuGame->addSeparator();
    menuGame->addAction(m_actionOpen);
    m_menuOpenRecent = menuGame->addMenu(tr("Open R&ecent"));
    for (auto& action : m_actionRecentFile)
        m_menuOpenRecent->addAction(action);
    menuGame->addSeparator();
    menuGame->addAction(m_actionSave);
    menuGame->addAction(m_actionSaveAs);
    m_menuExport = menuGame->addMenu(tr("E&xport"));
    m_menuExport->addAction(m_actionExportImage);
    m_menuExport->addAction(m_actionExportAsciiArt);
    menuGame->addSeparator();
    menuGame->addAction(m_actionQuit);

    auto menuGo = menuBar()->addMenu(tr("G&o"));
    menuGo->addAction(m_actionBeginning);
    menuGo->addAction(m_actionBackward);
    menuGo->addAction(m_actionForward);
    menuGo->addAction(m_actionEnd);
    menuGo->addSeparator();
    menuGo->addAction(m_actionNextVariation);
    menuGo->addAction(m_actionPreviousVariation);
    menuGo->addSeparator();
    menuGo->addAction(m_actionGotoMove);
    menuGo->addAction(m_actionBackToMainVariation);
    menuGo->addAction(m_actionBeginningOfBranch);
    menuGo->addSeparator();
    menuGo->addAction(m_actionFindNextComment);

    auto menuEdit = menuBar()->addMenu(tr("&Edit"));
    m_menuMoveAnnotation = menuEdit->addMenu(tr("&Move Annotation"));
    m_menuMoveAnnotation->addAction(m_actionGoodMove);
    m_menuMoveAnnotation->addAction(m_actionVeryGoodMove);
    m_menuMoveAnnotation->addAction(m_actionBadMove);
    m_menuMoveAnnotation->addAction(m_actionVeryBadMove);
    m_menuMoveAnnotation->addAction(m_actionInterestingMove);
    m_menuMoveAnnotation->addAction(m_actionDoubtfulMove);
    m_menuMoveAnnotation->addAction(m_actionNoMoveAnnotation);
    menuEdit->addSeparator();
    menuEdit->addAction(m_actionMakeMainVariation);
    menuEdit->addAction(m_actionMoveUpVariation);
    menuEdit->addAction(m_actionMoveDownVariation);
    menuEdit->addSeparator();
    menuEdit->addAction(m_actionDeleteAllVariations);
    menuEdit->addAction(m_actionTruncate);
    menuEdit->addAction(m_actionTruncateChildren);
    menuEdit->addAction(m_actionKeepOnlyPosition);
    menuEdit->addAction(m_actionKeepOnlySubtree);
    menuEdit->addSeparator();
    menuEdit->addAction(m_actionSetupMode);
    menuEdit->addAction(m_actionSelectNextColor);

    auto menuView = menuBar()->addMenu(tr("&View"));
    menuView->addAction(m_actionShowToolbar);
    m_menuToolBarText = menuView->addMenu(tr("Toolbar T&ext"));
    m_menuToolBarText->addAction(m_actionToolBarNoText);
    m_menuToolBarText->addAction(m_actionToolBarTextBesideIcons);
    m_menuToolBarText->addAction(m_actionToolBarTextBelowIcons);
    m_menuToolBarText->addAction(m_actionToolBarTextOnly);
    m_menuToolBarText->addAction(m_actionToolBarTextSystem);
    menuView->addAction(m_actionShowComment);
    menuView->addSeparator();
    auto menuMoveNumbers = menuView->addMenu(tr("&Move Marking"));
    menuMoveNumbers->addAction(m_actionMoveMarkingLastDot);
    menuMoveNumbers->addAction(m_actionMoveMarkingLastNumber);
    menuMoveNumbers->addAction(m_actionMoveMarkingAllNumber);
    menuMoveNumbers->addAction(m_actionMoveMarkingNone);
    menuView->addAction(m_actionCoordinates);
    menuView->addAction(m_actionShowVariations);
    menuView->addSeparator();
    menuView->addAction(m_actionFullscreen);

    auto menuComputer = menuBar()->addMenu(tr("&Computer"));
    menuComputer->addAction(m_actionComputerColors);
    menuComputer->addAction(m_actionPlay);
    menuComputer->addSeparator();
    menuComputer->addAction(m_actionPlaySingleMove);
    menuComputer->addAction(m_actionInterrupt);
    menuComputer->addSeparator();
    m_menuLevel = menuComputer->addMenu(tr("&Level"));
    for (auto& action : m_actionLevel)
        m_menuLevel->addAction(action);

    auto menuTools = menuBar()->addMenu(tr("&Tools"));
    menuTools->addAction(m_actionRating);
    menuTools->addAction(m_actionAnalyzeGame);

    auto menuHelp = menuBar()->addMenu(tr("&Help"));
    menuHelp->addAction(m_actionHelp);
    menuHelp->addAction(m_actionAbout);
}

QLayout* MainWindow::createOrientationButtonBoxLeft()
{
    auto outerLayout = new QVBoxLayout;
    auto layout = new QGridLayout;
    layout->addWidget(createOBoxToolButton(m_actionRotateAnticlockwise), 0, 0);
    layout->addWidget(createOBoxToolButton(m_actionRotateClockwise), 0, 1);
    layout->addWidget(createOBoxToolButton(m_actionFlipHorizontally), 1, 0);
    layout->addWidget(createOBoxToolButton(m_actionFlipVertically), 1, 1);
    outerLayout->addStretch();
    outerLayout->addLayout(layout);
    outerLayout->addStretch();
    return outerLayout;
}

QLayout* MainWindow::createOrientationButtonBoxRight()
{
    auto outerLayout = new QVBoxLayout;
    auto layout = new QGridLayout;
    layout->addWidget(createOBoxToolButton(m_actionPreviousPiece), 0, 0);
    layout->addWidget(createOBoxToolButton(m_actionNextPiece), 0, 1);
    layout->addWidget(createOBoxToolButton(m_actionClearPiece), 1, 0,
                      1, 2, Qt::AlignHCenter);
    outerLayout->addStretch();
    outerLayout->addLayout(layout);
    outerLayout->addStretch();
    return outerLayout;
}

QLayout* MainWindow::createOrientationSelector()
{
    auto layout = new QHBoxLayout;
    layout->addStretch();
    layout->addLayout(createOrientationButtonBoxLeft());
    layout->addSpacing(8);
    m_orientationDisplay = new OrientationDisplay(nullptr, m_bd);
    connect(m_orientationDisplay, SIGNAL(colorClicked(Color)),
            SLOT(orientationDisplayColorClicked(Color)));
    m_orientationDisplay->setSizePolicy(QSizePolicy::MinimumExpanding,
                                        QSizePolicy::MinimumExpanding);
    layout->addWidget(m_orientationDisplay);
    layout->addSpacing(8);
    layout->addLayout(createOrientationButtonBoxRight());
    layout->addStretch();
    return layout;
}

QLayout* MainWindow::createRightPanel()
{
    auto layout = new QVBoxLayout;
    layout->addLayout(createOrientationSelector(), 20);
    m_scoreDisplay = new ScoreDisplay;
    layout->addWidget(m_scoreDisplay, 6);
    auto pieceSelectorLayout = new SameHeightLayout;
    layout->addLayout(pieceSelectorLayout, 80);
    for (Color c : Color::Range(Color::range))
    {
        m_pieceSelector[c] = new PieceSelector(nullptr, m_bd, c);
        connect(m_pieceSelector[c],
                SIGNAL(pieceSelected(Color,Piece,const Transform*)),
                SLOT(selectPiece(Color,Piece,const Transform*)));
        pieceSelectorLayout->addWidget(m_pieceSelector[c]);
    }
    return layout;
}

void MainWindow::deleteAllVariations()
{
    QMessageBox msgBox(this);
    initQuestion(msgBox, tr("Delete all variations?"),
                 tr("All variations but the main variation will be"
                    " removed from the game tree."));
    auto deleteButton =
        msgBox.addButton(tr("Delete Variations"), QMessageBox::DestructiveRole);
    auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
    msgBox.setDefaultButton(cancelButton);
    msgBox.exec();
    if (msgBox.clickedButton() != deleteButton)
        return;
    bool currentNodeChanges = ! is_main_variation(m_game.get_current());
    if (currentNodeChanges)
        cancelThread();
    m_game.delete_all_variations();
    updateWindow(currentNodeChanges);
}

void MainWindow::doubtfulMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_doubtful_move();
    updateWindow(false);
}

void MainWindow::createToolBar()
{
    auto toolBar = new QToolBar;
    toolBar->setMovable(false);
    toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
    toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
    toolBar->addAction(m_actionNew);
    toolBar->addAction(m_actionRatedGame);
    toolBar->addAction(m_actionUndo);
    toolBar->addSeparator();
    toolBar->addAction(m_actionComputerColors);
    toolBar->addAction(m_actionPlay);
    toolBar->addSeparator();
    toolBar->addAction(m_actionBeginning);
    toolBar->addAction(m_actionBackward);
    toolBar->addAction(m_actionForward);
    toolBar->addAction(m_actionEnd);
    toolBar->addSeparator();
    toolBar->addAction(m_actionNextVariation);
    toolBar->addAction(m_actionPreviousVariation);
    // Is this the right way to enable autorepeat buttons? Using
    // QAction::autoRepeat applies only to keyboard and adding a QToolButton
    // with QToolBar::addWidget() makes the tool button not respect the
    // toolButtonStyle.
    for (auto button : toolBar->findChildren<QToolButton*>())
    {
        auto action = button->defaultAction();
        if (action == m_actionBackward || action == m_actionForward)
            button->setAutoRepeat(true);
    }
    addToolBar(toolBar);
    initToolBarText(toolBar);
    QSettings settings;
    auto toolBarText = settings.value("toolbar_text", "system").toString();
    if (toolBarText == "no_text")
        m_actionToolBarNoText->setChecked(true);
    else if (toolBarText == "beside_icons")
        m_actionToolBarTextBesideIcons->setChecked(true);
    else if (toolBarText == "below_icons")
        m_actionToolBarTextBelowIcons->setChecked(true);
    else if (toolBarText == "text_only")
        m_actionToolBarTextOnly->setChecked(true);
    else
        m_actionToolBarTextSystem->setChecked(true);
}

void MainWindow::deleteAutoSaveFile()
{
    QString autoSaveFile = getAutoSaveFile();
    QFile file(autoSaveFile);
    if (file.exists() && ! file.remove())
        showError(tr("Could not delete %1").arg(autoSaveFile));
}

void MainWindow::enablePieceSelector(Color c)
{
    for (Color i : m_bd.get_colors())
    {
        m_pieceSelector[i]->checkUpdate();
        m_pieceSelector[i]->setEnabled(i == c);
    }
}

void MainWindow::end()
{
    gotoNode(get_last_node(m_game.get_current()));
}

bool MainWindow::eventFilter(QObject* object, QEvent* event)
{
    // By default, Qt 4.7 shows status tips in the status bar if the mouse
    // goes over a menu. This is undesirable because it deletes the current
    // text in the status line (e.g. the "The computer is thinking..." status)
    if (event->type() == QEvent::StatusTip)
        return true;
    return QMainWindow::eventFilter(object, event);
}

void MainWindow::exportAsciiArt()
{
    QString file = QFileDialog::getSaveFileName(this, "", getLastDir(),
                                      tr("Text files (*.txt);;All files (*)"));
    if (file.isEmpty())
        return;
    rememberDir(file);
    ofstream out(file.toLocal8Bit().constData());
    m_bd.write(out, false);
    if (! out)
        showError(strerror(errno));
}

void MainWindow::exportImage()
{
    QSettings settings;
    auto size = settings.value("export_image_size", 420).toInt();
    QInputDialog dialog(this);
    dialog.setWindowFlags(dialog.windowFlags()
                          & ~Qt::WindowContextHelpButtonHint);
    dialog.setWindowTitle(tr("Export Image"));
    dialog.setLabelText(tr("Image size:"));
    dialog.setInputMode(QInputDialog::IntInput);
    dialog.setIntRange(0, 2147483647);
    dialog.setIntStep(40);
    dialog.setIntValue(size);
    if (! dialog.exec())
        return;
    size = dialog.intValue();
    settings.setValue("export_image_size", size);
    bool coordinates = m_actionCoordinates->isChecked();
    BoardPainter boardPainter;
    boardPainter.setCoordinates(coordinates);
    boardPainter.setCoordinateColor(QColor(100, 100, 100));
    QImage image(size, size, QImage::Format_ARGB32);
    image.fill(Qt::transparent);
    QPainter painter;
    painter.begin(&image);
    if (coordinates)
        painter.fillRect(0, 0, size, size, QColor(216, 216, 216));
    boardPainter.paintEmptyBoard(painter, size, size, m_bd.get_variant(),
                                 m_bd.get_geometry());
    Grid<unsigned> pieceId;
    if (m_bd.get_board_type() == BoardType::nexos)
    {
        pieceId.fill(0, m_bd.get_geometry());
        unsigned n = 0;
        for (Color c : m_bd.get_colors())
            for (Move mv : m_bd.get_setup().placements[c])
            {
                ++n;
                for (Point p : m_bd.get_move_info(mv))
                    pieceId[p] = n;
            }
        for (auto mv : m_bd.get_moves())
        {
            ++n;
            for (Point p : m_bd.get_move_info(mv.move))
                pieceId[p] = n;
        }
    }
    boardPainter.paintPieces(painter, m_bd.get_point_state(), pieceId,
                             &m_guiBoard->getLabels());
    painter.end();
    QString file;
    while (true)
    {
        file = QFileDialog::getSaveFileName(this, file, getLastDir());
        if (file.isEmpty())
            break;
        rememberDir(file);
        QImageWriter writer(file);
        if (writer.write(image))
            break;
        else
            showError(writer.errorString());
    }
}

void MainWindow::findMove()
{
    if (m_bd.is_game_over())
        return;
    if (! m_legalMoves)
        m_legalMoves.reset(new MoveList);
    if (m_legalMoves->empty())
    {
        if (! m_marker)
            m_marker.reset(new MoveMarker);
        m_bd.gen_moves(m_currentColor, *m_marker, *m_legalMoves);
        m_marker->clear(*m_legalMoves);
        sort(m_legalMoves->begin(), m_legalMoves->end(),
             [&](Move mv1, Move mv2) {
                 return getHeuristic(m_bd, mv1) > getHeuristic(m_bd, mv2);
             });
    }
    if (m_legalMoves->empty())
        return;
    if (m_legalMoveIndex >= m_legalMoves->size())
        m_legalMoveIndex = 0;
    auto mv = (*m_legalMoves)[m_legalMoveIndex];
    selectPiece(m_currentColor, m_bd.get_move_info(mv).get_piece());
    m_guiBoard->showMove(m_currentColor, mv);
    ++m_legalMoveIndex;
}

void MainWindow::findNextComment()
{
    auto& root = m_game.get_root();
    auto& current = m_game.get_current();
    auto node = find_next_comment(current);
    if (! node && &current != &root)
    {
        QMessageBox msgBox(this);
        initQuestion(msgBox, tr("The end of the tree was reached."),
                     tr("Continue the search from the start of the tree?"));
        auto continueButton =
            msgBox.addButton(tr("Continue From Start"),
                             QMessageBox::AcceptRole);
        msgBox.addButton(QMessageBox::Cancel);
        msgBox.setDefaultButton(continueButton);
        msgBox.exec();
        if (msgBox.clickedButton() == continueButton)
        {
            node = &root;
            if (! has_comment(*node))
                node = find_next_comment(*node);
        }
        else
            return;
    }
    if (! node)
    {
        showInfo(tr("No comment found"));
        return;
    }
    showComment(true);
    gotoNode(*node);
}

void MainWindow::flipHorizontally()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform = m_bd.get_transforms().get_mirrored_horizontally(transform);
    transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
}

void MainWindow::flipVertically()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform = m_bd.get_transforms().get_mirrored_vertically(transform);
    transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
}

void MainWindow::forward()
{
    gotoNode(m_game.get_current().get_first_child_or_null());
}

void MainWindow::fullscreen()
{
    if (isFullScreen())
    {
        // If F11 is pressed in fullscreen, we switch to normal
        leaveFullscreen();
        return;
    }
    QSettings settings;
    menuBar()->hide();
    findChild<QToolBar*>()->hide();
    settings.setValue("geometry", saveGeometry());
    m_wasMaximized = isMaximized();
    showFullScreen();
    if (! m_leaveFullscreenButton)
        m_leaveFullscreenButton =
            new LeaveFullscreenButton(this, m_actionLeaveFullscreen);
    m_leaveFullscreenButton->showButton();
}

void MainWindow::gameInfo()
{
    GameInfoDialog dialog(this, m_game);
    dialog.exec();
    updateWindow(false);
}

void MainWindow::gameOver()
{
    auto variant = m_bd.get_variant();
    auto nuColors = get_nu_colors(variant);
    auto nuPlayers = get_nu_players(variant);
    QString info;
    if (nuColors == 2)
    {
        int score = m_bd.get_score_twocolor(Color(0));
        if (score > 0)
            info = tr("Blue wins with %n point(s).", "", score);
        else if (score < 0)
            info = tr("Green wins with %n point(s).", "", -score);
        else
            info = tr("The game ends in a tie.");
    }
    else if (nuPlayers == 2)
    {
        LIBBOARDGAME_ASSERT(nuColors == 4);
        int score = m_bd.get_score_multicolor(Color(0));
        if (score > 0)
            info = tr("Blue/Red wins with %n point(s).", "", score);
        else if (score < 0)
            info = tr("Yellow/Green wins with %n point(s).", "", -score);
        else
            info = tr("The game ends in a tie.");
    }
    else if (nuPlayers == 3)
    {
        auto blue = m_bd.get_points(Color(0));
        auto yellow = m_bd.get_points(Color(1));
        auto red = m_bd.get_points(Color(2));
        auto maxPoints = max(blue, max(yellow, red));
        if (blue == yellow && yellow == red)
            info = tr("The game ends in a tie between all colors.");
        else if (blue == maxPoints && blue == yellow)
            info = tr("The game ends in a tie between Blue and Yellow.");
        else if (blue == maxPoints && blue == red)
            info = tr("The game ends in a tie between Blue and Red.");
        else if (yellow == maxPoints && yellow == red)
            info = tr("The game ends in a tie between Yellow and Red.");
        else if (blue == maxPoints)
            info = tr("Blue wins.");
        else if (yellow == maxPoints)
            info = tr("Yellow wins.");
        else
            info = tr("Red wins.");
    }
    else
    {
        LIBBOARDGAME_ASSERT(nuPlayers == 4);
        auto blue = m_bd.get_points(Color(0));
        auto yellow = m_bd.get_points(Color(1));
        auto red = m_bd.get_points(Color(2));
        auto green = m_bd.get_points(Color(3));
        auto maxPoints = max(blue, max(yellow, max(red, green)));
        if (blue == yellow && yellow == red && red == green)
            info = tr("The game ends in a tie between all colors.");
        else if (blue == maxPoints && blue == yellow && yellow == red)
            info = tr("The game ends in a tie between Blue, Yellow and Red.");
        else if (blue == maxPoints && blue == yellow && yellow == green)
            info =
                tr("The game ends in a tie between Blue, Yellow and Green.");
        else if (blue == maxPoints && blue == red && red == green)
            info = tr("The game ends in a tie between Blue, Red and Green.");
        else if (yellow == maxPoints && yellow == red && red == green)
            info = tr("The game ends in a tie between Yellow, Red and Green.");
        else if (blue == maxPoints && blue == yellow)
            info = tr("The game ends in a tie between Blue and Yellow.");
        else if (blue == maxPoints && blue == red)
            info = tr("The game ends in a tie between Blue and Red.");
        else if (blue == maxPoints && blue == green)
            info = tr("The game ends in a tie between Blue and Green.");
        else if (yellow == maxPoints && yellow == red)
            info = tr("The game ends in a tie between Yellow and Red.");
        else if (yellow == maxPoints && yellow == green)
            info = tr("The game ends in a tie between Yellow and Green.");
        else if (red == maxPoints && red == green)
            info = tr("The game ends in a tie between Red and Green.");
        else if (blue == maxPoints)
            info = tr("Blue wins.");
        else if (yellow == maxPoints)
            info = tr("Yellow wins.");
        else if (red == maxPoints)
            info = tr("Red wins.");
        else
            info = tr("Green wins.");
    }
    if (m_isRated)
    {
        QString detailText;
        int oldRating = m_history->getRating().to_int();
        unsigned place;
        bool isPlaceShared;
        m_bd.get_place(m_ratedGameColor, place, isPlaceShared);
        float gameResult;
        if (place == 0 && !isPlaceShared)
            gameResult = 1;
        else if (place == 0 && isPlaceShared)
            gameResult = 0.5;
        else
            gameResult = 0;
        unsigned nuOpp = get_nu_players(variant) - 1;
        Rating oppRating = m_player->get_rating(variant);
        QString date = QString(PentobiTree::get_date_today().c_str());
        m_history->addGame(gameResult, oppRating, nuOpp, m_ratedGameColor,
                           gameResult, date, m_level, m_game.get_tree());
        if (m_ratingDialog)
            m_ratingDialog->updateContent();
        int newRating = m_history->getRating().to_int();
        if (newRating > oldRating)
            detailText =
            tr("Your rating has increased from %1 to %2.")
            .arg(oldRating).arg(newRating);
        else if (newRating == oldRating)
            detailText = tr("Your rating stays at %1.").arg(oldRating);
        else
            detailText =
            tr("Your rating has decreased from %1 to %2.")
            .arg(oldRating).arg(newRating);
        setRated(false);
        QSettings settings;
        auto key = QString("next_rated_random_") + to_string_id(variant);
        settings.remove(key);
        QMessageBox msgBox(this);
        Util::setNoTitle(msgBox);
        msgBox.setIcon(QMessageBox::Information);
        msgBox.setText(info);
        msgBox.setInformativeText(detailText);
        auto showRatingButton =
            msgBox.addButton(tr("Show &Rating"), QMessageBox::AcceptRole);
        msgBox.addButton(QMessageBox::Close);
        msgBox.setDefaultButton(showRatingButton);
        msgBox.exec();
        auto result = msgBox.clickedButton();
        if (result == showRatingButton)
            showRating();
    }
    else
        showInfo(info);
}

void MainWindow::genMove(bool playSingleMove)
{
    cancelThread();
    ++m_genMoveId;
    setCursor(QCursor(Qt::BusyCursor));
    m_actionNextPiece->setEnabled(false);
    m_actionPreviousPiece->setEnabled(false);
    m_actionPlay->setEnabled(false);
    m_actionPlaySingleMove->setEnabled(false);
    m_actionInterrupt->setEnabled(true);
    showStatus(tr("The computer is thinking..."));
    clearPiece();
    clear_abort();
    m_lastRemainingSeconds = 0;
    m_lastRemainingMinutes = 0;
    m_player->set_level(m_level);
    QFuture<GenMoveResult> future =
        QtConcurrent::run(this, &MainWindow::asyncGenMove, m_currentColor,
                          m_genMoveId, playSingleMove);
    m_genMoveWatcher.setFuture(future);
    m_isGenMoveRunning = true;
    unsigned nuMoves = m_bd.get_nu_moves();
    if (m_lastComputerMovesBegin == 0 && ! computerPlaysAll())
    {
        m_lastComputerMovesBegin = nuMoves + 1;
        m_lastComputerMovesEnd = m_lastComputerMovesBegin;
    }
}

void MainWindow::genMoveFinished()
{
    m_actionInterrupt->setEnabled(false);
    clearStatus();
    GenMoveResult result = m_genMoveWatcher.future().result();
    if (result.genMoveId != m_genMoveId)
    {
        // Callback from a move generation canceled with cancelThread()
        return;
    }
    LIBBOARDGAME_ASSERT(m_isGenMoveRunning);
    m_isGenMoveRunning = false;
    setCursor(QCursor(Qt::ArrowCursor));
    if (get_abort() && computerPlaysAll())
        m_computerColors.fill(false);
    Color c = result.color;
    auto mv = result.move;
    if (mv.is_null() || ! m_bd.is_legal(c, mv))
    {
        // No need to translate this message, it should never occur if the
        // program is correct
        showError("Computer failed to generate a move");
        return;
    }
    play(c, mv);
    m_lastComputerMovesEnd = m_bd.get_nu_moves();
    if (computerPlaysAll())
        m_lastComputerMovesBegin = m_lastComputerMovesEnd;
    // Call updateWindow() before checkComputerMove() because checkComputerMove
    // resets m_lastComputerMovesBegin if computer doesn't play current color
    // and updateWindow needs m_lastComputerMovesBegin
    updateWindow(true);
    if (! result.playSingleMove)
        checkComputerMove();
}

QString MainWindow::getFilter() const
{
    return tr("Blokus games (*.blksgf);;All files (*)");
}

QString MainWindow::getLastDir()
{
    QSettings settings;
    auto dir = settings.value("last_dir", "").toString();
    if (dir.isEmpty() || ! QFileInfo(dir).exists())
        dir = QStandardPaths::writableLocation(
                                          QStandardPaths::DocumentsLocation);
    return dir;
}

QString MainWindow::getVersion() const
{
    QString version;
#ifdef VERSION
    version = VERSION;
#endif
    if (version.isEmpty())
        version = "UNKNOWN";
    return version;
}

void MainWindow::goodMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_good_move();
    updateWindow(false);
}

void MainWindow::gotoMove()
{
    QSettings settings;
    vector<const SgfNode*> nodes;
    auto& tree = m_game.get_tree();
    auto node = &m_game.get_current();
    do
    {
        if (! tree.get_move(*node).is_null())
            nodes.insert(nodes.begin(), node);
        node = node->get_parent_or_null();
    }
    while (node);
    node = m_game.get_current().get_first_child_or_null();
    while (node)
    {
        if (! tree.get_move(*node).is_null())
            nodes.push_back(node);
        node = node->get_first_child_or_null();
    }
    int maxMoves = int(nodes.size());
    if (maxMoves == 0)
        return;
    int defaultValue = m_bd.get_nu_moves();
    if (defaultValue == 0)
        defaultValue = maxMoves;
    QInputDialog dialog(this);
    dialog.setWindowFlags(dialog.windowFlags()
                          & ~Qt::WindowContextHelpButtonHint);
    dialog.setWindowTitle(tr("Go to Move"));
    dialog.setLabelText(tr("Move number:"));
    dialog.setInputMode(QInputDialog::IntInput);
    dialog.setIntRange(1, static_cast<int>(nodes.size()));
    dialog.setIntStep(1);
    dialog.setIntValue(defaultValue);
    if (dialog.exec())
        gotoNode(*nodes[dialog.intValue() - 1]);
}

void MainWindow::gotoNode(const SgfNode& node)
{
    cancelThread();
    leaveSetupMode();
    try
    {
        m_game.goto_node(node);
    }
    catch (const InvalidTree& e)
    {
        showInvalidFile(m_file, e);
        return;
    }
    m_currentColor = getCurrentColor(m_game);
    m_lastComputerMovesBegin = 0;
    if (m_analyzeGameWindow && m_analyzeGameWindow->isVisible())
        m_analyzeGameWindow->analyzeGameWidget
            ->setCurrentPosition(m_game, node);
    m_autoPlay = false;
    updateWindow(true);
}

void MainWindow::gotoNode(const SgfNode* node)
{
    if (node)
        gotoNode(*node);
}

void MainWindow::gotoPosition(Variant variant,
                              const vector<ColorMove>& moves)
{
    if (m_bd.get_variant() != variant)
        return;
    auto& tree = m_game.get_tree();
    auto node = &tree.get_root();
    if (tree.has_move_ignore_invalid(*node))
        // Move in root node not supported.
        return;
    for (ColorMove mv : moves)
    {
        bool found = false;
        for (auto& i : node->get_children())
            if (tree.get_move(i) == mv)
            {
                found = true;
                node = &i;
                break;
            }
        if (! found)
            return;
    }
    gotoNode(*node);
}

void MainWindow::help()
{
    if (m_helpWindow)
    {
        m_helpWindow->show();
        m_helpWindow->raise();
        return;
    }
    QString path = HelpWindow::findMainPage(m_helpDir, "pentobi");
    m_helpWindow = new HelpWindow(nullptr, tr("Pentobi Help"), path);
    initToolBarText(m_helpWindow->findChild<QToolBar*>());
    m_helpWindow->show();
}

void MainWindow::initGame()
{
    setRated(false);
    if (m_analyzeGameWindow)
    {
        delete m_analyzeGameWindow;
        m_analyzeGameWindow = nullptr;
    }
    m_game.init();
    m_game.set_charset("UTF-8");
#ifdef VERSION
    m_game.set_application("Pentobi", VERSION);
#else
    m_game.set_application("Pentobi");
#endif
    m_game.set_date_today();
    m_game.clear_modified();
    QSettings settings;
    if (! settings.value("computer_color_none").toBool())
    {
        for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
            m_computerColors[c] = ! m_bd.is_same_player(c, Color(0));
        m_autoPlay = true;
    }
    else
    {
        m_computerColors.fill(false);
        m_autoPlay = false;
    }
    m_currentColor = Color(0);
    leaveSetupMode();
    m_lastComputerMovesBegin = 0;
    m_gameFinished = false;
    m_isAutoSaveLoaded = false;
    setFile("");
}

void MainWindow::initVariantActions()
{
    switch (m_bd.get_variant())
    {
    case Variant::classic:
        m_actionVariantClassic->setChecked(true);
        break;
    case Variant::classic_2:
        m_actionVariantClassic2->setChecked(true);
        break;
    case Variant::classic_3:
        m_actionVariantClassic3->setChecked(true);
        break;
    case Variant::duo:
        m_actionVariantDuo->setChecked(true);
        break;
    case Variant::junior:
        m_actionVariantJunior->setChecked(true);
        break;
    case Variant::trigon:
        m_actionVariantTrigon->setChecked(true);
        break;
    case Variant::trigon_2:
        m_actionVariantTrigon2->setChecked(true);
        break;
    case Variant::trigon_3:
        m_actionVariantTrigon3->setChecked(true);
        break;
    case Variant::nexos:
        m_actionVariantNexos->setChecked(true);
        break;
    case Variant::nexos_2:
        m_actionVariantNexos2->setChecked(true);
        break;
    }
}

void MainWindow::initPieceSelectors()
{
    for (Color::IntType i = 0; i < Color::range; ++i)
    {
        bool isVisible = (i < m_bd.get_nu_colors());
        m_pieceSelector[Color(i)]->setVisible(isVisible);
        if (isVisible)
            m_pieceSelector[Color(i)]->init();
    }
}

void MainWindow::interestingMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_interesting_move();
    updateWindow(false);
}

void MainWindow::interrupt()
{
    cancelThread();
    m_autoPlay = false;
}

void MainWindow::interruptPlay()
{
    if (! m_isGenMoveRunning)
        return;
    set_abort();
    m_autoPlay = false;
}

bool MainWindow::isComputerToPlay() const
{
    if (m_game.get_variant() != Variant::classic_3
            || m_currentColor != Color(3))
        return m_computerColors[m_currentColor];
    return m_computerColors[Color(m_bd.get_alt_player())];
}

void MainWindow::keepOnlyPosition()
{
    QMessageBox msgBox(this);
    initQuestion(msgBox, tr("Keep only position?"),
                 tr("All previous and following moves and variations will"
                    " be removed from the game tree."));
    auto keepOnlyPositionButton =
        msgBox.addButton(tr("Keep Only Position"),
                         QMessageBox::DestructiveRole);
    auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
    msgBox.setDefaultButton(cancelButton);
    msgBox.exec();
    if (msgBox.clickedButton() != keepOnlyPositionButton)
        return;
    cancelThread();
    m_game.keep_only_position();
    updateWindow(true);
}

void MainWindow::keepOnlySubtree()
{
    QMessageBox msgBox(this);
    initQuestion(msgBox, tr("Keep only subtree?"),
                 tr("All previous moves and variations will be removed"
                    " from the game tree."));
    auto keepOnlySubtreeButton =
        msgBox.addButton(tr("Keep Only Subtree"),
                         QMessageBox::DestructiveRole);
    auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
    msgBox.setDefaultButton(cancelButton);
    msgBox.exec();
    if (msgBox.clickedButton() != keepOnlySubtreeButton)
        return;
    cancelThread();
    m_game.keep_only_subtree();
    updateWindow(true);
}

void MainWindow::leaveFullscreen()
{
    if (! isFullScreen())
        return;
    QSettings settings;
    auto showToolbar = settings.value("toolbar", true).toBool();
    menuBar()->show();
    findChild<QToolBar*>()->setVisible(showToolbar);
    // m_leaveFullscreenButton can be null if the window was put in fullscreen
    // mode by a "generic" method by the window manager (e.g. the title bar
    // menu on KDE) and not by MainWindow::fullscreen()
    if (m_leaveFullscreenButton)
        m_leaveFullscreenButton->hideButton();
    // Call showNormal() even if m_wasMaximized otherwise restoring the
    // maximized window state does not work correctly on Xfce
    showNormal();
    if (m_wasMaximized)
        showMaximized();
}

void MainWindow::leaveSetupMode()
{
    if (! m_actionSetupMode->isChecked())
        return;
    setupMode(false);
}

void MainWindow::levelTriggered(bool checked)
{
    if (checked)
        setLevel(qobject_cast<QAction*>(sender())->data().toUInt());
}

void MainWindow::loadHistory()
{
    auto variant = m_game.get_variant();
    if (m_history->getVariant() == variant)
        return;
    m_history->load(variant);
    if (m_ratingDialog)
        m_ratingDialog->updateContent();
}

void MainWindow::makeMainVariation()
{
    m_game.make_main_variation();
    updateWindow(false);
}

void MainWindow::moveDownVariation()
{
    m_game.move_down_variation();
    updateWindow(false);
}

void MainWindow::moveUpVariation()
{
    m_game.move_up_variation();
    updateWindow(false);
}

void MainWindow::nextPiece()
{
    if (m_bd.get_pieces_left(m_currentColor).empty())
        return;
    auto nuUniqPieces = m_bd.get_nu_uniq_pieces();
    Piece::IntType i;
    Piece selectedPiece = m_guiBoard->getSelectedPiece();
    if (! selectedPiece.is_null())
        i = static_cast<Piece::IntType>(selectedPiece.to_int() + 1);
    else
        i = 0;
    while (true)
    {
        if (i >= nuUniqPieces)
            i = 0;
        if (m_bd.is_piece_left(m_currentColor, Piece(i)))
            break;
        ++i;
    }
    selectPiece(m_currentColor, Piece(i));
}

void MainWindow::nextTransform()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform = m_bd.get_piece_info(piece).get_next_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
}

void MainWindow::nextVariation()
{
    gotoNode(m_game.get_current().get_sibling());
}

void MainWindow::newGame()
{
    if (! checkSave())
        return;
    cancelThread();
    initGame();
    deleteAutoSaveFile();
    updateWindow(true);
}

void MainWindow::noMoveAnnotation(bool checked)
{
    if (! checked)
        return;
    m_game.remove_move_annotation();
    updateWindow(false);
}

void MainWindow::open()
{
    if (! checkSave())
        return;
    QSettings settings;
    QString file = QFileDialog::getOpenFileName(this, tr("Open"), getLastDir(),
                                                getFilter());
    if (file.isEmpty())
        return;
    rememberDir(file);
    if (open(file))
        rememberFile(file);
}

bool MainWindow::open(const QString& file, bool isTemporary)
{
    if (file.isEmpty())
        return false;
    cancelThread();
    TreeReader reader;
    ifstream in(file.toLocal8Bit().constData());
    try
    {
        reader.read(in);
    }
    catch (const TreeReader::ReadError& e)
    {
        if (! in)
        {
            QString text =
                tr("Could not read file '%1'").arg(QFileInfo(file).fileName());
            showError(text, strerror(errno));
        }
        else
        {
            showInvalidFile(file, e);
        }
        return false;
    }
    m_isAutoSaveLoaded = false;
    if (! isTemporary)
    {
        setFile(file);
        deleteAutoSaveFile();
    }
    if (m_analyzeGameWindow)
    {
        delete m_analyzeGameWindow;
        m_analyzeGameWindow = nullptr;
    }
    setRated(false);
    try
    {
        auto tree = reader.get_tree_transfer_ownership();
        m_game.init(tree);
        if (! libpentobi_base::node_util::has_setup(m_game.get_root()))
            m_game.goto_node(get_last_node(m_game.get_root()));
        m_currentColor = getCurrentColor(m_game);
        initPieceSelectors();
    }
    catch (const InvalidTree& e)
    {
        showInvalidFile(file, e);
    }
    m_computerColors.fill(false);
    m_autoPlay = false;
    leaveSetupMode();
    m_lastComputerMovesBegin = 0;
    initVariantActions();
    restoreLevel(m_bd.get_variant());
    updateWindow(true);
    loadHistory();
    return true;
}

void MainWindow::openRecentFile()
{
     auto action = qobject_cast<QAction*>(sender());
     if (action)
         openCheckSave(action->data().toString());
}

void MainWindow::openCheckSave(const QString& file)
{
     if (checkSave())
         open(file);
}

void MainWindow::orientationDisplayColorClicked(Color)
{
    if (m_actionSetupMode->isChecked())
        selectNextColor();
}

void MainWindow::placePiece(Color c, Move mv)
{
    cancelThread();
    bool isSetupMode = m_actionSetupMode->isChecked();
    bool isAltColor =
            (m_bd.get_variant() == Variant::classic_3 && c.to_int() == 3);
    if ((! isAltColor && m_computerColors[c])
            || (isAltColor && m_computerColors[Color(m_bd.get_alt_player())])
            || isSetupMode)
        // If the user enters a move previously played by the computer (e.g.
        // after undoing moves) then it is unlikely that the user wants to keep
        // the computer color settings.
        m_computerColors.fill(false);
    if (isSetupMode)
    {
        m_game.add_setup(c, mv);
        setSetupPlayer();
        updateWindow(true);
    }
    else
    {
        play(c, mv);
        updateWindow(true);
        checkComputerMove();
    }
}

void MainWindow::play()
{
    cancelThread();
    leaveSetupMode();
    if (! isComputerToPlay())
    {
        m_computerColors.fill(false);
        if (m_bd.get_variant() == Variant::classic_3
                && m_currentColor == Color(3))
            m_computerColors[Color(m_bd.get_alt_player())] = true;
        else
        {
            m_computerColors[m_currentColor] = true;
            m_computerColors[m_bd.get_second_color(m_currentColor)] = true;
        }
        QSettings settings;
        settings.setValue("computer_color_none", false);
    }
    m_autoPlay = true;
    m_lastComputerMovesBegin = 0;
    genMove();
}

void MainWindow::play(Color c, Move mv)
{
    m_game.play(c, mv, false);
    m_gameFinished = false;
    if (m_bd.is_game_over())
    {
        updateWindow(true);
        repaint();
        gameOver();
        m_gameFinished = true;
        deleteAutoSaveFile();
        return;
    }
    m_currentColor = m_bd.get_effective_to_play();
}

void MainWindow::playSingleMove()
{
    cancelThread();
    leaveSetupMode();
    m_autoPlay = false;
    m_lastComputerMovesBegin = 0;
    genMove(true);
}

void MainWindow::pointClicked(Point p)
{
    // If a piece on the board is clicked on in setup mode, remove it and make
    // it the selected piece without changing its orientation.
    if (! m_actionSetupMode->isChecked())
        return;
    PointState s = m_bd.get_point_state(p);
    if (s.is_empty())
        return;
    Color c = s.to_color();
    Move mv = m_bd.get_move_at(p);
    m_game.remove_setup(c, mv);
    setSetupPlayer();
    updateWindow(true);
    Piece piece = m_bd.get_move_info(mv).get_piece();
    selectPiece(c, piece, m_bd.find_transform(mv));
    m_guiBoard->setSelectedPiecePoints(mv);
}

void MainWindow::previousPiece()
{
    if (m_bd.get_pieces_left(m_currentColor).empty())
        return;
    auto nuUniqPieces = m_bd.get_nu_uniq_pieces();
    Piece::IntType i;
    Piece selectedPiece = m_guiBoard->getSelectedPiece();
    if (! selectedPiece.is_null())
        i = selectedPiece.to_int();
    else
        i = 0;
    while (true)
    {
        if (i == 0)
            i = static_cast<Piece::IntType>(nuUniqPieces - 1);
        else
            --i;
        if (m_bd.is_piece_left(m_currentColor, Piece(i)))
            break;
    }
    selectPiece(m_currentColor, Piece(i));
}

void MainWindow::previousTransform()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform =
        m_bd.get_piece_info(piece).get_previous_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
}

void MainWindow::previousVariation()
{
    gotoNode(m_game.get_current().get_previous_sibling());
}

void MainWindow::ratedGame()
{
    if (! checkSave())
        return;
    cancelThread();
    if (m_history->getNuGames() == 0)
    {
        InitialRatingDialog dialog(this);
        if (dialog.exec() != QDialog::Accepted)
            return;
        m_history->init(Rating(static_cast<float>(dialog.getRating())));
    }
    int level;
    QSettings settings;
    unsigned random;
    auto variant = m_bd.get_variant();
    auto key = QString("next_rated_random_") + to_string_id(variant);
    if (settings.contains(key))
        random = settings.value(key).toUInt();
    else
    {
        // RandomGenerator::ResultType may be larger than unsigned
        random = static_cast<unsigned>(m_random.generate() % 1000);
        settings.setValue(key, random);
    }
    m_history->getNextRatedGameSettings(m_maxLevel, random,
                                        level, m_ratedGameColor);
    QMessageBox msgBox(this);
    initQuestion(msgBox, tr("Start rated game?"),
                 "<html>" +
                 tr("In this game, you play %1 against Pentobi level&nbsp;%2.")
                 .arg(getPlayerString(variant, m_ratedGameColor)).arg(level));
    auto startGameButton =
        msgBox.addButton(tr("&Start Game"), QMessageBox::AcceptRole);
    msgBox.addButton(QMessageBox::Cancel);
    msgBox.setDefaultButton(startGameButton);
    msgBox.exec();
    auto result = msgBox.clickedButton();
    if (result != startGameButton)
        return;
    setLevel(level);
    initGame();
    setFile("");
    setRated(true);
    m_computerColors.fill(true);
    for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
        if (m_bd.is_same_player(c, m_ratedGameColor))
            m_computerColors[c] = false;
    m_autoPlay = true;
    QString computerPlayerName =
        //: The first argument is the version of Pentobi
        tr("Pentobi %1 (level %2)").arg(getVersion()).arg(level);
    string charset = m_game.get_root().get_property("CA", "");
    string computerPlayerNameStdStr =
        Util::convertSgfValueFromQString(computerPlayerName, charset);
    string humanPlayerNameStdStr =
        Util::convertSgfValueFromQString(tr("Human"), charset);
    for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
        if (m_computerColors[c])
            m_game.set_player_name(c, computerPlayerNameStdStr);
        else
            m_game.set_player_name(c, humanPlayerNameStdStr);
    // Setting the player names marks the game as modified but there is nothing
    // important that would need to be saved yet
    m_game.clear_modified();
    deleteAutoSaveFile();
    updateWindow(true);
    checkComputerMove();
}

void MainWindow::rememberDir(const QString& file)
{
    if (file.isEmpty())
        return;
    QString canonicalFile = file;
    QString canonicalFilePath = QFileInfo(file).canonicalFilePath();
    if (! canonicalFilePath.isEmpty())
        canonicalFile = canonicalFilePath;
    QFileInfo info(canonicalFile);
    QSettings settings;
    settings.setValue("last_dir", info.dir().path());
}

void MainWindow::rememberFile(const QString& file)
{
    if (file.isEmpty())
        return;
    QString canonicalFile = file;
    QString canonicalFilePath = QFileInfo(file).canonicalFilePath();
    if (! canonicalFilePath.isEmpty())
        canonicalFile = canonicalFilePath;
    QFileInfo info(canonicalFile);
    QSettings settings;
    auto files = settings.value("recent_files").toStringList();
    files.removeAll(canonicalFile);
    files.prepend(canonicalFile);
    while (files.size() > maxRecentFiles)
        files.removeLast();
    settings.setValue("recent_files", files);
    settings.sync(); // updateRecentFiles() needs the new settings
    updateRecentFiles();
}

void MainWindow::restoreLevel(Variant variant)
{
    QSettings settings;
    QString key = QString("level_") + to_string_id(variant);
    m_level = settings.value(key, 1).toInt();
    if (m_level < 1)
        m_level = 1;
    if (m_level > m_maxLevel)
        m_level = m_maxLevel;
    m_actionLevel[m_level - 1]->setChecked(true);
}

void MainWindow::rotateAnticlockwise()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform = m_bd.get_transforms().get_rotated_anticlockwise(transform);
    transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
    updateFlipActions();
}

void MainWindow::rotateClockwise()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    transform = m_bd.get_transforms().get_rotated_clockwise(transform);
    transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->setSelectedPieceTransform(transform);
    updateFlipActions();
}

void MainWindow::save()
{
    if (m_file.isEmpty())
        saveAs();
    else if (save(m_file))
    {
        m_game.clear_modified();
        updateWindow(false);
    }
}

bool MainWindow::save(const QString& file)
{
    if (! writeGame(file.toLocal8Bit().constData()))
    {
        showError(tr("The file could not be saved."),
                  /*: Error message if file cannot be saved. %1 is
                    replaced by the file name, %2 by the error message
                    of the operating system. */
                  tr("%1: %2").arg(file).arg(strerror(errno)));
        return false;
    }
    else
    {
        Util::removeThumbnail(file);
        return true;
    }
}

void MainWindow::saveAs()
{
    QString file = m_file;
    if (file.isEmpty())
    {
        file = getLastDir();
        file.append(QDir::separator());
        file.append(tr("Untitled Game.blksgf"));
        if (QFileInfo(file).exists())
            for (unsigned i = 1; ; ++i)
            {
                file = getLastDir();
                file.append(QDir::separator());
                file.append(tr("Untitled Game %1.blksgf").arg(i));
                if (! QFileInfo(file).exists())
                    break;
            }
    }
    file = QFileDialog::getSaveFileName(this, tr("Save"), file, getFilter());
    if (! file.isEmpty())
    {
        if (save(file))
        {
            m_game.clear_modified();
            updateWindow(false);
        }
        setFile(file);
        rememberFile(file);
    }
}

void MainWindow::searchCallback(double elapsedSeconds, double remainingSeconds)
{
    // If the search is longer than 10 sec, we show the (maximum) remaining
    // time (only during a move generation, ignore search callbacks during
    // game analysis)
    if (! m_isGenMoveRunning || elapsedSeconds < 10)
        return;
    QString text;
    int seconds = static_cast<int>(ceil(remainingSeconds));
    if (seconds < 90)
    {
        if (seconds == m_lastRemainingSeconds)
            return;
        m_lastRemainingSeconds = seconds;
        text =
            tr("The computer is thinking... (max. %1 seconds remaining)")
            .arg(seconds);
    }
    else
    {
        int minutes = static_cast<int>(ceil(remainingSeconds / 60));
        if (minutes == m_lastRemainingMinutes)
            return;
        m_lastRemainingMinutes = minutes;
        text =
            tr("The computer is thinking... (max. %1 minutes remaining)")
            .arg(minutes);
    }
    QMetaObject::invokeMethod(statusBar(), "showMessage", Q_ARG(QString, text),
                              Q_ARG(int, 0));
}

void MainWindow::selectNamedPiece()
{
    string name(qobject_cast<QAction*>(sender())->data().toString()
                .toLocal8Bit().data());
    Board::PiecesLeftList pieces;
    for (Piece::IntType i = 0; i < m_bd.get_nu_uniq_pieces(); ++i)
    {
        Piece piece(i);
        if (m_bd.is_piece_left(m_currentColor, piece)
                && m_bd.get_piece_info(piece).get_name().find(name) == 0)
            pieces.push_back(piece);
    }
    if (pieces.empty())
        return;
    auto piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        piece = pieces[0];
    else
    {
        auto pos = std::find(pieces.begin(), pieces.end(), piece);
        if (pos == pieces.end())
            piece = pieces[0];
        else
        {
            ++pos;
            if (pos == pieces.end())
                piece = pieces[0];
            else
                piece = *pos;
        }
    }
    selectPiece(m_currentColor, piece);
}

void MainWindow::selectNextColor()
{
    m_currentColor = m_bd.get_next(m_currentColor);
    m_orientationDisplay->selectColor(m_currentColor);
    clearPiece();
    for (Color c : m_bd.get_colors())
        m_pieceSelector[c]->setEnabled(m_currentColor == c);
    if (m_actionSetupMode->isChecked())
        setSetupPlayer();
    updateWindow(false);
}

void MainWindow::selectPiece(Color c, Piece piece)
{
    selectPiece(c, piece, m_bd.get_transforms().get_default());
}

void MainWindow::selectPiece(Color c, Piece piece, const Transform* transform)
{
    if (m_isGenMoveRunning
            || (m_bd.is_game_over() && ! m_actionSetupMode->isChecked()))
        return;
    m_currentColor = c;
    m_guiBoard->selectPiece(c, piece);
    m_guiBoard->setSelectedPieceTransform(transform);
    m_orientationDisplay->selectColor(c);
    m_orientationDisplay->setSelectedPiece(piece);
    m_orientationDisplay->setSelectedPieceTransform(transform);
    bool can_rotate = m_bd.get_piece_info(piece).can_rotate();
    m_actionRotateClockwise->setEnabled(can_rotate);
    m_actionRotateAnticlockwise->setEnabled(can_rotate);
    updateFlipActions();
    m_actionClearPiece->setEnabled(true);
}

void MainWindow::setCommentText(const QString& text)
{
    m_ignoreCommentTextChanged = true;
    m_comment->setPlainText(text);
    m_ignoreCommentTextChanged = false;
    if (! text.isEmpty())
        m_comment->ensureCursorVisible();
    m_comment->clearFocus();
}

void MainWindow::setDeterministic()
{
    m_player->get_search().set_deterministic();
}

void MainWindow::setNoDelay()
{
    m_noDelay = true;
}

void MainWindow::setVariant(Variant variant)
{
    if (m_bd.get_variant() == variant)
        return;
    if (! checkSave())
    {
        initVariantActions();
        return;
    }
    cancelThread();
    QSettings settings;
    settings.setValue("variant", to_string_id(variant));
    clearPiece();
    m_game.init(variant);
    initPieceSelectors();
    newGame();
    loadHistory();
    restoreLevel(variant);
}

void MainWindow::setFile(const QString& file)
{
    m_file = file;
    // Don't use setWindowFilePath() because of QTBUG-16507
    if (m_file.isEmpty())
        setWindowTitle(tr("Pentobi"));
    else
        setWindowTitle(tr("[*]%1").arg(QFileInfo(m_file).fileName()));
}

void MainWindow::setLevel(unsigned level)
{
    if (level < 1 || level > m_maxLevel)
        return;
    m_level = level;
    m_actionLevel[level - 1]->setChecked(true);
    QSettings settings;
    settings.setValue(QString("level_") + to_string_id(m_bd.get_variant()),
                      m_level);
}

void MainWindow::setMoveMarkingAllNumber(bool checked)
{
    if (! checked)
        return;
    QSettings settings;
    settings.setValue("move_marking", "all_number");
    updateWindow(false);
}

void MainWindow::setMoveMarkingLastDot(bool checked)
{
    if (! checked)
        return;
    QSettings settings;
    settings.setValue("move_marking", "last_dot");
    updateWindow(false);
}

void MainWindow::setMoveMarkingLastNumber(bool checked)
{
    if (! checked)
        return;
    QSettings settings;
    settings.setValue("move_marking", "last_number");
    updateWindow(false);
}

void MainWindow::setMoveMarkingNone(bool checked)
{
    if (! checked)
        return;
    QSettings settings;
    settings.setValue("move_marking", "none");
    updateWindow(false);
}

void MainWindow::setPlayToolTip()
{
    m_actionPlay->setToolTip(
                m_computerColors[m_currentColor] ?
                    tr("Make the computer continue to play the current color") :
                    tr("Make the computer play the current color"));
}

void MainWindow::setRated(bool isRated)
{
    m_isRated = isRated;
    if (isRated)
    {
        statusBar()->addWidget(m_ratedGameLabelText);
        m_ratedGameLabelText->show();
    }
    else if (m_ratedGameLabelText->isVisible())
        statusBar()->removeWidget(m_ratedGameLabelText);
}

void MainWindow::setSetupPlayer()
{
    if (! m_game.has_setup())
        m_game.remove_player();
    else
        m_game.set_player(m_currentColor);
}

void MainWindow::setupMode(bool enable)
{
    // Currently, we allow setup mode only if no moves have been played. It
    // should also work in inner nodes but this might be confusing for users
    // and violate some assumptions in the user interface (e.g. node depth is
    // equal to move number). Therefore, m_actionSetupMode is disabled if the
    // root node has children, but we still need to check for it here because
    // due to bugs in the Unitiy interface in Ubuntu 11.10, menu items are
    // not always disabled if the corresponding action is disabled.
    if (enable && m_game.get_root().has_children())
    {
        showInfo(tr("Setup mode cannot be used if moves have been played."));
        enable = false;
    }
    m_actionSetupMode->setChecked(enable);
    m_guiBoard->setFreePlacement(enable);
    if (enable)
    {
        m_setupModeLabel->show();
        for (Color c : m_bd.get_colors())
            m_pieceSelector[c]->setEnabled(true);
        m_computerColors.fill(false);
    }
    else
    {
        setSetupPlayer();
        m_setupModeLabel->hide();
        m_currentColor = m_game.get_effective_to_play();
        enablePieceSelector(m_currentColor);
        updateWindow(false);
    }
}

void MainWindow::showComment(bool checked)
{
    QSettings settings;
    bool wasVisible = m_comment->isVisible();
    if (wasVisible && ! checked)
        settings.setValue("splitter_state", m_splitter->saveState());
    settings.setValue("show_comment", checked);
    m_comment->setVisible(checked);
    if (! wasVisible && checked)
        m_splitter->restoreState(
                               settings.value("splitter_state").toByteArray());

}

void MainWindow::showError(const QString& text, const QString& infoText,
                           const QString& detailText)
{
    ::showError(this, text, infoText, detailText);
}

void MainWindow::showInfo(const QString& text, const QString& infoText,
                          const QString& detailText, bool withIcon)
{
    ::showInfo(this, text, infoText, detailText, withIcon);
}

void MainWindow::showInvalidFile(QString file, const exception& e)
{
    showError(tr("Error in file '%1'").arg(QFileInfo(file).fileName()),
              tr("The file is not a valid Blokus SGF file."), e.what());
}

void MainWindow::showRating()
{
    if (! m_ratingDialog)
    {
        m_ratingDialog = new RatingDialog(this, *m_history);
        connect(m_ratingDialog, SIGNAL(openRecentFile(const QString&)),
                SLOT(openCheckSave(const QString&)));
    }
    loadHistory();
    m_ratingDialog->show();
}

void MainWindow::showStatus(const QString& text, bool temporary)
{
    int timeout = (temporary ? 4000 : 0);
    statusBar()->showMessage(text, timeout);
}

void MainWindow::showToolbar(bool checked)
{
    QSettings settings;
    settings.setValue("toolbar", checked);
    findChild<QToolBar*>()->setVisible(checked);
    m_menuToolBarText->setEnabled(checked);
}

QSize MainWindow::sizeHint() const
{
    return QSize(1020, 634);
}

void MainWindow::toolBarNoText(bool checked)
{
    if (checked)
        toolBarText("no_text", Qt::ToolButtonIconOnly);
}

void MainWindow::toolBarText(const QString& key, Qt::ToolButtonStyle style)
{
    QSettings settings;
    settings.setValue("toolbar_text", key);
    findChild<QToolBar*>()->setToolButtonStyle(style);
    if (m_helpWindow)
        m_helpWindow->findChild<QToolBar*>()->setToolButtonStyle(style);
}

void MainWindow::toolBarTextBesideIcons(bool checked)
{
    if (checked)
        toolBarText("beside_icons", Qt::ToolButtonTextBesideIcon);
}

void MainWindow::toolBarTextBelowIcons(bool checked)
{
    if (checked)
        toolBarText("below_icons", Qt::ToolButtonTextUnderIcon);
}

void MainWindow::toolBarTextOnly(bool checked)
{
    if (checked)
        toolBarText("text_only", Qt::ToolButtonTextOnly);
}

void MainWindow::toolBarTextSystem(bool checked)
{
    if (checked)
        toolBarText("system", Qt::ToolButtonFollowStyle);
}

void MainWindow::truncate()
{
    auto& current = m_game.get_current();
    if (! current.has_parent())
        return;
    cancelThread();
    if (current.has_children())
    {
        QMessageBox msgBox(this);
        initQuestion(msgBox, tr("Truncate this subtree?"),
                     tr("This position and all following moves and"
                        " variations will be removed from the game tree."));
        auto truncateButton =
            msgBox.addButton(tr("Truncate"),
                             QMessageBox::DestructiveRole);
        auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
        msgBox.setDefaultButton(cancelButton);
        msgBox.exec();
        if (msgBox.clickedButton() != truncateButton)
            return;
    }
    m_game.truncate();
    m_currentColor = getCurrentColor(m_game);
    m_lastComputerMovesBegin = 0;
    m_autoPlay = false;
    m_gameFinished = false;
    updateWindow(true);
}

void MainWindow::truncateChildren()
{
    if (! m_game.get_current().has_children())
        return;
    cancelThread();
    QMessageBox msgBox(this);
    initQuestion(msgBox, tr("Truncate children?"),
                 tr("All following moves and variations will"
                    " be removed from the game tree."));
    auto truncateButton =
        msgBox.addButton(tr("Truncate Children"),
                         QMessageBox::DestructiveRole);
    auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
    msgBox.setDefaultButton(cancelButton);
    msgBox.exec();
    if (msgBox.clickedButton() != truncateButton)
        return;
    m_game.truncate_children();
    m_gameFinished = false;
    updateWindow(false);
}

void MainWindow::showVariations(bool checked)
{
    {
        QSettings settings;
        settings.setValue("show_variations", checked);
    }
    updateWindow(false);
}

void MainWindow::undo()
{
    auto& current = m_game.get_current();
    if (current.has_children()
            || ! m_game.get_tree().has_move_ignore_invalid(current)
            || ! current.has_parent())
        return;
    cancelThread();
    m_game.undo();
    m_currentColor = getCurrentColor(m_game);
    m_lastComputerMovesBegin = 0;
    m_autoPlay = false;
    m_gameFinished = false;
    updateWindow(true);
}

void MainWindow::updateComment()
{
    string comment = m_game.get_comment();
    if (comment.empty())
    {
        setCommentText("");
        return;
    }
    string charset = m_game.get_root().get_property("CA", "");
    setCommentText(Util::convertSgfValueToQString(comment, charset));
}

void MainWindow::updateFlipActions()
{
    Piece piece = m_guiBoard->getSelectedPiece();
    if (piece.is_null())
        return;
    auto transform = m_guiBoard->getSelectedPieceTransform();
    bool can_flip_horizontally =
        m_bd.get_piece_info(piece).can_flip_horizontally(transform);
    m_actionFlipHorizontally->setEnabled(can_flip_horizontally);
    bool can_flip_vertically =
        m_bd.get_piece_info(piece).can_flip_vertically(transform);
    m_actionFlipVertically->setEnabled(can_flip_vertically);
}

void MainWindow::updateMoveAnnotationActions()
{
    if (m_game.get_move_ignore_invalid().is_null())
    {
        m_menuMoveAnnotation->setEnabled(false);
        return;
    }
    m_menuMoveAnnotation->setEnabled(true);
    double goodMove = m_game.get_good_move();
    if (goodMove > 1)
    {
        m_actionVeryGoodMove->setChecked(true);
        return;
    }
    if (goodMove > 0)
    {
        m_actionGoodMove->setChecked(true);
        return;
    }
    double badMove = m_game.get_bad_move();
    if (badMove > 1)
    {
        m_actionVeryBadMove->setChecked(true);
        return;
    }
    if (badMove > 0)
    {
        m_actionBadMove->setChecked(true);
        return;
    }
    if (m_game.is_interesting_move())
    {
        m_actionInterestingMove->setChecked(true);
        return;
    }
    if (m_game.is_doubtful_move())
    {
        m_actionDoubtfulMove->setChecked(true);
        return;
    }
    m_actionNoMoveAnnotation->setChecked(true);
}

void MainWindow::updateMoveNumber()
{
    auto& tree = m_game.get_tree();
    auto& current = m_game.get_current();
    unsigned move = get_move_number(tree, current);
    unsigned movesLeft = get_moves_left(tree, current);
    unsigned totalMoves = move + movesLeft;
    string variation = get_variation_string(current);
    QString text =
            QString::fromLocal8Bit(get_position_info(tree, current).c_str());
    QString toolTip;
    if (variation.empty())
    {
        if (movesLeft == 0)
        {
            if (move > 0)
                toolTip = tr("Move %1").arg(move);
        }
        else
        {
            if (move == 0)
                toolTip = tr("%n move(s)", "", totalMoves);
            else
                toolTip = tr("Move %1 of %2").arg(move).arg(totalMoves);
        }
    }
    else
        toolTip = tr("Move %1 of %2 in variation %3")
                    .arg(move).arg(totalMoves).arg(variation.c_str());
    if (text.isEmpty())
    {
        if (m_moveNumber->isVisible())
            statusBar()->removeWidget(m_moveNumber);
    }
    else
    {
        m_moveNumber->setText(text);
        m_moveNumber->setToolTip(toolTip);
        if (! m_moveNumber->isVisible())
        {
            statusBar()->addPermanentWidget(m_moveNumber);
            m_moveNumber->show();
        }
    }
}

void MainWindow::updateRecentFiles()
{
    QSettings settings;
    auto files = settings.value("recent_files").toStringList();
    for (int i = 0; i < files.size(); ++i)
        if (! QFileInfo(files[i]).exists())
        {
            files.removeAt(i);
            --i;
        }
    int nuRecentFiles = files.size();
    if (nuRecentFiles > maxRecentFiles)
        nuRecentFiles = maxRecentFiles;
    m_menuOpenRecent->setEnabled(nuRecentFiles > 0);
    for (int i = 0; i < nuRecentFiles; ++i)
    {
        QFileInfo info = QFileInfo(files[i]);
        QString name = info.absoluteFilePath();
        // Don't prepend the filename by a number for a shortcut key
        // because the file name may contain underscores and Ubuntu Unity does
        // not handle this correctly (Unity bug #1390373)
        m_actionRecentFile[i]->setText(name);
        m_actionRecentFile[i]->setData(files[i]);
        m_actionRecentFile[i]->setVisible(true);
    }
    for (int j = nuRecentFiles; j < maxRecentFiles; ++j)
        m_actionRecentFile[j]->setVisible(false);
}

void MainWindow::updateWindow(bool currentNodeChanged)
{
    updateWindowModified();
    m_guiBoard->copyFromBoard(m_bd);
    QSettings settings;
    auto markVariations = settings.value("show_variations", true).toBool();
    unsigned nuMoves = m_bd.get_nu_moves();
    unsigned markMovesBegin = 0;
    unsigned markMovesEnd = 0;
    if (m_actionMoveMarkingAllNumber->isChecked())
    {
        markMovesBegin = 1;
        markMovesEnd = nuMoves;
    }
    else if (m_actionMoveMarkingLastNumber->isChecked()
             || m_actionMoveMarkingLastDot->isChecked())
    {
        if (m_lastComputerMovesBegin != 0)
        {
            markMovesBegin = m_lastComputerMovesBegin;
            markMovesEnd = m_lastComputerMovesEnd;
        }
        else
        {
            markMovesBegin = nuMoves;
            markMovesEnd = nuMoves;
        }
    }
    gui_board_util::setMarkup(*m_guiBoard, m_game, markMovesBegin,
                              markMovesEnd, markVariations,
                              m_actionMoveMarkingLastDot->isChecked());
    m_scoreDisplay->updateScore(m_bd);
    if (m_legalMoves)
        m_legalMoves->clear();
    m_legalMoveIndex = 0;
    bool isGameOver = m_bd.is_game_over();
    if (isGameOver && ! m_actionSetupMode->isChecked())
        m_orientationDisplay->clearSelectedColor();
    else
        m_orientationDisplay->selectColor(m_currentColor);
    if (currentNodeChanged)
    {
        clearPiece();
        for (Color c : m_bd.get_colors())
            m_pieceSelector[c]->checkUpdate();
        if (! m_actionSetupMode->isChecked())
            enablePieceSelector(m_currentColor);
        updateComment();
        updateMoveAnnotationActions();
    }
    updateMoveNumber();
    setPlayToolTip();
    auto& tree = m_game.get_tree();
    auto& current = m_game.get_current();
    bool isMain = is_main_variation(current);
    bool hasEarlierVariation = has_earlier_variation(current);
    bool hasParent = current.has_parent();
    bool hasChildren = current.has_children();
    bool hasMove = tree.has_move_ignore_invalid(current);
    bool hasMoves = m_bd.has_moves(m_currentColor);
    bool isEmpty = libboardgame_sgf::util::is_empty(tree);
    m_actionAnalyzeGame->setEnabled(! m_isRated
                                    && tree.has_main_variation_moves());
    m_actionBackToMainVariation->setEnabled(! isMain);
    m_actionBeginning->setEnabled(! m_isRated && hasParent);
    m_actionBeginningOfBranch->setEnabled(hasEarlierVariation);
    m_actionBackward->setEnabled(! m_isRated && hasParent);
    m_actionComputerColors->setEnabled(! m_isRated);
    m_actionDeleteAllVariations->setEnabled(tree.has_variations());
    m_actionFindNextComment->setEnabled(! m_isRated);
    m_actionForward->setEnabled(hasChildren);
    m_actionEnd->setEnabled(hasChildren);
    m_actionFindMove->setEnabled(! isGameOver);
    m_actionGotoMove->setEnabled(! m_isRated &&
                                 hasCurrentVariationOtherMoves(tree, current));
    m_actionKeepOnlyPosition->setEnabled(! m_isRated
                                         && (hasParent || hasChildren));
    m_actionKeepOnlySubtree->setEnabled(hasParent && hasChildren);
    for (auto& action : m_actionLevel)
        action->setEnabled(! m_isRated);
    m_actionMakeMainVariation->setEnabled(! isMain);
    m_actionMoveDownVariation->setEnabled(current.get_sibling());
    m_actionMoveUpVariation->setEnabled(hasParent
                       && &current.get_parent().get_first_child() != &current);
    m_actionNew->setEnabled(! isEmpty);
    m_actionNextVariation->setEnabled(current.get_sibling());
    if (! m_isGenMoveRunning)
    {
        m_actionNextPiece->setEnabled(! isGameOver);
        m_actionPreviousPiece->setEnabled(! isGameOver);
        m_actionPlay->setEnabled(! m_isRated && hasMoves);
        m_actionPlaySingleMove->setEnabled(! m_isRated && hasMoves);
    }
    m_actionPreviousVariation->setEnabled(current.get_previous_sibling());
    m_actionRatedGame->setEnabled(! m_isRated);
    m_actionSave->setEnabled(! m_file.isEmpty() && m_game.is_modified());
    m_actionSaveAs->setEnabled(! isEmpty);
    // See also comment in setupMode()
    m_actionSetupMode->setEnabled(! m_isRated && ! hasParent && ! hasChildren);
    m_actionSelectNextColor->setEnabled(! m_isRated);
    m_actionTruncate->setEnabled(! m_isRated && hasParent);
    m_actionTruncateChildren->setEnabled(hasChildren);
    m_actionUndo->setEnabled(! m_isRated && hasParent && ! hasChildren
                             && hasMove);
    m_actionVariantClassic->setEnabled(! m_isRated);
    m_actionVariantClassic2->setEnabled(! m_isRated);
    m_actionVariantClassic3->setEnabled(! m_isRated);
    m_actionVariantDuo->setEnabled(! m_isRated);
    m_actionVariantJunior->setEnabled(! m_isRated);
    m_actionVariantTrigon->setEnabled(! m_isRated);
    m_actionVariantTrigon2->setEnabled(! m_isRated);
    m_actionVariantTrigon3->setEnabled(! m_isRated);
    m_actionVariantNexos->setEnabled(! m_isRated);
    m_actionVariantNexos2->setEnabled(! m_isRated);
    // Don't disable m_menuLevel but all level items such that it is still
    // possible to see what the current level is even if it cannot be changed
    // in rated games.
    for (auto& action : m_actionLevel)
        action->setEnabled(! m_isRated);
    m_menuVariant->setEnabled(! m_isRated);
}

void MainWindow::updateWindowModified()
{
    if (! m_file.isEmpty())
        setWindowModified(m_game.is_modified());
}

void MainWindow::variantClassic(bool checked)
{
    if (checked)
        setVariant(Variant::classic);
}

void MainWindow::variantClassic2(bool checked)
{
    if (checked)
        setVariant(Variant::classic_2);
}

void MainWindow::variantClassic3(bool checked)
{
    if (checked)
        setVariant(Variant::classic_3);
}

void MainWindow::variantDuo(bool checked)
{
    if (checked)
        setVariant(Variant::duo);
}

void MainWindow::variantJunior(bool checked)
{
    if (checked)
        setVariant(Variant::junior);
}

void MainWindow::variantTrigon(bool checked)
{
    if (checked)
        setVariant(Variant::trigon);
}

void MainWindow::variantTrigon2(bool checked)
{
    if (checked)
        setVariant(Variant::trigon_2);
}

void MainWindow::variantTrigon3(bool checked)
{
    if (checked)
        setVariant(Variant::trigon_3);
}

void MainWindow::variantNexos(bool checked)
{
    if (checked)
        setVariant(Variant::nexos);
}

void MainWindow::variantNexos2(bool checked)
{
    if (checked)
        setVariant(Variant::nexos_2);
}

void MainWindow::veryBadMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_bad_move(2);
    updateWindow(false);
}

void MainWindow::veryGoodMove(bool checked)
{
    if (! checked)
        return;
    m_game.set_good_move(2);
    updateWindow(false);
}

void MainWindow::wheelEvent(QWheelEvent* event)
{
    int delta = event->delta() / 8 / 15;
    if (delta > 0)
    {
        if (! m_guiBoard->getSelectedPiece().is_null())
            for (int i = 0; i < delta; ++i)
                nextTransform();
    }
    else if (delta < 0)
    {
        if (! m_guiBoard->getSelectedPiece().is_null())
            for (int i = 0; i < -delta; ++i)
                previousTransform();
    }
    event->accept();
}

bool MainWindow::writeGame(const string& file)
{
    ofstream out(file);
    PentobiTreeWriter writer(out, m_game.get_tree());
    writer.set_indent(1);
    writer.write();
    return static_cast<bool>(out);
}

//-----------------------------------------------------------------------------
