From 1d56792d53e3be2e2bc427c8443e5e525a641c86 Mon Sep 17 00:00:00 2001 From: lmat Date: Mon, 10 Jun 2024 16:29:47 -0400 Subject: [PATCH 1/2] Re #211 Select move from game time properly There was a problem with how gametime move selection worked: 1. Click a spot on game time (which selects a move in the game) 2. Press right arrow a couple times to select a different move 3. Click the same spot on game time This should rewind the game back to the spot clicked in game time, but if you didn't move the mouse between steps 1. and 3., the move wasn't updated. Another bug is fixed: any time the user clicked a spot in game time, the move was updated twice: once on mouse "press" and once on mouse "release". --- src/gui/chartwidget.cpp | 31 ++++++++++++++----------------- src/gui/chartwidget.h | 3 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/gui/chartwidget.cpp b/src/gui/chartwidget.cpp index 3a9d50f2..603720c3 100644 --- a/src/gui/chartwidget.cpp +++ b/src/gui/chartwidget.cpp @@ -121,19 +121,22 @@ void ChartWidget::resizeEvent(QResizeEvent*) updatePolygons(); } -void ChartWidget::handleMouseEvent(QMouseEvent *event) +void ChartWidget::handleMouseEvent(QMouseEvent *event, bool deduplicate) { - if (width() && m_values.size() && (m_values[0].count()>1)) + if (!width() || !m_values.size() || (m_values[0].count()<2)) { - QPointF p = EVENT_POSITION(event); - double multiplierW = ((double)width()) / (m_values[0].count()-1); - double x = 0.5 + (p.x() / multiplierW); - if (m_lastSentIndicator!=(int)x) - { - emit halfMoveRequested((int)x); - m_lastSentIndicator = (int)x; - } + return; + } + QPointF p = EVENT_POSITION(event); + double multiplierW = ((double)width()) / (m_values[0].count()-1); + double x = 0.5 + (p.x() / multiplierW); + int move_index = static_cast(x); + if (deduplicate && m_lastSentIndicator==move_index) + { + return; } + emit halfMoveRequested(move_index); + m_lastSentIndicator = move_index; } #if QT_VERSION < 0x060000 @@ -155,7 +158,7 @@ void ChartWidget::leaveEvent(QEvent *event) void ChartWidget::mousePressEvent(QMouseEvent *event) { - handleMouseEvent(event); + handleMouseEvent(event, false); QWidget::mousePressEvent(event); } @@ -165,12 +168,6 @@ void ChartWidget::mouseMoveEvent(QMouseEvent *event) QWidget::mouseMoveEvent(event); } -void ChartWidget::mouseReleaseEvent(QMouseEvent *event) -{ - handleMouseEvent(event); - QWidget::mouseReleaseEvent(event); -} - void ChartWidget::updatePly() { if (m_values.size() && (m_values[0].count()>1)) diff --git a/src/gui/chartwidget.h b/src/gui/chartwidget.h index c3d5c779..23d6380b 100644 --- a/src/gui/chartwidget.h +++ b/src/gui/chartwidget.h @@ -24,12 +24,11 @@ class ChartWidget : public QWidget void halfMoveRequested(int); protected: - void handleMouseEvent(QMouseEvent *event); + void handleMouseEvent(QMouseEvent *event, bool deduplicate = true); virtual void paintEvent(QPaintEvent* event); virtual void resizeEvent(QResizeEvent* event); virtual void mousePressEvent(QMouseEvent* event); virtual void mouseMoveEvent(QMouseEvent* event); - virtual void mouseReleaseEvent(QMouseEvent* event); #if QT_VERSION < 0x060000 virtual void enterEvent(QEvent *event); #else From 1ab9ae8ed38bad3689a554ccdee4867e326c3657 Mon Sep 17 00:00:00 2001 From: lmat Date: Mon, 10 Jun 2024 16:29:47 -0400 Subject: [PATCH 2/2] Re #211 New tool window shows centipawn loss graph There were two gametoolbars. One GameToolBar in src/gui/gametoolbar.{cpp,h}, and a certain MainWindow::gameToolBar. The former was a toolbar attached to the notation window showing a material graph and some clocks shown when "game time" was selected. The latter is a toolbar at the top of the main window showing "game"-related actions (such as flipping the board, etc.). The latter was not affected in this commit. Limit evaluations to +/- 10.0 --- chessx.pro | 6 +- src/CMakeLists.txt | 6 +- src/database/analysis.h | 2 + src/database/gameevaluation.cpp | 188 ++++++++++++++++++++++++++++++++ src/database/gameevaluation.h | 75 +++++++++++++ src/gui/centipawngraph.cpp | 132 ++++++++++++++++++++++ src/gui/centipawngraph.h | 47 ++++++++ src/gui/chartwidget.cpp | 4 - src/gui/chartwidget.h | 1 - src/gui/gametoolbar.cpp | 69 ------------ src/gui/gametoolbar.h | 34 ------ src/gui/mainwindow.cpp | 27 +++-- src/gui/mainwindow.h | 6 +- src/gui/mainwindowactions.cpp | 25 ++--- 14 files changed, 482 insertions(+), 140 deletions(-) create mode 100644 src/database/gameevaluation.cpp create mode 100644 src/database/gameevaluation.h create mode 100644 src/gui/centipawngraph.cpp create mode 100644 src/gui/centipawngraph.h delete mode 100644 src/gui/gametoolbar.cpp delete mode 100644 src/gui/gametoolbar.h diff --git a/chessx.pro b/chessx.pro index a571311d..1bce3569 100644 --- a/chessx.pro +++ b/chessx.pro @@ -204,6 +204,7 @@ HEADERS += src/database/board.h \ src/database/filteroperator.h \ src/database/filtersearch.h \ src/database/gamecursor.h \ + src/database/gameevaluation.h \ src/database/gameid.h \ src/database/gameundocommand.h \ src/database/gamex.h \ @@ -308,7 +309,7 @@ HEADERS += src/database/board.h \ src/gui/gamelist.h \ src/gui/gamelistsortmodel.h \ src/gui/gamenotationwidget.h \ - src/gui/gametoolbar.h \ + src/gui/centipawngraph.h \ src/gui/gamewindow.h \ src/gui/helpbrowser.h \ src/gui/helpbrowsershell.h \ @@ -387,6 +388,7 @@ SOURCES += \ src/database/filtermodel.cpp \ src/database/filtersearch.cpp \ src/database/gamecursor.cpp \ + src/database/gameevaluation.cpp \ src/database/gamex.cpp \ src/database/historylist.cpp \ src/database/index.cpp \ @@ -477,7 +479,7 @@ SOURCES += \ src/gui/gamelist.cpp \ src/gui/gamelistsortmodel.cpp \ src/gui/gamenotationwidget.cpp \ - src/gui/gametoolbar.cpp \ + src/gui/centipawngraph.cpp \ src/gui/gamewindow.cpp \ src/gui/helpbrowser.cpp \ src/gui/helpbrowsershell.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86b8a7d0..cee08d33 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -95,6 +95,8 @@ add_library(database-core STATIC database/gameid.h database/gamecursor.cpp database/gamecursor.h + database/gameevaluation.cpp + database/gameevaluation.h database/gamex.cpp database/gamex.h database/index.cpp @@ -339,6 +341,8 @@ add_library(gui STATIC gui/boardviewex.cpp gui/boardviewex.h gui/boardviewex.ui + gui/centipawngraph.cpp + gui/centipawngraph.h gui/chartwidget.cpp gui/chartwidget.h gui/chessbrowser.cpp @@ -380,8 +384,6 @@ add_library(gui STATIC gui/gamelistsortmodel.h gui/gamenotationwidget.cpp gui/gamenotationwidget.h - gui/gametoolbar.cpp - gui/gametoolbar.h gui/gamewindow.cpp gui/gamewindow.h gui/gamewindow.ui diff --git a/src/database/analysis.h b/src/database/analysis.h index e0d06e5c..1220ff9c 100644 --- a/src/database/analysis.h +++ b/src/database/analysis.h @@ -62,6 +62,8 @@ class Analysis /** Moves to mate. */ /** Convert analysis to formatted text. */ QString toString(const BoardX& board, bool hiddenLine=false) const; + /** If bestMove is true, then only the bestMove, and variation members + * are valid. */ void setBestMove(bool bestMove); bool bestMove() const; diff --git a/src/database/gameevaluation.cpp b/src/database/gameevaluation.cpp new file mode 100644 index 00000000..869c2830 --- /dev/null +++ b/src/database/gameevaluation.cpp @@ -0,0 +1,188 @@ +#include +#include "enginex.h" +#include "gamex.h" +#include "gameevaluation.h" + +GameEvaluation::GameEvaluation(int engineIndex, int msPerMove, GameX game) noexcept + : engineIndex{engineIndex} + , msPerMove{msPerMove} + , game{game} + , targetThreadCount{std::max(1, QThread::idealThreadCount())} + , moveNumbers{0} +{ + if (targetThreadCount > 4) + // We'll leave one logical core idle if we can afford it. + --targetThreadCount; + connect(&timer, &QTimer::timeout, this, &GameEvaluation::update); +} + +void GameEvaluation::start() +{ + if (running) + { + throw std::logic_error{"Game evaluation already running"}; + } + running = true; + game.moveToStart(); + workers.clear(); + timer.stop(); + timer.start(std::chrono::milliseconds{100}); + // We place the first worker for the starting position here. + workers.emplace_back(engineIndex, game.startingBoard(), game.board(), msPerMove, moveNumbers++); +} + +void GameEvaluation::update() noexcept +{ + std::unordered_map evaluations; + for (std::list::iterator i{workers.begin()}; i != workers.end(); ++i) + { + i->update(); + evaluations.emplace(i->moveNumber, i->getLastScore()); + if (!i->isRunning()) + { + // Erase *after* getting the value because erasing will destroy the worker + i = workers.erase(i); + --i; + } + } + emit evaluationChanged(evaluations); + while (static_cast(workers.size()) < targetThreadCount) + { + if (!game.forward()) + { + break; + } + try + { + int tempNum {moveNumbers++}; + workers.emplace_back(engineIndex, game.startingBoard(), game.board(), msPerMove, tempNum); + } + catch (...) + { + // Destroy any workers that have started. + workers.clear(); + break; + } + } + if (!workers.size()) + { + timer.stop(); + emit evaluationComplete(); + running = false; + } +} + +void GameEvaluation::stop() noexcept +{ + workers.clear(); + timer.stop(); + emit evaluationComplete(); +} + +GameEvaluation::~GameEvaluation() noexcept +{ + workers.clear(); + timer.stop(); +} + +GameEvaluationWorker::GameEvaluationWorker(int engineIndex, BoardX const & startPosition, BoardX const & currentPosition, + int msPerMove, int moveNumber) + : moveNumber{moveNumber} + , currentPosition{currentPosition} + , msPerMove{msPerMove} + , engine{EngineX::newEngine(engineIndex)} +{ + if (!engine) + { + throw std::runtime_error{"Failed to instantiate engine"}; + } + if (!engine->m_mapOptionValues.contains("Threads")) + { + throw std::runtime_error{"Could not set engine threads to 1."}; + } + engine->m_mapOptionValues["Threads"] = 1; + engine->setStartPos(startPosition); + connect(engine, &EngineX::activated, this, &GameEvaluationWorker::engineActivated); + connect(engine, &EngineX::analysisStarted, this, &GameEvaluationWorker::engineAnalysisStarted); + connect(engine, &EngineX::analysisUpdated, this, &GameEvaluationWorker::engineAnalysisUpdated); + engine->activate(); + running = true; +} + +GameEvaluationWorker::~GameEvaluationWorker() noexcept +{ + if (!engine) + { + return; + } + try + { + engine->deactivate(); + delete engine; + } + catch (...) + { + // I don't think we can do anything about this exception. + } +} + +void GameEvaluationWorker::engineActivated() +{ + EngineParameter parameters {msPerMove}; + engine->startAnalysis(currentPosition, 1, parameters, false, ""); +} + +void GameEvaluationWorker::engineAnalysisStarted() +{ + startTimestamp = QDateTime::currentDateTimeUtc(); +} + +void GameEvaluationWorker::engineAnalysisUpdated(Analysis const & analysis) +{ + if (analysis.bestMove()) + { + // When the engine reports a best move, no score is reported, so we skip it + return; + } + if (analysis.isMate()) + { + lastScore = static_cast(10); + if (std::signbit(analysis.score())) + lastScore *= -1; + // If it's black's turn and black is winning, analysis.score() returns a + // positive number in a "mating" condition. We need a score from white's + // perspective. + bool blacksTurn {static_cast(analysis.variation().size() % 2)}; + if (!blacksTurn) + lastScore *= -1; + } + else + { + lastScore = analysis.fscore(); + } +} + +double GameEvaluationWorker::getLastScore() const noexcept +{ + return lastScore; +} + +bool GameEvaluationWorker::isRunning() const noexcept +{ + return running; +} + +void GameEvaluationWorker::update() noexcept +{ + if (!startTimestamp) + { + // Maybe have a timeout and kill the engine if analysis still hasn't started? + return; + } + if (startTimestamp->msecsTo(QDateTime::currentDateTimeUtc()) < msPerMove) + return; + engine->deactivate(); + running = false; + // We'll let the destructor delete the engine since this is called during computation when + // processing time is more premium than later. +} diff --git a/src/database/gameevaluation.h b/src/database/gameevaluation.h new file mode 100644 index 00000000..eaae3849 --- /dev/null +++ b/src/database/gameevaluation.h @@ -0,0 +1,75 @@ +#ifndef GAME_EVALUATION_H_INCLUDED +#define GAME_EVALUATION_H_INCLUDED +#include +#include "board.h" +#include "gamex.h" +#include "enginex.h" + +/** @ingroup Core + The GameEvaluation class represents an algorith for evaluating the centipawn score at + every move in a game. +*/ + +class GameEvaluationWorker final : public QObject +{ + Q_OBJECT +public: + GameEvaluationWorker(int engineIndex, BoardX const & startPosition, BoardX const & currentPosition, + int msPerMove, int moveNumber); + // QObjects can't be copied or moved + GameEvaluationWorker & operator=(GameEvaluationWorker &&) = delete; + GameEvaluationWorker(GameEvaluationWorker&&) = delete; + ~GameEvaluationWorker() noexcept; + + double getLastScore() const noexcept; + bool isRunning() const noexcept; + void update() noexcept; + + int const moveNumber; + +private: + BoardX currentPosition; + int msPerMove; + EngineX * engine; + double lastScore{0}; + bool running{false}; + std::optional startTimestamp; + +private slots: + void engineActivated(); + void engineAnalysisStarted(); + void engineAnalysisUpdated(Analysis const &); +}; + +class GameEvaluation final : public QObject +{ + Q_OBJECT +public : + GameEvaluation(int engineIndex, int msPerMove, GameX) noexcept; + ~GameEvaluation() noexcept; + + // This function does not block, but uses a QT Timer to call the + // evaluationChanged signal periodically. + void start(); + void stop() noexcept; +signals: + void evaluationChanged(std::unordered_map const & blah); + void evaluationComplete(); + +private: + int const engineIndex; + int const msPerMove; + GameX game; + QTimer timer; + int targetThreadCount; + QString line; + int moveNumbers; + + bool running{false}; + std::list workers; + void update() noexcept; + +}; + +#endif // GAME_EVALUATION_H_INCLUDED + diff --git a/src/gui/centipawngraph.cpp b/src/gui/centipawngraph.cpp new file mode 100644 index 00000000..7aa84764 --- /dev/null +++ b/src/gui/centipawngraph.cpp @@ -0,0 +1,132 @@ +#include "centipawngraph.h" + +#include +#include +#include +#include +#include + +#include "chartwidget.h" +#include "gameevaluation.h" + +CentipawnGraph::CentipawnGraph(QWidget* parent) + : QWidget(parent) + , evaluation{nullptr} +{ + setObjectName("CentipawnGraph"); + QGridLayout * layout = new QGridLayout(this); + m_startAnalysis = new QPushButton{"Start Analysis"}; + m_startAnalysis->setDefault(true); + connect(m_startAnalysis, &QPushButton::clicked, this, &CentipawnGraph::analysisRequested); + m_engineList = new QComboBox; + setupEngineList(); + layout->addWidget(m_startAnalysis, /*row*/0, 0); + layout->addWidget(m_engineList, 0, 1); + m_chart = new ChartWidget(); + m_chart->setObjectName("ChartWidget"); + m_chart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->addWidget(m_chart, 1, 0, 1, 2); + m_chart->show(); + connect(m_chart, &ChartWidget::halfMoveRequested, this, &CentipawnGraph::requestPly); + adjustSize(); +} + +void CentipawnGraph::setupEngineList() noexcept +{ + m_engineList->clear(); + EngineList enginesList; + enginesList.restore(); + QStringList names = enginesList.names(); + m_engineList->setEditable(false); + if (!names.size()) + { + m_engineList->addItem("No Engines Configured"); + m_engineList->setEnabled(false); + m_startAnalysis->setEnabled(false); + } + else + { + m_engineList->addItems(names); + m_engineList->setEnabled(true); + m_startAnalysis->setEnabled(true); + m_engineList->setToolTip("Select a pre-configured engine"); + } +} + +void CentipawnGraph::slotDisplayCurrentPly(int ply) +{ + updateGeometry(); + m_chart->setPly(ply); + m_chart->show(); + m_chart->show(); +} + +void CentipawnGraph::slotDisplayMaterial(const QList& material) +{ + m_chart->setValues(0, material); +} + +void CentipawnGraph::slotDisplayEvaluations(const QList& evaluations) +{ + // If we don't have a computed evaluation, use this one retrieved from annotations. + m_chart->setValues(1, evaluations); +} + +void CentipawnGraph::analysisRequested(bool /*checked*/) noexcept +{ + // In order to start analysis, we need the current game which we don't have. + // We need to emit a signal that the main window can handle so that the + // main window can call back with the current game to get analysis started. + emit startAnalysisRequested(); +} + +void CentipawnGraph::startAnalysis(GameX const & game) noexcept +{ + try + { + if (evaluation) + { + return; + } + m_startAnalysis->setEnabled(false); + m_engineList->setEnabled(false); + evaluation = std::make_unique(m_engineList->currentIndex(), 1000, game); + connect(evaluation.get(), &GameEvaluation::evaluationChanged, this, &CentipawnGraph::evaluationChanged); + connect(evaluation.get(), &GameEvaluation::evaluationComplete, this, &CentipawnGraph::evaluationComplete); + currentGame = game; + scores.clear(); + scores.reserve(currentGame.cursor().countMoves()+1); + for (int i{0}; i < currentGame.cursor().countMoves()+1; ++i) + scores << 0; + evaluation->start(); + } + catch (...) + { + // Failed to start evaluation; swallowing error + } +} + +void CentipawnGraph::evaluationComplete() noexcept +{ + evaluation = nullptr; + m_startAnalysis->setEnabled(true); + m_engineList->setEnabled(true); +} + +void CentipawnGraph::evaluationChanged(std::unordered_map const & scoreUpdates) noexcept +{ + GameX tempGame{currentGame}; + tempGame.moveToStart(); + for (std::pair const & score : scoreUpdates) + { + int moveNumber = score.first; + if (scores.size() <= moveNumber) + { + continue; + } + double newScore {std::clamp(score.second, -10.0, 10.0)}; + scores.replace(moveNumber, newScore); + } + m_chart->setValues(1, scores); +} diff --git a/src/gui/centipawngraph.h b/src/gui/centipawngraph.h new file mode 100644 index 00000000..7da7fe69 --- /dev/null +++ b/src/gui/centipawngraph.h @@ -0,0 +1,47 @@ +#ifndef CENTIPAWNGRAPH_H +#define CENTIPAWNGRAPH_H + +#include +#include +#include "piece.h" +#include "gamex.h" +#include "gameevaluation.h" + +class ChartWidget; +class QPushButton; +class QComboBox; + +class CentipawnGraph final : public QWidget +{ + Q_OBJECT + +public: + CentipawnGraph(QWidget* parent = nullptr); + void startAnalysis(GameX const &) noexcept; + +signals: + void requestPly(int); + void startAnalysisRequested(); + +public slots: + void slotDisplayCurrentPly(int ply); + void slotDisplayMaterial(const QList& material); + void slotDisplayEvaluations(const QList& evaluations); + void evaluationChanged(std::unordered_map const &) noexcept; + void evaluationComplete() noexcept; + +private: + ChartWidget* m_chart {nullptr}; + QPushButton* m_startAnalysis {nullptr}; + QComboBox* m_engineList {nullptr}; + std::unique_ptr evaluation; + GameX currentGame; + QList scores; + + void setupEngineList() noexcept; + +private slots: + void analysisRequested(bool) noexcept; +}; + +#endif diff --git a/src/gui/chartwidget.cpp b/src/gui/chartwidget.cpp index 603720c3..1e8577bf 100644 --- a/src/gui/chartwidget.cpp +++ b/src/gui/chartwidget.cpp @@ -29,10 +29,6 @@ ChartWidget::ChartWidget(QWidget *parent) : setUpdatesEnabled(true); } -ChartWidget::~ChartWidget() -{ -} - void ChartWidget::setValues(int line, const QList& values) { if (line >= m_values.size()) diff --git a/src/gui/chartwidget.h b/src/gui/chartwidget.h index 23d6380b..2cec8031 100644 --- a/src/gui/chartwidget.h +++ b/src/gui/chartwidget.h @@ -15,7 +15,6 @@ class ChartWidget : public QWidget Q_OBJECT public: explicit ChartWidget(QWidget *parent = nullptr); - virtual ~ChartWidget(); void setValues(int line, const QList &values); void setPly(int ply); diff --git a/src/gui/gametoolbar.cpp b/src/gui/gametoolbar.cpp deleted file mode 100644 index a0150ef0..00000000 --- a/src/gui/gametoolbar.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "gametoolbar.h" - -#include - -#include "chartwidget.h" - -GameToolBar::GameToolBar(const QString& title, QWidget* parent) - : QToolBar(title, parent) - , m_clock1(nullptr) - , m_clock2(nullptr) - , m_chart(nullptr) -{ - setObjectName("GameToolBar"); - - m_clock1 = new QLCDNumber(7, this); - m_clock1->setSegmentStyle(QLCDNumber::Flat); - m_clock1->setObjectName("Clock0"); - m_clock1->display("1:00:00"); - addWidget(m_clock1); - - m_chart = new ChartWidget(this); - m_chart->setObjectName("ChartWidget"); - m_chart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - addWidget(m_chart); - - m_clock2 = new QLCDNumber(7, this); - m_clock2->setSegmentStyle(QLCDNumber::Flat); - m_clock2->setObjectName("Clock1"); - m_clock2->display("1:00:00"); - addWidget(m_clock2); - - connect(m_chart, &ChartWidget::halfMoveRequested, this, &GameToolBar::requestPly); -} - -void GameToolBar::slotDisplayCurrentPly(int ply) -{ - m_chart->setPly(ply); -} - -void GameToolBar::slotDisplayMaterial(const QList& material) -{ - m_chart->setValues(0, material); -} - -void GameToolBar::slotDisplayEvaluations(const QList& evaluations) -{ - m_chart->setValues(1, evaluations); -} - -void GameToolBar::slotDisplayTime(const QString& timeWhite, const QString &timeBlack) -{ - m_clock1->display(timeWhite); - m_clock2->display(timeBlack); -} - -void GameToolBar::slotDisplayTime(Color color, const QString& text) -{ - switch (color) - { - case White: - m_clock1->display(text); - break; - case Black: - m_clock2->display(text); - break; - default: - break; - } -} diff --git a/src/gui/gametoolbar.h b/src/gui/gametoolbar.h deleted file mode 100644 index 221772b3..00000000 --- a/src/gui/gametoolbar.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef GAMETOOLBAR_H -#define GAMETOOLBAR_H - -#include - -#include "piece.h" - -class QLCDNumber; -class ChartWidget; - -class GameToolBar : public QToolBar -{ - Q_OBJECT - -public: - GameToolBar(const QString& title, QWidget* parent = nullptr); - -signals: - void requestPly(int); - -public slots: - void slotDisplayCurrentPly(int ply); - void slotDisplayMaterial(const QList& material); - void slotDisplayEvaluations(const QList& evaluations); - void slotDisplayTime(const QString& timeWhite, const QString& timeBlack); - void slotDisplayTime(Color color, const QString& time); - -private: - QLCDNumber* m_clock1; - QLCDNumber* m_clock2; - ChartWidget* m_chart; -}; - -#endif diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 5416641e..2b923bb7 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -13,6 +13,7 @@ #include "annotationwidget.h" #include "boardview.h" #include "boardviewex.h" +#include "centipawngraph.h" #include "chessxsettings.h" #include "clipboarddatabase.h" #include "commentdialog.h" @@ -32,7 +33,6 @@ #include "gamelist.h" #include "GameMimeData.h" #include "gamewindow.h" -#include "gametoolbar.h" #include "gamenotationwidget.h" #include "helpbrowsershell.h" #include "historylabel.h" @@ -93,7 +93,7 @@ MainWindow::MainWindow() : QMainWindow(), m_tabDragIndex(-1), m_pDragTabBar(nullptr), m_gameWindow(nullptr), - m_gameToolBar(0), + m_centipawnGraph(nullptr), m_operationFlag(0), m_currentFrom(InvalidSquare), m_currentTo(InvalidSquare), @@ -189,19 +189,25 @@ MainWindow::MainWindow() : QMainWindow(), connect(this, SIGNAL(reconfigure()), m_ficsConsole, SLOT(slotReconfigure())); m_ficsConsole->setEnabled(false); + DockWidgetEx* centipawnDock = new DockWidgetEx(tr("Centipawn Loss Graph"), this); + centipawnDock->setObjectName("CentipawnDock"); + centipawnDock->toggleViewAction()->setShortcut(Qt::CTRL | Qt::Key_W); + m_menuView->addAction(centipawnDock->toggleViewAction()); + m_centipawnGraph = new CentipawnGraph(centipawnDock); + centipawnDock->adjustSize(); + connect(m_centipawnGraph, &CentipawnGraph::requestPly, this, &MainWindow::slotGameMoveToPly); + addDockWidget(Qt::RightDockWidgetArea, centipawnDock); + centipawnDock->setVisible(AppSettings->getValue("/MainWindow/CentipawnGraph").toBool()); + centipawnDock->setWidget(m_centipawnGraph); + connect(m_centipawnGraph, &CentipawnGraph::startAnalysisRequested, this, &MainWindow::slotStartAnalysisRequested); + /* Game view */ DockWidgetEx* gameTextDock = new DockWidgetEx(tr("Notation"), this); gameTextDock->setObjectName("GameTextDock"); m_gameWindow = new GameWindow(gameTextDock); connect(this, SIGNAL(reconfigure()), m_gameWindow, SLOT(slotReconfigure())); - m_gameToolBar = new GameToolBar(tr("Game Time"), m_gameWindow); - m_gameToolBar->setMovable(false); - m_gameWindow->addToolBar(Qt::BottomToolBarArea, m_gameToolBar); - connect(m_gameToolBar, &GameToolBar::requestPly, this, &MainWindow::slotGameMoveToPly); - m_menuView->addAction(m_gameToolBar->toggleViewAction()); - m_gameToolBar->setVisible(AppSettings->getValue("/MainWindow/GameToolBar").toBool()); m_gameView = m_gameWindow->browser(); connect(m_gameView, &GameNotationWidget::anchorClicked, this, &MainWindow::slotGameViewLinkUrl); connect(m_gameView, &GameNotationWidget::actionRequested, this, &MainWindow::slotGameModify); @@ -659,7 +665,7 @@ void MainWindow::closeEvent(QCloseEvent* e) AppSettings->setLayout(this); AppSettings->beginGroup("/MainWindow/"); AppSettings->setValue("BoardSplit", m_boardSplitter->saveState()); - AppSettings->setValue("GameToolBar", m_gameToolBar->isVisible()); + AppSettings->setValue("CentipawnGraph", m_centipawnGraph->isVisible()); AppSettings->endGroup(); } else @@ -1850,7 +1856,6 @@ void MainWindow::setupActions() QMenu *gameMenu = menuBar()->addMenu(tr("&Game")); gameToolBar = addToolBar(tr("Game")); gameToolBar->setObjectName("GameToolBarMain"); - dbToolBar = addToolBar(tr("Database")); dbToolBar->setObjectName("DbToolBarMain"); @@ -1897,10 +1902,8 @@ void MainWindow::setupActions() gameToolBar->addSeparator(); autoGroup = new ExclusiveActionGroup(this); - m_match = createAction(tr("Match"), SLOT(slotToggleGameMode()), Qt::CTRL | Qt::Key_M, gameToolBar, ":/images/black_chess.png"); m_match->setCheckable(true); - autoGroup->addAction(m_match); gameMenu->addAction(m_match); gameMenu->addSeparator(); diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index cf809138..e96a86b5 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -47,7 +47,7 @@ class FicsClient; class FicsConsole; class GameList; class GameNotationWidget; -class GameToolBar; +class CentipawnGraph; class GameWindow; class HistoryLabel; class OpeningTreeWidget; @@ -609,6 +609,8 @@ private slots: void slotVersionFound(int major, int minor, int build); void slotUpdateOpeningTreeWidget(); void slotDatabaseEditTag(); + void slotStartAnalysisRequested(); + private: /** Create single menu action. */ QAction* createAction(QString name, const char* slot, const QKeySequence& key = QKeySequence(), @@ -703,7 +705,7 @@ private slots: QLabel* m_sliderText; QPointer m_comboEngine; GameWindow* m_gameWindow; - GameToolBar* m_gameToolBar; + CentipawnGraph* m_centipawnGraph; QTabWidget* m_tabWidget; AnnotationWidget* annotationWidget; /* Status */ diff --git a/src/gui/mainwindowactions.cpp b/src/gui/mainwindowactions.cpp index 4da9e781..c56b047b 100644 --- a/src/gui/mainwindowactions.cpp +++ b/src/gui/mainwindowactions.cpp @@ -38,7 +38,7 @@ #include "gameid.h" #include "gamelist.h" #include "gamenotationwidget.h" -#include "gametoolbar.h" +#include "centipawngraph.h" #include "gamewindow.h" #include "GameMimeData.h" #include "historylabel.h" @@ -393,8 +393,8 @@ void MainWindow::UpdateMaterialWidget() { if(databaseInfo()) { - m_gameToolBar->slotDisplayMaterial(databaseInfo()->material()); - m_gameToolBar->slotDisplayEvaluations(databaseInfo()->evaluations()); + m_centipawnGraph->slotDisplayMaterial(databaseInfo()->material()); + m_centipawnGraph->slotDisplayEvaluations(databaseInfo()->evaluations()); } } @@ -1329,22 +1329,12 @@ void MainWindow::moveChanged() auto timeThis = g.timeAnnotation(m, GameX::BeforeMove); auto timeThat = g.timeAnnotation(m, GameX::AfterMove); - if (g.board().toMove() == White) - { - m_gameToolBar->slotDisplayTime(White, timeThis); - m_gameToolBar->slotDisplayTime(Black, timeThat); - } - else - { - m_gameToolBar->slotDisplayTime(Black, timeThis); - m_gameToolBar->slotDisplayTime(White, timeThat); - } // Highlight current move m_gameView->showMove(m); if (g.isMainline()) { - m_gameToolBar->slotDisplayCurrentPly(g.ply()); + m_centipawnGraph->slotDisplayCurrentPly(g.ply()); } displayVariations(); @@ -4449,3 +4439,10 @@ void MainWindow::delaySpeechTimeout() } #endif + +void MainWindow::slotStartAnalysisRequested() +{ + if (!m_centipawnGraph) + return; + m_centipawnGraph->startAnalysis(game()); +}