diff --git a/Userland/Applications/ThemeEditor/PreviewWidget.cpp b/Userland/Applications/ThemeEditor/PreviewWidget.cpp index ea64fb31b7..0947ffc5e7 100644 --- a/Userland/Applications/ThemeEditor/PreviewWidget.cpp +++ b/Userland/Applications/ThemeEditor/PreviewWidget.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, Andreas Kling * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2021, Antonio Di Stefano * * SPDX-License-Identifier: BSD-2-Clause */ @@ -20,6 +21,7 @@ #include #include #include +#include namespace ThemeEditor { @@ -152,6 +154,12 @@ void PreviewWidget::set_theme_from_file(String const& path, int fd) on_theme_load_from_file(path); } +void PreviewWidget::set_color_filter(OwnPtr color_filter) +{ + m_color_filter = move(color_filter); + repaint(); +} + void PreviewWidget::paint_event(GUI::PaintEvent& event) { GUI::Frame::paint_event(event); @@ -219,6 +227,24 @@ void PreviewWidget::paint_event(GUI::PaintEvent& event) paint_window("Active window", active_rect, Gfx::WindowTheme::WindowState::Active, *m_inactive_window_icon); } +void PreviewWidget::second_paint_event(GUI::PaintEvent&) +{ + if (!m_color_filter) + return; + + GUI::Painter painter(*this); + + auto target = painter.target(); + auto bitmap_clone_or_error = target->clone(); + if (bitmap_clone_or_error.is_error()) + return; + + auto clone = bitmap_clone_or_error.release_value(); + auto rect = target->rect(); + + m_color_filter->apply(*target, rect, *clone, rect); +} + void PreviewWidget::resize_event(GUI::ResizeEvent&) { m_gallery->set_relative_rect(Gfx::IntRect(0, 0, 320, 240).centered_within(rect()).translated(0, 20)); diff --git a/Userland/Applications/ThemeEditor/PreviewWidget.h b/Userland/Applications/ThemeEditor/PreviewWidget.h index 530fe6c98f..7f952b3503 100644 --- a/Userland/Applications/ThemeEditor/PreviewWidget.h +++ b/Userland/Applications/ThemeEditor/PreviewWidget.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, Andreas Kling * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2021, Antonio Di Stefano * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,6 +10,7 @@ #include #include +#include #include namespace ThemeEditor { @@ -25,6 +27,8 @@ public: void set_preview_palette(const Gfx::Palette&); void set_theme_from_file(String const& path, int fd); + void set_color_filter(OwnPtr); + Function on_theme_load_from_file; private: @@ -33,11 +37,14 @@ private: void load_theme_bitmaps(); virtual void paint_event(GUI::PaintEvent&) override; + virtual void second_paint_event(GUI::PaintEvent&) override; virtual void resize_event(GUI::ResizeEvent&) override; virtual void drop_event(GUI::DropEvent&) override; Gfx::Palette m_preview_palette; + OwnPtr m_color_filter = nullptr; + RefPtr m_active_window_icon; RefPtr m_inactive_window_icon; diff --git a/Userland/Applications/ThemeEditor/main.cpp b/Userland/Applications/ThemeEditor/main.cpp index e48b5cb77e..b6b67792a0 100644 --- a/Userland/Applications/ThemeEditor/main.cpp +++ b/Userland/Applications/ThemeEditor/main.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2020, Andreas Kling * Copyright (c) 2021, Jakob-Niklas See * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2021, Antonio Di Stefano * * SPDX-License-Identifier: BSD-2-Clause */ @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include #include @@ -316,6 +319,67 @@ ErrorOr serenity_main(Main::Arguments arguments) TRY(file_menu->try_add_separator()); TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }))); + auto accessibility_menu = TRY(window->try_add_menu("&Accessibility")); + + auto default_accessibility_action = GUI::Action::create_checkable("Default - non-impaired", { Mod_AltGr, Key_1 }, [&](auto&) { + preview_widget.set_color_filter(nullptr); + }); + default_accessibility_action->set_checked(true); + + auto pratanopia_accessibility_action = GUI::Action::create_checkable("Protanopia", { Mod_AltGr, Key_2 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_protanopia()); + }); + + auto pratanomaly_accessibility_action = GUI::Action::create_checkable("Protanomaly", { Mod_AltGr, Key_3 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_protanomaly()); + }); + + auto tritanopia_accessibility_action = GUI::Action::create_checkable("Tritanopia", { Mod_AltGr, Key_4 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_tritanopia()); + }); + + auto tritanomaly_accessibility_action = GUI::Action::create_checkable("Tritanomaly", { Mod_AltGr, Key_5 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_tritanomaly()); + }); + + auto deuteranopia_accessibility_action = GUI::Action::create_checkable("Deuteranopia", { Mod_AltGr, Key_6 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_deuteranopia()); + }); + + auto deuteranomaly_accessibility_action = GUI::Action::create_checkable("Deuteranomaly", { Mod_AltGr, Key_7 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_deuteranomaly()); + }); + + auto achromatopsia_accessibility_action = GUI::Action::create_checkable("Achromatopsia", { Mod_AltGr, Key_8 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_achromatopsia()); + }); + + auto achromatomaly_accessibility_action = GUI::Action::create_checkable("Achromatomaly", { Mod_AltGr, Key_9 }, [&](auto&) { + preview_widget.set_color_filter(Gfx::ColorBlindnessFilter::create_achromatomaly()); + }); + + auto preview_type_action_group = make(); + preview_type_action_group->set_exclusive(true); + preview_type_action_group->add_action(*default_accessibility_action); + preview_type_action_group->add_action(*pratanopia_accessibility_action); + preview_type_action_group->add_action(*pratanomaly_accessibility_action); + preview_type_action_group->add_action(*tritanopia_accessibility_action); + preview_type_action_group->add_action(*tritanomaly_accessibility_action); + preview_type_action_group->add_action(*deuteranopia_accessibility_action); + preview_type_action_group->add_action(*deuteranomaly_accessibility_action); + preview_type_action_group->add_action(*achromatopsia_accessibility_action); + preview_type_action_group->add_action(*achromatomaly_accessibility_action); + + TRY(accessibility_menu->try_add_action(default_accessibility_action)); + TRY(accessibility_menu->try_add_action(pratanopia_accessibility_action)); + TRY(accessibility_menu->try_add_action(pratanomaly_accessibility_action)); + TRY(accessibility_menu->try_add_action(tritanopia_accessibility_action)); + TRY(accessibility_menu->try_add_action(tritanomaly_accessibility_action)); + TRY(accessibility_menu->try_add_action(deuteranopia_accessibility_action)); + TRY(accessibility_menu->try_add_action(deuteranomaly_accessibility_action)); + TRY(accessibility_menu->try_add_action(achromatopsia_accessibility_action)); + TRY(accessibility_menu->try_add_action(achromatomaly_accessibility_action)); + auto help_menu = TRY(window->try_add_menu("&Help")); TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Theme Editor", app_icon, window))); diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index d048a52df8..3086cd75d2 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -13,6 +13,7 @@ set(SOURCES DDSLoader.cpp DisjointRectSet.cpp Emoji.cpp + Filters/ColorBlindnessFilter.cpp Filters/FastBoxBlurFilter.cpp FontDatabase.cpp GIFLoader.cpp diff --git a/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.cpp b/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.cpp new file mode 100644 index 0000000000..7924ccd69c --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021, Antonio Di Stefano + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ColorBlindnessFilter.h" + +namespace Gfx { + +NonnullOwnPtr ColorBlindnessFilter::create_protanopia() +{ + return make( + .56, .44, .0, + .55, .45, .0, + .0, .24, .76); +} + +NonnullOwnPtr ColorBlindnessFilter::create_protanomaly() +{ + return make( + .82, .18, .0, + .33, .67, .0, + .0, .13, .87); +} + +NonnullOwnPtr ColorBlindnessFilter::create_deuteranopia() +{ + return make( + .63, .37, .0, + .7, .3, .0, + .0, .3, .7); +} + +NonnullOwnPtr ColorBlindnessFilter::create_deuteranomaly() +{ + return make( + .8, .2, .0, + .26, .74, .0, + .0, .15, .85); +} + +NonnullOwnPtr ColorBlindnessFilter::create_tritanopia() +{ + return make( + .95, .05, .0, + .0, .44, .56, + .0, .48, .52); +} + +NonnullOwnPtr ColorBlindnessFilter::create_tritanomaly() +{ + return make( + .97, .03, .0, + .0, .73, .27, + .0, .18, .82); +} + +NonnullOwnPtr ColorBlindnessFilter::create_achromatopsia() +{ + return make( + .3, .59, .11, + .3, .59, .11, + .3, .59, .11); +} + +NonnullOwnPtr ColorBlindnessFilter::create_achromatomaly() +{ + return make( + .62, .32, .06, + .16, .78, .06, + .16, .32, .52); +} + +Color ColorBlindnessFilter::convert_color(Color original) +{ + return Color( + (u8)(original.red() * m_red_in_red_band + original.green() * m_green_in_red_band + original.blue() * m_blue_in_red_band), + (u8)(original.red() * m_red_in_green_band + original.green() * m_green_in_green_band + original.blue() * m_blue_in_green_band), + (u8)(original.red() * m_red_in_blue_band + original.green() * m_green_in_blue_band + original.blue() * m_blue_in_blue_band), + original.alpha()); +}; + +} diff --git a/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.h b/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.h new file mode 100644 index 0000000000..71a36639b9 --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/ColorBlindnessFilter.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021, Antonio Di Stefano + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "ColorFilter.h" +#include + +namespace Gfx { +class ColorBlindnessFilter : public ColorFilter { +public: + ColorBlindnessFilter( + double red_in_red_band, + double green_in_red_band, + double blue_in_red_band, + double red_in_green_band, + double green_in_green_band, + double blue_in_green_band, + double red_in_blue_band, + double green_in_blue_band, + double blue_in_blue_band) + : m_red_in_red_band(red_in_red_band) + , m_green_in_red_band(green_in_red_band) + , m_blue_in_red_band(blue_in_red_band) + , m_red_in_green_band(red_in_green_band) + , m_green_in_green_band(green_in_green_band) + , m_blue_in_green_band(blue_in_green_band) + , m_red_in_blue_band(red_in_blue_band) + , m_green_in_blue_band(green_in_blue_band) + , m_blue_in_blue_band(blue_in_blue_band) + { + } + + virtual ~ColorBlindnessFilter() = default; + virtual char const* class_name() const override { return "ColorBlindnessFilter"; } + + static NonnullOwnPtr create_protanopia(); + static NonnullOwnPtr create_protanomaly(); + static NonnullOwnPtr create_deuteranopia(); + static NonnullOwnPtr create_deuteranomaly(); + static NonnullOwnPtr create_tritanopia(); + static NonnullOwnPtr create_tritanomaly(); + static NonnullOwnPtr create_achromatopsia(); + static NonnullOwnPtr create_achromatomaly(); + +protected: + Color convert_color(Color original) override; + +private: + double m_red_in_red_band; + double m_green_in_red_band; + double m_blue_in_red_band; + double m_red_in_green_band; + double m_green_in_green_band; + double m_blue_in_green_band; + double m_red_in_blue_band; + double m_green_in_blue_band; + double m_blue_in_blue_band; +}; +} diff --git a/Userland/Libraries/LibGfx/Filters/ColorFilter.h b/Userland/Libraries/LibGfx/Filters/ColorFilter.h new file mode 100644 index 0000000000..248eb8113c --- /dev/null +++ b/Userland/Libraries/LibGfx/Filters/ColorFilter.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021, Antonio Di Stefano + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Filter.h" + +namespace Gfx { + +class ColorFilter : public Filter { +public: + virtual ~ColorFilter() = default; + + virtual void apply(Bitmap& target_bitmap, IntRect const& target_rect, Bitmap const& source_bitmap, IntRect const& source_rect) override + { + VERIFY(source_rect.size() == target_rect.size()); + VERIFY(target_bitmap.rect().contains(target_rect)); + VERIFY(source_bitmap.rect().contains(source_rect)); + + for (auto y = 0; y < source_rect.height(); ++y) { + ssize_t source_y = y + source_rect.y(); + ssize_t target_y = y + target_rect.y(); + for (auto x = 0; x < source_rect.width(); ++x) { + ssize_t source_x = x + source_rect.x(); + ssize_t target_x = x + target_rect.x(); + + auto source_pixel = source_bitmap.get_pixel(source_x, source_y); + auto target_color = convert_color(source_pixel); + + target_bitmap.set_pixel(target_x, target_y, target_color); + } + } + } + +protected: + ColorFilter() = default; + + virtual Color convert_color(Color) = 0; +}; + +}