// Copyright (c) 2005, Niels Martin Hansen // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of the Aegisub Group nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // Aegisub Project http://www.aegisub.org/ /// @file dialog_colorpicker.cpp /// @brief Custom colour-selection dialogue box /// @ingroup tools_ui /// #include "config.h" #include "dialog_colorpicker.h" #include "ass_style.h" #include "colorspace.h" #include "compat.h" #include "help_button.h" #include "libresrc/libresrc.h" #include "options.h" #include "persist_location.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __WXMAC__ #include #endif namespace { enum class PickerDirection { HorzVert, Horz, Vert }; static const int spectrum_horz_vert_arrow_size = 4; wxDEFINE_EVENT(EVT_SPECTRUM_CHANGE, wxCommandEvent); class ColorPickerSpectrum final : public wxControl { int x; int y; wxBitmap *background; PickerDirection direction; void OnPaint(wxPaintEvent &evt) { if (!background) return; int height = background->GetHeight(); int width = background->GetWidth(); wxPaintDC dc(this); wxMemoryDC memdc; memdc.SelectObject(*background); dc.Blit(1, 1, width, height, &memdc, 0, 0); wxPoint arrow[3]; wxRect arrow_box; wxPen invpen(*wxWHITE, 3); invpen.SetCap(wxCAP_BUTT); dc.SetLogicalFunction(wxXOR); dc.SetPen(invpen); switch (direction) { case PickerDirection::HorzVert: // Make a little cross dc.DrawLine(x-4, y+1, x+7, y+1); dc.DrawLine(x+1, y-4, x+1, y+7); break; case PickerDirection::Horz: // Make a vertical line stretching all the way across dc.DrawLine(x+1, 1, x+1, height+1); // Points for arrow arrow[0] = wxPoint(x+1, height+2); arrow[1] = wxPoint(x+1-spectrum_horz_vert_arrow_size, height+2+spectrum_horz_vert_arrow_size); arrow[2] = wxPoint(x+1+spectrum_horz_vert_arrow_size, height+2+spectrum_horz_vert_arrow_size); arrow_box.SetLeft(0); arrow_box.SetTop(height + 2); arrow_box.SetRight(width + 1 + spectrum_horz_vert_arrow_size); arrow_box.SetBottom(height + 2 + spectrum_horz_vert_arrow_size); break; case PickerDirection::Vert: // Make a horizontal line stretching all the way across dc.DrawLine(1, y+1, width+1, y+1); // Points for arrow arrow[0] = wxPoint(width+2, y+1); arrow[1] = wxPoint(width+2+spectrum_horz_vert_arrow_size, y+1-spectrum_horz_vert_arrow_size); arrow[2] = wxPoint(width+2+spectrum_horz_vert_arrow_size, y+1+spectrum_horz_vert_arrow_size); arrow_box.SetLeft(width + 2); arrow_box.SetTop(0); arrow_box.SetRight(width + 2 + spectrum_horz_vert_arrow_size); arrow_box.SetBottom(height + 1 + spectrum_horz_vert_arrow_size); break; } if (direction == PickerDirection::Horz || direction == PickerDirection::Vert) { wxBrush bgBrush; bgBrush.SetColour(GetBackgroundColour()); dc.SetLogicalFunction(wxCOPY); dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(bgBrush); dc.DrawRectangle(arrow_box); // Arrow pointing at current point dc.SetBrush(*wxBLACK_BRUSH); dc.DrawPolygon(3, arrow); } // Border around the spectrum wxPen blkpen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), 1); blkpen.SetCap(wxCAP_BUTT); dc.SetLogicalFunction(wxCOPY); dc.SetPen(blkpen); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(0, 0, background->GetWidth()+2, background->GetHeight()+2); } void OnMouse(wxMouseEvent &evt) { evt.Skip(); // We only care about mouse move events during a drag if (evt.Moving()) return; if (evt.LeftDown()) { CaptureMouse(); SetCursor(wxCursor(wxCURSOR_BLANK)); } else if (evt.LeftUp() && HasCapture()) { ReleaseMouse(); SetCursor(wxNullCursor); } if (evt.LeftDown() || (HasCapture() && evt.LeftIsDown())) { // Adjust for the 1px black border around the control int newx = mid(0, evt.GetX() - 1, GetClientSize().x - 3); int newy = mid(0, evt.GetY() - 1, GetClientSize().y - 3); SetXY(newx, newy); wxCommandEvent evt2(EVT_SPECTRUM_CHANGE, GetId()); AddPendingEvent(evt2); } } bool AcceptsFocusFromKeyboard() const override { return false; } public: ColorPickerSpectrum(wxWindow *parent, PickerDirection direction, wxSize size) : wxControl(parent, -1, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) , x(-1) , y(-1) , background(nullptr) , direction(direction) { size.x += 2; size.y += 2; if (direction == PickerDirection::Vert) size.x += spectrum_horz_vert_arrow_size + 1; if (direction == PickerDirection::Horz) size.y += spectrum_horz_vert_arrow_size + 1; SetClientSize(size); SetMinSize(GetSize()); Bind(wxEVT_LEFT_DOWN, &ColorPickerSpectrum::OnMouse, this); Bind(wxEVT_LEFT_UP, &ColorPickerSpectrum::OnMouse, this); Bind(wxEVT_MOTION, &ColorPickerSpectrum::OnMouse, this); Bind(wxEVT_PAINT, &ColorPickerSpectrum::OnPaint, this); } int GetX() const { return x; } int GetY() const { return y; } void SetXY(int xx, int yy) { if (x != xx || y != yy) { x = xx; y = yy; Refresh(false); } } /// @brief Set the background image for this spectrum /// @param new_background New background image /// @param force Repaint even if it appears to be the same image void SetBackground(wxBitmap *new_background, bool force = false) { if (background == new_background && !force) return; background = new_background; Refresh(false); } }; #ifdef WIN32 #define STATIC_BORDER_FLAG wxSTATIC_BORDER #else #define STATIC_BORDER_FLAG wxSIMPLE_BORDER #endif wxDEFINE_EVENT(EVT_RECENT_SELECT, wxThreadEvent); /// @class ColorPickerRecent /// @brief A grid of recently used colors which can be selected by clicking on them class ColorPickerRecent final : public wxStaticBitmap { int rows; ///< Number of rows of colors int cols; ///< Number of cols of colors int cellsize; ///< Width/Height of each cell /// The colors currently displayed in the control std::vector colors; void OnClick(wxMouseEvent &evt) { wxSize cs = GetClientSize(); int cx = evt.GetX() * cols / cs.x; int cy = evt.GetY() * rows / cs.y; if (cx < 0 || cx > cols || cy < 0 || cy > rows) return; int i = cols*cy + cx; if (i >= 0 && i < (int)colors.size()) { wxThreadEvent evnt(EVT_RECENT_SELECT, GetId()); evnt.SetPayload(colors[i]); AddPendingEvent(evnt); } } void UpdateBitmap() { wxSize sz = GetClientSize(); wxBitmap background(sz.x, sz.y); wxMemoryDC dc(background); dc.SetPen(*wxTRANSPARENT_PEN); for (int cy = 0; cy < rows; cy++) { for (int cx = 0; cx < cols; cx++) { int x = cx * cellsize; int y = cy * cellsize; dc.SetBrush(wxBrush(to_wx(colors[cy * cols + cx]))); dc.DrawRectangle(x, y, x+cellsize, y+cellsize); } } { wxEventBlocker blocker(this); SetBitmap(background); } Refresh(false); } bool AcceptsFocusFromKeyboard() const override { return false; } public: ColorPickerRecent(wxWindow *parent, int cols, int rows, int cellsize) : wxStaticBitmap(parent, -1, wxBitmap(), wxDefaultPosition, wxDefaultSize, STATIC_BORDER_FLAG) , rows(rows) , cols(cols) , cellsize(cellsize) { colors.resize(rows * cols); SetClientSize(cols*cellsize, rows*cellsize); SetMinSize(GetSize()); SetMaxSize(GetSize()); SetCursor(*wxCROSS_CURSOR); Bind(wxEVT_LEFT_DOWN, &ColorPickerRecent::OnClick, this); Bind(wxEVT_SIZE, [=](wxSizeEvent&) { UpdateBitmap(); }); } /// Load the colors to show void Load(std::vector const& recent_colors) { colors = recent_colors; colors.resize(rows * cols); UpdateBitmap(); } /// Get the list of recent colors std::vector Save() const { return colors; } /// Add a color to the beginning of the recent list void AddColor(agi::Color color) { auto existing = find(colors.begin(), colors.end(), color); if (existing != colors.end()) rotate(colors.begin(), existing, existing + 1); else { colors.insert(colors.begin(), color); colors.pop_back(); } UpdateBitmap(); } }; wxDEFINE_EVENT(EVT_DROPPER_SELECT, wxThreadEvent); class ColorPickerScreenDropper final : public wxControl { wxBitmap capture; int resx, resy; int magnification; void OnMouse(wxMouseEvent &evt) { int x = evt.GetX(); int y = evt.GetY(); if (x >= 0 && x < capture.GetWidth() && y >= 0 && y < capture.GetHeight()) { wxNativePixelData pd(capture, wxRect(x, y, 1, 1)); wxNativePixelData::Iterator pdi(pd.GetPixels()); agi::Color color(pdi.Red(), pdi.Green(), pdi.Blue(), 0); wxThreadEvent evnt(EVT_DROPPER_SELECT, GetId()); evnt.SetPayload(color); AddPendingEvent(evnt); } } void OnPaint(wxPaintEvent &evt) { wxPaintDC(this).DrawBitmap(capture, 0, 0); } bool AcceptsFocusFromKeyboard() const override { return false; } public: ColorPickerScreenDropper(wxWindow *parent, int resx, int resy, int magnification) : wxControl(parent, -1, wxDefaultPosition, wxDefaultSize, STATIC_BORDER_FLAG) , capture(resx * magnification, resy * magnification, wxNativePixelFormat::BitsPerPixel) , resx(resx) , resy(resy) , magnification(magnification) { SetClientSize(resx*magnification, resy*magnification); SetMinSize(GetSize()); SetMaxSize(GetSize()); SetCursor(*wxCROSS_CURSOR); wxMemoryDC capdc(capture); capdc.SetPen(*wxTRANSPARENT_PEN); capdc.SetBrush(*wxWHITE_BRUSH); capdc.DrawRectangle(0, 0, capture.GetWidth(), capture.GetHeight()); Bind(wxEVT_PAINT, &ColorPickerScreenDropper::OnPaint, this); Bind(wxEVT_LEFT_DOWN, &ColorPickerScreenDropper::OnMouse, this); } void DropFromScreenXY(int x, int y); }; void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) { wxMemoryDC capdc(capture); capdc.SetPen(*wxTRANSPARENT_PEN); #ifndef __WXMAC__ wxScreenDC screen; capdc.StretchBlit(0, 0, resx * magnification, resy * magnification, &screen, x - resx / 2, y - resy / 2, resx, resy); #else // wxScreenDC doesn't work on recent versions of OS X so do it manually // Doesn't bother handling the case where the rect overlaps two monitors CGDirectDisplayID display_id; uint32_t display_count; CGGetDisplaysWithPoint(CGPointMake(x, y), 1, &display_id, &display_count); agi::scoped_holder img(CGDisplayCreateImageForRect(display_id, CGRectMake(x - resx / 2, y - resy / 2, resx, resy)), CGImageRelease); NSUInteger width = CGImageGetWidth(img); NSUInteger height = CGImageGetHeight(img); std::vector imgdata(height * width * 4); agi::scoped_holder colorspace(CGColorSpaceCreateDeviceRGB(), CGColorSpaceRelease); agi::scoped_holder bmp_context(CGBitmapContextCreate(&imgdata[0], width, height, 8, 4 * width, colorspace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big), CGContextRelease); CGContextDrawImage(bmp_context, CGRectMake(0, 0, width, height), img); for (int x = 0; x < resx; x++) { for (int y = 0; y < resy; y++) { uint8_t *pixel = &imgdata[y * width * 4 + x * 4]; capdc.SetBrush(wxBrush(wxColour(pixel[0], pixel[1], pixel[2]))); capdc.DrawRectangle(x * magnification, y * magnification, magnification, magnification); } } #endif Refresh(false); } class DialogColorPicker final : public wxDialog { std::unique_ptr persist; agi::Color cur_color; ///< Currently selected colour bool spectrum_dirty; ///< Does the spectrum image need to be regenerated? ColorPickerSpectrum *spectrum; ///< The 2D color spectrum ColorPickerSpectrum *slider; ///< The 1D slider for the color component not in the slider ColorPickerSpectrum *alpha_slider; wxChoice *colorspace_choice; ///< The dropdown list to select colorspaces wxSpinCtrl *rgb_input[3]; wxBitmap rgb_spectrum[3]; ///< x/y spectrum bitmap where color "i" is excluded from wxBitmap rgb_slider[3]; ///< z spectrum for color "i" wxSpinCtrl *hsl_input[3]; wxBitmap hsl_spectrum; ///< h/s spectrum wxBitmap hsl_slider; ///< l spectrum wxSpinCtrl *hsv_input[3]; wxBitmap hsv_spectrum; ///< s/v spectrum wxBitmap hsv_slider; ///< h spectrum wxBitmap alpha_slider_img; wxTextCtrl *ass_input; wxTextCtrl *html_input; wxSpinCtrl *alpha_input; /// The eyedropper is set to a blank icon when it's clicked, so store its normal bitmap wxBitmap eyedropper_bitmap; /// The point where the eyedropper was click, used to make it possible to either /// click the eyedropper or drag the eyedropper wxPoint eyedropper_grab_point; bool eyedropper_is_grabbed; wxStaticBitmap *preview_box; ///< A box which simply shows the current color ColorPickerRecent *recent_box; ///< A grid of recently used colors ColorPickerScreenDropper *screen_dropper; wxStaticBitmap *screen_dropper_icon; /// Update all other controls as a result of modifying an RGB control void UpdateFromRGB(bool dirty = true); /// Update all other controls as a result of modifying an HSL control void UpdateFromHSL(bool dirty = true); /// Update all other controls as a result of modifying an HSV control void UpdateFromHSV(bool dirty = true); /// Update all other controls as a result of modifying the ASS format control void UpdateFromAss(); /// Update all other controls as a result of modifying the HTML format control void UpdateFromHTML(); void UpdateFromAlpha(); void SetRGB(agi::Color new_color); void SetHSL(unsigned char r, unsigned char g, unsigned char b); void SetHSV(unsigned char r, unsigned char g, unsigned char b); /// Redraw the spectrum display void UpdateSpectrumDisplay(); wxBitmap *MakeGBSpectrum(); wxBitmap *MakeRBSpectrum(); wxBitmap *MakeRGSpectrum(); wxBitmap *MakeHSSpectrum(); wxBitmap *MakeSVSpectrum(); /// Constructor helper function for making the color input box sizers template wxSizer *MakeColorInputSizer(wxString (&labels)[N], Control *(&inputs)[N]); void OnChangeMode(wxCommandEvent &evt); void OnSpectrumChange(wxCommandEvent &evt); void OnSliderChange(wxCommandEvent &evt); void OnAlphaSliderChange(wxCommandEvent &evt); void OnRecentSelect(wxThreadEvent &evt); // also handles dropper pick void OnDropperMouse(wxMouseEvent &evt); void OnMouse(wxMouseEvent &evt); void OnCaptureLost(wxMouseCaptureLostEvent&); std::function callback; public: DialogColorPicker(wxWindow *parent, agi::Color initial_color, std::function callback, bool alpha); ~DialogColorPicker(); void SetColor(agi::Color new_color); void AddColorToRecent(); }; static const int slider_width = 10; ///< width in pixels of the color slider control static const int alpha_box_size = 5; template static wxBitmap make_slider_img(Func func) { unsigned char *slid = (unsigned char *)calloc(slider_width * 256 * 3, 1); func(slid); wxImage img(slider_width, 256, slid); return wxBitmap(img); } template static wxBitmap make_slider(Func func) { return make_slider_img([&](unsigned char *slid) { for (int y = 0; y < 256; ++y) { unsigned char rgb[3]; func(y, rgb); for (int x = 0; x < slider_width; ++x) memcpy(slid + y * slider_width * 3 + x * 3, rgb, 3); } }); } DialogColorPicker::DialogColorPicker(wxWindow *parent, agi::Color initial_color, std::function callback, bool alpha) : wxDialog(parent, -1, _("Select Color")) , callback(std::move(callback)) { // generate spectrum slider bar images for (int i = 0; i < 3; ++i) { rgb_slider[i] = make_slider([=](int y, unsigned char *rgb) { memset(rgb, 0, 3); rgb[i] = y; }); } hsl_slider = make_slider([](int y, unsigned char *rgb) { memset(rgb, y, 3); }); hsv_slider = make_slider([](int y, unsigned char *rgb) { hsv_to_rgb(y, 255, 255, rgb, rgb + 1, rgb + 2); }); // Create the controls for the dialog wxSizer *spectrum_box = new wxStaticBoxSizer(wxVERTICAL, this, _("Color spectrum")); spectrum = new ColorPickerSpectrum(this, PickerDirection::HorzVert, wxSize(256, 256)); slider = new ColorPickerSpectrum(this, PickerDirection::Vert, wxSize(slider_width, 256)); alpha_slider = new ColorPickerSpectrum(this, PickerDirection::Vert, wxSize(slider_width, 256)); wxString modes[] = { _("RGB/R"), _("RGB/G"), _("RGB/B"), _("HSL/L"), _("HSV/H") }; colorspace_choice = new wxChoice(this, -1, wxDefaultPosition, wxDefaultSize, 5, modes); wxSize colorinput_size = GetTextExtent(" &H10117B& "); colorinput_size.SetHeight(-1); wxSize colorinput_labelsize(40, -1); wxSizer *rgb_box = new wxStaticBoxSizer(wxHORIZONTAL, this, _("RGB color")); wxSizer *hsl_box = new wxStaticBoxSizer(wxVERTICAL, this, _("HSL color")); wxSizer *hsv_box = new wxStaticBoxSizer(wxVERTICAL, this, _("HSV color")); for (auto& elem : rgb_input) elem = new wxSpinCtrl(this, -1, "", wxDefaultPosition, colorinput_size, wxSP_ARROW_KEYS, 0, 255); ass_input = new wxTextCtrl(this, -1, "", wxDefaultPosition, colorinput_size); html_input = new wxTextCtrl(this, -1, "", wxDefaultPosition, colorinput_size); alpha_input = new wxSpinCtrl(this, -1, "", wxDefaultPosition, colorinput_size, wxSP_ARROW_KEYS, 0, 255); for (auto& elem : hsl_input) elem = new wxSpinCtrl(this, -1, "", wxDefaultPosition, colorinput_size, wxSP_ARROW_KEYS, 0, 255); for (auto& elem : hsv_input) elem = new wxSpinCtrl(this, -1, "", wxDefaultPosition, colorinput_size, wxSP_ARROW_KEYS, 0, 255); preview_box = new wxStaticBitmap(this, -1, wxBitmap(40, 40, 24), wxDefaultPosition, wxSize(40, 40), STATIC_BORDER_FLAG); recent_box = new ColorPickerRecent(this, 8, 4, 16); eyedropper_bitmap = GETIMAGE(eyedropper_tool_24); eyedropper_bitmap.SetMask(new wxMask(eyedropper_bitmap, wxColour(255, 0, 255))); screen_dropper_icon = new wxStaticBitmap(this, -1, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxRAISED_BORDER); screen_dropper = new ColorPickerScreenDropper(this, 7, 7, 8); // Arrange the controls in a nice way wxSizer *spectop_sizer = new wxBoxSizer(wxHORIZONTAL); spectop_sizer->Add(new wxStaticText(this, -1, _("Spectrum mode:")), 0, wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT|wxRIGHT, 5); spectop_sizer->Add(colorspace_choice, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_LEFT); spectop_sizer->Add(5, 5, 1, wxEXPAND); spectop_sizer->Add(preview_box, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT); wxSizer *spectrum_sizer = new wxFlexGridSizer(3, 5, 5); spectrum_sizer->Add(spectop_sizer, wxEXPAND); spectrum_sizer->AddStretchSpacer(1); spectrum_sizer->AddStretchSpacer(1); spectrum_sizer->Add(spectrum); spectrum_sizer->Add(slider); spectrum_sizer->Add(alpha_slider); if (!alpha) spectrum_sizer->Hide(alpha_slider); spectrum_box->Add(spectrum_sizer, 0, wxALL, 3); wxString rgb_labels[] = { _("Red:"), _("Green:"), _("Blue:") }; rgb_box->Add(MakeColorInputSizer(rgb_labels, rgb_input), 1, wxALL|wxEXPAND, 3); wxString ass_labels[] = { "ASS:", "HTML:", _("Alpha:") }; wxControl *ass_ctrls[] = { ass_input, html_input, alpha_input }; auto ass_colors_sizer = MakeColorInputSizer(ass_labels, ass_ctrls); if (!alpha) ass_colors_sizer->Hide(alpha_input); rgb_box->Add(ass_colors_sizer, 0, wxALL|wxCENTER|wxEXPAND, 3); wxString hsl_labels[] = { _("Hue:"), _("Sat.:"), _("Lum.:") }; hsl_box->Add(MakeColorInputSizer(hsl_labels, hsl_input), 0, wxALL|wxEXPAND, 3); wxString hsv_labels[] = { _("Hue:"), _("Sat.:"), _("Value:") }; hsv_box->Add(MakeColorInputSizer(hsv_labels, hsv_input), 0, wxALL|wxEXPAND, 3); wxSizer *hsx_sizer = new wxBoxSizer(wxHORIZONTAL); hsx_sizer->Add(hsl_box); hsx_sizer->AddSpacer(5); hsx_sizer->Add(hsv_box); wxSizer *picker_sizer = new wxBoxSizer(wxHORIZONTAL); picker_sizer->AddStretchSpacer(); picker_sizer->Add(screen_dropper_icon, 0, wxALIGN_CENTER|wxRIGHT, 5); picker_sizer->Add(screen_dropper, 0, wxALIGN_CENTER); picker_sizer->AddStretchSpacer(); picker_sizer->Add(recent_box, 0, wxALIGN_CENTER); picker_sizer->AddStretchSpacer(); wxStdDialogButtonSizer *button_sizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL | wxHELP); wxSizer *input_sizer = new wxBoxSizer(wxVERTICAL); input_sizer->Add(rgb_box, 0, wxALIGN_CENTER|wxEXPAND); input_sizer->AddSpacer(5); input_sizer->Add(hsx_sizer, 0, wxALIGN_CENTER|wxEXPAND); input_sizer->AddStretchSpacer(1); input_sizer->Add(picker_sizer, 0, wxALIGN_CENTER|wxEXPAND); input_sizer->AddStretchSpacer(2); input_sizer->Add(button_sizer, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM); wxSizer *main_sizer = new wxBoxSizer(wxHORIZONTAL); main_sizer->Add(spectrum_box, 1, wxALL | wxEXPAND, 5); main_sizer->Add(input_sizer, 0, (wxALL&~wxLEFT)|wxEXPAND, 5); SetSizerAndFit(main_sizer); persist = agi::util::make_unique(this, "Tool/Colour Picker"); // Fill the controls int mode = OPT_GET("Tool/Colour Picker/Mode")->GetInt(); if (mode < 0 || mode > 4) mode = 3; // HSL default colorspace_choice->SetSelection(mode); SetColor(initial_color); recent_box->Load(OPT_GET("Tool/Colour Picker/Recent Colours")->GetListColor()); using std::bind; for (int i = 0; i < 3; ++i) { rgb_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromRGB, this, true)); rgb_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromRGB, this, true)); hsl_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromHSL, this, true)); hsl_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHSL, this, true)); hsv_input[i]->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromHSV, this, true)); hsv_input[i]->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHSV, this, true)); } ass_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromAss, this)); html_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromHTML, this)); alpha_input->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromAlpha, this)); alpha_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromAlpha, this)); screen_dropper_icon->Bind(wxEVT_MOTION, &DialogColorPicker::OnDropperMouse, this); screen_dropper_icon->Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnDropperMouse, this); screen_dropper_icon->Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnDropperMouse, this); screen_dropper_icon->Bind(wxEVT_MOUSE_CAPTURE_LOST, &DialogColorPicker::OnCaptureLost, this); Bind(wxEVT_MOTION, &DialogColorPicker::OnMouse, this); Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnMouse, this); Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnMouse, this); spectrum->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSpectrumChange, this); slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSliderChange, this); alpha_slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnAlphaSliderChange, this); recent_box->Bind(EVT_RECENT_SELECT, &DialogColorPicker::OnRecentSelect, this); screen_dropper->Bind(EVT_DROPPER_SELECT, &DialogColorPicker::OnRecentSelect, this); colorspace_choice->Bind(wxEVT_CHOICE, &DialogColorPicker::OnChangeMode, this); button_sizer->GetHelpButton()->Bind(wxEVT_BUTTON, bind(&HelpButton::OpenPage, "Colour Picker")); } template wxSizer *DialogColorPicker::MakeColorInputSizer(wxString (&labels)[N], Control *(&inputs)[N]) { auto sizer = new wxFlexGridSizer(2, 5, 5); for (int i = 0; i < N; ++i) { sizer->Add(new wxStaticText(this, -1, labels[i]), wxSizerFlags(1).Center().Left()); sizer->Add(inputs[i]); } sizer->AddGrowableCol(0,1); return sizer; } DialogColorPicker::~DialogColorPicker() { if (screen_dropper_icon->HasCapture()) screen_dropper_icon->ReleaseMouse(); } static void change_value(wxSpinCtrl *ctrl, int value) { wxEventBlocker blocker(ctrl); ctrl->SetValue(value); } void DialogColorPicker::SetColor(agi::Color new_color) { change_value(alpha_input, new_color.a); alpha_slider->SetXY(0, new_color.a); cur_color.a = new_color.a; SetRGB(new_color); spectrum_dirty = true; UpdateFromRGB(); } void DialogColorPicker::AddColorToRecent() { recent_box->AddColor(cur_color); OPT_SET("Tool/Colour Picker/Recent Colours")->SetListColor(recent_box->Save()); } void DialogColorPicker::SetRGB(agi::Color new_color) { change_value(rgb_input[0], new_color.r); change_value(rgb_input[1], new_color.g); change_value(rgb_input[2], new_color.b); new_color.a = cur_color.a; cur_color = new_color; } void DialogColorPicker::SetHSL(unsigned char r, unsigned char g, unsigned char b) { unsigned char h, s, l; rgb_to_hsl(r, g, b, &h, &s, &l); change_value(hsl_input[0], h); change_value(hsl_input[1], s); change_value(hsl_input[2], l); } void DialogColorPicker::SetHSV(unsigned char r, unsigned char g, unsigned char b) { unsigned char h, s, v; rgb_to_hsv(r, g, b, &h, &s, &v); change_value(hsv_input[0], h); change_value(hsv_input[1], s); change_value(hsv_input[2], v); } void DialogColorPicker::UpdateFromRGB(bool dirty) { unsigned char r = rgb_input[0]->GetValue(); unsigned char g = rgb_input[1]->GetValue(); unsigned char b = rgb_input[2]->GetValue(); SetHSL(r, g, b); SetHSV(r, g, b); cur_color = agi::Color(r, g, b, cur_color.a); ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted())); html_input->ChangeValue(to_wx(cur_color.GetHexFormatted())); if (dirty) spectrum_dirty = true; UpdateSpectrumDisplay(); } void DialogColorPicker::UpdateFromHSL(bool dirty) { unsigned char r, g, b, h, s, l; h = hsl_input[0]->GetValue(); s = hsl_input[1]->GetValue(); l = hsl_input[2]->GetValue(); hsl_to_rgb(h, s, l, &r, &g, &b); SetRGB(agi::Color(r, g, b)); SetHSV(r, g, b); ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted())); html_input->ChangeValue(to_wx(cur_color.GetHexFormatted())); if (dirty) spectrum_dirty = true; UpdateSpectrumDisplay(); } void DialogColorPicker::UpdateFromHSV(bool dirty) { unsigned char r, g, b, h, s, v; h = hsv_input[0]->GetValue(); s = hsv_input[1]->GetValue(); v = hsv_input[2]->GetValue(); hsv_to_rgb(h, s, v, &r, &g, &b); SetRGB(agi::Color(r, g, b)); SetHSL(r, g, b); ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted())); html_input->ChangeValue(to_wx(cur_color.GetHexFormatted())); if (dirty) spectrum_dirty = true; UpdateSpectrumDisplay(); } void DialogColorPicker::UpdateFromAss() { agi::Color color(from_wx(ass_input->GetValue())); SetRGB(color); SetHSL(color.r, color.g, color.b); SetHSV(color.r, color.g, color.b); html_input->ChangeValue(to_wx(cur_color.GetHexFormatted())); spectrum_dirty = true; UpdateSpectrumDisplay(); } void DialogColorPicker::UpdateFromHTML() { agi::Color color(from_wx(html_input->GetValue())); SetRGB(color); SetHSL(color.r, color.g, color.b); SetHSV(color.r, color.g, color.b); ass_input->ChangeValue(to_wx(cur_color.GetAssOverrideFormatted())); spectrum_dirty = true; UpdateSpectrumDisplay(); } void DialogColorPicker::UpdateFromAlpha() { cur_color.a = alpha_input->GetValue(); alpha_slider->SetXY(0, cur_color.a); callback(cur_color); } void DialogColorPicker::UpdateSpectrumDisplay() { int i = colorspace_choice->GetSelection(); if (spectrum_dirty) { switch (i) { case 0: spectrum->SetBackground(MakeGBSpectrum(), true); break; case 1: spectrum->SetBackground(MakeRBSpectrum(), true); break; case 2: spectrum->SetBackground(MakeRGSpectrum(), true); break; case 3: spectrum->SetBackground(MakeHSSpectrum(), true); break; case 4: spectrum->SetBackground(MakeSVSpectrum(), true); break; } } switch (i) { case 0: case 1: case 2: slider->SetBackground(&rgb_slider[i]); slider->SetXY(0, rgb_input[i]->GetValue()); spectrum->SetXY(rgb_input[2 - (i == 2)]->GetValue(), rgb_input[1 == 0]->GetValue()); break; case 3: slider->SetBackground(&hsl_slider); slider->SetXY(0, hsl_input[2]->GetValue()); spectrum->SetXY(hsl_input[1]->GetValue(), hsl_input[0]->GetValue()); break; case 4: slider->SetBackground(&hsv_slider); slider->SetXY(0, hsv_input[0]->GetValue()); spectrum->SetXY(hsv_input[1]->GetValue(), hsv_input[2]->GetValue()); break; } spectrum_dirty = false; wxBitmap tempBmp = preview_box->GetBitmap(); { wxMemoryDC previewdc; previewdc.SelectObject(tempBmp); previewdc.SetPen(*wxTRANSPARENT_PEN); previewdc.SetBrush(wxBrush(to_wx(cur_color))); previewdc.DrawRectangle(0, 0, 40, 40); } preview_box->SetBitmap(tempBmp); alpha_slider_img = make_slider_img([=](unsigned char *slid) { static_assert(slider_width % alpha_box_size == 0, "Slider width must be a multiple of alpha box width"); for (int y = 0; y < 256; ++y) { unsigned char inv_y = 0xFF - y; unsigned char box_colors[] = { static_cast(0x66 - inv_y * 0x66 / 0xFF), static_cast(0x99 - inv_y * 0x99 / 0xFF) }; if ((y / alpha_box_size) & 1) std::swap(box_colors[0], box_colors[1]); unsigned char colors[2][3]; for (int i = 0; i < 2; ++i) { colors[i][0] = cur_color.r * inv_y / 0xFF + box_colors[i]; colors[i][1] = cur_color.g * inv_y / 0xFF + box_colors[i]; colors[i][2] = cur_color.b * inv_y / 0xFF + box_colors[i]; } for (int x = 0; x < slider_width; ++x) { *slid++ = colors[x / alpha_box_size][0]; *slid++ = colors[x / alpha_box_size][1]; *slid++ = colors[x / alpha_box_size][2]; } } }); alpha_slider->SetBackground(&alpha_slider_img, true); callback(cur_color); } template static wxBitmap *make_spectrum(wxBitmap *bitmap, Func func) { wxImage spectrum_image(256, 256); func(spectrum_image.GetData()); *bitmap = wxBitmap(spectrum_image); return bitmap; } wxBitmap *DialogColorPicker::MakeGBSpectrum() { return make_spectrum(&rgb_spectrum[0], [=](unsigned char *spec) { for (int g = 0; g < 256; g++) { for (int b = 0; b < 256; b++) { *spec++ = cur_color.r; *spec++ = g; *spec++ = b; } } }); } wxBitmap *DialogColorPicker::MakeRBSpectrum() { return make_spectrum(&rgb_spectrum[1], [=](unsigned char *spec) { for (int r = 0; r < 256; r++) { for (int b = 0; b < 256; b++) { *spec++ = r; *spec++ = cur_color.g; *spec++ = b; } } }); } wxBitmap *DialogColorPicker::MakeRGSpectrum() { return make_spectrum(&rgb_spectrum[2], [=](unsigned char *spec) { for (int r = 0; r < 256; r++) { for (int g = 0; g < 256; g++) { *spec++ = r; *spec++ = g; *spec++ = cur_color.b; } } }); } wxBitmap *DialogColorPicker::MakeHSSpectrum() { int l = hsl_input[2]->GetValue(); return make_spectrum(&hsl_spectrum, [=](unsigned char *spec) { for (int h = 0; h < 256; h++) { unsigned char maxr, maxg, maxb; hsl_to_rgb(h, 255, l, &maxr, &maxg, &maxb); for (int s = 0; s < 256; s++) { *spec++ = maxr * s / 256 + (255-s) * l / 256; *spec++ = maxg * s / 256 + (255-s) * l / 256; *spec++ = maxb * s / 256 + (255-s) * l / 256; } } }); } wxBitmap *DialogColorPicker::MakeSVSpectrum() { int h = hsv_input[0]->GetValue(); unsigned char maxr, maxg, maxb; hsv_to_rgb(h, 255, 255, &maxr, &maxg, &maxb); return make_spectrum(&hsv_spectrum, [=](unsigned char *spec) { for (int v = 0; v < 256; v++) { int rr = (255-maxr) * v / 256; int rg = (255-maxg) * v / 256; int rb = (255-maxb) * v / 256; for (int s = 0; s < 256; s++) { *spec++ = 255 - rr * s / 256 - (255-v); *spec++ = 255 - rg * s / 256 - (255-v); *spec++ = 255 - rb * s / 256 - (255-v); } } }); } void DialogColorPicker::OnChangeMode(wxCommandEvent &) { spectrum_dirty = true; OPT_SET("Tool/Colour Picker/Mode")->SetInt(colorspace_choice->GetSelection()); UpdateSpectrumDisplay(); } void DialogColorPicker::OnSpectrumChange(wxCommandEvent &) { int i = colorspace_choice->GetSelection(); switch (i) { case 0: case 1: case 2: change_value(rgb_input[2 - (i == 2)], spectrum->GetX()); change_value(rgb_input[i == 0], spectrum->GetY()); break; case 3: change_value(hsl_input[1], spectrum->GetX()); change_value(hsl_input[0], spectrum->GetY()); break; case 4: change_value(hsv_input[1], spectrum->GetX()); change_value(hsv_input[2], spectrum->GetY()); break; } switch (i) { case 0: case 1: case 2: UpdateFromRGB(false); break; case 3: UpdateFromHSL(false); break; case 4: UpdateFromHSV(false); break; } } void DialogColorPicker::OnSliderChange(wxCommandEvent &) { spectrum_dirty = true; int i = colorspace_choice->GetSelection(); switch (i) { case 0: case 1: case 2: change_value(rgb_input[i], slider->GetY()); UpdateFromRGB(false); break; case 3: change_value(hsl_input[2], slider->GetY()); UpdateFromHSL(false); break; case 4: change_value(hsv_input[0], slider->GetY()); UpdateFromHSV(false); break; } } void DialogColorPicker::OnAlphaSliderChange(wxCommandEvent &) { change_value(alpha_input, alpha_slider->GetY()); cur_color.a = alpha_slider->GetY(); callback(cur_color); } void DialogColorPicker::OnRecentSelect(wxThreadEvent &evt) { agi::Color new_color = evt.GetPayload(); new_color.a = cur_color.a; SetColor(new_color); } void DialogColorPicker::OnDropperMouse(wxMouseEvent &evt) { if (evt.LeftDown() && !screen_dropper_icon->HasCapture()) { #ifdef WIN32 screen_dropper_icon->SetCursor(wxCursor("eyedropper_cursor")); #else screen_dropper_icon->SetCursor(*wxCROSS_CURSOR); #endif screen_dropper_icon->SetBitmap(wxNullBitmap); screen_dropper_icon->CaptureMouse(); eyedropper_grab_point = evt.GetPosition(); eyedropper_is_grabbed = false; } if (evt.LeftUp()) { wxPoint ptdiff = evt.GetPosition() - eyedropper_grab_point; bool release_now = eyedropper_is_grabbed || abs(ptdiff.x) + abs(ptdiff.y) > 7; if (release_now) { screen_dropper_icon->ReleaseMouse(); eyedropper_is_grabbed = false; screen_dropper_icon->SetCursor(wxNullCursor); screen_dropper_icon->SetBitmap(eyedropper_bitmap); } else eyedropper_is_grabbed = true; } if (screen_dropper_icon->HasCapture()) { wxPoint scrpos = screen_dropper_icon->ClientToScreen(evt.GetPosition()); screen_dropper->DropFromScreenXY(scrpos.x, scrpos.y); } } /// @brief Hack to redirect events to the screen dropper icon void DialogColorPicker::OnMouse(wxMouseEvent &evt) { if (!screen_dropper_icon->HasCapture()) { evt.Skip(); return; } wxPoint dropper_pos = screen_dropper_icon->ScreenToClient(ClientToScreen(evt.GetPosition())); evt.m_x = dropper_pos.x; evt.m_y = dropper_pos.y; screen_dropper_icon->GetEventHandler()->ProcessEvent(evt); } void DialogColorPicker::OnCaptureLost(wxMouseCaptureLostEvent&) { eyedropper_is_grabbed = false; screen_dropper_icon->SetCursor(wxNullCursor); screen_dropper_icon->SetBitmap(eyedropper_bitmap); } } bool GetColorFromUser(wxWindow* parent, agi::Color original, bool alpha, std::function callback) { DialogColorPicker dialog(parent, original, callback, alpha); bool ok = dialog.ShowModal() == wxID_OK; if (!ok) callback(original); else dialog.AddColorToRecent(); return ok; }