diff --git a/Userland/Games/Chess/CMakeLists.txt b/Userland/Games/Chess/CMakeLists.txt index 21ed49fd56e..1f483b6160c 100644 --- a/Userland/Games/Chess/CMakeLists.txt +++ b/Userland/Games/Chess/CMakeLists.txt @@ -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) diff --git a/Userland/Games/Chess/Chess.gml b/Userland/Games/Chess/Chess.gml new file mode 100644 index 00000000000..70fe7db793e --- /dev/null +++ b/Userland/Games/Chess/Chess.gml @@ -0,0 +1,8 @@ +@Chess::MainWidget { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout {} + + @Chess::ChessWidget { + name: "chess_widget" + } +} diff --git a/Userland/Games/Chess/ChessWidget.cpp b/Userland/Games/Chess/ChessWidget.cpp index a54f1965df0..179994c1ec0 100644 --- a/Userland/Games/Chess/ChessWidget.cpp +++ b/Userland/Games/Chess/ChessWidget.cpp @@ -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(); } diff --git a/Userland/Games/Chess/MainWidget.h b/Userland/Games/Chess/MainWidget.h new file mode 100644 index 00000000000..b1bbbab407b --- /dev/null +++ b/Userland/Games/Chess/MainWidget.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, the SerenityOS developers + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Chess { + +class MainWidget : public GUI::Widget { + C_OBJECT_ABSTRACT(MainWidget) +public: + static ErrorOr> try_create(); + virtual ~MainWidget() override = default; + +private: + MainWidget() = default; +}; + +} diff --git a/Userland/Games/Chess/PromotionDialog.cpp b/Userland/Games/Chess/PromotionDialog.cpp index affb21146e7..eb159d03c7a 100644 --- a/Userland/Games/Chess/PromotionDialog.cpp +++ b/Userland/Games/Chess/PromotionDialog.cpp @@ -11,28 +11,34 @@ namespace Chess { -PromotionDialog::PromotionDialog(ChessWidget& chess_widget) +ErrorOr> 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 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(); - main_widget->set_frame_style(Gfx::FrameStyle::SunkenContainer); - main_widget->set_fill_with_background_color(true); - main_widget->set_layout(); - - for (auto const& type : { Chess::Type::Queen, Chess::Type::Knight, Chess::Type::Rook, Chess::Type::Bishop }) { - auto& button = main_widget->add(); - 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(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) diff --git a/Userland/Games/Chess/PromotionDialog.h b/Userland/Games/Chess/PromotionDialog.h index 54a349ca73b..5085f9692cf 100644 --- a/Userland/Games/Chess/PromotionDialog.h +++ b/Userland/Games/Chess/PromotionDialog.h @@ -7,17 +7,19 @@ #pragma once #include "ChessWidget.h" +#include "PromotionWidget.h" #include namespace Chess { class PromotionDialog final : public GUI::Dialog { - C_OBJECT(PromotionDialog) + C_OBJECT_ABSTRACT(PromotionDialog) public: + static ErrorOr> try_create(ChessWidget& chess_widget); Chess::Type selected_piece() const { return m_selected_piece; } private: - explicit PromotionDialog(ChessWidget& chess_widget); + PromotionDialog(NonnullRefPtr promotion_widget, ChessWidget& chess_widget); virtual void event(Core::Event&) override; Chess::Type m_selected_piece; diff --git a/Userland/Games/Chess/PromotionWidget.gml b/Userland/Games/Chess/PromotionWidget.gml new file mode 100644 index 00000000000..31620b4d873 --- /dev/null +++ b/Userland/Games/Chess/PromotionWidget.gml @@ -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" + } +} diff --git a/Userland/Games/Chess/PromotionWidget.h b/Userland/Games/Chess/PromotionWidget.h new file mode 100644 index 00000000000..1d2d99b1ea3 --- /dev/null +++ b/Userland/Games/Chess/PromotionWidget.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, the SerenityOS developers + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Chess { + +class PromotionWidget : public GUI::Widget { + C_OBJECT_ABSTRACT(PromotionWidget) +public: + static ErrorOr> try_create(); + virtual ~PromotionWidget() override = default; + +private: + PromotionWidget() = default; +}; + +} diff --git a/Userland/Games/Chess/main.cpp b/Userland/Games/Chess/main.cpp index e202bfef589..b1883cd7071 100644 --- a/Userland/Games/Chess/main.cpp +++ b/Userland/Games/Chess/main.cpp @@ -8,6 +8,7 @@ */ #include "ChessWidget.h" +#include "MainWidget.h" #include #include #include @@ -64,8 +65,11 @@ ErrorOr 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_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 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 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 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 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 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 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 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(); }