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>
This commit is contained in:
Noisyfox
2026-10-15 15:29:49 +08:00
committed by GitHub
co-authored by yw4z
parent 5d73410281
commit 26f5fd6251
29 changed files with 4066 additions and 449 deletions
+74 -165
View File
@@ -2,6 +2,7 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Field.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "wxExtensions.hpp"
#include "Plater.hpp"
@@ -45,7 +46,11 @@
#include <wx/arrstr.h>
#include <wx/anybutton.h>
#include <wx/colour.h>
#include <wx/clrpicker.h>
#include <wx/button.h>
#include <wx/brush.h>
#include <wx/pen.h>
#include <optional>
#include <variant>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/dc.h>
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
// field the same height as other parameter buttons before drawing its swatch.
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
// Validate the color
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk()) {
clr = wxTransparentColour;
}
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
window = button;
button->SetBitmapMargins(0, 0);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
convert_to_picker_widget(temp);
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
#ifdef __WXMSW__
temp,
#endif
this](wxCommandEvent e) {
#ifdef __WXMSW__
draw_bmp_btn(temp, temp->GetColour());
#endif
on_change_field();
}), temp->GetId());
// ORCA reset value to default on right click. previously no way to switch back on windows
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
#ifdef __WXMSW__
temp->SetColour(wxTransparentColour);
draw_bmp_btn(temp, wxTransparentColour);
#else
set_undef_value(temp);
#endif
on_change_field();
e.Skip();
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
button->SetFont(wxGetApp().normal_font());
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
set_value(boost::any(clr_str));
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
apply_user_color(wxTransparentColour);
event.Skip();
});
temp->SetToolTip(get_tooltip_text(clr_str));
// Native layouts may assign the final field size after construction.
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
draw_bmp_btn();
event.Skip();
});
button->SetToolTip(get_tooltip_text(clr_str));
}
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
void ColourPicker::draw_bmp_btn()
{
field->SetColour(wxTransparentColour);
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
wxImage image(btn->GetBitmap().GetSize());
image.InitAlpha();
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return;
#ifdef __WXMSW__
wxGCDC dc2(dc);
#else
wxDC &dc2(dc);
#endif
dc2.SetPen(wxPen("#F1754E", 1));
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
dc.SelectObject(wxNullBitmap);
btn->SetBitmapLabel(bmp);
}
// ORCA match style with button on windows
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
{
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
btn->SetBackgroundColour(*wxWHITE);
if (!window) return;
auto* btn = static_cast<wxButton*>(window);
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
wxGetApp().UpdateDarkUI(btn);
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
wxSize btn_sz = btn->GetSize();
wxImage image(btn_sz);
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return bmp;
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
wxGCDC dc2(dc);
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
int padding = btn->FromDIP(5);
dc2.SetPen(*wxTRANSPARENT_PEN);
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
return bmp;
};
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
}
void ColourPicker::set_value(const boost::any& value, bool change_event)
void ColourPicker::set_value(const std::string& value, bool change_event)
{
m_disable_change_event = !change_event;
const wxString clr_str(boost::any_cast<wxString>(value));
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
set_value(boost::any(wxString::FromUTF8(value)), change_event);
}
#ifdef __WXMSW__
const wxColour parsed_clr(clr_str);
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
field->SetColour(clr);
draw_bmp_btn(field, clr);
#else
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk())
set_undef_value(field);
else
field->SetColour(clr);
#endif
m_disable_change_event = false;
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
{
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
const wxString clr_str = boost::any_cast<wxString>(value);
const wxColour parsed(clr_str);
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
draw_bmp_btn();
}
boost::any& ColourPicker::get_value()
{
save_colors_to_config();
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
if (colour == wxTransparentColour)
m_value = std::string("");
else {
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
}
return m_value;
m_value = m_colour == wxTransparentColour ? std::string() :
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
return m_value;
}
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
{
if (!color || !color->IsOk() || *color == m_colour) return;
const std::string previous = boost::any_cast<std::string>(get_value());
m_colour = *color;
draw_bmp_btn();
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
}
void ColourPicker::msw_rescale()
{
Field::msw_rescale();
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
if (m_opt.height >= 0)
size.SetHeight(m_opt.height * m_em_unit);
else if (parent_is_custom_ctrl && opt_height > 0)
size.SetHeight(lround(opt_height * m_em_unit));
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
if (parent_is_custom_ctrl)
field->SetSize(size);
else
field->SetMinSize(size);
#ifdef __WXMSW__
draw_bmp_btn(field, field->GetColour());
#else
if (field->GetColour() == wxTransparentColour)
set_undef_value(field);
#endif
if (parent_is_custom_ctrl) window->SetSize(size);
else window->SetMinSize(size);
draw_bmp_btn();
}
void ColourPicker::sys_color_changed()
{
#ifdef _WIN32
if (wxWindow* win = this->getWindow())
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
draw_bmp_btn(picker, picker->GetColour());
}
#endif
if (window) wxGetApp().UpdateDarkUI(window, true);
draw_bmp_btn();
}
void ColourPicker::on_button_click(wxCommandEvent &event) {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
for (int i = 0; i < colors.size(); i++) {
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
}
}
m_picker_widget->OnButtonClick(event);
#endif
}
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
{
#if !defined(__linux__) && !defined(__LINUX__)
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
if (m_picker_widget) {
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
m_clrData = m_picker_widget->GetColourData();
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
ColorPickerDialog dialog(m_parent, color, {}, false, window);
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
const auto& result = std::get<ColorRGBA>(*dialog.selection());
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
}
#endif
}
void ColourPicker::save_colors_to_config() {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors;
if (colors.size() != CUSTOM_COLOR_COUNT) {
colors.resize(CUSTOM_COLOR_COUNT);
}
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
}
wxGetApp().app_config->save_custom_color_to_config(colors);
}
#endif
}
void PointCtrl::BUILD()