Files
OrcaSlicer/src/slic3r/GUI/ColorPickerDialog.cpp
T
Noisyfoxandyw4z 26f5fd6251 Add a themed WebView color picker with persistent favorites (#16197)
* Define validated color picker selection contract

* Integrate web color picker with gradient and alpha controls

Co-authored-by: yw4z <ywsyildiz@gmail.com>

* Apply shared Orca theme to web color picker

* Host color picker in a validated WebView dialog

* Persist color picker favorites in user preferences

* Integrate localized web color picker with filament actions

* Add color picker browser regressions and verify direct includes

* Fit color picker content in a borderless floating panel

* Anchor color picker panels to their triggering controls

* Use accent foreground for selected color picker switches

* Integrate web color picker into generic color fields

* Use web color picker for AMS custom colors

* Use shared web color picker from Linux sidebar menu

* Use web color picker when adding texture import material

* Preserve alpha in filament color picker actions

* Clean up color picker asset whitespace

* Use application translations for color picker validation

* Preserve existing formatting in color picker integration

* Match SpeedDial rounded window handling on macOS

* Align color picker host behavior with SpeedDial

* Consolidate color picker implementation into its dialog

* Remove native color picker fallback

* Limit color picker interfaces to production consumers

* Use shared WebView translation field name

* Reapply anchored color picker placement after GTK show

* update

* fix increment buttons not working on spin inputs

* Use thumb color from its own channel

* init gradients with shifted color for second color

* fix hsl sliders cannot move freely and lock downs other one

* unify save button and color preview & increase stored color count to 32

* switch to hsl based calculation and fix plot and slider drag events

* update

* add support for multi colored gradients

* Update test_filament_color_picker.cpp

* add filtering color palette and store sidebar and combo state

* open dialog with calculated size

* apply shadow to dialog on windows

* add tooltip for gradient add color button

* Restore draggable fixed-size color picker frame

* Fix color picker sidebar sizing in WebKit

* Fix invisible default filament color button on macOS

* Remove duplicated color defs for color picker

* Remove duplicate HTML color picker frame

* Match color picker tests to current theme roles

* update icons

* use focus color on background of filter box

* align gradient switch with hex input

* fix initial window sizing

* Preserve caller color order when drawing filament icons

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-15 15:29:49 +08:00

1090 lines
45 KiB
C++

#include "ColorPickerDialog.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "Widgets/WebView.hpp"
#include <wx/dialog.h>
#include <wx/button.h>
#include <wx/stattext.h>
#include <wx/toplevel.h>
#include <wx/frame.h>
#include <wx/colour.h>
#include <wx/display.h>
#include <algorithm>
#include <cmath>
#include <wx/webview.h>
#include "GUI_App.hpp"
#include "GUI_Utils.hpp"
#include "slic3r/Utils/MacDarkMode.hpp"
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include <atomic>
#include <memory>
#include <utility>
#include <vector>
#include <nlohmann/json.hpp>
#include <wx/defs.h>
#include <wx/chartype.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/sizer.h>
#include <wx/string.h>
#include <wx/window.h>
#ifdef __linux__
#include <gtk/gtk.h>
#endif
#include "libslic3r/Color.hpp"
#include <optional>
#include <variant>
#include <cstddef>
#include <stdexcept>
#include <set>
#include <string>
#include <map>
#include <array>
#include <charconv>
#include <system_error>
namespace Slic3r::GUI {
namespace {
struct ColorPickerFavoritesState {
std::vector<ColorSelection> favorites;
bool writable = true;
};
bool normalize_color(ColorRGBA& color, bool allow_alpha)
{
for (int i = 0; i < 4; ++i)
if (!std::isfinite(color[i]) || color[i] < 0.0f || color[i] > 1.0f)
return false;
if (!allow_alpha)
color.a(1.0f);
return true;
}
bool parse_color(const nlohmann::json& value, ColorRGBA& color)
{
if (!value.is_string())
return false;
const std::string& hex = value.get_ref<const std::string&>();
if (!can_decode_color(hex))
return false;
// decode_color accepts invalid hex digits, so validate before reusing it.
for (std::size_t i = 1; i < hex.size(); ++i) {
const char c = hex[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return decode_color(hex, color);
}
std::string rgba_hex(const ColorRGBA& color)
{
constexpr char digits[] = "0123456789ABCDEF";
std::string hex(9, '#');
for (int i = 0; i < 4; ++i) {
const unsigned int byte = static_cast<unsigned int>(std::lround(color[i] * 255.0f));
hex[1 + 2 * i] = digits[byte >> 4];
hex[2 + 2 * i] = digits[byte & 15];
}
return hex;
}
std::optional<ColorSelection> normalize_color_selection(const ColorSelection& selection, ColorPickerOptions options)
{
ColorSelection normalized = selection;
if (auto* solid = std::get_if<ColorRGBA>(&normalized)) {
if (!normalize_color(*solid, options.allow_alpha))
return std::nullopt;
} else {
auto& gradient = std::get<ColorGradient>(normalized);
if (!options.allow_gradient || gradient.size() < 2 || gradient.size() > COLOR_PICKER_MAX_GRADIENT_COLORS)
return std::nullopt;
for (ColorRGBA& color : gradient)
if (!normalize_color(color, options.allow_alpha))
return std::nullopt;
}
return normalized;
}
std::optional<ColorSelection> color_selection_from_json(const nlohmann::json& value, ColorPickerOptions options)
{
if (!value.is_object())
return std::nullopt;
const auto type = value.find("type");
const auto colors = value.find("colors");
if (type == value.end() || !type->is_string() || colors == value.end() || !colors->is_array())
return std::nullopt;
if (*type == "solid" && colors->size() == 1) {
ColorRGBA solid;
if (parse_color((*colors)[0], solid))
return normalize_color_selection(solid, options);
} else if (*type == "gradient" && options.allow_gradient && colors->size() >= 2 && colors->size() <= COLOR_PICKER_MAX_GRADIENT_COLORS) {
ColorGradient gradient(colors->size());
for (std::size_t i = 0; i < gradient.size(); ++i)
if (!parse_color((*colors)[i], gradient[i]))
return std::nullopt;
return normalize_color_selection(gradient, options);
}
return std::nullopt;
}
nlohmann::json color_selection_to_json(const ColorSelection& selection)
{
const auto normalized = normalize_color_selection(selection, {true, true});
if (!normalized)
throw std::invalid_argument("Invalid color selection channels");
if (const auto* solid = std::get_if<ColorRGBA>(&*normalized))
return {{"type", "solid"}, {"colors", {rgba_hex(*solid)}}};
nlohmann::json colors = nlohmann::json::array();
for (const ColorRGBA& color : std::get<ColorGradient>(*normalized))
colors.push_back(rgba_hex(color));
return {{"type", "gradient"}, {"colors", std::move(colors)}};
}
std::optional<std::vector<ColorSelection>> color_favorites_from_json(const nlohmann::json& values)
{
if (!values.is_array() || values.size() > 24)
return std::nullopt;
std::vector<ColorSelection> favorites;
favorites.reserve(values.size());
std::set<std::string> seen;
for (const auto& value : values) {
const auto selection = color_selection_from_json(value, {true, true});
if (!selection)
return std::nullopt;
if (seen.insert(color_selection_to_json(*selection).dump()).second)
favorites.push_back(*selection);
}
return favorites;
}
std::optional<ColorSelection> legacy_color(const std::string& value)
{
if (!value.empty() && value.front() == '#')
return color_selection_from_json({{"type", "solid"}, {"colors", {value}}}, {false, true});
// string_to_wxColor uses stoi without checking the consumed length or byte
// range. Parse the actual color_to_string RGBA format strictly for migration.
std::array<unsigned int, 4> bytes;
std::size_t begin = 0;
for (std::size_t i = 0; i < bytes.size(); ++i) {
const auto comma = value.find(',', begin);
if ((i < 3 && comma == std::string::npos) || (i == 3 && comma != std::string::npos))
return std::nullopt;
const auto end = comma == std::string::npos ? value.size() : comma;
const auto parsed = std::from_chars(value.data() + begin, value.data() + end, bytes[i]);
if (parsed.ec != std::errc{} || parsed.ptr != value.data() + end || bytes[i] > 255)
return std::nullopt;
begin = end + 1;
}
return ColorRGBA(static_cast<unsigned char>(bytes[0]), static_cast<unsigned char>(bytes[1]),
static_cast<unsigned char>(bytes[2]), static_cast<unsigned char>(bytes[3]));
}
bool save_color_picker_favorites(AppConfig& config, const nlohmann::json& values)
{
if (config.has_section("color_picker") && config.get("color_picker", "version") != "1")
return false;
const auto favorites = color_favorites_from_json(values);
if (!favorites)
return false;
nlohmann::json canonical = nlohmann::json::array();
for (const auto& selection : *favorites)
canonical.push_back(color_selection_to_json(selection));
// set_section is a raw replacement and does not mark AppConfig dirty itself.
config.set_section("color_picker", {{"version", "1"}, {"favorites", canonical.dump()}});
config.set_dirty();
config.save();
return true;
}
ColorPickerFavoritesState load_color_picker_favorites(AppConfig& config)
{
if (config.has_section("color_picker")) {
if (config.get("color_picker", "version") != "1")
return {{}, false};
const auto values = nlohmann::json::parse(config.get("color_picker", "favorites"), nullptr, false);
const auto favorites = color_favorites_from_json(values);
return {favorites ? *favorites : std::vector<ColorSelection>{}, true};
}
nlohmann::json imported = nlohmann::json::array();
for (const std::string& value_string : config.get_custom_color_from_config()) {
const auto selection = legacy_color(value_string);
if (!selection)
continue;
const auto value = color_selection_to_json(*selection);
bool duplicate = false;
for (const auto& existing : imported)
if (existing == value) {
duplicate = true;
break;
}
if (!duplicate)
imported.push_back(value);
if (imported.size() == 24)
break;
}
// Persist the version even for an empty import: clearing never reimports.
save_color_picker_favorites(config, imported);
return {*color_favorites_from_json(imported), true};
}
} // namespace
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value)
{
return color_selection_from_json({{"type", value.gradient ? "gradient" : "solid"}, {"colors", value.colors}}, {true, true});
}
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value)
{
if (const auto exact = filament_color_picker_selection(value))
return *exact;
if (!value.colors.empty()) {
if (const auto primary = color_selection_from_json({{"type", "solid"}, {"colors", {value.colors.front()}}}, {false, true}))
return *primary;
}
if (const auto primary = color_selection_from_json({{"type", "solid"}, {"colors", {value.primary}}}, {false, true}))
return *primary;
return ColorRGBA(0.f, 0.f, 0.f, 1.f);
}
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection)
{
// Keep legacy RGB spelling for opaque colors; retain RGBA for transparency.
const auto canonical = color_selection_to_json(selection);
FilamentColorPickerValue value;
value.gradient = canonical.at("type") == "gradient";
for (const auto& color : canonical.at("colors")) {
auto hex = color.get<std::string>();
if (hex.compare(7, 2, "FF") == 0) hex.resize(7);
value.colors.push_back(std::move(hex));
}
return value;
}
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result)
{
// Compare encoded bytes, not ColorRGBA's tolerant floating point equality.
// An unchanged seed also leaves unrepresentable multi-color data untouched.
return result && color_selection_to_json(initial) != color_selection_to_json(*result);
}
namespace {
// Sidebar state lives in its own section: "color_picker" is replaced wholesale when favorites are
// saved, and its presence alone marks favorites as already migrated.
constexpr const char* COLOR_PICKER_UI_SECTION = "color_picker_ui";
// The page's own size in DIP (style.css): body padding 10 oneach side around the 330 x 360 picker,
// and the 160px sidebar plus its 10px gap. Opening at this size means the
// first resize message from the page normally changes nothing, so the window doesn't jump.
constexpr int COLOR_PICKER_PAGE_WIDTH = 330 + 2 * 10;
constexpr int COLOR_PICKER_PAGE_HEIGHT = 360 + 2 * 10;
constexpr int COLOR_PICKER_SIDEBAR_WIDTH = 160 + 10;
// Palette ids are short lowercase words defined by the page (basic, orca, ral, gradient, ...).
bool valid_palette_id(const std::string& id)
{
return !id.empty() && id.size() <= 32 &&
std::all_of(id.begin(), id.end(), [](char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; });
}
// English palette names are stable translation keys; RAL codes stay in the page data.
nlohmann::json color_picker_ui_strings()
{
return {
{"Color Picker", _u8L("Color Picker")},
{"Basic Colors", _u8L("Basic Colors")},
{"Orca Palette", _u8L("Orca Palette")},// TRN: Keep the brand name "Orca" unchanged; translate only the palette descriptor.
{"RAL Classic", _u8L("RAL Classic")}, // TRN: Keep the standard/brand name "RAL" unchanged.
{"Gradients", _u8L("Gradients")},
{"Solid", _u8L("Solid")},
{"Gradient", _u8L("Gradient")},
{"Gradient start", _u8L("Gradient start")},
{"Gradient end", _u8L("Gradient end")},
{"Gradient color", _u8L("Gradient color")},
{"Add color", _u8L("Add color")},
{"Remove color", _u8L("Remove color")},
{"Saturation", _u8L("Saturation")},
{"Original color", _u8L("Original color")},
// TRN: Clicking the current color preview adds it to the saved favorites.
{"Save current color", _u8L("Save current color")},
{"Palettes", _u8L("Palettes")},
{"Search colors", _u8L("Search colors")},
{"Filter colors", _u8L("Filter colors")},
{"No matching colors", _u8L("No matching colors")},
{"Hex color", _u8L("Hex color")},
{"Opacity", _u8L("Opacity")},
{"Opacity percent", _u8L("Opacity percent")},
{"Save color", _u8L("Save color")},
{"Cancel", _u8L("Cancel")},
{"OK", _u8L("OK")},
{"Enter a complete hexadecimal color", _u8L("Enter a complete hexadecimal color")},
{"Invalid input", _u8L("Invalid input")},
{"Value is out of range.", _u8L("Value is out of range.")},
{"The current multi-color selection is kept until you choose a different color.", _u8L("The current multi-color selection is kept until you choose a different color.")},
{"Up to %d colors supported.", _u8L("Up to %d colors supported.")},
{"Red", _u8L("Red")},
{"Crimson", _u8L("Crimson")},
{"Orange red", _u8L("Orange red")},
{"Tomato", _u8L("Tomato")},
{"Orange", _u8L("Orange")},
{"Dark orange", _u8L("Dark orange")},
{"Gold", _u8L("Gold")},
{"Burly wood", _u8L("Burly wood")},
{"Bisque", _u8L("Bisque")},
{"Misty rose", _u8L("Misty rose")},
{"Yellow", _u8L("Yellow")},
{"Light yellow", _u8L("Light yellow")},
{"Lemon chiffon", _u8L("Lemon chiffon")},
{"Beige", _u8L("Beige")},
{"Light golden rod yellow", _u8L("Light golden rod yellow")},
{"Pale golden rod", _u8L("Pale golden rod")},
{"Khaki", _u8L("Khaki")},
{"Dark khaki", _u8L("Dark khaki")},
{"Peach puff", _u8L("Peach puff")},
{"Moccasin", _u8L("Moccasin")},
{"Papaya whip", _u8L("Papaya whip")},
{"Blanched almond", _u8L("Blanched almond")},
{"Antique white", _u8L("Antique white")},
{"Navajo white", _u8L("Navajo white")},
{"Wheat", _u8L("Wheat")},
{"Golden rod", _u8L("Golden rod")},
{"Chocolate", _u8L("Chocolate")},
{"Peru", _u8L("Peru")},
{"Fire brick", _u8L("Fire brick")},
{"Sienna", _u8L("Sienna")},
{"Saddle brown", _u8L("Saddle brown")},
{"Brown", _u8L("Brown")},
{"Rosy brown", _u8L("Rosy brown")},
{"Salmon", _u8L("Salmon")},
{"Light salmon", _u8L("Light salmon")},
{"Dark salmon", _u8L("Dark salmon")},
{"Sandy brown", _u8L("Sandy brown")},
{"Coral", _u8L("Coral")},
{"Light coral", _u8L("Light coral")},
{"Indian red", _u8L("Indian red")},
{"Dark red", _u8L("Dark red")},
{"Maroon", _u8L("Maroon")},
{"Green yellow", _u8L("Green yellow")},
{"Yellow green", _u8L("Yellow green")},
{"Chartreuse", _u8L("Chartreuse")},
{"Lawn green", _u8L("Lawn green")},
{"Lime green", _u8L("Lime green")},
{"Spring green", _u8L("Spring green")},
{"Medium spring green", _u8L("Medium spring green")},
{"Lime", _u8L("Lime")},
{"Green", _u8L("Green")},
{"Forest green", _u8L("Forest green")},
{"Dark green", _u8L("Dark green")},
{"Olive drab", _u8L("Olive drab")},
{"Olive", _u8L("Olive")},
{"Dark olive green", _u8L("Dark olive green")},
{"Pale green", _u8L("Pale green")},
{"Light green", _u8L("Light green")},
{"Medium sea green", _u8L("Medium sea green")},
{"Sea green", _u8L("Sea green")},
{"Dark sea green", _u8L("Dark sea green")},
{"Cadet blue", _u8L("Cadet blue")},
{"Medium aqua marine", _u8L("Medium aqua marine")},
{"Turquoise", _u8L("Turquoise")},
{"Medium turquoise", _u8L("Medium turquoise")},
{"Dark turquoise", _u8L("Dark turquoise")},
{"Pale turquoise", _u8L("Pale turquoise")},
{"Aqua", _u8L("Aqua")},
{"Cyan", _u8L("Cyan")},
{"Light cyan", _u8L("Light cyan")},
{"Mint cream", _u8L("Mint cream")},
{"Honey dew", _u8L("Honey dew")},
{"Aquamarine", _u8L("Aquamarine")},
{"Light sea green", _u8L("Light sea green")},
{"Dark cyan", _u8L("Dark cyan")},
{"Teal", _u8L("Teal")},
{"Dark slate gray", _u8L("Dark slate gray")},
{"Light slate gray", _u8L("Light slate gray")},
{"Slate gray", _u8L("Slate gray")},
{"Deep sky blue", _u8L("Deep sky blue")},
{"Dodger Blue", _u8L("Dodger Blue")},
{"Sky blue", _u8L("Sky blue")},
{"Light sky blue", _u8L("Light sky blue")},
{"Light blue", _u8L("Light blue")},
{"Powder blue", _u8L("Powder blue")},
{"Light steel blue", _u8L("Light steel blue")},
{"Steel blue", _u8L("Steel blue")},
{"Cornflower blue", _u8L("Cornflower blue")},
{"Royal blue", _u8L("Royal blue")},
{"Blue", _u8L("Blue")},
{"Medium blue", _u8L("Medium blue")},
{"Midnight blue", _u8L("Midnight blue")},
{"Dark blue", _u8L("Dark blue")},
{"Navy", _u8L("Navy")},
{"Slate blue", _u8L("Slate blue")},
{"Dark slate blue", _u8L("Dark slate blue")},
{"Medium slate blue", _u8L("Medium slate blue")},
{"Blue violet", _u8L("Blue violet")},
{"Indigo", _u8L("Indigo")},
{"Dark violet", _u8L("Dark violet")},
{"Dark orchid", _u8L("Dark orchid")},
{"Medium orchid", _u8L("Medium orchid")},
{"Orchid", _u8L("Orchid")},
{"Violet", _u8L("Violet")},
{"Medium purple", _u8L("Medium purple")},
{"Purple", _u8L("Purple")},
{"Rebecca purple", _u8L("Rebecca purple")},
{"Medium violet red", _u8L("Medium violet red")},
{"PaleV violet red", _u8L("PaleV violet red")},
{"Deep pink", _u8L("Deep pink")},
{"Hot pink", _u8L("Hot pink")},
{"Light pink", _u8L("Light pink")},
{"Pink", _u8L("Pink")},
{"Plum", _u8L("Plum")},
{"Thistle", _u8L("Thistle")},
{"Magenta", _u8L("Magenta")},
{"Fuchsia", _u8L("Fuchsia")},
{"Dark magenta", _u8L("Dark magenta")},
{"Linen", _u8L("Linen")},
{"Old lace", _u8L("Old lace")},
{"Sea shell", _u8L("Sea shell")},
{"Ivory", _u8L("Ivory")},
{"Floral white", _u8L("Floral white")},
{"Lavender blush", _u8L("Lavender blush")},
{"Ghost white", _u8L("Ghost white")},
{"Alice blue", _u8L("Alice blue")},
{"Lavender", _u8L("Lavender")},
{"White smoke", _u8L("White smoke")},
{"Gainsboro", _u8L("Gainsboro")},
{"Black", _u8L("Black")},
{"White", _u8L("White")},
{"Coral fire", _u8L("Coral fire")},
{"Sea anemone", _u8L("Sea anemone")},
{"Deep abyss", _u8L("Deep abyss")},
{"Lobster red", _u8L("Lobster red")},
{"Sandy clay", _u8L("Sandy clay")},
{"Driftwood", _u8L("Driftwood")},
{"Sea buckthorn", _u8L("Sea buckthorn")},
{"Sunset reef", _u8L("Sunset reef")},
{"Tiger coral", _u8L("Tiger coral")},
{"Pink coral", _u8L("Pink coral")},
{"Seashell glow", _u8L("Seashell glow")},
{"Sand", _u8L("Sand")},
{"Golden kelp", _u8L("Golden kelp")},
{"Sunlit dune", _u8L("Sunlit dune")},
{"Amber nautilus", _u8L("Amber nautilus")},
{"Sea lemon", _u8L("Sea lemon")},
{"Pale starfish", _u8L("Pale starfish")},
{"Spring tide", _u8L("Spring tide")},
{"Lime algae", _u8L("Lime algae")},
{"Sea grass", _u8L("Sea grass")},
{"Neon plankton", _u8L("Neon plankton")},
{"Kelp forest", _u8L("Kelp forest")},
{"Emerald wave", _u8L("Emerald wave")},
{"Whale shadow", _u8L("Whale shadow")},
{"Sea turtle", _u8L("Sea turtle")},
{"Lagoon", _u8L("Lagoon")},
{"Tropical current", _u8L("Tropical current")},
{"Midnight trench", _u8L("Midnight trench")},
{"Dark kelp", _u8L("Dark kelp")},
{"Crystal surf", _u8L("Crystal surf")},
{"Turquoise bay", _u8L("Turquoise bay")},
{"Sky over reef", _u8L("Sky over reef")},
{"Shallow sky", _u8L("Shallow sky")},
{"Electric bel", _u8L("Electric bel")},
{"Dolphin", _u8L("Dolphin")},
{"Ocean depths", _u8L("Ocean depths")},
{"Whale", _u8L("Whale")},
{"Indigo abyss", _u8L("Indigo abyss")},
{"Sea urchin", _u8L("Sea urchin")},
{"Lavender coral", _u8L("Lavender coral")},
{"Violet tide", _u8L("Violet tide")},
{"Deep sea orchid", _u8L("Deep sea orchid")},
{"Neon jelly", _u8L("Neon jelly")},
{"Fuchsia coral", _u8L("Fuchsia coral")},
{"Pink jellyfish", _u8L("Pink jellyfish")},
{"Foam", _u8L("Foam")},
{"Seashell white", _u8L("Seashell white")},
{"Pebble gray", _u8L("Pebble gray")},
{"Storm cloud", _u8L("Storm cloud")},
{"Ink black", _u8L("Ink black")},
{"Green beige", _u8L("Green beige")},
{"Sand yellow", _u8L("Sand yellow")},
{"Signal yellow", _u8L("Signal yellow")},
{"Golden yellow", _u8L("Golden yellow")},
{"Honey yellow", _u8L("Honey yellow")},
{"Maize yellow", _u8L("Maize yellow")},
{"Dalmatian yellow", _u8L("Dalmatian yellow")},
{"Brown beige", _u8L("Brown beige")},
{"Lemon yellow", _u8L("Lemon yellow")},
{"Oyster white", _u8L("Oyster white")},
{"Light ivory", _u8L("Light ivory")},
{"Zinc yellow", _u8L("Zinc yellow")},
{"Grey beige", _u8L("Grey beige")},
{"Olive yellow", _u8L("Olive yellow")},
{"Rape yellow", _u8L("Rape yellow")},
{"Traffic yellow", _u8L("Traffic yellow")},
{"Ochre yellow", _u8L("Ochre yellow")},
{"Traffic orange yellow", _u8L("Traffic orange yellow")},
{"Curry", _u8L("Curry")},
{"Melon yellow", _u8L("Melon yellow")},
{"Broom yellow", _u8L("Broom yellow")},
{"Dahlia yellow", _u8L("Dahlia yellow")},
{"Pastel yellow", _u8L("Pastel yellow")},
{"Pearl beige", _u8L("Pearl beige")},
{"Pearl gold", _u8L("Pearl gold")},
{"Sun yellow", _u8L("Sun yellow")},
{"Yellow orange", _u8L("Yellow orange")},
{"Deep orange", _u8L("Deep orange")},
{"Vermilion", _u8L("Vermilion")},
{"Pearl orange", _u8L("Pearl orange")},
{"Pure orange", _u8L("Pure orange")},
{"Luminous orange", _u8L("Luminous orange")},
{"Luminous bright orange", _u8L("Luminous bright orange")},
{"Bright red orange", _u8L("Bright red orange")},
{"Traffic orange", _u8L("Traffic orange")},
{"Signal orange", _u8L("Signal orange")},
{"Salmon orange", _u8L("Salmon orange")},
{"Pearl pink", _u8L("Pearl pink")},
{"Copper orange", _u8L("Copper orange")},
{"Signal brown", _u8L("Signal brown")},
{"Fir orange", _u8L("Fir orange")},
{"Signal red", _u8L("Signal red")},
{"Luminous red", _u8L("Luminous red")},
{"Carmine red", _u8L("Carmine red")},
{"Pearl carmine red", _u8L("Pearl carmine red")},
{"Pure red", _u8L("Pure red")},
{"Luminous deep red", _u8L("Luminous deep red")},
{"Currant red", _u8L("Currant red")},
{"Pearl ruby red", _u8L("Pearl ruby red")},
{"Fire red", _u8L("Fire red")},
{"Ruby red", _u8L("Ruby red")},
{"Purple red", _u8L("Purple red")},
{"Wine red", _u8L("Wine red")},
{"Black red", _u8L("Black red")},
{"Oxide red", _u8L("Oxide red")},
{"Brown red", _u8L("Brown red")},
{"Pine red", _u8L("Pine red")},
{"Tomato red", _u8L("Tomato red")},
{"Antique pink", _u8L("Antique pink")},
{"Coral red", _u8L("Coral red")},
{"Rose", _u8L("Rose")},
{"Strawberry red", _u8L("Strawberry red")},
{"Traffic red", _u8L("Traffic red")},
{"Salmon pink", _u8L("Salmon pink")},
{"Luminous rose", _u8L("Luminous rose")},
{"Luminous bright red", _u8L("Luminous bright red")},
{"Raspberry red", _u8L("Raspberry red")},
{"Orient red", _u8L("Orient red")},
{"Pearl ruby", _u8L("Pearl ruby")},
{"Red purple", _u8L("Red purple")},
{"Red violet", _u8L("Red violet")},
{"Heather violet", _u8L("Heather violet")},
{"Bordeaux violet", _u8L("Bordeaux violet")},
{"Mahogany", _u8L("Mahogany")},
{"Grey purple", _u8L("Grey purple")},
{"Signal violet", _u8L("Signal violet")},
{"Fir green", _u8L("Fir green")},
{"Sabine grey", _u8L("Sabine grey")},
{"Pearl violet", _u8L("Pearl violet")},
{"Pearl blackberry", _u8L("Pearl blackberry")},
{"Green blue", _u8L("Green blue")},
{"Ultramarine blue", _u8L("Ultramarine blue")},
{"Sapphire blue", _u8L("Sapphire blue")},
{"Black blue", _u8L("Black blue")},
{"Signal blue", _u8L("Signal blue")},
{"Brilliant blue", _u8L("Brilliant blue")},
{"Grey blue", _u8L("Grey blue")},
{"Azure blue", _u8L("Azure blue")},
{"Gentian blue", _u8L("Gentian blue")},
{"Cobalt blue", _u8L("Cobalt blue")},
{"Pigeon blue", _u8L("Pigeon blue")},
{"Traffic blue", _u8L("Traffic blue")},
{"Turquoise blue", _u8L("Turquoise blue")},
{"Capsanthin violet", _u8L("Capsanthin violet")},
{"Ocean blue", _u8L("Ocean blue")},
{"Water blue", _u8L("Water blue")},
{"Night blue", _u8L("Night blue")},
{"Distant blue", _u8L("Distant blue")},
{"Pastel blue", _u8L("Pastel blue")},
{"Pearl gentian blue", _u8L("Pearl gentian blue")},
{"Pearl midnight blue", _u8L("Pearl midnight blue")},
{"Patina green", _u8L("Patina green")},
{"Emerald green", _u8L("Emerald green")},
{"Leaf green", _u8L("Leaf green")},
{"Olive green", _u8L("Olive green")},
{"Blue green", _u8L("Blue green")},
{"Moss green", _u8L("Moss green")},
{"Grey olive", _u8L("Grey olive")},
{"Pine green", _u8L("Pine green")},
{"Pine brown", _u8L("Pine brown")},
{"Grass green", _u8L("Grass green")},
{"Reseda green", _u8L("Reseda green")},
{"Brown green", _u8L("Brown green")},
{"Reed green", _u8L("Reed green")},
{"Black green", _u8L("Black green")},
{"Turquoise green", _u8L("Turquoise green")},
{"May green", _u8L("May green")},
{"Chrome oxide green", _u8L("Chrome oxide green")},
{"Traffic green", _u8L("Traffic green")},
{"Fern green", _u8L("Fern green")},
{"Opal green", _u8L("Opal green")},
{"Mint green", _u8L("Mint green")},
{"Bronze green", _u8L("Bronze green")},
{"Pearl green", _u8L("Pearl green")},
{"Sax turquoise", _u8L("Sax turquoise")},
{"Pearl turquoise", _u8L("Pearl turquoise")},
{"Pearl jade", _u8L("Pearl jade")},
{"Pure green", _u8L("Pure green")},
{"Luminous green", _u8L("Luminous green")},
{"Squirrel grey", _u8L("Squirrel grey")},
{"Silver grey", _u8L("Silver grey")},
{"Olivestone", _u8L("Olivestone")},
{"Moss grey", _u8L("Moss grey")},
{"Signal grey", _u8L("Signal grey")},
{"Mouse grey", _u8L("Mouse grey")},
{"Beige grey", _u8L("Beige grey")},
{"Khaki grey", _u8L("Khaki grey")},
{"Green grey", _u8L("Green grey")},
{"Tarpaulin grey", _u8L("Tarpaulin grey")},
{"Iron grey", _u8L("Iron grey")},
{"Basalt grey", _u8L("Basalt grey")},
{"Brown grey", _u8L("Brown grey")},
{"Slate grey", _u8L("Slate grey")},
{"Anthracite grey", _u8L("Anthracite grey")},
{"Light grey", _u8L("Light grey")},
{"Umber grey", _u8L("Umber grey")},
{"Concrete grey", _u8L("Concrete grey")},
{"Graphite grey", _u8L("Graphite grey")},
{"Granite grey", _u8L("Granite grey")},
{"Stone grey", _u8L("Stone grey")},
{"Pebble grey", _u8L("Pebble grey")},
{"Cement grey", _u8L("Cement grey")},
{"Yellow grey", _u8L("Yellow grey")},
{"Platinum grey", _u8L("Platinum grey")},
{"Dusty grey", _u8L("Dusty grey")},
{"Agate grey", _u8L("Agate grey")},
{"Quartz grey", _u8L("Quartz grey")},
{"Window grey", _u8L("Window grey")},
{"Traffic grey A", _u8L("Traffic grey A")},
{"Traffic grey B", _u8L("Traffic grey B")},
{"Silk grey", _u8L("Silk grey")},
{"Telegrey 1", _u8L("Telegrey 1")},
{"Telegrey 2", _u8L("Telegrey 2")},
{"Telegrey 4", _u8L("Telegrey 4")},
{"Pearl mouse grey", _u8L("Pearl mouse grey")},
{"Green brown", _u8L("Green brown")},
{"Ochre brown", _u8L("Ochre brown")},
{"Clay brown", _u8L("Clay brown")},
{"Copper brown", _u8L("Copper brown")},
{"Chocolate brown", _u8L("Chocolate brown")},
{"Olive brown", _u8L("Olive brown")},
{"Nut brown", _u8L("Nut brown")},
{"Red brown", _u8L("Red brown")},
{"Sepia brown", _u8L("Sepia brown")},
{"Chestnut brown", _u8L("Chestnut brown")},
{"Mahogany brown", _u8L("Mahogany brown")},
{"Grey brown", _u8L("Grey brown")},
{"Pearl copper", _u8L("Pearl copper")},
{"Beige brown", _u8L("Beige brown")},
{"Pearl mocha brown", _u8L("Pearl mocha brown")},
{"Earth brown", _u8L("Earth brown")},
{"Pearl brown", _u8L("Pearl brown")},
{"Cream", _u8L("Cream")},
{"Grey white", _u8L("Grey white")},
{"Signal white", _u8L("Signal white")},
{"Signal black", _u8L("Signal black")},
{"Jet black", _u8L("Jet black")},
{"White aluminium", _u8L("White aluminium")},
{"Grey aluminium", _u8L("Grey aluminium")},
{"Pure white", _u8L("Pure white")},
{"Graphite black", _u8L("Graphite black")},
{"Traffic white", _u8L("Traffic white")},
{"Traffic black", _u8L("Traffic black")},
{"Papyrus white", _u8L("Papyrus white")},
{"Pearl light grey", _u8L("Pearl light grey")},
{"Metal: Silver", _u8L("Metal: Silver")},
{"Metal: Brass", _u8L("Metal: Brass")},
{"Metal: Gold", _u8L("Metal: Gold")},
{"Metal: Copper", _u8L("Metal: Copper")},
{"Metal: Bronze", _u8L("Metal: Bronze")},
{"Nature: Pearl", _u8L("Nature: Pearl")},
{"Nature: Rainbow", _u8L("Nature: Rainbow")},
{"Nature: Clear Sky", _u8L("Nature: Clear Sky")},
{"Nature: Sunset", _u8L("Nature: Sunset")},
{"Wood: Dark", _u8L("Wood: Dark")},
{"Wood: Medium", _u8L("Wood: Medium")},
{"Wood: Light", _u8L("Wood: Light")}
};
}
} // namespace
namespace {
// All geometry uses native screen coordinates, including negative monitor origins.
wxPoint color_picker_panel_position(const wxRect& anchor, const wxRect& work_area, const wxSize& size, int gap)
{
const int below = anchor.y + anchor.height + gap;
const int above = anchor.y - gap - size.y;
const int y = below + size.y <= work_area.y + work_area.height ? below : above;
return {std::clamp(anchor.x, work_area.x, work_area.x + std::max(0, work_area.width - size.x)),
std::clamp(y, work_area.y, work_area.y + std::max(0, work_area.height - size.y))};
}
} // namespace
ColorPickerDialog::ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options,
bool preserve_multi_color, wxWindow* anchor)
: WebViewHostDialog(parent, wxID_ANY, _L("Color Picker"), wxDefaultPosition, wxDefaultSize,
wxCAPTION | wxSYSTEM_MENU | wxCLOSE_BOX | wxFRAME_NO_TASKBAR),
m_initial(initial), m_options(options), m_preserve_multi_color(preserve_multi_color)
{
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { finish(wxID_CANCEL); });
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) {
if (event.GetKeyCode() == WXK_ESCAPE)
finish(wxID_CANCEL);
else
event.Skip();
});
SetBackgroundColour(wxGetApp().get_window_default_clr());
wxGetApp().UpdateDlgDarkUI(this);
#ifdef __WXGTK__
// wxGTK dialogs default to CENTER_ON_PARENT, which conflicts with the anchor.
gtk_window_set_position(GTK_WINDOW(GetHandle()), GTK_WIN_POS_NONE);
#endif
// Snapshot the trigger instead of following the mouse or retaining a raw pointer.
wxWindow* trigger = anchor ? anchor : parent;
if (trigger)
m_anchor_rect = trigger->GetScreenRect();
int display_index = wxDisplay::GetFromPoint(m_anchor_rect.GetPosition() + wxPoint(m_anchor_rect.width / 2, m_anchor_rect.height / 2));
if (display_index == wxNOT_FOUND)
display_index = 0;
m_work_area = wxDisplay(static_cast<unsigned int>(display_index)).GetClientArea();
const auto normalized = normalize_color_selection(initial, options);
if (normalized)
m_initial = *normalized;
// Read the sidebar state before the WebView exists: add_user_scripts() passes it to the page,
// and the window opens at the matching size instead of resizing after the page loads.
if (wxGetApp().app_config) {
const AppConfig& config = *wxGetApp().app_config;
m_sidebar_open = config.get(COLOR_PICKER_UI_SECTION, "sidebar_open") == "1";
if (const std::string palette = config.get(COLOR_PICKER_UI_SECTION, "palette"); valid_palette_id(palette))
m_palette = palette;
}
m_content_width = COLOR_PICKER_PAGE_WIDTH + (m_sidebar_open ? COLOR_PICKER_SIDEBAR_WIDTH : 0);
m_content_height = COLOR_PICKER_PAGE_HEIGHT;
if (!normalized || !create_webview("web/dialog/ColorPickerDialog/index.html", _L("Color Picker"),
wxSize(m_content_width, m_content_height),
wxSize(COLOR_PICKER_PAGE_WIDTH, COLOR_PICKER_PAGE_HEIGHT))) {
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, FromDIP(20)));
sizer->Add(new wxButton(this, wxID_CANCEL, _L("Cancel")), wxSizerFlags().Right().Border(wxALL, FromDIP(8)));
SetSizerAndFit(sizer);
Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { finish(wxID_CANCEL); }, wxID_CANCEL);
Bind(wxEVT_SHOW, [this](wxShowEvent& event) {
if (event.IsShown() && !m_initial_positioned) {
move_to_anchor();
wxGetApp().CallAfter([this, alive = m_alive] {
if (alive->load(std::memory_order_acquire) && !m_closing) {
move_to_anchor();
m_initial_positioned = true;
}
});
}
event.Skip();
});
wxGetApp().UpdateDlgDarkUI(this);
move_to_anchor();
return;
}
browser()->EnableBrowserAcceleratorKeys(false);
browser()->Bind(EVT_WEBVIEW_RECREATED, [this](wxCommandEvent& event) {
wxGetApp().UpdateDlgDarkUI(this);
event.Skip(); // The shared host updates the page theme.
});
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
if (event.GetActive() && IsShown() && !m_closing)
focus_webview();
event.Skip();
});
if (wxGetApp().app_config) {
const auto stored = load_color_picker_favorites(*wxGetApp().app_config);
m_favorites = stored.favorites;
m_favorites_writable = stored.writable;
}
// The shared host attaches its sizer before constructing the full layout.
GetSizer()->SetSizeHints(this);
Move(m_work_area.GetPosition());
// SetSizeHints may fit the window to the WebView's small initial best size.
// Restore the intended dialog size only after applying the layout hints.
resize_to_content(m_content_height);
move_to_anchor();
Bind(wxEVT_SHOW, [this](wxShowEvent& event) {
if (event.IsShown() && !m_initial_positioned) {
resize_to_content(m_content_height);
move_to_anchor();
// GTK may ignore the requested position until the window is shown.
wxGetApp().CallAfter([this, alive = m_alive] {
if (alive->load(std::memory_order_acquire) && !m_closing) {
resize_to_content(m_content_height);
move_to_anchor();
m_initial_positioned = true;
focus_webview();
}
});
}
event.Skip();
});
}
ColorPickerDialog::~ColorPickerDialog() { m_alive->store(false, std::memory_order_release); }
void ColorPickerDialog::position_panel()
{
if (m_positioning)
return;
m_positioning = true;
// Both dimensions come from the page: width changes when the palette sidebar opens or closes.
wxSize size = FromDIP(wxSize(m_content_width, m_content_height));
// The work-area limit includes the native title bar and fixed frame.
const wxSize frame = GetSize() - GetClientSize();
size.x = std::min(size.x, std::max(1, m_work_area.width - frame.x));
size.y = std::min(size.y, std::max(1, m_work_area.height - frame.y - FromDIP(16)));
SetMinSize(wxSize(std::min(FromDIP(320), size.x), std::min(FromDIP(360), size.y)));
if (size != GetClientSize())
SetClientSize(size);
m_positioning = false;
}
void ColorPickerDialog::move_to_anchor()
{
const wxPoint position = color_picker_panel_position(m_anchor_rect, m_work_area, GetSize(), FromDIP(4));
if (position != GetPosition())
Move(position);
#ifdef __WXGTK__
// wxGTK caches requested coordinates: Move can skip a post-show request even
// when the window manager ignored the initial one. Reapply at native level.
gtk_window_move(GTK_WINDOW(GetHandle()), position.x, position.y);
#endif
}
void ColorPickerDialog::resize_to_content(int height)
{
m_content_height = height;
position_panel();
Layout();
#ifdef __WXOSX__
// Like SpeedDial, explicitly match WKWebView's viewport even at unchanged size.
if (wxWebView* view = browser())
view->SetSize(GetClientSize());
#endif
repaint_webview();
}
void ColorPickerDialog::focus_webview()
{
wxWebView* view = browser();
if (!view)
return;
#ifdef __linux__
if (void* backend = view->GetNativeBackend())
gtk_widget_grab_focus(static_cast<GtkWidget*>(backend));
#else
view->SetFocus();
#endif
if (m_page_ready)
run_script("window.ColorPickerDialog.focusInput();");
}
void ColorPickerDialog::repaint_webview()
{
wxWebView* view = browser();
if (!view)
return;
view->Refresh();
#ifdef __WXOSX__
if (void* backend = view->GetNativeBackend())
WKWebView_force_display(backend);
view->Update();
#elif defined(__linux__)
if (void* backend = view->GetNativeBackend())
gtk_widget_queue_draw(static_cast<GtkWidget*>(backend));
#else
view->Update();
#endif
}
void ColorPickerDialog::on_dpi_changed(const wxRect&)
{
resize_to_content(m_content_height);
Refresh();
}
void ColorPickerDialog::on_sys_color_changed()
{
wxGetApp().UpdateDlgDarkUI(this);
}
void ColorPickerDialog::add_user_scripts()
{
if (wxWebView* view = browser()) {
auto language = wxGetApp().current_language_code().ToStdString();
std::replace(language.begin(), language.end(), '_', '-');
const std::string script = "window.ORCA_UI_STRINGS = " +
color_picker_ui_strings().dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore) +
";window.ORCA_COLOR_PICKER_LANGUAGE = " + nlohmann::json(language).dump() +
// Lets the page lay out the sidebar before its first paint, matching the window size.
";window.ORCA_COLOR_PICKER_UI = " + nlohmann::json({{"sidebar_open", m_sidebar_open}, {"palette", m_palette}}).dump() + ";";
view->AddUserScript(wxString::FromUTF8(script));
}
}
void ColorPickerDialog::on_script_message(const nlohmann::json& payload)
{
// GTK/WebKit may call on the native script-message stack. All dialog work,
// including EndModal, runs after that callback and checks the lifetime token.
wxGetApp().CallAfter([this, alive = m_alive, payload] {
if (alive->load(std::memory_order_acquire) && !m_closing)
handle_web_command(payload);
});
}
void ColorPickerDialog::handle_web_command(const nlohmann::json& payload)
{
if (!browser() || !payload.is_object())
return;
const auto command = payload.find("command");
const auto page_id = payload.find("page_id");
if (command == payload.end() || !command->is_string() || page_id == payload.end() || !page_id->is_string())
return;
const auto& id = page_id->get_ref<const std::string&>();
if (id.empty() || id.size() > 64)
return;
if (*command == "ready") {
// A repeated ready from the same document must not reset its edits. A new
// document (e.g. WebView recreation) gets a fresh initialization handshake.
if (id != m_page_id) {
m_page_id = id;
m_page_ready = false;
m_init_sent = false;
}
if (!m_init_sent)
send_initial_state();
return;
}
if (id != m_page_id)
return;
if (*command == "cancel") {
finish(wxID_CANCEL);
return;
}
if (*command == "initialized") {
if (m_init_sent && !m_page_ready) {
m_page_ready = true;
focus_webview();
repaint_webview();
}
return;
}
if (!m_page_ready)
return;
if (*command == "resize") {
const auto height = payload.find("height");
if (height == payload.end() || !height->is_number())
return;
const double value = height->get<double>();
if (!std::isfinite(value) || value < 300 || value > 900)
return;
// Width is optional so an older page that reports only height keeps the current width.
const auto width = payload.find("width");
if (width != payload.end()) {
if (!width->is_number())
return;
const double width_value = width->get<double>();
if (!std::isfinite(width_value) || width_value < 300 || width_value > 1200)
return;
m_content_width = static_cast<int>(std::ceil(width_value));
}
resize_to_content(static_cast<int>(std::ceil(value)));
return;
}
if (*command == "confirm") {
const auto value = payload.find("selection");
if (value == payload.end())
return;
const auto selection = color_selection_from_json(*value, m_options);
if (!selection)
return;
m_selection = *selection;
finish(wxID_OK);
} else if (*command == "update_favorites") {
const auto values = payload.find("favorites");
if (values == payload.end() || !m_favorites_writable || !wxGetApp().app_config)
return;
const auto favorites = color_favorites_from_json(*values);
if (favorites && save_color_picker_favorites(*wxGetApp().app_config, *values)) {
m_favorites = *favorites;
nlohmann::json canonical = nlohmann::json::array();
for (const ColorSelection& selection : m_favorites)
canonical.push_back(color_selection_to_json(selection));
const nlohmann::json response = {{"command", "favorites"}, {"page_id", m_page_id}, {"favorites", std::move(canonical)}};
run_script(wxString::FromUTF8("window.ColorPickerDialog.handleMessage(" + response.dump() + ")"));
}
} else if (*command == "update_ui") {
// Sidebar open/closed and the chosen palette, restored the next time the picker opens.
const auto open = payload.find("sidebar_open");
const auto palette = payload.find("palette");
if (open == payload.end() || !open->is_boolean() || palette == payload.end() || !palette->is_string() || !wxGetApp().app_config)
return;
const auto& id = palette->get_ref<const std::string&>();
if (!valid_palette_id(id))
return;
m_sidebar_open = open->get<bool>();
m_palette = id;
// set() marks the config dirty; it is written with the rest of the app config.
AppConfig& config = *wxGetApp().app_config;
config.set(COLOR_PICKER_UI_SECTION, "sidebar_open", m_sidebar_open ? "1" : "0");
config.set(COLOR_PICKER_UI_SECTION, "palette", m_palette);
}
}
void ColorPickerDialog::send_initial_state()
{
nlohmann::json favorites = nlohmann::json::array();
for (const ColorSelection& selection : m_favorites)
favorites.push_back(color_selection_to_json(selection));
const nlohmann::json payload = {{"command", "init"}, {"page_id", m_page_id},
{"options", {{"allow_gradient", m_options.allow_gradient}, {"allow_alpha", m_options.allow_alpha},
{"max_gradient_colors", COLOR_PICKER_MAX_GRADIENT_COLORS}}},
{"selection", color_selection_to_json(m_initial)}, {"favorites", std::move(favorites)},
{"favorites_writable", m_favorites_writable}, {"preserve_multi_color", m_preserve_multi_color},
{"ui", {{"sidebar_open", m_sidebar_open}, {"palette", m_palette}}}};
m_init_sent = true;
// Already deferred and guarded: avoid the shared call_web_handler's unguarded
// second CallAfter, which could run after this modal dialog is destroyed.
if (!run_script(wxString::FromUTF8("window.ColorPickerDialog.handleMessage(" + payload.dump() + ")")))
m_init_sent = false;
}
void ColorPickerDialog::finish(int return_code)
{
if (m_closing)
return;
m_closing = true;
if (return_code != wxID_OK)
m_selection.reset();
if (IsModal())
EndModal(return_code);
else
Hide();
}
} // namespace Slic3r::GUI