aboutsummaryrefslogtreecommitdiff
path: root/src/game_tab
diff options
context:
space:
mode:
authorLoic Guegan <manzerbredes@mailbox.org>2022-02-23 18:11:55 +0100
committerLoic Guegan <manzerbredes@mailbox.org>2022-02-23 18:11:55 +0100
commitce941c146aea7925bded6b9d2a0d0559d3156ad3 (patch)
tree4c52e02600e3fd127bfb28b3e974d45541ec9e4e /src/game_tab
Create repository
Diffstat (limited to 'src/game_tab')
-rw-r--r--src/game_tab/Game.cpp143
-rw-r--r--src/game_tab/Game.hpp38
-rw-r--r--src/game_tab/GameTab.cpp52
-rw-r--r--src/game_tab/GameTab.hpp28
-rw-r--r--src/game_tab/HalfMove.cpp213
-rw-r--r--src/game_tab/HalfMove.hpp57
-rw-r--r--src/game_tab/board/BoardCanvas.cpp294
-rw-r--r--src/game_tab/board/BoardCanvas.hpp68
-rw-r--r--src/game_tab/board/BoardPanel.cpp96
-rw-r--r--src/game_tab/board/BoardPanel.hpp30
-rw-r--r--src/game_tab/board/Theme.cpp184
-rw-r--r--src/game_tab/board/Theme.hpp37
-rw-r--r--src/game_tab/editor/EditorCanvas.cpp186
-rw-r--r--src/game_tab/editor/EditorCanvas.hpp33
-rw-r--r--src/game_tab/editor/EditorPanel.cpp207
-rw-r--r--src/game_tab/editor/EditorPanel.hpp44
16 files changed, 1710 insertions, 0 deletions
diff --git a/src/game_tab/Game.cpp b/src/game_tab/Game.cpp
new file mode 100644
index 0000000..ad712bb
--- /dev/null
+++ b/src/game_tab/Game.cpp
@@ -0,0 +1,143 @@
+#include "Game.hpp"
+
+Game::Game() : current(NULL), moves(NULL) {
+ tags["White"] = "";
+ tags["Black"] = "";
+ initial_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
+ board = "rnbqkbnrpppppppp PPPPPPPPRNBQKBNR";
+}
+
+Game::Game(std::string fen) : current(NULL), moves(NULL) {
+ tags["White"] = "";
+ tags["Black"] = "";
+ tags["FEN"] = fen;
+ initial_fen = fen;
+ board = chessarbiter::FENParser::Parse(fen).board;
+}
+
+Game::Game(HalfMove *m) {
+ moves = m;
+ current = m;
+ initial_fen = m->GetFen();
+ board = chessarbiter::FENParser::Parse(initial_fen).board;
+}
+
+std::string Game::GetBoard() { return (board); }
+
+std::string Game::GetTag(std::string tagname) { return (tags[tagname]); }
+
+void Game::SetTag(std::string tagname, std::string value) {
+ tags[tagname] = value;
+}
+bool Game::IsBlackToPlay() {
+ if (current == NULL) {
+ return (false);
+ }
+ return (!current->IsBlack);
+}
+
+void Game::DeleteTag(std::string tagname) { tags.erase(tagname); }
+
+void Game::DeleteMove(HalfMove *m) {
+ if (moves == m) {
+ current = NULL;
+ moves = NULL;
+ delete m;
+ } else {
+ if (m != NULL) {
+ current = m->GetParent();
+ if (current != NULL) {
+ current->RemoveChild(m);
+ }
+ delete m;
+ }
+ }
+}
+
+HalfMove *Game::GetCurrentMove() { return (current); }
+HalfMove *Game::GetMoves() { return (moves); }
+
+void Game::PromoteMove(HalfMove *m) {
+ if (m != NULL) {
+ current = m;
+ m->Promote();
+ }
+}
+
+void Game::SetMoveAsMainline(HalfMove *m) {
+ if (m != NULL) {
+ current = m;
+ m->SetAsMainline();
+ }
+}
+
+bool Game::Play(std::string move) {
+ wxLogDebug("Playing move %s", move);
+ std::string fen = GetFen();
+ arbiter.Setup(fen);
+ if (arbiter.Play(move)) {
+ HalfMove *m = new HalfMove(arbiter.GetSAN(), arbiter.GetFEN());
+ char capture = arbiter.GetCapture();
+ if (capture != ' ') {
+ wxLogDebug("%c", capture);
+ m->SetCapture(capture);
+ }
+ if (current != NULL) {
+ current->AddMove(m);
+ } else if (moves != NULL) {
+ moves->AddVariation(m);
+ }
+ current = m;
+ if (moves == NULL) {
+ moves = m;
+ }
+ wxLogDebug("%s",GetPGN());
+ return (true);
+ }
+ return (false);
+}
+
+void Game::Previous() {
+ if (current != NULL) {
+ current = current->GetParent();
+ }
+}
+
+std::vector<std::string> Game::ListTags() {
+ std::vector<std::string> keys;
+ for (auto const &element : tags) {
+ keys.push_back(element.first);
+ }
+ return (keys);
+}
+
+void Game::Next() {
+ if (current != NULL) {
+ HalfMove *m = current->GetMainline();
+ if (m != NULL) {
+ current = m;
+ }
+ } else {
+ current = moves;
+ }
+}
+
+void Game::SetCurrent(HalfMove *m) { current = m; }
+
+std::string Game::GetFen() {
+ if (current == NULL) {
+ return (initial_fen);
+ }
+ return (current->GetFen());
+}
+
+std::string Game::GetPGN() {
+ std::string pgn;
+ if (moves != NULL) {
+ for (auto const &element : tags) {
+ pgn += '[' + element.first + " \"" + element.second + "\"]\n";
+ }
+ pgn += moves->GetPGN();
+ }
+ return (pgn);
+} \ No newline at end of file
diff --git a/src/game_tab/Game.hpp b/src/game_tab/Game.hpp
new file mode 100644
index 0000000..790d18c
--- /dev/null
+++ b/src/game_tab/Game.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "ChessArbiter.hpp"
+#include "HalfMove.hpp"
+#include "ochess.hpp"
+#include <unordered_map>
+
+class Game {
+ std::string board;
+ std::string initial_fen;
+ std::unordered_map<std::string, std::string> tags;
+ HalfMove *moves;
+ HalfMove *current;
+ chessarbiter::ChessArbiter arbiter;
+
+public:
+ Game();
+ Game(std::string fen);
+ Game(HalfMove *m);
+
+ std::string GetBoard();
+ std::string GetTag(std::string tagname);
+ void SetTag(std::string tagname, std::string value);
+ void DeleteTag(std::string tagname);
+ HalfMove *GetCurrentMove();
+ HalfMove *GetMoves();
+ std::string GetFen();
+ bool Play(std::string move);
+ bool IsBlackToPlay();
+ void Previous();
+ void Next();
+ void DeleteMove(HalfMove *m);
+ void PromoteMove(HalfMove *m);
+ void SetMoveAsMainline(HalfMove *m);
+ void SetCurrent(HalfMove *m);
+ std::vector<std::string> ListTags();
+ std::string GetPGN();
+}; \ No newline at end of file
diff --git a/src/game_tab/GameTab.cpp b/src/game_tab/GameTab.cpp
new file mode 100644
index 0000000..3e32b4e
--- /dev/null
+++ b/src/game_tab/GameTab.cpp
@@ -0,0 +1,52 @@
+#include "GameTab.hpp"
+#include <wx/clipbrd.h>
+
+wxDEFINE_EVENT(GAME_CHANGE, wxCommandEvent);
+
+GameTab::GameTab(wxFrame *parent, Game *game)
+ : wxPanel(parent), game(game), TabInfos(TabInfos::GAME) {
+ // Splitter
+ wxSplitterWindow *splitter = new wxSplitterWindow(this, wxID_ANY);
+ splitter->SetMinimumPaneSize(100);
+
+ // Panels
+ board_panel = new BoardPanel((wxFrame *)splitter, game);
+ editor_panel = new EditorPanel((wxFrame *)splitter, game);
+ splitter->SplitVertically(board_panel, editor_panel);
+
+ // Setup splitter
+ wxBoxSizer *topSizer = new wxBoxSizer(wxHORIZONTAL);
+ topSizer->Add(splitter, 1, wxEXPAND);
+ SetSizerAndFit(topSizer);
+
+ // Refresh panels
+ wxCommandEvent event(REFRESH_TAB_TITLE, GetId());
+ event.SetEventObject(this);
+ OnRefreshTabTitle(event);
+ board_panel->Notify();
+ editor_panel->Notify();
+
+ Bind(REFRESH_TAB_TITLE, &GameTab::OnRefreshTabTitle, this, wxID_ANY);
+ Bind(GAME_CHANGE, &GameTab::OnGameChange, this, wxID_ANY);
+}
+
+void GameTab::OnGameChange(wxCommandEvent &event) {
+ board_panel->Notify();
+ editor_panel->Notify();
+}
+
+void GameTab::OnRefreshTabTitle(wxCommandEvent &event) {
+ std::string white = game->GetTag("White");
+ std::string black = game->GetTag("Black");
+ if (white.size() == 0 && black.size() == 0) {
+ SetLabel("New Game");
+ } else {
+ SetLabel(white + "-" + black);
+ }
+ event.SetEventObject(this);
+ event.Skip();
+}
+
+void GameTab::ApplyPreferences() {
+ board_panel->ApplyPreferences();
+}
diff --git a/src/game_tab/GameTab.hpp b/src/game_tab/GameTab.hpp
new file mode 100644
index 0000000..7686f12
--- /dev/null
+++ b/src/game_tab/GameTab.hpp
@@ -0,0 +1,28 @@
+#pragma once
+#include "ChessArbiter.hpp"
+#include "Game.hpp"
+#include "HalfMove.hpp"
+#include "board/BoardPanel.hpp"
+#include "editor/EditorPanel.hpp"
+#include "ochess.hpp"
+#include <utility>
+#include <wx/collpane.h>
+#include <wx/splitter.h>
+#include <wx/textctrl.h>
+
+wxDECLARE_EVENT(REFRESH_TAB_TITLE, wxCommandEvent);
+wxDECLARE_EVENT(GAME_CHANGE, wxCommandEvent);
+
+
+class GameTab : public wxPanel, public TabInfos {
+ EditorPanel *editor_panel;
+ BoardPanel *board_panel;
+ Game *game;
+ void RefreshLabel();
+ void OnRefreshTabTitle(wxCommandEvent &event);
+ void OnGameChange(wxCommandEvent &event);
+public:
+ GameTab(wxFrame *parent, Game *game);
+ void ApplyPreferences();
+
+};
diff --git a/src/game_tab/HalfMove.cpp b/src/game_tab/HalfMove.cpp
new file mode 100644
index 0000000..72ee24c
--- /dev/null
+++ b/src/game_tab/HalfMove.cpp
@@ -0,0 +1,213 @@
+#include "HalfMove.hpp"
+
+HalfMove::HalfMove(std::string move) : capture(' ') {
+ this->move = move;
+ fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
+}
+
+HalfMove::HalfMove(std::string move, std::string fen) : fen(fen), capture(' ') {
+ this->move = move;
+}
+
+HalfMove::~HalfMove() {
+ for (HalfMove *m : variations) {
+ delete m;
+ }
+}
+
+void HalfMove::AddVariation(HalfMove *m) {
+ m->IsBlack = this->IsBlack;
+ m->Number = this->Number;
+ HalfMove::variations.push_back(m);
+ cgeditor::CGEHalfMove::variations.push_back(m);
+ m->SetParent(this);
+}
+
+std::map<char, std::uint8_t> HalfMove::GetLineCaptures() {
+ std::map<char, std::uint8_t> captures;
+ HalfMove *m = this;
+ do {
+ char c = m->capture;
+ if (captures.find(c) != captures.end()) {
+ captures[c]++;
+ } else {
+ captures[c] = 1;
+ }
+ m = m->parent;
+ } while (m != NULL);
+ return (captures);
+}
+
+void HalfMove::SetCapture(char c) { capture = c; }
+
+void HalfMove::AddMove(HalfMove *m) {
+ if (this->mainline == NULL) {
+ SetMainline(m);
+ } else {
+ if (mainline != NULL) {
+ mainline->AddVariation(m);
+ }
+ }
+}
+
+void HalfMove::SetMainline(HalfMove *m) {
+ if (!this->IsBlack) {
+ m->IsBlack = true;
+ m->Number = this->Number;
+ } else {
+ m->IsBlack = false;
+ m->Number = this->Number + 1;
+ }
+ HalfMove::mainline = m;
+ cgeditor::CGEHalfMove::MainLine = m;
+ if (m != NULL) {
+ m->SetParent(this);
+ }
+}
+
+void HalfMove::SetParent(HalfMove *m) {
+ HalfMove::parent = m;
+ CGEHalfMove::Parent = m;
+}
+
+void HalfMove::RemoveChild(HalfMove *m) {
+ std::uint32_t i = 0;
+ bool found = false;
+ for (i; i < HalfMove::variations.size(); i++) {
+ if (HalfMove::variations[i] == m) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ HalfMove::variations.erase(HalfMove::variations.begin() + i);
+ }
+ if (HalfMove::mainline == m) {
+ HalfMove::mainline = NULL;
+ }
+ cgeditor::CGEHalfMove::RemoveChild((CGEHalfMove *)m);
+}
+
+HalfMove *HalfMove::GetParent() { return (parent); }
+
+HalfMove *HalfMove::GetRoot() {
+ HalfMove *m = this;
+ HalfMove *p = HalfMove::parent;
+ while (p != NULL) {
+ if (p->mainline != m) {
+ return (m);
+ }
+ m = p;
+ p = m->HalfMove::parent;
+ }
+ return (m);
+}
+
+void HalfMove::SetAsMainline() {
+ HalfMove *root = GetRoot();
+ HalfMove *lastRoot;
+ do {
+ lastRoot = root;
+ root->HalfMove::Promote();
+ root = GetRoot();
+ } while (root != lastRoot);
+}
+
+HalfMove *HalfMove::GetMainline() { return (mainline); }
+
+HalfMove::HalfMove(pgnp::HalfMove *m, std::string initial_fen) {
+ chessarbiter::ChessArbiter arbiter;
+ arbiter.Setup(initial_fen);
+ arbiter.Play(arbiter.ParseSAN(m->move));
+ this->fen = arbiter.GetFEN();
+ this->move = m->move;
+ this->IsBlack = m->isBlack;
+ this->SetComment(m->comment);
+ this->Number = m->count;
+ if (m->MainLine != NULL) {
+ this->SetMainline(new HalfMove(m->MainLine, arbiter.GetFEN()));
+ }
+ for (pgnp::HalfMove *v : m->variations) {
+ arbiter.Setup(initial_fen);
+ arbiter.Play(arbiter.ParseSAN(v->move));
+ this->AddVariation(new HalfMove(v, arbiter.GetFEN()));
+ }
+}
+
+void HalfMove::SetFen(std::string fen) { this->fen = fen; }
+
+void HalfMove::Promote() {
+ HalfMove *root = GetRoot();
+ if (root->parent != NULL) {
+ HalfMove *p = root->parent;
+ if (p->HalfMove::mainline != root) {
+ if (root->parent->HalfMove::parent != NULL) {
+ HalfMove *pp = root->parent->HalfMove::parent;
+ if (pp->HalfMove::mainline == p) {
+ pp->HalfMove::SetMainline(root);
+ } else {
+ pp->AddVariation(root);
+ pp->HalfMove::RemoveChild(p);
+ }
+ }
+ if (p->HalfMove::mainline == root) {
+ p->HalfMove::SetMainline(NULL);
+ } else {
+ p->HalfMove::RemoveChild(root);
+ }
+ root->AddVariation(p);
+ }
+ }
+}
+
+bool HalfMove::IsVariation() {
+ HalfMove *m = this;
+ HalfMove *p = HalfMove::parent;
+ while (p != NULL) {
+ if (p->mainline != m) {
+ return (true);
+ }
+ m = p;
+ p = m->HalfMove::parent;
+ }
+ return (false);
+}
+
+std::string HalfMove::GetFen() { return (fen); }
+
+std::string HalfMove::GetPGN() { return (GetPGN(IsBlack)); }
+
+std::string HalfMove::GetPGN(bool needDots) {
+ std::string part;
+ bool newNeedDots = false;
+
+ if (!IsBlack || needDots) {
+ part += std::to_string(Number) + ".";
+ if (needDots) {
+ part += "..";
+ }
+ }
+ part += move;
+
+ if (GetNbLineComment() > 0) {
+ part += " {";
+ part += GetComment();
+ part += "}";
+ newNeedDots = true;
+ }
+
+ if (variations.size() > 0) {
+ newNeedDots = true;
+ for (HalfMove *v : variations) {
+ part += " (";
+ part += v->GetPGN(IsBlack);
+ part += ")";
+ }
+ }
+
+ if (mainline != NULL) {
+ part += " " + mainline->GetPGN(newNeedDots);
+ }
+
+ return (part);
+}
diff --git a/src/game_tab/HalfMove.hpp b/src/game_tab/HalfMove.hpp
new file mode 100644
index 0000000..c8ef8c0
--- /dev/null
+++ b/src/game_tab/HalfMove.hpp
@@ -0,0 +1,57 @@
+#pragma once
+#include "CGEditor.hpp"
+#include "ChessArbiter.hpp"
+#include "ochess.hpp"
+#include "pgnp.hpp"
+#include <map>
+#include <vector>
+
+/**
+ * @brief Create your custom half move class
+ *
+ * The implementation of the class should give you
+ * an overview of how to keep your move sync with the one of CGEditor
+ *
+ */
+class HalfMove : public cgeditor::CGEHalfMove {
+ HalfMove *parent = NULL;
+ HalfMove *mainline = NULL;
+ std::vector<HalfMove *> variations;
+ std::string fen;
+ char capture;
+ std::string GetPGN(bool needDots);
+
+public:
+ HalfMove(std::string move);
+ HalfMove(std::string move, std::string fen);
+ HalfMove(pgnp::HalfMove *m, std::string initial_fen);
+
+ ~HalfMove();
+ /// @brief Add variation to current move
+ void AddVariation(HalfMove *m);
+ /// @brief Remove the specified child from mainline and/or variations
+ void RemoveChild(HalfMove *m);
+ void AddMove(HalfMove *m);
+ /// @brief Set value of the mailine
+ void SetMainline(HalfMove *m);
+ /// @brief Set this move as mainline
+ void SetAsMainline();
+ /// @brief Promote the current move and submove
+ void Promote();
+ /// @brief Check if current half move is within a variation
+ bool IsVariation();
+ /// @brief Get the root of a variation
+ HalfMove *GetRoot();
+ /// @brief Get parent of the current move
+ HalfMove *GetParent();
+ HalfMove *GetMainline();
+ std::map<char, std::uint8_t> GetLineCaptures();
+
+ /// @brief Set parent of the current move
+ void SetParent(HalfMove *m);
+ std::string GetFen();
+ void SetFen(std::string fen);
+ void SetCapture(char c);
+ std::string GetPGN();
+};
+
diff --git a/src/game_tab/board/BoardCanvas.cpp b/src/game_tab/board/BoardCanvas.cpp
new file mode 100644
index 0000000..237a68a
--- /dev/null
+++ b/src/game_tab/board/BoardCanvas.cpp
@@ -0,0 +1,294 @@
+#include "BoardCanvas.hpp"
+
+wxDEFINE_EVENT(PLAY_MOVE_EVENT, wxCommandEvent);
+
+BoardCanvas::BoardCanvas(wxFrame *parent)
+ : wxPanel(parent), black_side(false), is_black_turn(true), frozen(false),
+ lock_square_size(false), t(new Theme()), t_captures(new Theme()) {
+ board = "rnbqkbnrpppppppp PPPPPPPPRNBQKBNR";
+ is_dragging = false;
+ valid_drag = false;
+ t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
+ SetClockTime(-1, -1, -1, false);
+ SetClockTime(-1, -1, -1, true);
+ ApplyPreferences();
+}
+
+BoardCanvas::BoardCanvas(wxFrame *parent, std::uint32_t square_width,
+ bool frozen)
+ : BoardCanvas(parent) {
+ t->ResizeSquaresAndPieces(square_width);
+ t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
+ this->frozen = true;
+ lock_square_size = true;
+}
+
+void BoardCanvas::OnPaint(wxPaintEvent &event) {
+ wxPaintDC dc(this);
+ REFRESH_MOUSE_LOCATION();
+ square_width = t->GetSquaresSizes();
+ canvas_size = dc.GetSize();
+ boardX = (canvas_size.x - (8 * square_width)) / 2;
+ boardY = (canvas_size.y - (8 * square_width)) / 2;
+ if (boardX > canvas_size.x)
+ boardX = 0;
+ if (boardY > canvas_size.y)
+ boardY = 0;
+ DrawBoard(dc);
+}
+
+void BoardCanvas::ApplyPreferences() {
+ if (t != NULL)
+ delete t;
+ if (t_captures != NULL)
+ delete t_captures;
+ t = new Theme();
+ t_captures = new Theme();
+
+ CONFIG_OPEN(config);
+ black_side = config->Read("board/black_by_default", false);
+ if (lock_square_size) {
+ t->ResizeSquaresAndPieces(square_width);
+ } else {
+ t->ResizeSquaresAndPieces(config->Read("board/square_size", 80));
+ }
+ t->SetSquareRadius(config->Read("board/corner_radius", 10));
+ t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
+ CONFIG_CLOSE(config);
+
+ Refresh();
+}
+
+void BoardCanvas::SetupBoard(std::string board, bool is_black_turn,
+ std::map<char, std::uint8_t> captures) {
+ this->board = board;
+ this->is_black_turn = is_black_turn;
+ this->captures = captures;
+ Refresh();
+}
+
+void BoardCanvas::DrawBoard(wxPaintDC &dc) {
+ std::uint32_t piece_width = t->GetPiecesSizes();
+ std::uint32_t centrer_offset = (square_width - piece_width) / 2;
+
+ bool DrawDraggingPiece = false;
+ char dp = 'p';
+ std::uint32_t dpx = 0, dpy = 0;
+ for (std::int8_t file = 7; file >= 0; file--) {
+ for (std::uint8_t rank = 0; rank <= 7; rank++) {
+ std::uint32_t x = boardX + (7 - file) * square_width;
+ std::uint32_t y = boardY + rank * square_width;
+ if ((file + rank) % 2 == 0) {
+ if (file == 0 && rank == 0) {
+ dc.DrawBitmap(*t->Get('1'), x, y, true);
+ } else if (file == 7 && rank == 7) {
+ dc.DrawBitmap(*t->Get('2'), x, y, true);
+ } else {
+ dc.DrawBitmap(*t->Get('s'), x, y, true);
+ }
+ } else {
+ if (file == 7 && rank == 0) {
+ dc.DrawBitmap(*t->Get('0'), x, y, true);
+ } else if (file == 0 && rank == 7) {
+ dc.DrawBitmap(*t->Get('3'), x, y, true);
+ } else {
+ dc.DrawBitmap(*t->Get('S'), x, y, true);
+ }
+ }
+
+ std::uint8_t prank = rank;
+ std::uint8_t pfile = file;
+ if (black_side) {
+ prank = 7 - rank;
+ pfile = 7 - file;
+ }
+ std::uint32_t px = x + centrer_offset;
+ std::uint32_t py = y + centrer_offset;
+ char piece = board[(7 - pfile) + 8 * prank];
+ if (is_dragging && (7 - pfile) == active_square.x &&
+ (7 - prank) == active_square.y) {
+ dp = piece;
+ dpx = px - (lastClickX - mouseX);
+ dpy = py - (lastClickY - mouseY);
+ DrawDraggingPiece = true;
+ continue;
+ }
+ if (piece != ' ') {
+ dc.DrawBitmap(*t->Get(piece), px, py, false);
+ }
+ }
+ }
+
+ // Draw badge
+ dc.SetPen(wxPen(*wxBLACK, 3));
+ std::uint32_t badgeY = boardY;
+ std::uint32_t badgeWidth = square_width / 2;
+ if (is_black_turn) {
+ dc.SetBrush(*wxBLACK_BRUSH);
+ if (black_side) {
+ badgeY = boardY + (8 * square_width) - badgeWidth;
+ }
+ } else {
+ dc.SetBrush(*wxWHITE_BRUSH);
+ if (!black_side) {
+ badgeY = boardY + (8 * square_width) - badgeWidth;
+ }
+ }
+ wxRect badge(boardX + (8 * square_width) + badgeWidth / 2, badgeY, badgeWidth,
+ badgeWidth);
+ dc.DrawRectangle(badge);
+
+ // Draw captures first for white then for black
+ std::uint32_t captures_size = t_captures->GetPiecesSizes();
+ std::uint8_t padding = 10;
+ std::uint32_t offsetX = 0;
+ std::uint32_t offsetY = -(captures_size + padding);
+ if (black_side) {
+ offsetY = 8 * square_width + padding;
+ }
+ for (char p : {'P', 'N', 'B', 'R', 'Q'}) {
+ if (captures.find(p) != captures.end()) {
+ for (std::uint8_t i = 0; i < captures[p]; i++) {
+ dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY);
+ offsetX += captures_size / 2;
+ }
+ offsetX += captures_size / 2;
+ }
+ }
+ offsetX = 0;
+ if (black_side) {
+ offsetY = -(captures_size + padding);
+ } else {
+ offsetY = 8 * square_width + padding;
+ }
+ for (char p : {'p', 'n', 'b', 'r', 'q'}) {
+ if (captures.find(p) != captures.end()) {
+ for (std::uint8_t i = 0; i < captures[p]; i++) {
+ dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY);
+ offsetX += captures_size / 2;
+ }
+ offsetX += captures_size / 2;
+ }
+ }
+
+ // Draw dragging piece
+ if (DrawDraggingPiece) {
+ dc.DrawBitmap(*t->Get(dp), dpx, dpy, false);
+ }
+
+ // Draw numbers
+ for (char l = 'a'; l < 'a' + 8; l++) {
+ dc.DrawText(wxString(l), wxPoint(boardX + l - 'a' * square_width,
+ boardY + 8 * square_width + 10));
+ }
+
+ // Draw Clocks
+ if (std::get<0>(black_time) >= 0) {
+ wxFont font = dc.GetFont();
+ ClockTime clock = black_side ? white_time : black_time;
+ wxString time =
+ wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock));
+ if (std::get<0>(clock) > 0) {
+ time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock));
+ } else if (std::get<1>(clock) > 0) {
+ time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock));
+ }
+ dc.SetFont(font.Scale(1.5).MakeBold());
+ wxCoord width, height;
+ dc.GetTextExtent(time, &width, &height);
+ dc.DrawText(time,
+ wxPoint(boardX + square_width * 8 - width, boardY - height));
+ clock = black_side ? black_time : white_time;
+ time = wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock));
+ if (std::get<0>(clock) > 0) {
+ time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock));
+ } else if (std::get<1>(clock) > 0) {
+ time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock));
+ }
+ dc.GetTextExtent(time, &width, &height);
+ dc.DrawText(time, wxPoint(boardX + square_width * 8 - width,
+ boardY + square_width * 8));
+ }
+}
+
+void BoardCanvas::MouseEvent(wxMouseEvent &event) {
+ if (!frozen) {
+ if (event.Dragging()) {
+ if (valid_drag) {
+ is_dragging = true;
+ Refresh();
+ }
+ } else {
+ if (is_dragging) {
+ is_dragging = false;
+ valid_drag = false;
+ // Handle drop
+ REFRESH_MOUSE_LOCATION();
+ INIT_CURRENT_SQUARE();
+
+ if (IsCurrentSquareValid) {
+ wxLogDebug("Drag end on square (%d,%d)", file, rank);
+ /// Play the move
+ wxCommandEvent event(PLAY_MOVE_EVENT, GetId());
+ event.SetEventObject(this);
+ std::string move = ((char)('a' + active_square.x)) +
+ std::to_string(+active_square.y + 1) +
+ ((char)('a' + file)) + std::to_string(rank + 1);
+ event.SetString(move);
+ ProcessEvent(event);
+ }
+ }
+ if (event.LeftDown()) {
+ SetFocus();
+ REFRESH_MOUSE_LOCATION();
+ lastClickX = mouseX;
+ lastClickY = mouseY;
+ INIT_CURRENT_SQUARE();
+ if (IsCurrentSquareValid) {
+ active_square.x = file;
+ active_square.y = rank;
+ if (board[(7 - rank) * 8 + file] != ' ') {
+ wxLogDebug("Drag start on square (%d,%d)", file, rank);
+ valid_drag = true;
+ }
+ }
+ }
+ }
+ }
+}
+
+void BoardCanvas::Zoom(std::int32_t zoom) {
+ t->Zoom(zoom);
+ t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
+ Refresh();
+}
+
+void BoardCanvas::Swap() {
+ black_side = !black_side;
+ Refresh();
+}
+
+void BoardCanvas::OnKeyEvent(wxKeyEvent &event) {
+ if (event.GetKeyCode() == WXK_LEFT) {
+ wxCommandEvent previousEvent(PREVIOUS_MOVE_EVENT, GetId());
+ previousEvent.SetEventObject(this);
+ ProcessEvent(previousEvent);
+ } else if (event.GetKeyCode() == WXK_RIGHT) {
+ wxCommandEvent nextEvent(NEXT_MOVE_EVENT, GetId());
+ nextEvent.SetEventObject(this);
+ ProcessEvent(nextEvent);
+ }
+}
+
+void BoardCanvas::SetClockTime(short hours, short min, short sec,
+ bool IsBlack) {
+ if (IsBlack) {
+ black_time = std::make_tuple(hours, min, sec);
+ } else {
+ white_time = std::make_tuple(hours, min, sec);
+ }
+}
+
+wxBEGIN_EVENT_TABLE(BoardCanvas, wxPanel) EVT_PAINT(BoardCanvas::OnPaint)
+ EVT_MOUSE_EVENTS(BoardCanvas::MouseEvent)
+ EVT_CHAR_HOOK(BoardCanvas::OnKeyEvent) wxEND_EVENT_TABLE()
diff --git a/src/game_tab/board/BoardCanvas.hpp b/src/game_tab/board/BoardCanvas.hpp
new file mode 100644
index 0000000..34c4911
--- /dev/null
+++ b/src/game_tab/board/BoardCanvas.hpp
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "Theme.hpp"
+#include "ochess.hpp"
+#include <map>
+#include <tuple>
+#include <utility>
+#include <vector>
+#include <wx/artprov.h>
+
+// Local events
+wxDECLARE_EVENT(PLAY_MOVE_EVENT, wxCommandEvent);
+
+// Foreign events
+wxDECLARE_EVENT(PREVIOUS_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(NEXT_MOVE_EVENT, wxCommandEvent);
+
+#define REFRESH_MOUSE_LOCATION() \
+ { \
+ const wxPoint pt = wxGetMousePosition(); \
+ mouseX = pt.x - this->GetScreenPosition().x; \
+ mouseY = pt.y - this->GetScreenPosition().y; \
+ }
+
+#define INIT_CURRENT_SQUARE() \
+ std::uint32_t file = 7 - (mouseX - boardX) / square_width; \
+ std::uint32_t rank = (mouseY - boardY) / square_width; \
+ if (!black_side) { \
+ file = 7 - file; \
+ rank = 7 - rank; \
+ } \
+ bool IsCurrentSquareValid = file >= 0 && file <= 7 && rank >= 0 && rank <= 7;
+
+#define MOUSE_ON(x, y, width, height) \
+ (mouseX >= (x) && mouseX <= ((x) + (width)) && mouseY >= (y) && \
+ mouseY <= ((y) + (height)))
+
+#define CAPTURE_FACTOR 0.5
+
+typedef std::tuple<short, short, short> ClockTime;
+
+class BoardCanvas : public wxPanel {
+ Theme *t, *t_captures;
+ std::string board;
+ bool black_side, is_dragging, valid_drag, is_black_turn;
+ std::uint32_t boardX, boardY, square_width, mouseX, mouseY, lastClickX,
+ lastClickY;
+ wxSize canvas_size;
+ wxPoint active_square;
+ std::map<char, std::uint8_t> captures;
+ ClockTime black_time, white_time;
+ bool frozen,lock_square_size;
+
+public:
+ BoardCanvas(wxFrame *parent);
+ BoardCanvas(wxFrame *parent,std::uint32_t square_width, bool frozen);
+ void ApplyPreferences();
+ void DrawBoard(wxPaintDC &dc);
+ void OnPaint(wxPaintEvent &event);
+ void OnKeyEvent(wxKeyEvent &event);
+ void MouseEvent(wxMouseEvent &event);
+ void Zoom(std::int32_t zoom);
+ void Swap();
+ void SetupBoard(std::string board, bool is_black_turn,
+ std::map<char, std::uint8_t> captures);
+ void SetClockTime(short hours, short min, short sec, bool IsBlack);
+ DECLARE_EVENT_TABLE()
+};
diff --git a/src/game_tab/board/BoardPanel.cpp b/src/game_tab/board/BoardPanel.cpp
new file mode 100644
index 0000000..ff4fb62
--- /dev/null
+++ b/src/game_tab/board/BoardPanel.cpp
@@ -0,0 +1,96 @@
+#include "BoardPanel.hpp"
+#include <wx/clipbrd.h>
+
+BoardPanel::BoardPanel(wxFrame *parent, Game *game)
+ : wxPanel(parent), game(game) {
+
+ wxBoxSizer *board_panel_sizer = new wxBoxSizer(wxVERTICAL);
+ board_canvas = new BoardCanvas((wxFrame *)this);
+ board_panel_sizer->Add(board_canvas, 1, wxEXPAND);
+
+ // Left Panel buttons
+ wxBoxSizer *board_panel_button_sizer = new wxBoxSizer(wxHORIZONTAL);
+ board_panel_button_sizer->Add(
+ new wxBitmapButton(this, SWAP_BTN, LoadPNG("swap")), 0);
+ board_panel_button_sizer->Add(
+ new wxBitmapButton(this, ZOOM_IN_BTN, LoadPNG("zoomin")), 0);
+ board_panel_button_sizer->Add(
+ new wxBitmapButton(this, ZOOM_OUT_BTN, LoadPNG("zoomout")), 0);
+ board_panel_button_sizer->Add(new wxButton(this, COPY_FEN_BTN, L"Copy FEN"),
+ 0, wxEXPAND);
+ board_panel_sizer->Add(board_panel_button_sizer, 0);
+ this->SetSizer(board_panel_sizer);
+
+ Bind(PLAY_MOVE_EVENT, &BoardPanel::OnPlay, this, wxID_ANY);
+ Bind(PREVIOUS_MOVE_EVENT, &BoardPanel::OnPreviousMove, this, wxID_ANY);
+ Bind(NEXT_MOVE_EVENT, &BoardPanel::OnNextMove, this, wxID_ANY);
+ Bind(wxEVT_BUTTON, &BoardPanel::OnSwap, this, SWAP_BTN);
+ Bind(wxEVT_BUTTON, &BoardPanel::OnZoomIn, this, ZOOM_IN_BTN);
+ Bind(wxEVT_BUTTON, &BoardPanel::OnZoomOut, this, ZOOM_OUT_BTN);
+ Bind(wxEVT_BUTTON, &BoardPanel::OnCopyFEN, this, COPY_FEN_BTN);
+}
+
+void BoardPanel::OnPreviousMove(wxCommandEvent &event) {
+ game->Previous();
+ Notify();
+ NotifyEditor();
+}
+
+void BoardPanel::OnZoomIn(wxCommandEvent &event) {
+ wxLogDebug("Clicked on zoom in");
+ board_canvas->Zoom(10);
+}
+
+void BoardPanel::OnZoomOut(wxCommandEvent &event) {
+ wxLogDebug("Clicked on zoom out");
+ board_canvas->Zoom(-10);
+}
+
+void BoardPanel::OnSwap(wxCommandEvent &event) {
+ wxLogDebug("Clicked on swap");
+ board_canvas->Swap();
+}
+
+void BoardPanel::OnNextMove(wxCommandEvent &event) {
+ wxLogDebug("Game tab received NEXT_MOVE_EVENT");
+ game->Next();
+ Notify();
+ NotifyEditor();
+}
+
+void BoardPanel::OnPlay(wxCommandEvent &event) {
+ wxLogDebug("Game tab received PLAY_MOVE_EVENT");
+ if (game->Play(event.GetString().ToStdString())) {
+ NotifyEditor();
+ }
+ Notify();
+}
+
+void BoardPanel::OnCopyFEN(wxCommandEvent &event) {
+ wxLogDebug("Clicked on copy fen");
+ if (wxTheClipboard->Open()) {
+ wxTheClipboard->SetData(new wxTextDataObject(game->GetFen()));
+ wxTheClipboard->Close();
+ }
+}
+
+void BoardPanel::Notify() {
+ std::string fen = game->GetFen();
+ std::map<char, std::uint8_t> captures;
+ HalfMove *m = game->GetCurrentMove();
+ if (m != NULL) {
+ captures = m->GetLineCaptures();
+ }
+ board_canvas->SetupBoard(chessarbiter::FENParser::Parse(fen).board,
+ game->IsBlackToPlay(), captures);
+}
+
+void BoardPanel::NotifyEditor() {
+ wxCommandEvent previousEvent(GAME_CHANGE, GetId());
+ previousEvent.SetEventObject(this);
+ ProcessEvent(previousEvent);
+}
+
+void BoardPanel::ApplyPreferences() {
+ board_canvas->ApplyPreferences();
+}
diff --git a/src/game_tab/board/BoardPanel.hpp b/src/game_tab/board/BoardPanel.hpp
new file mode 100644
index 0000000..3c4bf77
--- /dev/null
+++ b/src/game_tab/board/BoardPanel.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "../Game.hpp"
+#include "BoardCanvas.hpp"
+#include "ochess.hpp"
+
+// Foreign events
+wxDECLARE_EVENT(GAME_CHANGE, wxCommandEvent);
+
+enum { COPY_FEN_BTN = wxID_HIGHEST + 1, ZOOM_IN_BTN, ZOOM_OUT_BTN, SWAP_BTN };
+
+class BoardPanel : public wxPanel {
+ Game *game;
+ BoardCanvas *board_canvas;
+ void NotifyEditor();
+
+public:
+ BoardPanel(wxFrame *parent, Game *game);
+ void Notify();
+ void OnPlay(wxCommandEvent &event);
+ void OnGotoMove(wxCommandEvent &event);
+ void OnPreviousMove(wxCommandEvent &event);
+ void OnNextMove(wxCommandEvent &event);
+ void OnCopyFEN(wxCommandEvent &event);
+ void OnZoomIn(wxCommandEvent &event);
+ void OnZoomOut(wxCommandEvent &event);
+ void OnSwap(wxCommandEvent &event);
+ void OnRefreshBoard(wxCommandEvent &event);
+ void ApplyPreferences();
+}; \ No newline at end of file
diff --git a/src/game_tab/board/Theme.cpp b/src/game_tab/board/Theme.cpp
new file mode 100644
index 0000000..6ecbafc
--- /dev/null
+++ b/src/game_tab/board/Theme.cpp
@@ -0,0 +1,184 @@
+#include "Theme.hpp"
+#include <wx/filename.h>
+
+Theme::Theme() : square_radius(10) {
+ // Load config
+ CONFIG_OPEN(config);
+ std::string piece =
+ config->Read("board/theme/pieces/path", "default").ToStdString();
+ wxFileName piece_file(piece);
+ std::string square =
+ config->Read("board/theme/squares/path", "default").ToStdString();
+ wxFileName square_file(square);
+ CONFIG_CLOSE(config);
+ // Piece
+ if (piece == "default" || !piece_file.FileExists()) {
+ wxLogDebug("Loading piece skin from binres");
+ LoadPiecesSkin(LoadPNG("cburnett").ConvertToImage());
+ } else {
+ wxLogDebug("Loading piece skin: %s", piece);
+ LoadPiecesSkin(wxImage(piece, wxBITMAP_TYPE_PNG));
+ }
+ // Square
+ if (square == "default" || !square_file.FileExists()) {
+ wxLogDebug("Loading square skin from binres");
+ LoadSquaresSkin(LoadPNG("chesscom_8bits").ConvertToImage());
+ } else {
+ wxLogDebug("Loading square skin: %s", square);
+ LoadSquaresSkin(wxImage(square, wxBITMAP_TYPE_PNG));
+ }
+}
+
+Theme::Theme(std::string piece, std::string square) : square_radius(10) {
+ wxLogDebug("Loading piece skin: %s", piece);
+ LoadPiecesSkin(wxImage(piece, wxBITMAP_TYPE_PNG));
+ wxLogDebug("Loading square skin: %s", square);
+ LoadSquaresSkin(wxImage(square, wxBITMAP_TYPE_PNG));
+}
+
+Theme::~Theme() {
+ for (std::pair<char, wxBitmap *> c : skin_scaled) {
+ delete c.second;
+ }
+}
+
+std::uint8_t Theme::GetSquareRadius() { return (square_radius); }
+
+void Theme::LoadPiecesSkin(wxImage iskin) {
+ if (iskin.GetWidth() != 2 * ELT_DIM || iskin.GetHeight() != 6 * ELT_DIM) {
+ throw "Invalid piece theme";
+ }
+
+ int offset = 0;
+ // Kings
+ skin['k'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['K'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+ // Queen
+ offset = ELT_DIM;
+ skin['q'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['Q'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+ // Rook
+ offset = ELT_DIM * 2;
+ skin['r'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['R'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+ // Bishop
+ offset = ELT_DIM * 3;
+ skin['b'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['B'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+ // Knight
+ offset = ELT_DIM * 4;
+ skin['n'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['N'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+ // Pawn
+ offset = ELT_DIM * 5;
+ skin['p'] = iskin.GetSubImage(wxRect(0, offset, ELT_DIM, ELT_DIM));
+ skin['P'] = iskin.GetSubImage(wxRect(ELT_DIM, offset, ELT_DIM, ELT_DIM));
+
+ // Update scaled version
+ ResizePieces(DEFAULT_SIZE * PIECE_SIZE_FACTOR);
+}
+
+void Theme::LoadSquaresSkin(wxImage iskin) {
+ if (iskin.GetWidth() != 2 * ELT_DIM || iskin.GetHeight() != ELT_DIM) {
+ throw "Invalid piece theme";
+ }
+
+ // Square
+ skin['s'] = iskin.GetSubImage(wxRect(0, 0, ELT_DIM, ELT_DIM));
+ skin['S'] = iskin.GetSubImage(wxRect(ELT_DIM, 0, ELT_DIM, ELT_DIM));
+
+ // Update scaled version
+ ResizeSquares(DEFAULT_SIZE);
+}
+
+wxBitmap *Theme::Get(char c) { return (skin_scaled[c]); }
+
+void Theme::ResizePieces(std::uint32_t width) {
+ for (std::pair<char, wxImage> c : skin) {
+ if (c.first != 's' && c.first != 'S') {
+ if (skin_scaled.count(c.first))
+ delete skin_scaled[c.first];
+ skin_scaled[c.first] =
+ new wxBitmap(c.second.Scale(width, width, wxIMAGE_QUALITY_HIGH));
+ }
+ }
+}
+
+void Theme::ResizeSquaresAndPieces(std::uint32_t width) {
+ ResizeSquares(width);
+ ResizePieces(width * PIECE_SIZE_FACTOR);
+}
+
+void Theme::ResizeSquares(std::uint32_t width) {
+ if (skin_scaled.count('s'))
+ delete skin_scaled['s'];
+ skin_scaled['s'] =
+ new wxBitmap(skin['s'].Scale(width, width, wxIMAGE_QUALITY_HIGH));
+ if (skin_scaled.count('S'))
+ delete skin_scaled['S'];
+ skin_scaled['S'] =
+ new wxBitmap(skin['S'].Scale(width, width, wxIMAGE_QUALITY_HIGH));
+
+ skin_scaled['0'] = new wxBitmap(*skin_scaled['S']);
+ skin_scaled['1'] = new wxBitmap(*skin_scaled['s']);
+ skin_scaled['2'] = new wxBitmap(*skin_scaled['s']);
+ skin_scaled['3'] = new wxBitmap(*skin_scaled['S']);
+
+ skin_scaled['0']->SetMask(RoundedMask(width, 0));
+ skin_scaled['1']->SetMask(RoundedMask(width, 1));
+ skin_scaled['2']->SetMask(RoundedMask(width, 2));
+ skin_scaled['3']->SetMask(RoundedMask(width, 3));
+}
+
+void Theme::Zoom(int amount) {
+ double width = skin_scaled['s']->GetWidth() + amount;
+ ResizeSquares(std::max(width, 1.0));
+ ResizePieces(std::max(width * PIECE_SIZE_FACTOR, 1.0));
+}
+
+void Theme::SetSquareRadius(std::uint8_t radius) {
+ square_radius = radius;
+ Zoom(0); // Refresh scale
+}
+
+/**
+ * This will never fail since k entry always exists (cf constructor and
+ * ResizePieces)
+ */
+double Theme::GetPiecesSizes() { return (skin_scaled['k']->GetWidth()); }
+
+/**
+ * This will never fail since s entry always exists (cf constructor and
+ * ResizeSquares)
+ */
+double Theme::GetSquaresSizes() { return (skin_scaled['s']->GetWidth()); }
+
+wxMask *Theme::RoundedMask(std::uint32_t width, std::uint8_t corner) {
+
+ wxBitmap b(width, width, 1);
+ wxMemoryDC dc;
+ dc.SelectObject(b);
+ dc.SetPen(*wxBLACK_PEN);
+ dc.SetBrush(*wxBLACK_BRUSH);
+ dc.DrawRectangle(0, 0, width, width);
+ dc.SetBrush(*wxWHITE_BRUSH);
+ dc.SetPen(*wxWHITE_PEN);
+ dc.DrawRoundedRectangle(0, 0, width, width, square_radius);
+
+ dc.SetBrush(*wxWHITE_BRUSH);
+ dc.SetPen(*wxWHITE_PEN);
+ if (corner == 0) {
+ dc.DrawRectangle(0, width / 2, width, width);
+ dc.DrawRectangle(width / 2, 0, width, width);
+ } else if (corner == 1) {
+ dc.DrawRectangle(0, 0, width / 2, width);
+ dc.DrawRectangle(0, width / 2, width, width);
+ } else if (corner == 2) {
+ dc.DrawRectangle(0, 0, width, width / 2);
+ dc.DrawRectangle(width / 2, 0, width, width);
+ } else if (corner == 3) {
+ dc.DrawRectangle(0, 0, width / 2, width);
+ dc.DrawRectangle(0, 0, width, width / 2);
+ }
+ return (new wxMask(b));
+}
diff --git a/src/game_tab/board/Theme.hpp b/src/game_tab/board/Theme.hpp
new file mode 100644
index 0000000..88036d8
--- /dev/null
+++ b/src/game_tab/board/Theme.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "ochess.hpp"
+
+#include <string>
+#include <unordered_map>
+
+#define ELT_DIM 200
+#define DEFAULT_SIZE 80
+#define PIECE_SIZE_FACTOR 0.8 // Should be between 0 and 1
+#define DEFAULT_PIECE_THEME "assets/pieces/cburnett.png"
+#define DEFAULT_SQUARE_THEME "assets/boards/chesscom_8bits.png"
+
+class Theme {
+private:
+ std::unordered_map<char, wxImage> skin;
+ std::unordered_map<char, wxBitmap *> skin_scaled;
+ std::uint8_t square_radius;
+ wxMask *RoundedMask(std::uint32_t width, std::uint8_t corner);
+
+public:
+ Theme();
+ Theme(std::string piece, std::string square);
+ ~Theme();
+ void LoadPiecesSkin(wxImage skin);
+ void LoadSquaresSkin(wxImage iskin);
+ void ResizePieces(std::uint32_t width);
+ void ResizeSquares(std::uint32_t width);
+ void ResizeSquaresAndPieces(std::uint32_t width);
+ void SetSquareRadius(std::uint8_t radius);
+ std::uint8_t GetSquareRadius();
+ void Zoom(int amount);
+ double GetPiecesSizes();
+ double GetSquaresSizes();
+
+ wxBitmap *Get(char c);
+};
diff --git a/src/game_tab/editor/EditorCanvas.cpp b/src/game_tab/editor/EditorCanvas.cpp
new file mode 100644
index 0000000..441118d
--- /dev/null
+++ b/src/game_tab/editor/EditorCanvas.cpp
@@ -0,0 +1,186 @@
+#include "EditorCanvas.hpp"
+
+EditorCanvas::EditorCanvas(wxFrame *parent)
+ : wxPanel(parent), NeedRedraw(false) {
+ hide_icon = LoadPNG("hide", wxSize(CGEditor::status.MoveIconWidth,
+ CGEditor::status.MoveIconWidth));
+ t.ResizePieces(CGEditor::status.MoveIconWidth);
+}
+
+void EditorCanvas::OnPaint(wxPaintEvent &event) {
+ wxPaintDC current_dc(this);
+ dc = &current_dc;
+
+ // Refresh canvas size
+ wxSize sz = GetClientSize();
+ CGEditor::status.CanvasWidth = sz.GetWidth();
+ CGEditor::status.CanvasHeight = sz.GetHeight();
+ CGEditor::status.UseMoveIcons =
+ true; // Piece image should be drawn before the move ?
+
+ const wxPoint pt = wxGetMousePosition();
+ CGEditor::status.MouseX = pt.x - this->GetScreenPosition().x;
+ CGEditor::status.MouseY = pt.y - this->GetScreenPosition().y;
+ CGEditor::Draw();
+}
+
+/**
+ * @brief Convenient fonction to center text
+ *
+ * @param e Element to center
+ * @return wxPoint The centered version of e according to wdWidget API
+ */
+wxPoint EditorCanvas::Middle(cgeditor::Element e) {
+ wxSize sz = dc->GetTextExtent(e.text);
+ return (wxPoint(e.x + (e.width - sz.GetWidth()) / 2,
+ e.y + (e.height - sz.GetHeight()) / 2));
+}
+
+void EditorCanvas::DrawElement(const cgeditor::Element &e) {
+ dc->SetPen(wxNullPen);
+ dc->SetBrush(*wxRED_BRUSH);
+ if (e.prop & cgeditor::Property::Rectangle) {
+ if (e.prop & cgeditor::Property::Scrollbarbg) {
+ dc->SetBrush(*wxCYAN_BRUSH);
+ } else if (e.prop & cgeditor::Property::Scrollbar) {
+ dc->SetBrush(*wxGREY_BRUSH);
+ } else if (e.prop & cgeditor::Property::Margin) {
+ dc->SetBrush(*wxLIGHT_GREY_BRUSH);
+ } else if (e.prop & cgeditor::Property::Button) {
+ if (e.prop & cgeditor::Property::On) {
+ dc->DrawBitmap(hide_icon, e.x, e.y);
+ return;
+ }
+ dc->SetBrush(*wxBLACK_BRUSH);
+ }
+ wxRect recToDraw(e.x, e.y, e.width, e.height);
+ dc->DrawRectangle(recToDraw);
+ } else if (e.prop & cgeditor::Property::Text ||
+ e.prop & cgeditor::Property::Image) {
+ if (e.prop & cgeditor::Property::Image) {
+ // Draw your pieces images instead
+ std::uint32_t y = Middle(e).y - CGEditor::status.MoveIconWidth / 2;
+ char p = 'P';
+ if (e.prop & cgeditor::Property::Knight) {
+ p = 'N';
+ } else if (e.prop & cgeditor::Property::Bishop) {
+ p = 'B';
+ } else if (e.prop & cgeditor::Property::Queen) {
+ p = 'Q';
+ } else if (e.prop & cgeditor::Property::King) {
+ p = 'K';
+ }
+ if (e.prop & cgeditor::Property::Black) {
+ p = std::tolower(p);
+ }
+ if (e.prop & cgeditor::Property::Current) {
+ wxRect recToDraw(e.x, e.y, e.width, e.height);
+ dc->SetBrush(*wxLIGHT_GREY_BRUSH);
+ dc->DrawRectangle(recToDraw);
+ }
+ dc->DrawBitmap(*t.Get(p), e.x, y);
+ } else if (e.prop & cgeditor::Property::Comment) {
+ wxRect recToDraw(e.x, e.y, e.width, e.height);
+ dc->SetBrush(*wxYELLOW_BRUSH);
+ dc->DrawRectangle(recToDraw);
+ dc->DrawText(wxString(e.text), wxPoint(e.x, e.y));
+ } else if (e.prop & cgeditor::Property::Menuitem) {
+ wxRect recToDraw(e.x, e.y, e.width, e.height);
+ dc->SetBrush(*wxLIGHT_GREY_BRUSH);
+ dc->DrawRectangle(recToDraw);
+ dc->DrawText(wxString(e.text), wxPoint(e.x, Middle(e).y));
+ } else {
+ if (e.prop & cgeditor::Property::Move) {
+ if (e.prop & cgeditor::Property::Current) {
+ wxRect recToDraw(e.x, e.y, e.width, e.height);
+ dc->SetBrush(*wxLIGHT_GREY_BRUSH);
+ dc->DrawRectangle(recToDraw);
+ }
+ if (CGEditor::status.UseMoveIcons) {
+ dc->DrawText(wxString(e.text), wxPoint(e.x, Middle(e).y));
+ } else {
+ dc->DrawText(wxString(e.text), Middle(e));
+ }
+ } else {
+ dc->DrawText(wxString(e.text), Middle(e));
+ }
+ }
+ }
+}
+void EditorCanvas::HandleEvent(const cgeditor::Event &e) {
+ wxLogDebug("Editor event!");
+ if (e.type == cgeditor::Event::Goto) {
+ wxCommandEvent event(GOTO_MOVE_EVENT, GetId());
+ event.SetEventObject(this);
+ event.SetClientData(e.move);
+ ProcessEvent(event);
+ } else if (e.type == cgeditor::Event::Delete) {
+ wxCommandEvent event(DELETE_MOVE_EVENT, GetId());
+ event.SetEventObject(this);
+ event.SetClientData(e.move);
+ ProcessEvent(event);
+ } else if (e.type == cgeditor::Event::Promote) {
+ wxCommandEvent event(PROMOTE_MOVE_EVENT, GetId());
+ event.SetEventObject(this);
+ event.SetClientData(e.move);
+ ProcessEvent(event);
+ } else if (e.type == cgeditor::Event::SetAsMainline) {
+ wxCommandEvent event(SET_AS_MAINLINE_EVENT, GetId());
+ event.SetEventObject(this);
+ event.SetClientData(e.move);
+ ProcessEvent(event);
+ }
+}
+
+void EditorCanvas::MouseEvent(wxMouseEvent &event) {
+ if (event.Dragging()) {
+ CGEditor::status.LeftClick = false;
+ CGEditor::status.IsDrag = true;
+ Refresh();
+ } else if (event.LeftDown()) {
+ SetFocus();
+ CGEditor::status.LeftClick = true;
+ Refresh();
+ } else if (event.RightDown()) {
+ SetFocus();
+ CGEditor::status.RightClick = true;
+ Refresh();
+ } else if (event.GetWheelRotation() != 0) {
+ SetFocus();
+ if (event.GetWheelRotation() < 0) {
+ CGEditor::status.EventVScroll = 50;
+ } else {
+ CGEditor::status.EventVScroll = -50;
+ }
+ Refresh();
+ }
+
+ // Should another draw of CGEditor be made?
+ if (NeedRedraw) {
+ Refresh();
+ NeedRedraw = false;
+ }
+}
+
+void EditorCanvas::SetMoves(HalfMove *moves, HalfMove *current) {
+ CGEditor::status.Moves = moves;
+ CGEditor::status.CurrentMove = current;
+
+ Refresh();
+}
+
+void EditorCanvas::OnKeyEvent(wxKeyEvent &event) {
+ if (event.GetKeyCode() == WXK_LEFT) {
+ wxCommandEvent previousEvent(PREVIOUS_MOVE_EVENT, GetId());
+ previousEvent.SetEventObject(this);
+ ProcessEvent(previousEvent);
+ } else if (event.GetKeyCode() == WXK_RIGHT) {
+ wxCommandEvent nextEvent(NEXT_MOVE_EVENT, GetId());
+ nextEvent.SetEventObject(this);
+ ProcessEvent(nextEvent);
+ }
+}
+
+wxBEGIN_EVENT_TABLE(EditorCanvas, wxPanel) EVT_PAINT(EditorCanvas::OnPaint)
+ EVT_MOUSE_EVENTS(EditorCanvas::MouseEvent)
+ EVT_CHAR_HOOK(EditorCanvas::OnKeyEvent) wxEND_EVENT_TABLE()
diff --git a/src/game_tab/editor/EditorCanvas.hpp b/src/game_tab/editor/EditorCanvas.hpp
new file mode 100644
index 0000000..b3bb29f
--- /dev/null
+++ b/src/game_tab/editor/EditorCanvas.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "../HalfMove.hpp"
+#include "CGEditor.hpp"
+#include "ochess.hpp"
+#include "../board/Theme.hpp"
+
+// Foreign events
+wxDECLARE_EVENT(GOTO_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(PREVIOUS_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(NEXT_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(DELETE_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(PROMOTE_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(SET_AS_MAINLINE_EVENT, wxCommandEvent);
+
+class EditorCanvas : public wxPanel, public cgeditor::CGEditor {
+ wxPaintDC *dc;
+ bool NeedRedraw;
+ wxPoint Middle(cgeditor::Element e);
+ wxBitmap hide_icon;
+ Theme t;
+
+public:
+ EditorCanvas(wxFrame *parent);
+ void OnPaint(wxPaintEvent &event);
+ void MouseEvent(wxMouseEvent &event);
+ void DrawElement(const cgeditor::Element &e);
+ void HandleEvent(const cgeditor::Event &e);
+ void SetMoves(HalfMove *moves, HalfMove *current);
+ void OnKeyEvent(wxKeyEvent &event);
+
+ DECLARE_EVENT_TABLE()
+};
diff --git a/src/game_tab/editor/EditorPanel.cpp b/src/game_tab/editor/EditorPanel.cpp
new file mode 100644
index 0000000..7abe09a
--- /dev/null
+++ b/src/game_tab/editor/EditorPanel.cpp
@@ -0,0 +1,207 @@
+#include "EditorPanel.hpp"
+
+wxDEFINE_EVENT(GOTO_MOVE_EVENT, wxCommandEvent);
+wxDEFINE_EVENT(DELETE_MOVE_EVENT, wxCommandEvent);
+wxDEFINE_EVENT(PROMOTE_MOVE_EVENT, wxCommandEvent);
+wxDEFINE_EVENT(SET_AS_MAINLINE_EVENT, wxCommandEvent);
+wxDEFINE_EVENT(PREVIOUS_MOVE_EVENT, wxCommandEvent);
+wxDEFINE_EVENT(NEXT_MOVE_EVENT, wxCommandEvent);
+
+EditorPanel::EditorPanel(wxFrame *parent, Game *game)
+ : wxPanel(parent), game(game), selected_item(-1) {
+
+ // ----- Init -----
+ wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
+ wxNotebook *notebook = new wxNotebook(this, wxID_ANY);
+
+ //----- CGEditor Page -----
+ wxPanel *cgeditor_panel = new wxPanel(notebook, wxID_ANY);
+ wxBoxSizer *cgeditor_panel_sizer = new wxBoxSizer(wxVERTICAL);
+ editor_canvas = new EditorCanvas((wxFrame *)cgeditor_panel);
+ cgeditor_panel_sizer->Add(editor_canvas, 1, wxEXPAND);
+ // Comment box
+ wxStaticBox *commentBox =
+ new wxStaticBox(cgeditor_panel, wxID_ANY, "Move Comment");
+ wxBoxSizer *commentBoxSizer = new wxBoxSizer(wxVERTICAL);
+ comment_input = new wxTextCtrl(
+ commentBox, COMMENT_INPUT_BOX, wxEmptyString, // Use right ID
+ wxDefaultPosition, wxSize(0, 150), wxTE_MULTILINE);
+ commentBoxSizer->Add(comment_input, 0, wxEXPAND);
+ commentBox->SetSizer(commentBoxSizer);
+ cgeditor_panel_sizer->Add(commentBox, 0, wxEXPAND);
+ cgeditor_panel->SetSizer(cgeditor_panel_sizer);
+
+ //----- Tags Page -----
+ wxPanel *tag_panel = new wxPanel(notebook, wxID_ANY);
+ wxBoxSizer *tag_panel_sizer = new wxBoxSizer(wxVERTICAL);
+ wxPanel *tag_edit_panel = new wxPanel(tag_panel, wxID_ANY);
+ wxBoxSizer *tag_edit_panel_sizer = new wxBoxSizer(wxVERTICAL);
+ tagTextCtrl = new wxTextCtrl(tag_edit_panel, wxID_ANY);
+ tagTextCtrl->SetHint(wxString("Name"));
+ tag_edit_panel_sizer->Add(tagTextCtrl, 1, wxEXPAND);
+ valueTextCtrl = new wxTextCtrl(tag_edit_panel, wxID_ANY);
+ valueTextCtrl->SetHint(wxString("Value"));
+ tag_edit_panel_sizer->Add(valueTextCtrl, 1, wxEXPAND);
+ tag_edit_panel_sizer->Add(new wxButton(tag_edit_panel, UPDATE_BTN, L"Update"),
+ 1, wxEXPAND);
+ tag_edit_panel->SetSizer(tag_edit_panel_sizer);
+ tag_panel_sizer->Add(tag_edit_panel, 0, wxEXPAND);
+ tags_list = new wxListCtrl(tag_panel, wxID_ANY, wxDefaultPosition,
+ wxDefaultSize, wxLC_REPORT);
+ tags_list->InsertColumn(0, L"Name");
+ tags_list->InsertColumn(1, L"Value", wxLIST_FORMAT_LEFT, 500);
+ RefreshTagsList();
+ tag_panel_sizer->Add(tags_list, 1, wxEXPAND);
+ delete_button = new wxButton(tag_panel, DELETE_BTN, L"Delete");
+ delete_button->Enable(false);
+ tag_panel_sizer->Add(delete_button, 0, wxEXPAND);
+ tag_panel->SetSizer(tag_panel_sizer);
+
+ //----- Notebook -----
+ notebook->AddPage(cgeditor_panel, L"Editor");
+ notebook->AddPage(tag_panel, L"Tags");
+ sizer->Add(notebook, 1, wxEXPAND);
+
+ //----- Sizer -----
+ this->SetSizer(sizer);
+
+ // Bind events
+ this->Bind(wxEVT_TEXT, &EditorPanel::OnCommentChange, this,
+ COMMENT_INPUT_BOX);
+ this->Bind(GOTO_MOVE_EVENT, &EditorPanel::OnGotoMove, this, wxID_ANY);
+ this->Bind(DELETE_MOVE_EVENT, &EditorPanel::OnMoveDelete, this, wxID_ANY);
+ this->Bind(PROMOTE_MOVE_EVENT, &EditorPanel::OnMovePromote, this, wxID_ANY);
+ this->Bind(SET_AS_MAINLINE_EVENT, &EditorPanel::OnMoveSetAsMainline, this,
+ wxID_ANY);
+ this->Bind(NEXT_MOVE_EVENT, &EditorPanel::OnNextMove, this, wxID_ANY);
+ this->Bind(PREVIOUS_MOVE_EVENT, &EditorPanel::OnPreviousMove, this, wxID_ANY);
+ this->Bind(wxEVT_LIST_ITEM_SELECTED, &EditorPanel::OnTagSelected, this,
+ wxID_ANY);
+ this->Bind(wxEVT_LIST_ITEM_DESELECTED, &EditorPanel::OnTagDeselected, this,
+ wxID_ANY);
+ this->Bind(wxEVT_BUTTON, &EditorPanel::OnApply, this, UPDATE_BTN);
+ this->Bind(wxEVT_BUTTON, &EditorPanel::OnDelete, this, DELETE_BTN);
+}
+
+void EditorPanel::OnTagSelected(wxListEvent &event) {
+ wxListItem item = event.GetItem();
+ std::string key = item.GetText().ToStdString();
+ tagTextCtrl->ChangeValue(key);
+ item.SetColumn(1);
+ tags_list->GetItem(item);
+ valueTextCtrl->ChangeValue(item.GetText().ToStdString());
+ selected_item = item.GetId();
+ delete_button->Enable(true);
+}
+
+void EditorPanel::OnTagDeselected(wxListEvent &event) {
+ selected_item = -1;
+ delete_button->Enable(false);
+}
+
+void EditorPanel::NotifyBoard() {
+ wxCommandEvent previousEvent(GAME_CHANGE, GetId());
+ previousEvent.SetEventObject(this);
+ ProcessEvent(previousEvent);
+}
+
+void EditorPanel::OnCommentChange(wxCommandEvent &event) {
+ wxLogDebug("EditorPanel: comment input change");
+ HalfMove *m = game->GetCurrentMove();
+ if (m != NULL) {
+ m->SetComment(event.GetString().ToStdString());
+ }
+ editor_canvas->Refresh();
+}
+
+void EditorPanel::OnApply(wxCommandEvent &event) {
+ std::string key = tagTextCtrl->GetValue().ToStdString();
+ if (key == "FEN") {
+ SHOW_DIALOG_ERROR("Editing the FEN tag is forbidden");
+ return;
+ }
+ if (key.size() > 0) {
+ std::string value = valueTextCtrl->GetValue().ToStdString();
+ game->SetTag(key, value);
+ RefreshTagsList();
+ wxCommandEvent event(REFRESH_TAB_TITLE, GetId());
+ event.SetEventObject(this);
+ ProcessEvent(event);
+ }
+}
+
+void EditorPanel::OnDelete(wxCommandEvent &event) {
+ if (selected_item >= 0) {
+ wxListItem item;
+ item.SetColumn(0);
+ item.SetId(selected_item);
+ tags_list->GetItem(item);
+ std::string key = item.GetText().ToStdString();
+ if (key != "FEN") {
+ game->DeleteTag(key);
+ selected_item = -1;
+ RefreshTagsList();
+ } else {
+ SHOW_DIALOG_ERROR("Deleting the FEN tag is forbidden.");
+ }
+ }
+}
+
+void EditorPanel::OnGotoMove(wxCommandEvent &event) {
+ wxLogDebug("EditorPanel: received GOTO_MOVE_EVENT");
+ game->SetCurrent((HalfMove *)event.GetClientData());
+ NotifyBoard();
+ editor_canvas->Refresh();
+}
+
+void EditorPanel::OnMoveDelete(wxCommandEvent &event) {
+ game->DeleteMove((HalfMove *)event.GetClientData());
+ NotifyBoard();
+ editor_canvas->Refresh();
+}
+
+void EditorPanel::OnMovePromote(wxCommandEvent &event) {
+ wxLogDebug("EditorPanel: promote move called");
+ game->PromoteMove((HalfMove *)event.GetClientData());
+ NotifyBoard();
+ editor_canvas->Refresh();
+}
+
+void EditorPanel::OnMoveSetAsMainline(wxCommandEvent &event) {
+ wxLogDebug("EditorPanel: set move as mainline called");
+ game->SetMoveAsMainline((HalfMove *)event.GetClientData());
+ NotifyBoard();
+ editor_canvas->Refresh();
+}
+
+void EditorPanel::Notify() {
+ HalfMove *m = game->GetCurrentMove();
+ if (m != NULL) {
+ comment_input->ChangeValue(
+ m->GetComment()); // ChangeValue do not raise events
+ }
+ editor_canvas->SetMoves(game->GetMoves(), m);
+}
+
+void EditorPanel::RefreshTagsList() {
+ tags_list->DeleteAllItems();
+ for (std::string s : game->ListTags()) {
+ long index = tags_list->InsertItem(0, s);
+ tags_list->SetItem(index, 1, game->GetTag(s));
+ if (s == "FEN") {
+ tags_list->SetItemBackgroundColour(index, wxColour(200, 200, 200));
+ }
+ }
+}
+
+void EditorPanel::OnPreviousMove(wxCommandEvent &event) {
+ game->Previous();
+ Notify();
+ NotifyBoard();
+}
+
+void EditorPanel::OnNextMove(wxCommandEvent &event) {
+ game->Next();
+ Notify();
+ NotifyBoard();
+} \ No newline at end of file
diff --git a/src/game_tab/editor/EditorPanel.hpp b/src/game_tab/editor/EditorPanel.hpp
new file mode 100644
index 0000000..0a7c0d0
--- /dev/null
+++ b/src/game_tab/editor/EditorPanel.hpp
@@ -0,0 +1,44 @@
+#include "../Game.hpp"
+#include "EditorCanvas.hpp"
+#include "ochess.hpp"
+#include <wx/listctrl.h>
+#include <wx/notebook.h>
+
+// Local events
+wxDECLARE_EVENT(GOTO_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(DELETE_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(PROMOTE_MOVE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(SET_AS_MAINLINE_EVENT, wxCommandEvent);
+wxDECLARE_EVENT(REFRESH_TAB_TITLE, wxCommandEvent);
+
+// Foreign events
+wxDECLARE_EVENT(GAME_CHANGE, wxCommandEvent);
+
+enum { COMMENT_INPUT_BOX = wxID_HIGHEST + 100, UPDATE_BTN, DELETE_BTN };
+
+class EditorPanel : public wxPanel {
+ Game *game;
+ EditorCanvas *editor_canvas;
+ wxTextCtrl *comment_input;
+ wxListCtrl *tags_list;
+ wxTextCtrl *tagTextCtrl, *valueTextCtrl;
+ wxButton *delete_button;
+ long selected_item;
+
+public:
+ EditorPanel(wxFrame *parent, Game *game);
+ void NotifyBoard();
+ void Notify();
+ void OnCommentChange(wxCommandEvent &event);
+ void OnGotoMove(wxCommandEvent &event);
+ void OnMoveDelete(wxCommandEvent &event);
+ void OnMovePromote(wxCommandEvent &event);
+ void OnMoveSetAsMainline(wxCommandEvent &event);
+ void RefreshTagsList();
+ void OnTagSelected(wxListEvent &event);
+ void OnTagDeselected(wxListEvent &event);
+ void OnApply(wxCommandEvent &event);
+ void OnDelete(wxCommandEvent &event);
+ void OnPreviousMove(wxCommandEvent &event);
+ void OnNextMove(wxCommandEvent &event);
+}; \ No newline at end of file