From 6b621a0f0d3f3909db9a57c68827f91401ce54a6 Mon Sep 17 00:00:00 2001 From: lmat Date: Thu, 1 Aug 2024 10:59:48 -0400 Subject: [PATCH] Re #225 Created dockable window for duplicate positions finder When preparing an opening repertoine, it is important to note duplicate positions reached through transposition. When such a transposition is noted, if there is more than one continuation, the opening book becomes conflicting and suboptimal. Even if the continuations after the duplicated position match, the opening book is in danger of conflicting with itself. The added Duplicate Positions finder makes it very easy to see at a glance any such duplicated positions and whether they potentially conflict or not. Clicking on a move list for a duplicated position opens that position on the main board. --- chessx.pro | 2 + src/CMakeLists.txt | 2 + src/database/gamex.cpp | 136 +++++++++ src/database/gamex.h | 25 ++ src/gui/duplicatepositionswidget.cpp | 418 +++++++++++++++++++++++++++ src/gui/duplicatepositionswidget.h | 90 ++++++ src/gui/mainwindow.cpp | 8 + src/gui/mainwindow.h | 2 + src/gui/mainwindowactions.cpp | 5 + 9 files changed, 688 insertions(+) create mode 100644 src/gui/duplicatepositionswidget.cpp create mode 100644 src/gui/duplicatepositionswidget.h diff --git a/chessx.pro b/chessx.pro index a571311d..e09cead1 100644 --- a/chessx.pro +++ b/chessx.pro @@ -296,6 +296,7 @@ HEADERS += src/database/board.h \ src/gui/databaselistmodel.h \ src/gui/digitalclock.h \ src/gui/dockwidgetex.h \ + src/gui/duplicatepositionswidget.h \ src/gui/ecolistwidget.h \ src/gui/ecothread.h \ src/gui/engineoptiondialog.h \ @@ -466,6 +467,7 @@ SOURCES += \ src/gui/databaselistmodel.cpp \ src/gui/digitalclock.cpp \ src/gui/dockwidgetex.cpp \ + src/gui/duplicatepositionswidget.cpp \ src/gui/ecolistwidget.cpp \ src/gui/engineoptiondialog.cpp \ src/gui/engineoptionlist.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86b8a7d0..3e3bfbd2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -355,6 +355,8 @@ add_library(gui STATIC gui/digitalclock.h gui/dockwidgetex.cpp gui/dockwidgetex.h + gui/duplicatepositionswidget.cpp + gui/duplicatepositionswidget.h gui/ecolistwidget.cpp gui/ecolistwidget.h gui/ecothread.h diff --git a/src/database/gamex.cpp b/src/database/gamex.cpp index 2550e3fa..898171f5 100644 --- a/src/database/gamex.cpp +++ b/src/database/gamex.cpp @@ -1826,3 +1826,139 @@ int GameX::isBetterOrEqual(const GameX& game) const (m_annotations.count() >= game.m_annotations.count()) && (m_variationStartAnnotations.count() >= game.m_variationStartAnnotations.count())); } + +// Given a game at a certain position, return all the moves that led to that position +// starting with the first move. +static DuplicateMoveList getMoves(GameX const & game) noexcept +{ + DuplicateMoveList ret; + ret.moves.reserve(game.plyCount()); + MoveId currentSpot {game.currentMove()}; + // Save the tip of this move list so that it can be linked in the frontend + ret.lastMove = currentSpot; + // Walk back toward the root node recording the moves on the way + while (currentSpot != NO_MOVE && currentSpot != ROOT_NODE) + { + ret.moves.push_back(game.move(currentSpot)); + currentSpot = game.cursor().prevMove(currentSpot); + } + std::reverse(ret.moves.begin(), ret.moves.end()); + return ret; +} + +// Traverses all positions of all variations of the game recording the position (FEN) and the +// move list that resulted in that position. +// positions is a map from a position to the various sets of moves that resulted in that position +static void getPositions(GameX & game, std::unordered_map & positions) noexcept +{ + if (game.nextMove() == NO_MOVE) + { + return; + } + game.forward(); + DuplicateMoveList moves {getMoves(game)}; + // If two positions are transpositionally equivalent but one of them ended with + // a double-advance of a pawn, they will be considered different because there + // may be an ability to capture en passant (even if there is no pawn that could + // capture en passant). + QString fenForBoard {game.board().toFen(true)}; + // We don't care about the number of moves to reach this position. Id est, + // if the two positions were reachable in a different amount of moves, + // everything else being equal, it doesn't matter, they're still duplicates. + // The location of the space before the halfmove clock + qsizetype const ultimateSpace{fenForBoard.lastIndexOf(' ')}; + if (ultimateSpace < 1) + { + // Unable to find the halfmove clock + return; + } + // The location of the space before the fullmove number + qsizetype const penultimateSpace{fenForBoard.lastIndexOf(' ', ultimateSpace-1)}; + if (penultimateSpace < 1) + { + // Unable to find the fullmove number + return; + } + fenForBoard = fenForBoard.first(penultimateSpace); + // This will create an item in the positions map if it doesn't exist, or add to the + // set of move lists that resulted in that position. + positions[fenForBoard].moveLists.push_back(moves); + // We only get subsequent positions if this position isn't a duplicate. + // If this position is a duplicated position, then all the subsequent positions + // will also be duplicates. We short-circuit that duplicating of duplicates with this + // if statement. + if (positions[fenForBoard].moveLists.size() < 2) + { + getPositions(game, positions); + } + // Next we go back one step in the game to put it back where we found it + game.backward(); + if (positions[fenForBoard].moveLists.size() > 1) + { + // We don't want subsequent duplicated positions, even if they're in variations + return; + } + if (!game.variationCount()) + { + return; + } + for (MoveId const & variation_move : game.variations()) + { + game.enterVariation(variation_move); + getPositions(game, positions); + game.backward(); + } +} + +// Duplicate positions are divided into classes: +// 1. At most one move list that leads to a position continues +// 2. Both move lists that lead to a position continue past the duplicated position +static void addWarnings(GameX & game, std::vector & positions) noexcept +{ + for (DuplicatedPosition & position : positions) + { + unsigned continuations{0}; + for (DuplicateMoveList & moves : position.moveLists) + { + game.moveToStart(); + // Get the game to the end of this move list + for (Move const & move : moves.moves) + { + if (!game.findNextMove(move)) + { + // This is an (fatal?) internal error. + break; + } + } + // The game is at the end of the move list. Is there a continuation? + if (game.cursor().nextMove() != NO_MOVE) + { + ++continuations; + } + } + position.warning = (continuations > 1) ? DuplicatedPosition::BothMove : DuplicatedPosition::None; + } +} + +std::vector GameX::getDuplicatePositions() const noexcept +{ + std::unordered_map positions; + GameX copy {*this}; + copy.moveToStart(); + if (copy.nextMove() == NO_MOVE) + { + return {}; + } + getPositions(copy, positions); + std::vector duplicates; + for (auto it{positions.begin()}; it != positions.end(); ++it) + { + if (it->second.moveLists.size() > 1) + { + duplicates.emplace_back(std::move(it->second)); + duplicates.back().fen = it->first; + } + } + addWarnings(copy, duplicates); + return duplicates; +} diff --git a/src/database/gamex.h b/src/database/gamex.h index 051a4c50..05837ce7 100644 --- a/src/database/gamex.h +++ b/src/database/gamex.h @@ -48,6 +48,28 @@ class SaveRestoreMove; typedef QHash TagMap; typedef QHashIterator TagMapIterator; +class DuplicateMoveList +{ +public: + std::vector moves; + MoveId lastMove; +}; + +class DuplicatedPosition final +{ +public: + enum WarningLevel + { + // At most only one variation has moves after it + None, + // More than one variation has moves after + BothMove, + }; + std::vector moveLists; + WarningLevel warning; + QString fen; +}; + class GameX : public QObject { Q_OBJECT @@ -155,6 +177,9 @@ public : bool editAnnotation(QString annotation, MoveId moveId = CURRENT_MOVE, Position position = AfterMove); /** Append to existing annotations associated with move at node @p moveId */ bool appendAnnotation(QString annotation, MoveId moveId = CURRENT_MOVE, Position position = AfterMove); + /** Finds duplicate positions within a game. + * Returns a set of positions that have more than one move list leading to that position. */ + std::vector getDuplicatePositions() const noexcept; /** Append a square to the existing lists of square annotations, if there is none, create one */ bool appendSquareAnnotation(chessx::Square s, QChar colorCode); diff --git a/src/gui/duplicatepositionswidget.cpp b/src/gui/duplicatepositionswidget.cpp new file mode 100644 index 00000000..e16ffa06 --- /dev/null +++ b/src/gui/duplicatepositionswidget.cpp @@ -0,0 +1,418 @@ +#include "duplicatepositionswidget.h" +#include +#include +#include +#include "settings.h" + +QBrush const darkModeWarningBackground {Qt::darkRed}; +// A light red +QBrush const lightModeWarningBackground {QColor{0xff, 0xb6, 0xc1}}; + +DuplicatePositionItem::DuplicatePositionItem(QString const & datum, DuplicatedPosition::WarningLevel warningLevel, + DuplicatePositionItem * parent) + : warning{warningLevel}, m_itemDatum{datum}, m_parentItem{parent}, m_move{CURRENT_MOVE} +{ + if (m_parentItem) + { + m_parentItem->appendChild(this); + } +} + +DuplicatePositionItem::DuplicatePositionItem(QString const & datum, MoveId move, DuplicatePositionItem * parent) + : warning{DuplicatedPosition::WarningLevel::None}, m_itemDatum{datum}, m_parentItem{parent}, m_move{move} +{ + if (m_parentItem) + { + m_parentItem->appendChild(this); + } +} + +DuplicatePositionItem::~DuplicatePositionItem() +{ + deleteChildren(); +} + +MoveId DuplicatePositionItem::move() const +{ + return m_move; +} + +void DuplicatePositionItem::appendChild(DuplicatePositionItem * item) +{ + m_childItems.append(item); +} + +DuplicatePositionItem * DuplicatePositionItem::child(int row) +{ + if (row < 0 || row >= m_childItems.size()) + { + return nullptr; + } + return m_childItems.at(row); +} + +int DuplicatePositionItem::childCount() const +{ + return m_childItems.count(); +} + +int DuplicatePositionItem::row() const +{ + if (m_parentItem) + { + return m_parentItem->m_childItems.indexOf(const_cast(this)); + } + return 0; +} + +QString DuplicatePositionItem::datum() const +{ + return m_itemDatum; +} + +DuplicatePositionItem * DuplicatePositionItem::parentItem() +{ + return m_parentItem; +} + +void DuplicatePositionItem::deleteChildren() noexcept +{ + qDeleteAll(m_childItems); + m_childItems.clear(); +} + +DuplicatePositionModel::DuplicatePositionModel(QObject * parent) + : QAbstractItemModel(parent) +{ + rootItem = new DuplicatePositionItem("Duplicate Positions", ROOT_NODE); +} + +DuplicatePositionModel::~DuplicatePositionModel() +{ + delete rootItem; +} + +QModelIndex DuplicatePositionModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + { + return {}; + } + DuplicatePositionItem * parentItem {nullptr}; + if (!parent.isValid()) + { + parentItem = rootItem; + } + else + { + parentItem = static_cast(parent.internalPointer()); + } + DuplicatePositionItem * childItem {parentItem->child(row)}; + if (!childItem) + { + return {}; + } + return createIndex(row, column, childItem); +} + +QModelIndex DuplicatePositionModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + { + return {}; + } + DuplicatePositionItem * childItem = static_cast(index.internalPointer()); + DuplicatePositionItem * parentItem = childItem->parentItem(); + if (parentItem == rootItem) + { + return {}; + } + return createIndex(parentItem->row(), 0, parentItem); +} + +int DuplicatePositionModel::rowCount(const QModelIndex &parentIndex) const +{ + DuplicatePositionItem * parentItem; + if (parentIndex.column() > 0) + { + return 0; + } + if (!parentIndex.isValid()) + { + parentItem = rootItem; + } + else + { + parentItem = static_cast(parentIndex.internalPointer()); + } + return parentItem->childCount(); +} + +int DuplicatePositionModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +QVariant DuplicatePositionModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.column() != 0) + { + return {}; + } + if (role != Qt::DisplayRole && role != Qt::BackgroundRole) + { + return {}; + } + DuplicatePositionItem * item = static_cast(index.internalPointer()); + if (role == Qt::DisplayRole) + { + return item->datum(); + } + // Return a background setting + if (item->warning == DuplicatedPosition::WarningLevel::None) + { + return {}; + } + if (AppSettings->getValue("/MainWindow/DarkTheme").toBool()) + { + return darkModeWarningBackground; + } + else + { + return lightModeWarningBackground; + } +} + +std::optional DuplicatePositionModel::getLink(const QModelIndex &index) const +{ + if (!index.isValid() || index.column() != 0) + { + return {}; + } + DuplicatePositionItem * item {static_cast(index.internalPointer())}; + return item->move(); +} + +Qt::ItemFlags DuplicatePositionModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + { + return Qt::NoItemFlags; + } + return QAbstractItemModel::flags(index); +} + +QVariant DuplicatePositionModel::headerData(int /*section*/, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) + { + return {}; + } + return rootItem->datum(); +} + +void DuplicatePositionModel::setData(std::vector const & duplicates, GameX const & rawGame) noexcept +{ + beginResetModel(); + if (!rootItem) + { + // A terrible internal error + return; + } + rootItem->deleteChildren(); + GameX game {rawGame}; + QVector parents; + for (DuplicatedPosition const & dupe : duplicates) + { + // This item repsents one position that has duplicates + DuplicatePositionItem * duplicatePosition{new DuplicatePositionItem{dupe.fen, dupe.warning, rootItem}}; + for (DuplicateMoveList const & ml : dupe.moveLists) + { + game.moveToStart(); + QString list; + for (unsigned i{0}; i < ml.moves.size(); ++i) + { + if (!game.findNextMove(ml.moves[i])) + { + list.append("Could not find moves for line"); + break; + } + list.append(game.moveToSan(GameX::MoveStringFlags::MoveOnly, GameX::NextPreviousMove::PreviousMove)); + if (i+1 < ml.moves.size()) + { + list.append(", "); + } + } + new DuplicatePositionItem{list, ml.lastMove, duplicatePosition}; + } + } + endResetModel(); +} + +DuplicatePositionsWidget::DuplicatePositionsWidget(QWidget* parent) + : QWidget{parent} + , m_requestFindDuplicates{nullptr} + , m_expandAll{nullptr} + , m_treeView{nullptr} + , m_treeModel{nullptr} +{ + setObjectName("duplicate positions widget"); + m_requestFindDuplicates = new QPushButton{"Find Duplicate Positions"}; + m_requestFindDuplicates->setObjectName("request duplicates"); + connect(m_requestFindDuplicates, &QPushButton::clicked, this, &DuplicatePositionsWidget::findDuplicatesRequested); + m_expandAll = new QPushButton{"Expand All"}; + m_expandAll->setObjectName("duplicates expand all"); + connect(m_expandAll, &QPushButton::clicked, this, &DuplicatePositionsWidget::toggleExpandAll); + m_treeView = new QTreeView; + m_treeView->setObjectName("duplicate positions tree view"); + m_treeModel = new DuplicatePositionModel; + m_treeView->setHeaderHidden(true); + m_treeView->setModel(m_treeModel); + connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &DuplicatePositionsWidget::selectionChanged); + connect(m_treeView, &QTreeView::expanded, this, &DuplicatePositionsWidget::treeViewExpanded); + connect(m_treeView, &QTreeView::collapsed, this, &DuplicatePositionsWidget::treeViewExpanded); + QGridLayout * layout {new QGridLayout{this}}; + m_requestFindDuplicates->setDefault(true); + layout->addWidget(m_requestFindDuplicates, /*row*/0, 0); + layout->addWidget(m_expandAll, 0, 1); + layout->addWidget(m_treeView, 1, 0, /*rowSpan*/1, /*columnSpan*/2); +} + +DuplicatePositionsWidget::~DuplicatePositionsWidget() noexcept +{ + delete m_treeModel; +} + +void DuplicatePositionsWidget::findDuplicatesRequested() +{ + if (m_treeModel) + { + m_treeModel->setData(game.getDuplicatePositions(), game); + } +} + +void DuplicatePositionsWidget::toggleExpandAll() +{ + if (!m_expandAll || !m_treeView) + { + return; + } + if (m_expandAll->text().contains("Expand")) + { + m_treeView->expandAll(); + m_expandAll->setText("Collapse All"); + } + else + { + m_treeView->collapseAll(); + m_expandAll->setText("Expand All"); + } +} + +void DuplicatePositionsWidget::gameChanged(GameX const & game) +{ + // Save a copy of the game + this->game = game; + if (!m_treeModel) + { + return; + } + // Clear the duplicate positions because they haven't been calculated for this game yet + m_treeModel->setData({}, game); +} + +void DuplicatePositionsWidget::selectionChanged(QItemSelection const & selected, QItemSelection const & /*deselected*/) +{ + if (!m_treeModel) + { + return; + } + QModelIndexList indexes {selected.indexes()}; + for (QModelIndex const & index : indexes) + { + std::optional maybeMove {m_treeModel->getLink(index)}; + if (!maybeMove) + { + continue; + } + MoveId move {*maybeMove}; + if (move < 1) + { + // Next move, previous move, etc. don't have any meaning here + continue; + } + linkClicked("move:" + QString::number(move)); + break; + } +} + +// Returns the depth of the given index +// Pass index by copy so we can change it +static unsigned getDepth(QModelIndex index) noexcept +{ + unsigned depth{0}; + while (index.isValid()) + { + ++depth; + index = index.parent(); + } + return depth; +} + +// Returns the next index in the model that has the same depth as the given index +static QModelIndex getNextSibling(QModelIndex const & start, QAbstractItemModel const * itemModel) +{ + int currentRow = start.row(); + unsigned const startDepth = getDepth(start); + QModelIndex currentIndex {start}; + while (true) + { + ++currentRow; + currentIndex = itemModel->index(currentRow, 0); + if (!currentIndex.isValid()) + { + // We've gone through all the rows of the model, return an invalid index + return currentIndex; + } + unsigned const currentDepth{getDepth(currentIndex)}; + if (currentDepth != startDepth) + { + // This index is perhaps a child index, not a sibling + continue; + } + return currentIndex; + } +} + +// If the tree view has all its items expanded, change the button text to +// "Collapse All". +void DuplicatePositionsWidget::treeViewExpanded() +{ + if (!m_treeModel || !m_treeModel->rowCount() || !m_treeView) + { + return; + } + QModelIndex currentItem = m_treeModel->index(0,0); + bool allExpanded = true; + bool allCollapsed = true; + while (currentItem.isValid()) + { + if (m_treeView->isExpanded(currentItem)) + { + allCollapsed = false; + } + else + { + allExpanded = false; + } + currentItem = getNextSibling(currentItem, m_treeModel); + } + if (allExpanded) + { + m_expandAll->setText("Collapse All"); + } + else if(allCollapsed) + { + m_expandAll->setText("Expand All"); + } +} diff --git a/src/gui/duplicatepositionswidget.h b/src/gui/duplicatepositionswidget.h new file mode 100644 index 00000000..99930547 --- /dev/null +++ b/src/gui/duplicatepositionswidget.h @@ -0,0 +1,90 @@ +#ifndef DUPLICATEPOSITIONSWIDGET_H +#define DUPLICATEPOSITIONSWIDGET_H + +#include +#include "gamex.h" + +class QPushButton; +class QTreeView; + +// Represents either a duplicated position or its child node: a +// set of moves that led to a duplicated position +class DuplicatePositionItem +{ +public: + DuplicatePositionItem(QString const &, MoveId = CURRENT_MOVE, DuplicatePositionItem * parent = nullptr); + DuplicatePositionItem(QString const &, DuplicatedPosition::WarningLevel = DuplicatedPosition::WarningLevel::None, + DuplicatePositionItem * parent = nullptr); + ~DuplicatePositionItem(); + + void appendChild(DuplicatePositionItem *child); + DuplicatePositionItem *child(int row); + int childCount() const; + QString datum() const; + MoveId move() const; + int row() const; + DuplicatePositionItem *parentItem(); + DuplicatedPosition::WarningLevel const warning; + void deleteChildren() noexcept; + +private: + QVector m_childItems; + QString m_itemDatum; + DuplicatePositionItem *m_parentItem; + // Used to link to the list of moves that led to a duplicate position + MoveId m_move; +}; + +class DuplicatePositionModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit DuplicatePositionModel(QObject *parent = nullptr); + ~DuplicatePositionModel(); + + // A GameX is necessary to be able to convert to standard algebraic notation. + void setData(std::vector const &, GameX const &)noexcept; + + QVariant data(QModelIndex const &index, int role) const override; + Qt::ItemFlags flags(QModelIndex const &index) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, QModelIndex const &parent = {}) const override; + QModelIndex parent(QModelIndex const &index) const override; + int rowCount(QModelIndex const &parent = {}) const override; + int columnCount(QModelIndex const &parent = {}) const override; + std::optional getLink(QModelIndex const &) const; + +private: + DuplicatePositionItem *rootItem; +}; + +class DuplicatePositionsWidget final : public QWidget +{ + Q_OBJECT + +public: + DuplicatePositionsWidget(QWidget* parent = nullptr); + ~DuplicatePositionsWidget() noexcept; + + void gameChanged(GameX const &); + +signals: + void linkClicked(QString const &); + +private slots: + void findDuplicatesRequested(); + void toggleExpandAll(); + void treeViewExpanded(); + +private: + QPushButton * m_requestFindDuplicates; + QPushButton * m_expandAll; + QTreeView * m_treeView; + DuplicatePositionModel * m_treeModel; + GameX game; + + void selectionChanged(QItemSelection const &, QItemSelection const &); +}; + +#endif // DUPLICATEPOSITIONSWIDGET_H diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 5416641e..b55c2fef 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -21,6 +21,7 @@ #include "databaselistmodel.h" #include "dockwidgetex.h" #include "downloadmanager.h" +#include "duplicatepositionswidget.h" #include "ecolistwidget.h" #include "ecothread.h" #include "eventlistwidget.h" @@ -93,6 +94,7 @@ MainWindow::MainWindow() : QMainWindow(), m_tabDragIndex(-1), m_pDragTabBar(nullptr), m_gameWindow(nullptr), + m_duplicatePositionsWidget(nullptr), m_gameToolBar(0), m_operationFlag(0), m_currentFrom(InvalidSquare), @@ -213,6 +215,12 @@ MainWindow::MainWindow() : QMainWindow(), addDockWidget(Qt::RightDockWidgetArea, gameTextDock); connect(m_gameWindow, SIGNAL(linkActivated(QString)), this, SLOT(slotGameViewLink(QString))); + DockWidgetEx* duplicatePositionsDock = new DockWidgetEx("Duplicate Game Positions", this); + duplicatePositionsDock->setObjectName("Duplicate Positions Dock"); + m_duplicatePositionsWidget = new DuplicatePositionsWidget{duplicatePositionsDock}; + duplicatePositionsDock->setWidget(m_duplicatePositionsWidget); + connect(m_duplicatePositionsWidget, &DuplicatePositionsWidget::linkClicked, this, &MainWindow::slotGameViewLink); + m_menuView->addAction(gameTextDock->toggleViewAction()); gameTextDock->toggleViewAction()->setShortcut(Qt::CTRL | Qt::Key_E); diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index cf809138..9c82c40b 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -49,6 +49,7 @@ class GameList; class GameNotationWidget; class GameToolBar; class GameWindow; +class DuplicatePositionsWidget; class HistoryLabel; class OpeningTreeWidget; class PlayerListWidget; @@ -703,6 +704,7 @@ private slots: QLabel* m_sliderText; QPointer m_comboEngine; GameWindow* m_gameWindow; + DuplicatePositionsWidget* m_duplicatePositionsWidget; GameToolBar* m_gameToolBar; QTabWidget* m_tabWidget; AnnotationWidget* annotationWidget; diff --git a/src/gui/mainwindowactions.cpp b/src/gui/mainwindowactions.cpp index 4da9e781..4f2355cc 100644 --- a/src/gui/mainwindowactions.cpp +++ b/src/gui/mainwindowactions.cpp @@ -26,6 +26,7 @@ #include "databasetagdialog.h" #include "dlgsavebook.h" #include "downloadmanager.h" +#include "duplicatepositionswidget.h" #include "duplicatesearch.h" #include "ecolistwidget.h" #include "editaction.h" @@ -1914,6 +1915,10 @@ void MainWindow::slotGameChanged(bool /*bModified*/) UpdateGameText(); UpdateGameTitle(); moveChanged(); + if (m_duplicatePositionsWidget) + { + m_duplicatePositionsWidget->gameChanged(game()); + } } void MainWindow::slotGameViewLinkUrl(const QUrl& url)