#include "ColorPickerDialog.hpp" #include "Widgets/WebViewHostDialog.hpp" #include "Widgets/WebView.hpp" #include #include #include #include #include #include #include #include #include #include #include "GUI_App.hpp" #include "GUI_Utils.hpp" #include "slic3r/Utils/MacDarkMode.hpp" #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #endif #include "libslic3r/Color.hpp" #include #include #include #include #include #include #include #include #include #include namespace Slic3r::GUI { namespace { struct ColorPickerFavoritesState { std::vector 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(); 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(std::lround(color[i] * 255.0f)); hex[1 + 2 * i] = digits[byte >> 4]; hex[2 + 2 * i] = digits[byte & 15]; } return hex; } std::optional normalize_color_selection(const ColorSelection& selection, ColorPickerOptions options) { ColorSelection normalized = selection; if (auto* solid = std::get_if(&normalized)) { if (!normalize_color(*solid, options.allow_alpha)) return std::nullopt; } else { auto& gradient = std::get(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 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(&*normalized)) return {{"type", "solid"}, {"colors", {rgba_hex(*solid)}}}; nlohmann::json colors = nlohmann::json::array(); for (const ColorRGBA& color : std::get(*normalized)) colors.push_back(rgba_hex(color)); return {{"type", "gradient"}, {"colors", std::move(colors)}}; } std::optional> color_favorites_from_json(const nlohmann::json& values) { if (!values.is_array() || values.size() > 24) return std::nullopt; std::vector favorites; favorites.reserve(values.size()); std::set 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 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 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(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(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{}, 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 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(); 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& 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(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(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(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(); 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(); 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(); if (!std::isfinite(width_value) || width_value < 300 || width_value > 1200) return; m_content_width = static_cast(std::ceil(width_value)); } resize_to_content(static_cast(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(); if (!valid_palette_id(id)) return; m_sidebar_open = open->get(); 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