From 05913b853a686bd0e9c9721a0f16158b743f5739 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 1 Feb 2023 21:09:32 +0000 Subject: [PATCH] GamesSettings: Add chess settings :^) This adds a tab for configuring the appearance of Chess, along with a preview. --- .../Applications/GamesSettings/CMakeLists.txt | 11 +- .../GamesSettings/ChessSettingsWidget.cpp | 305 ++++++++++++++++++ .../GamesSettings/ChessSettingsWidget.gml | 58 ++++ .../GamesSettings/ChessSettingsWidget.h | 39 +++ Userland/Applications/GamesSettings/main.cpp | 9 +- 5 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 Userland/Applications/GamesSettings/ChessSettingsWidget.cpp create mode 100644 Userland/Applications/GamesSettings/ChessSettingsWidget.gml create mode 100644 Userland/Applications/GamesSettings/ChessSettingsWidget.h diff --git a/Userland/Applications/GamesSettings/CMakeLists.txt b/Userland/Applications/GamesSettings/CMakeLists.txt index 848b7e16e9..6edfea6ca8 100644 --- a/Userland/Applications/GamesSettings/CMakeLists.txt +++ b/Userland/Applications/GamesSettings/CMakeLists.txt @@ -1,19 +1,22 @@ serenity_component( - GamesSettings - REQUIRED - TARGETS GamesSettings + GamesSettings + REQUIRED + TARGETS GamesSettings ) compile_gml(CardSettingsWidget.gml CardSettingsWidgetGML.h card_settings_widget_gml) +compile_gml(ChessSettingsWidget.gml ChessSettingsWidgetGML.h chess_settings_widget_gml) set(SOURCES main.cpp CardSettingsWidget.cpp + ChessSettingsWidget.cpp ) set(GENERATED_SOURCES CardSettingsWidgetGML.h + ChessSettingsWidgetGML.h ) serenity_app(GamesSettings ICON games) -target_link_libraries(GamesSettings PRIVATE LibConfig LibCore LibGfx LibGUI LibMain LibCards) +target_link_libraries(GamesSettings PRIVATE LibConfig LibCore LibGfx LibGUI LibMain LibCards LibChess) diff --git a/Userland/Applications/GamesSettings/ChessSettingsWidget.cpp b/Userland/Applications/GamesSettings/ChessSettingsWidget.cpp new file mode 100644 index 0000000000..881916b84a --- /dev/null +++ b/Userland/Applications/GamesSettings/ChessSettingsWidget.cpp @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ChessSettingsWidget.h" +#include "AK/String.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GamesSettings { + +struct BoardTheme { + StringView name; + Color dark_square_color; + Color light_square_color; +}; + +// The following colors have been taken from lichess.org, but I'm pretty sure they took them from chess.com. +Array s_board_themes { + BoardTheme { "Beige"sv, Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) }, + BoardTheme { "Blue"sv, Color::from_rgb(0x8ca2ad), Color::from_rgb(0xdee3e6) }, + BoardTheme { "Green"sv, Color::from_rgb(0x86a666), Color::from_rgb(0xffffdd) }, +}; + +static BoardTheme& get_board_theme(StringView name) +{ + for (size_t i = 0; i < s_board_themes.size(); ++i) { + if (s_board_themes[i].name == name) + return s_board_themes[i]; + } + return s_board_themes[0]; +} + +class BoardThemeModel final : public GUI::Model { +public: + static ErrorOr> create() + { + return adopt_nonnull_ref_or_enomem(new (nothrow) BoardThemeModel()); + } + + ~BoardThemeModel() = default; + + virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override + { + return s_board_themes.size(); + } + + virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override + { + return 1; + } + + virtual GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role = GUI::ModelRole::Display) const override + { + if (role != GUI::ModelRole::Display) + return {}; + if (!is_within_range(index)) + return {}; + + return s_board_themes.at(index.row()).name; + } + +private: + BoardThemeModel() + { + } +}; + +static ErrorOr> load_piece_image(StringView set, StringView image) +{ + auto path = TRY(String::formatted("/res/icons/chess/sets/{}/{}", set, image)); + return Gfx::Bitmap::load_from_file(path.bytes_as_string_view()); +} + +class ChessGamePreview final : public GUI::Frame { + C_OBJECT_ABSTRACT(ChessGamePreview) + +public: + static ErrorOr> try_create() + { + auto preview = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ChessGamePreview())); + return preview; + } + + virtual ~ChessGamePreview() = default; + + ErrorOr set_piece_set_name(DeprecatedString const& piece_set_name) + { + if (m_piece_set_name == piece_set_name.view()) + return {}; + + m_piece_set_name = TRY(String::from_utf8(piece_set_name)); + m_piece_images.clear(); + + m_piece_images.set({ Chess::Color::White, Chess::Type::Pawn }, TRY(load_piece_image(m_piece_set_name, "white-pawn.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::Pawn }, TRY(load_piece_image(m_piece_set_name, "black-pawn.png"sv))); + m_piece_images.set({ Chess::Color::White, Chess::Type::Knight }, TRY(load_piece_image(m_piece_set_name, "white-knight.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::Knight }, TRY(load_piece_image(m_piece_set_name, "black-knight.png"sv))); + m_piece_images.set({ Chess::Color::White, Chess::Type::Bishop }, TRY(load_piece_image(m_piece_set_name, "white-bishop.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::Bishop }, TRY(load_piece_image(m_piece_set_name, "black-bishop.png"sv))); + m_piece_images.set({ Chess::Color::White, Chess::Type::Rook }, TRY(load_piece_image(m_piece_set_name, "white-rook.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::Rook }, TRY(load_piece_image(m_piece_set_name, "black-rook.png"sv))); + m_piece_images.set({ Chess::Color::White, Chess::Type::Queen }, TRY(load_piece_image(m_piece_set_name, "white-queen.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::Queen }, TRY(load_piece_image(m_piece_set_name, "black-queen.png"sv))); + m_piece_images.set({ Chess::Color::White, Chess::Type::King }, TRY(load_piece_image(m_piece_set_name, "white-king.png"sv))); + m_piece_images.set({ Chess::Color::Black, Chess::Type::King }, TRY(load_piece_image(m_piece_set_name, "black-king.png"sv))); + + update(); + + return {}; + } + + void set_dark_square_color(Gfx::Color dark_square_color) + { + if (m_dark_square_color == dark_square_color) + return; + + m_dark_square_color = dark_square_color; + update(); + } + + void set_light_square_color(Gfx::Color light_square_color) + { + if (m_light_square_color == light_square_color) + return; + + m_light_square_color = light_square_color; + update(); + } + + void set_show_coordinates(bool show_coordinates) + { + if (m_show_coordinates == show_coordinates) + return; + + m_show_coordinates = show_coordinates; + update(); + } + +private: + ChessGamePreview() = default; + + virtual void paint_event(GUI::PaintEvent& event) override + { + GUI::Frame::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + painter.add_clip_rect(frame_inner_rect()); + + auto& coordinate_font = Gfx::FontDatabase::default_font().bold_variant(); + + // To show all the piece graphics, we need at least 12 squares visible. + // With the same preview size as we use for card games, a nice fit is 2 ranks of 6. + // There are definitely better ways of doing this, but it'll do. ;^) + size_t tile_size = 61; + + auto rect_for_square = [&](Chess::Square const& square) { + return Gfx::IntRect { + frame_inner_rect().left() + square.file * tile_size, + frame_inner_rect().bottom() + 1 - (square.rank + 1) * tile_size, + tile_size, + tile_size + }; + }; + + for (int rank = 0; rank < 3; ++rank) { + for (int file = 0; file < 8; ++file) { + Chess::Square square { rank, file }; + auto tile_rect = rect_for_square(square); + painter.fill_rect(tile_rect, square.is_light() ? m_light_square_color : m_dark_square_color); + + if (m_show_coordinates) { + auto coord = square.to_algebraic(); + auto text_color = square.is_light() ? m_dark_square_color : m_light_square_color; + auto shrunken_rect = tile_rect.shrunken(4, 4); + + if (square.rank == 0) + painter.draw_text(shrunken_rect, coord.substring_view(0, 1), coordinate_font, Gfx::TextAlignment::BottomRight, text_color); + + if (square.file == 0) + painter.draw_text(shrunken_rect, coord.substring_view(1, 1), coordinate_font, Gfx::TextAlignment::TopLeft, text_color); + } + } + } + + auto draw_piece = [&](Chess::Piece const& piece, Chess::Square const& square) { + auto& bitmap = *m_piece_images.get(piece).value(); + painter.draw_scaled_bitmap( + rect_for_square(square), + bitmap, + bitmap.rect(), + 1.0f, + Gfx::Painter::ScalingMode::BilinearBlend); + }; + + draw_piece({ Chess::Color::White, Chess::Type::King }, { 0, 0 }); + draw_piece({ Chess::Color::Black, Chess::Type::King }, { 1, 0 }); + draw_piece({ Chess::Color::White, Chess::Type::Queen }, { 0, 1 }); + draw_piece({ Chess::Color::Black, Chess::Type::Queen }, { 1, 1 }); + draw_piece({ Chess::Color::White, Chess::Type::Rook }, { 0, 2 }); + draw_piece({ Chess::Color::Black, Chess::Type::Rook }, { 1, 2 }); + draw_piece({ Chess::Color::White, Chess::Type::Bishop }, { 0, 3 }); + draw_piece({ Chess::Color::Black, Chess::Type::Bishop }, { 1, 3 }); + draw_piece({ Chess::Color::White, Chess::Type::Knight }, { 0, 4 }); + draw_piece({ Chess::Color::Black, Chess::Type::Knight }, { 1, 4 }); + draw_piece({ Chess::Color::White, Chess::Type::Pawn }, { 0, 5 }); + draw_piece({ Chess::Color::Black, Chess::Type::Pawn }, { 1, 5 }); + } + + HashMap> m_piece_images; + + Gfx::Color m_dark_square_color { s_board_themes[0].dark_square_color }; + Gfx::Color m_light_square_color { s_board_themes[0].light_square_color }; + bool m_show_coordinates { true }; + String m_piece_set_name; +}; + +ErrorOr> ChessSettingsWidget::try_create() +{ + auto chess_settings_widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ChessSettingsWidget)); + TRY(chess_settings_widget->initialize()); + return chess_settings_widget; +} + +ErrorOr ChessSettingsWidget::initialize() +{ + TRY(load_from_gml(chess_settings_widget_gml)); + + auto piece_set_name = Config::read_string("Games"sv, "Chess"sv, "PieceSet"sv, "stelar7"sv); + auto board_theme = get_board_theme(Config::read_string("Games"sv, "Chess"sv, "BoardTheme"sv, "Beige"sv)); + auto show_coordinates = Config::read_bool("Games"sv, "Chess"sv, "ShowCoordinates"sv, true); + + m_preview = find_descendant_of_type_named("chess_preview"); + + m_piece_set_combobox = find_descendant_of_type_named("piece_set"); + Core::DirIterator piece_set_iterator { "/res/icons/chess/sets/", Core::DirIterator::SkipParentAndBaseDir }; + while (piece_set_iterator.has_next()) + m_piece_sets.append(piece_set_iterator.next_path()); + auto piece_set_model = GUI::ItemListModel::create(m_piece_sets); + m_piece_set_combobox->set_model(piece_set_model); + m_piece_set_combobox->set_text(piece_set_name, GUI::AllowCallback::No); + m_piece_set_combobox->on_change = [&](auto& value, auto&) { + set_modified(true); + m_preview->set_piece_set_name(value).release_value_but_fixme_should_propagate_errors(); + }; + + m_board_theme_combobox = find_descendant_of_type_named("board_theme"); + m_board_theme_combobox->set_model(TRY(BoardThemeModel::create())); + m_board_theme_combobox->set_text(board_theme.name, GUI::AllowCallback::No); + m_board_theme_combobox->on_change = [&](auto&, auto& index) { + set_modified(true); + + auto& theme = s_board_themes[index.row()]; + m_preview->set_dark_square_color(theme.dark_square_color); + m_preview->set_light_square_color(theme.light_square_color); + }; + + m_show_coordinates_checkbox = find_descendant_of_type_named("show_coordinates"); + m_show_coordinates_checkbox->set_checked(show_coordinates, GUI::AllowCallback::No); + m_show_coordinates_checkbox->on_checked = [&](bool checked) { + set_modified(true); + m_preview->set_show_coordinates(checked); + }; + + TRY(m_preview->set_piece_set_name(piece_set_name)); + m_preview->set_dark_square_color(board_theme.dark_square_color); + m_preview->set_light_square_color(board_theme.light_square_color); + m_preview->set_show_coordinates(show_coordinates); + + return {}; +} + +void ChessSettingsWidget::apply_settings() +{ + Config::write_string("Games"sv, "Chess"sv, "PieceSet"sv, m_piece_set_combobox->text()); + Config::write_string("Games"sv, "Chess"sv, "BoardTheme"sv, m_board_theme_combobox->text()); + Config::write_bool("Games"sv, "Chess"sv, "ShowCoordinates"sv, m_show_coordinates_checkbox->is_checked()); +} + +void ChessSettingsWidget::reset_default_values() +{ + // FIXME: `set_text()` on a combobox doesn't trigger the `on_change` callback, but it probably should! + // Until then, we have to manually tell the preview to update. + m_piece_set_combobox->set_text("stelar7"); + (void)m_preview->set_piece_set_name("stelar7"); + auto& board_theme = get_board_theme("Beige"sv); + m_board_theme_combobox->set_text(board_theme.name); + m_preview->set_dark_square_color(board_theme.dark_square_color); + m_preview->set_light_square_color(board_theme.light_square_color); + m_show_coordinates_checkbox->set_checked(true); +} + +} + +REGISTER_WIDGET(GamesSettings, ChessGamePreview); diff --git a/Userland/Applications/GamesSettings/ChessSettingsWidget.gml b/Userland/Applications/GamesSettings/ChessSettingsWidget.gml new file mode 100644 index 0000000000..edb914b1f9 --- /dev/null +++ b/Userland/Applications/GamesSettings/ChessSettingsWidget.gml @@ -0,0 +1,58 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [8] + } + + @GamesSettings::ChessGamePreview { + name: "chess_preview" + fill_with_background_color: true + fixed_height: 160 + } + + @GUI::GroupBox { + title: "Appearance" + max_height: "shrink" + layout: @GUI::VerticalBoxLayout { + margins: [8] + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 16 + } + + @GUI::Label { + text: "Piece Set:" + text_alignment: "CenterLeft" + } + + @GUI::ComboBox { + name: "piece_set" + model_only: true + } + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout { + spacing: 16 + } + + @GUI::Label { + text: "Board Theme:" + text_alignment: "CenterLeft" + } + + @GUI::ComboBox { + name: "board_theme" + model_only: true + } + } + + @GUI::CheckBox { + name: "show_coordinates" + text: "Show coordinates" + checkbox_position: "Right" + } + } +} diff --git a/Userland/Applications/GamesSettings/ChessSettingsWidget.h b/Userland/Applications/GamesSettings/ChessSettingsWidget.h new file mode 100644 index 0000000000..df55705b04 --- /dev/null +++ b/Userland/Applications/GamesSettings/ChessSettingsWidget.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace GamesSettings { + +class ChessGamePreview; + +class ChessSettingsWidget final : public GUI::SettingsWindow::Tab { + C_OBJECT_ABSTRACT(ChessSettingsWidget) +public: + static ErrorOr> try_create(); + virtual ~ChessSettingsWidget() override = default; + + virtual void apply_settings() override; + virtual void reset_default_values() override; + +private: + ChessSettingsWidget() = default; + ErrorOr initialize(); + + Vector m_piece_sets; + + RefPtr m_preview; + RefPtr m_piece_set_combobox; + RefPtr m_board_theme_combobox; + RefPtr m_show_coordinates_checkbox; +}; + +} diff --git a/Userland/Applications/GamesSettings/main.cpp b/Userland/Applications/GamesSettings/main.cpp index b5e474ce6a..3e887f9495 100644 --- a/Userland/Applications/GamesSettings/main.cpp +++ b/Userland/Applications/GamesSettings/main.cpp @@ -1,10 +1,11 @@ /* - * Copyright (c) 2022, Sam Atkins + * Copyright (c) 2022-2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include "CardSettingsWidget.h" +#include "ChessSettingsWidget.h" #include #include #include @@ -21,10 +22,13 @@ ErrorOr serenity_main(Main::Arguments arguments) StringView selected_tab; Core::ArgsParser args_parser; - args_parser.add_option(selected_tab, "Tab, must be 'cards'", "open-tab", 't', "tab"); + args_parser.add_option(selected_tab, "Tab, one of 'cards' or 'chess'", "open-tab", 't', "tab"); args_parser.parse(arguments); TRY(Core::System::unveil("/res", "r")); + // Both of these are used by the GUI::FileSystemModel in CardSettingsWidget. + TRY(Core::System::unveil("/etc/passwd", "r")); + TRY(Core::System::unveil("/etc/group", "r")); TRY(Core::System::unveil(nullptr, nullptr)); auto app_icon = GUI::Icon::default_icon("games"sv); @@ -32,6 +36,7 @@ ErrorOr serenity_main(Main::Arguments arguments) auto window = TRY(GUI::SettingsWindow::create("Games Settings", GUI::SettingsWindow::ShowDefaultsButton::Yes)); window->set_icon(app_icon.bitmap_for_size(16)); (void)TRY(window->add_tab("Cards"sv, "cards"sv)); + (void)TRY(window->add_tab("Chess"sv, "chess"sv)); window->set_active_tab(selected_tab); window->show();