Chess: Port application to GML

This commit ports the chess application to GML in order to
facilitate the addition of widgets in the future.
This commit is contained in:
dgaston 2024-05-15 10:54:32 -04:00 committed by Sam Atkins
parent 10dbadced8
commit 55fe04a6fa
Notes: sideshowbarker 2024-07-17 11:33:34 +09:00
9 changed files with 141 additions and 41 deletions

View File

@ -5,11 +5,16 @@ serenity_component(
DEPENDS ChessEngine
)
compile_gml(Chess.gml ChessGML.cpp chess_gml)
compile_gml(PromotionWidget.gml PromotionWidgetGML.cpp promotionWidget_gml)
set(SOURCES
main.cpp
ChessWidget.cpp
PromotionDialog.cpp
Engine.cpp
ChessGML.cpp
PromotionWidgetGML.cpp
)
serenity_app(Chess ICON app-chess)

View File

@ -0,0 +1,8 @@
@Chess::MainWidget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {}
@Chess::ChessWidget {
name: "chess_widget"
}
}

View File

@ -289,7 +289,7 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
Chess::Move move = { m_moving_square, target_square.release_value() };
if (board().is_promotion_move(move)) {
auto promotion_dialog = PromotionDialog::construct(*this);
auto promotion_dialog = MUST(PromotionDialog::try_create(*this));
if (promotion_dialog->exec() == PromotionDialog::ExecResult::OK)
move.promote_to = promotion_dialog->selected_piece();
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024, the SerenityOS developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Widget.h>
namespace Chess {
class MainWidget : public GUI::Widget {
C_OBJECT_ABSTRACT(MainWidget)
public:
static ErrorOr<NonnullRefPtr<MainWidget>> try_create();
virtual ~MainWidget() override = default;
private:
MainWidget() = default;
};
}

View File

@ -11,28 +11,34 @@
namespace Chess {
PromotionDialog::PromotionDialog(ChessWidget& chess_widget)
ErrorOr<NonnullRefPtr<PromotionDialog>> PromotionDialog::try_create(ChessWidget& chess_widget)
{
auto promotion_widget = TRY(Chess::PromotionWidget::try_create());
auto promotion_dialog = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PromotionDialog(move(promotion_widget), chess_widget)));
return promotion_dialog;
}
PromotionDialog::PromotionDialog(NonnullRefPtr<Chess::PromotionWidget> promotion_widget, ChessWidget& chess_widget)
: Dialog(chess_widget.window())
, m_selected_piece(Chess::Type::None)
{
set_title("Choose piece to promote to");
set_icon(chess_widget.window()->icon());
resize(70 * 4, 70);
set_main_widget(promotion_widget);
auto main_widget = set_main_widget<GUI::Frame>();
main_widget->set_frame_style(Gfx::FrameStyle::SunkenContainer);
main_widget->set_fill_with_background_color(true);
main_widget->set_layout<GUI::HorizontalBoxLayout>();
for (auto const& type : { Chess::Type::Queen, Chess::Type::Knight, Chess::Type::Rook, Chess::Type::Bishop }) {
auto& button = main_widget->add<GUI::Button>();
button.set_fixed_height(70);
button.set_icon(chess_widget.get_piece_graphic({ chess_widget.board().turn(), type }));
button.on_click = [this, type](auto) {
m_selected_piece = type;
auto initialize_promotion_button = [&](StringView button_name, Chess::Type piece) {
auto button = promotion_widget->find_descendant_of_type_named<GUI::Button>(button_name);
button->set_icon(chess_widget.get_piece_graphic({ chess_widget.board().turn(), piece }));
button->on_click = [this, piece](auto) {
m_selected_piece = piece;
done(ExecResult::OK);
};
}
};
initialize_promotion_button("queen_button"sv, Type::Queen);
initialize_promotion_button("knight_button"sv, Type::Knight);
initialize_promotion_button("rook_button"sv, Type::Rook);
initialize_promotion_button("bishop_button"sv, Type::Bishop);
}
void PromotionDialog::event(Core::Event& event)

View File

@ -7,17 +7,19 @@
#pragma once
#include "ChessWidget.h"
#include "PromotionWidget.h"
#include <LibGUI/Dialog.h>
namespace Chess {
class PromotionDialog final : public GUI::Dialog {
C_OBJECT(PromotionDialog)
C_OBJECT_ABSTRACT(PromotionDialog)
public:
static ErrorOr<NonnullRefPtr<PromotionDialog>> try_create(ChessWidget& chess_widget);
Chess::Type selected_piece() const { return m_selected_piece; }
private:
explicit PromotionDialog(ChessWidget& chess_widget);
PromotionDialog(NonnullRefPtr<Chess::PromotionWidget> promotion_widget, ChessWidget& chess_widget);
virtual void event(Core::Event&) override;
Chess::Type m_selected_piece;

View File

@ -0,0 +1,29 @@
@Chess::PromotionWidget {
fixed_height: 70
fill_with_background_color: true
layout: @GUI::HorizontalBoxLayout {}
@GUI::Button {
fixed_width: 70
fixed_height: 70
name: "queen_button"
}
@GUI::Button {
fixed_width: 70
fixed_height: 70
name: "knight_button"
}
@GUI::Button {
fixed_width: 70
fixed_height: 70
name: "rook_button"
}
@GUI::Button {
fixed_width: 70
fixed_height: 70
name: "bishop_button"
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024, the SerenityOS developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Widget.h>
namespace Chess {
class PromotionWidget : public GUI::Widget {
C_OBJECT_ABSTRACT(PromotionWidget)
public:
static ErrorOr<NonnullRefPtr<PromotionWidget>> try_create();
virtual ~PromotionWidget() override = default;
private:
PromotionWidget() = default;
};
}

View File

@ -8,6 +8,7 @@
*/
#include "ChessWidget.h"
#include "MainWidget.h"
#include <LibConfig/Client.h>
#include <LibCore/System.h>
#include <LibDesktop/Launcher.h>
@ -64,8 +65,11 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-chess"sv));
auto window = GUI::Window::construct();
auto widget = TRY(Chess::ChessWidget::try_create());
window->set_main_widget(widget);
auto main_widget = TRY(Chess::MainWidget::try_create());
auto& chess_widget = *main_widget->find_descendant_of_type_named<Chess::ChessWidget>("chess_widget");
window->set_main_widget(main_widget);
window->set_focused_widget(&chess_widget);
auto engines = TRY(available_engines());
for (auto const& engine : engines)
@ -85,19 +89,19 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
window->set_icon(app_icon.bitmap_for_size(16));
widget->set_piece_set(Config::read_string("Games"sv, "Chess"sv, "PieceSet"sv, "Classic"sv));
widget->set_board_theme(Config::read_string("Games"sv, "Chess"sv, "BoardTheme"sv, "Beige"sv));
widget->set_coordinates(Config::read_bool("Games"sv, "Chess"sv, "ShowCoordinates"sv, true));
widget->set_show_available_moves(Config::read_bool("Games"sv, "Chess"sv, "ShowAvailableMoves"sv, true));
widget->set_highlight_checks(Config::read_bool("Games"sv, "Chess"sv, "HighlightChecks"sv, true));
chess_widget.set_piece_set(Config::read_string("Games"sv, "Chess"sv, "PieceSet"sv, "Classic"sv));
chess_widget.set_board_theme(Config::read_string("Games"sv, "Chess"sv, "BoardTheme"sv, "Beige"sv));
chess_widget.set_coordinates(Config::read_bool("Games"sv, "Chess"sv, "ShowCoordinates"sv, true));
chess_widget.set_show_available_moves(Config::read_bool("Games"sv, "Chess"sv, "ShowAvailableMoves"sv, true));
chess_widget.set_highlight_checks(Config::read_bool("Games"sv, "Chess"sv, "HighlightChecks"sv, true));
auto game_menu = window->add_menu("&Game"_string);
game_menu->add_action(GUI::Action::create("&Resign", { Mod_None, Key_F3 }, [&](auto&) {
widget->resign();
chess_widget.resign();
}));
game_menu->add_action(GUI::Action::create("&Flip Board", { Mod_Ctrl, Key_F }, [&](auto&) {
widget->flip_board();
chess_widget.flip_board();
}));
game_menu->add_separator();
@ -112,7 +116,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
if (result.is_error())
return;
if (auto maybe_error = widget->import_pgn(*result.value().release_stream()); maybe_error.is_error()) {
if (auto maybe_error = chess_widget.import_pgn(*result.value().release_stream()); maybe_error.is_error()) {
auto error_message = maybe_error.release_error().message();
dbgln("Failed to import PGN: {}", error_message);
GUI::MessageBox::show(window, error_message, "Import Error"sv, GUI::MessageBox::Type::Information);
@ -125,23 +129,23 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
if (result.is_error())
return;
if (auto maybe_error = widget->export_pgn(*result.value().release_stream()); maybe_error.is_error())
if (auto maybe_error = chess_widget.export_pgn(*result.value().release_stream()); maybe_error.is_error())
dbgln("Failed to export PGN: {}", maybe_error.release_error());
else
dbgln("Exported PGN file to {}", result.value().filename());
}));
game_menu->add_action(GUI::Action::create("&Copy FEN", { Mod_Ctrl, Key_C }, [&](auto&) {
GUI::Clipboard::the().set_data(widget->get_fen().release_value_but_fixme_should_propagate_errors().bytes());
GUI::Clipboard::the().set_data(chess_widget.get_fen().release_value_but_fixme_should_propagate_errors().bytes());
GUI::MessageBox::show(window, "Board state copied to clipboard as FEN."sv, "Copy FEN"sv, GUI::MessageBox::Type::Information);
}));
game_menu->add_separator();
game_menu->add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"sv)), [&](auto&) {
if (widget->board().game_result() == Chess::Board::Result::NotFinished) {
if (widget->resign() < 0)
if (chess_widget.board().game_result() == Chess::Board::Result::NotFinished) {
if (chess_widget.resign() < 0)
return;
}
widget->reset();
chess_widget.reset();
}));
game_menu->add_separator();
@ -154,11 +158,11 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
game_menu->add_action(settings_action);
auto show_available_moves_action = GUI::Action::create_checkable("Show Available Moves", [&](auto& action) {
widget->set_show_available_moves(action.is_checked());
widget->update();
chess_widget.set_show_available_moves(action.is_checked());
chess_widget.update();
Config::write_bool("Games"sv, "Chess"sv, "ShowAvailableMoves"sv, action.is_checked());
});
show_available_moves_action->set_checked(widget->show_available_moves());
show_available_moves_action->set_checked(chess_widget.show_available_moves());
game_menu->add_action(show_available_moves_action);
game_menu->add_separator();
@ -172,7 +176,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
engines_action_group.set_exclusive(true);
auto engine_submenu = engine_menu->add_submenu("&Engine"_string);
auto human_engine_checkbox = GUI::Action::create_checkable("Human", [&](auto&) {
widget->set_engine(nullptr);
chess_widget.set_engine(nullptr);
});
human_engine_checkbox->set_checked(true);
engines_action_group.add_action(human_engine_checkbox);
@ -182,17 +186,17 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto action = GUI::Action::create_checkable(engine.name, [&](auto&) {
auto new_engine = Engine::construct(engine.path);
new_engine->on_connection_lost = [&]() {
if (!widget->want_engine_move())
if (!chess_widget.want_engine_move())
return;
auto rc = GUI::MessageBox::show(window, "Connection to the chess engine was lost while waiting for a move. Do you want to try again?"sv, "Chess"sv, GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo);
if (rc == GUI::Dialog::ExecResult::Yes)
widget->input_engine_move();
chess_widget.input_engine_move();
else
human_engine_checkbox->activate();
};
widget->set_engine(move(new_engine));
widget->input_engine_move();
chess_widget.set_engine(move(new_engine));
chess_widget.input_engine_move();
});
engines_action_group.add_action(*action);
engine_submenu->add_action(*action);
@ -211,7 +215,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
help_menu->add_action(GUI::CommonActions::make_about_action("Chess"_string, app_icon, window));
window->show();
widget->reset();
chess_widget.reset();
return app->exec();
}