//
// "$Id: Fl_Color_Chooser.cxx,v 1.41 2005/01/24 08:07:18 spitzak Exp $"
//
// Color chooser for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2003 by Bill Spitzak and others.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
// USA.
//
// Please report all bugs and problems to "fltk-bugs@fltk.org".
//
#include <fltk/events.h>
#include <fltk/damage.h>
#include <fltk/ColorChooser.h>
#include <fltk/Box.h>
#include <fltk/draw.h>
#include <fltk/Item.h>
#include <fltk/math.h>
#include <fltk/PopupMenu.h>
#include <fltk/ValueInput.h>
#include <fltk/layout.h>
#include <stdio.h>
using namespace fltk;
// Besides being a useful object on it's own, the ColorChooser was
// an attempt to make a complex composite object that could be easily
// imbedded into a user interface. If you wish to make complex objects
// of your own, be sure to read this code.
// The function color_chooser() creates a window containing a color
// chooser and a few buttons and current-color indicators. It is an
// easier interface for simple programs that just need a color.
// The "hue box" can be a circle or rectilinear.
// You get a circle by defining this:
// #define CIRCLE 1
// And the "hue box" can auto-update when the value changes
// you get this by defining this:
#define UPDATE_HUE_BOX 1
void ColorChooser::hsv2rgb(
float H, float S, float V, float& r, float& g, float& b) {
if (S < 5.0e-6) {
r = g = b = V;
} else {
int i = (int)H;
float f = H - (float)i;
float p1 = V*(1.0f-S);
float p2 = V*(1.0f-S*f);
float p3 = V*(1.0f-S*(1.0f-f));
switch (i) {
case 0: r = V; g = p3; b = p1; break;
case 1: r = p2; g = V; b = p1; break;
case 2: r = p1; g = V; b = p3; break;
case 3: r = p1; g = p2; b = V; break;
case 4: r = p3; g = p1; b = V; break;
case 5: r = V; g = p1; b = p2; break;
}
}
}
void ColorChooser::rgb2hsv(
float r, float g, float b, float& H, float& S, float& V) {
float maxv = r > g ? r : g; if (b > maxv) maxv = b;
V = maxv;
if (maxv>0) {
float minv = r < g ? r : g; if (b < minv) minv = b;
S = 1.0f - float(minv)/maxv;
if (maxv > minv) {
if (maxv == r) {H = (g-b)/float(maxv-minv); if (H<0) H += 6;}
else if (maxv == g) H = 2+(b-r)/float(maxv-minv);
else H = 4+(r-g)/float(maxv-minv);
}
}
}
bool ColorChooser::rgb(float r, float g, float b) {
if (r == r_ && g == g_ && b == b_) return false;
r_ = r; g_ = g; b_ = b;
float ph = hue_;
float ps = saturation_;
float pv = value_;
rgb2hsv(r,g,b,hue_,saturation_,value_);
if (value_ != pv) {
#ifdef UPDATE_HUE_BOX
huebox.redraw(DAMAGE_CONTENTS);
#endif
valuebox.redraw(DAMAGE_VALUE);
alphabox.redraw(DAMAGE_CONTENTS);
}
if (hue_ != ph || saturation_ != ps) {
huebox.redraw(DAMAGE_VALUE);
valuebox.redraw(DAMAGE_CONTENTS);
alphabox.redraw(DAMAGE_CONTENTS);
}
return true;
}
bool ColorChooser::hsv(float h, float s, float v) {
h = fmodf(h,6.0f); if (h < 0) h += 6;
if (s < 0) s = 0; else if (s > 1) s = 1;
if (v < 0) v = 0; else if (v > 1) v = 1;
if (h == hue_ && s == saturation_ && v == value_) return false;
float ph = hue_;
float ps = saturation_;
float pv = value_;
hue_ = h; saturation_ = s; value_ = v;
if (value_ != pv) {
#ifdef UPDATE_HUE_BOX
huebox.redraw(DAMAGE_CONTENTS);
#endif
valuebox.redraw(DAMAGE_VALUE);
alphabox.redraw(DAMAGE_CONTENTS);
}
if (hue_ != ph || saturation_ != ps) {
huebox.redraw(DAMAGE_VALUE);
valuebox.redraw(DAMAGE_CONTENTS);
alphabox.redraw(DAMAGE_CONTENTS);
}
hsv2rgb(h,s,v,r_,g_,b_);
return 1;
}
bool ColorChooser::a(float a) {
alphabox.show();
if (a == a_) return false;
a_ = a;
alphabox.redraw(DAMAGE_VALUE);
return true;
}
void ColorChooser::hide_a() {
alphabox.hide();
}
////////////////////////////////////////////////////////////////
static void tohs(float x, float y, float& h, float& s) {
#ifdef CIRCLE
x = 2*x-1;
y = 1-2*y;
s = sqrtf(x*x+y*y); if (s > 1) s = 1;
h = float(3.0/M_PI)*atan2f(y,x);
if (h<0) h += 6;
#else
h = fmodf(6*x,6.0f); if (h < 0) h += 6;
s = 1-y; if (s < 0) s = 0; else if (s > 1) s = 1;
#endif
}
int ccHueBox::handle(int e) {
static float is;
static float ih;
ColorChooser* c = (ColorChooser*)parent();
switch (e) {
case PUSH:
is = c->s();
ih = c->h();
case DRAG: {
float Xf, Yf, H, S;
Rectangle r(w(),h()); box()->inset(r);
Xf = (event_x()-r.x())/float(r.w());
Yf = (event_y()-r.y())/float(r.h());
tohs(Xf, Yf, H, S);
if (fabsf(H-ih) < 3*6.0f/w()) H = (float)ih;
if (fabsf(S-is) < 3*1.0f/h()) S = (float)is;
if (event_state(CTRL)) H = c->h();
if (c->hsv(H, S, c->v())) c->do_callback();
} return 1;
default:
return Widget::handle(e);
}
}
static const uchar* generate_image(void* vv, int X, int Y, int W, uchar* p) {
ccHueBox* v = (ccHueBox*)vv;
Rectangle r(v->w(),v->h()); v->box()->inset(r);
float Yf = float(Y)/r.h();
#ifdef UPDATE_HUE_BOX
const float V = ((ColorChooser*)(v->parent()))->v();
#else
const float V = 1.0f;
#endif
uchar* buf = p;
for (int x = X; x < X+W; x++) {
float Xf = float(x)/r.w();
float H,S; tohs(Xf,Yf,H,S);
float r,g,b;
ColorChooser::hsv2rgb(H,S,V,r,g,b);
*buf++ = uchar(255*r+.5f);
*buf++ = uchar(255*g+.5f);
*buf++ = uchar(255*b+.5f);
}
return p;
}
void ccHueBox::draw() {
if (damage()&DAMAGE_ALL) draw_frame();
Rectangle r(w(),h()); box()->inset(r);
if (damage() == DAMAGE_VALUE) {
push_clip(Rectangle(r.x()+px,r.y()+py,6,6));
}
drawimage(generate_image, this, RGB, r);
if (damage() == DAMAGE_VALUE) pop_clip();
ColorChooser* c = (ColorChooser*)parent();
#ifdef CIRCLE
int X = int(.5f*(cosf(c->h()*float(M_PI/3.0))*c->s()+1) * (r.w()-6));
int Y = int(.5f*(1-sinf(c->h()*float(M_PI/3.0))*c->s()) * (r.h()-6));
#else
int X = int(c->h()/6.0f*(r.w()-6));
int Y = int((1-c->s())*(r.h()-6));
#endif
if (X < 0) X = 0; else if (X > r.w()-6) X = r.w()-6;
if (Y < 0) Y = 0; else if (Y > r.h()-6) Y = r.h()-6;
// color(c->v()>.75 ? BLACK : WHITE);
buttonbox()->draw(Rectangle(r.x()+X, r.y()+Y, 6, 6), style(), 0);
px = X; py = Y;
}
////////////////////////////////////////////////////////////////
bool ccValueBox::is_alpha() const {
ColorChooser* c = (ColorChooser*)parent();
return this == &(c->alphabox);
}
int ccValueBox::handle(int e) {
static float iv;
ColorChooser* c = (ColorChooser*)parent();
switch (e) {
case PUSH:
iv = is_alpha() ? c->a() : c->v();
case DRAG: {
float Yf;
Rectangle r(w(),h()); box()->inset(r);
Yf = 1-(event_y()-r.y())/float(r.h());
if (fabsf(Yf-iv)<(3*1.0f/h())) Yf = iv;
if (is_alpha()) {
if (c->a(Yf)) c->do_callback();
} else {
if (c->hsv(c->h(),c->s(),Yf)) c->do_callback();
}} return 1;
default:
return Widget::handle(e);
}
}
struct Idata {
float r, g, b;
Rectangle wh;
bool aimage;
};
static const uchar* generate_vimage(void* vv, int X, int Y, int W, uchar* p) {
const Idata& i = *((Idata*)vv);
float yy = 255*(float(Y-3)/(i.wh.h()-6));
if (yy<0) yy = 0; else if (yy > 255) yy = 255;
float Yf = 255-yy;
uchar r = uchar(i.r*Yf+.5f);
uchar g = uchar(i.g*Yf+.5f);
uchar b = uchar(i.b*Yf+.5f);
uchar* buf = p;
if (i.aimage) {
uchar r1 = uchar(r+yy);
uchar g1 = uchar(g+yy);
uchar b1 = uchar(b+yy);
int xr = Y&8;
for (int x = X; x < X+W; x++) {
if ((x^xr)&8) {
*buf++ = r1; *buf++ = g1; *buf++ = b1;
} else {
*buf++ = r; *buf++ = g; *buf++ = b;
}
}
} else {
for (int x = X; x < X+W; x++) {
*buf++ = r; *buf++ = g; *buf++ = b;
}
}
return p;
}
void ccValueBox::draw() {
if (damage()&DAMAGE_ALL) draw_frame();
ColorChooser* c = (ColorChooser*)parent();
Idata i;
i.wh.set(0,0,w(),h()); box()->inset(i.wh);
float v;
if (is_alpha()) {
i.r = c->r(); i.g = c->g(); i.b = c->b();
v = c->a();
i.aimage = true;
} else {
c->hsv2rgb(c->h(),c->s(),1.0f,i.r,i.g,i.b);
v = c->v();
i.aimage = false;
}
if (damage() == DAMAGE_VALUE) {
push_clip(Rectangle(i.wh.x(),i.wh.y()+py,i.wh.w(),6));
}
drawimage(generate_vimage, &i, RGB, i.wh);
if (damage() == DAMAGE_VALUE) pop_clip();
int Y = int((1-v) * (i.wh.h()-6));
if (Y < 0) Y = 0; else if (Y > i.wh.h()-6) Y = i.wh.h()-6;
buttonbox()->draw(Rectangle(i.wh.x(), i.wh.y()+Y, i.wh.w(), 6), style(), 0);
py = Y;
}
////////////////////////////////////////////////////////////////
#define ROWS 4
#define COLS 16
FL_API Color fl_color_cells[ROWS*COLS] = {
// first the 16 assignable fltk color indexes:
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,
// then the closest gray ramps to 0,1/4,1/3,1/2,2/3,3/4,1:
32,32,32,37,37,39,39,43,43,47,47,49,49,55,55,55,
// repeat it twice:
32,32,32,37,37,39,39,43,43,47,47,49,49,55,55,55};
void ccCellBox::draw() {
int yy = 0;
for (int Y = 0; Y < ROWS; Y++) {
int hh = (Y+1)*h()/ROWS - yy;
int xx = 0;
for (int X = 0; X < COLS; X++) {
int ww = (X+1)*w()/COLS - xx;
Rectangle r(xx,yy,ww,hh);
THIN_DOWN_BOX->draw(r, style(), 0);
r.inset(1);
setcolor(fl_color_cells[Y*COLS+X]);
fillrect(r);
xx += ww;
}
yy += hh;
}
}
int ccCellBox::handle(int e) {
switch (e) {
case PUSH: return 1;
case DRAG: return 1;
case RELEASE: {
int X = event_x()*COLS/w();
if (X < 0 || X >= COLS) return 1;
int Y = event_y()*ROWS/h();
if (Y < 0 || Y >= ROWS) return 1;
X = X+Y*COLS;
ColorChooser* c = (ColorChooser*)parent();
if (event_button() > 1) {
fl_color_cells[X] = c->value();
redraw();
} else {
if (c->value(fl_color_cells[X])) c->do_callback();
}
return 1;}
}
return Widget::handle(e);
}
////////////////////////////////////////////////////////////////
void ColorChooser::layout() {
if (layout_damage()&(LAYOUT_WH|LAYOUT_DAMAGE)) {
int h = Widget::h();
int ch = w()*ROWS/COLS;
if (ch > h/3) ch = h/3;
cellbox.resize(0,h-ch,w(),ch);
h = h-ch;
int ww = w()*20/140;
int maxw = 24+valuebox.box()->dw();
if (ww>maxw) ww = maxw;
int xx = w()-ww;
if (alphabox.visible()) {
alphabox.resize(xx,0,ww,h);
xx -= ww;
}
valuebox.resize(xx,0,ww,h);
huebox.resize(0,0,xx,h);
}
Widget::layout();
}
ColorChooser::ColorChooser(int X, int Y, int W, int H, const char* L)
: Group(0,0,140,180,L,true),
huebox(0,0,100,100),
valuebox(100,0,20,100),
alphabox(120,0,20,100),
cellbox(0,100,140,80)
{
alphabox.hide();
huebox.tooltip("Drag to change hue & saturation\n"
"Ctrl+drag to change saturation only");
valuebox.tooltip("Drag to change value (brightness)");
alphabox.tooltip("Drag to change alpha");
cellbox.tooltip("Left-click to select color from cell.\n"
"Right-click to put current color into cell.");
end();
// resizable(huebox);
// sizes();
resize(X,Y,W,H);
r_ = g_ = b_ = 0;
a_ = 1.0;
hue_ = 0.0;
saturation_ = 0.0;
value_ = 0.0;
}
Color ColorChooser::value() const {
Color ret =
fltk::color(uchar(255*r()+.5f), uchar(255*g()+.5f), uchar(255*b()+.5f));
return ret ? ret : BLACK;
}
bool ColorChooser::value(Color c) {
uchar r,g,b; split_color(c,r,g,b);
return rgb(r/255.0f, g/255.0f, b/255.0f);
}
////////////////////////////////////////////////////////////////
// color_chooser():
#include <fltk/Window.h>
#include <fltk/ReturnButton.h>
#include <fltk/ask.h>
class FL_API ccValueInput : public ValueInput {
public:
int format(char*);
ccValueInput(int X, int Y, int W, int H) : ValueInput(X,Y,W,H) {}
};
enum {M_RGB, M_BYTE, M_HEX, M_HSV}; // modes
static int mode = M_RGB;
static bool recursion;
int ccValueInput::format(char* buf) {
if (mode == M_HEX) return sprintf(buf,"0x%02X", int(value()));
else return Valuator::format(buf);
}
static ccValueInput* rvalue, *gvalue, *bvalue, *avalue;
static void setstep(float step) {
if (mode != M_HSV) {rvalue->step(step); rvalue->linesize(step);}
gvalue->step(step); gvalue->linesize(step);
bvalue->step(step); bvalue->linesize(step);
avalue->step(step); avalue->linesize(step);
}
static void set_valuators(ColorChooser* c) {
if (recursion) return;
switch (mode) {
case M_RGB:
setstep(.001);
rvalue->range(0,1); rvalue->value(c->r());
gvalue->range(0,1); gvalue->value(c->g());
bvalue->range(0,1); bvalue->value(c->b());
avalue->range(0,1); avalue->value(c->a());
break;
case M_BYTE:
case M_HEX:
setstep(1);
rvalue->range(0,255); rvalue->value(int(255*c->r()+.5f));
gvalue->range(0,255); gvalue->value(int(255*c->g()+.5f));
bvalue->range(0,255); bvalue->value(int(255*c->b()+.5f));
avalue->range(0,255); avalue->value(int(255*c->a()+.5f));
break;
case M_HSV:
setstep(.001);
rvalue->range(0,360); rvalue->step(1); rvalue->linesize(1); rvalue->value(c->h()*60);
gvalue->range(0,1); gvalue->value(c->s());
bvalue->range(0,1); bvalue->value(c->v());
avalue->range(0,1); avalue->value(c->a());
break;
}
}
static Window* window;
static ColorChooser* chooser;
static Widget* ok_color;
static Button* ok_button;
static Widget* cancel_color;
static Button* cancel_button;
static void input_cb(Widget* o, void*) {
float r = float(rvalue->value());
float g = float(gvalue->value());
float b = float(bvalue->value());
float a = float(avalue->value());
recursion = true;
if (mode == M_HSV) {
if (chooser->hsv(r/60,g,b)) chooser->do_callback();
} else {
if (mode != M_RGB) {
r = r/255;
g = g/255;
b = b/255;
}
if (chooser->rgb(r,g,b)) chooser->do_callback();
}
if (avalue->visible()) {
if (chooser->a(a)) chooser->do_callback();
}
recursion = false;
}
static void mode_cb(Widget* o, long int v) {
mode = int(v);
// force them to redraw even if value is the same:
rvalue->value(-2);
gvalue->value(-2);
bvalue->value(-2);
avalue->value(-2);
set_valuators(chooser);
}
// this location is used to preserve index Color values:
static Color picked_color;
static void chooser_cb(Widget*, void*) {
ok_color->color(picked_color = chooser->value());
ok_color->redraw();
set_valuators(chooser);
}
static void ok_cb(Widget* w, void*) {
w->window()->set_value();
w->window()->hide();
}
static void cancel_cb(Widget* w, void*) {
w->window()->hide();
}
static int run_it(const char* name, bool alpha,
float r, float g, float b, float a)
{
if (!window) {
window = new Window(210, 160+5+26+21+21+5);
window->begin();
chooser = new ColorChooser(5, 5, 200, 155);
chooser->callback(chooser_cb);
int y = 165;
int x = 5;
rvalue = new ccValueInput(x,y,50,21); x += 50;
rvalue->callback(input_cb);
gvalue = new ccValueInput(x,y,50,21); x += 50;
gvalue->callback(input_cb);
bvalue = new ccValueInput(x,y,50,21); x += 50;
bvalue->callback(input_cb);
avalue = new ccValueInput(x,y,50,21); x += 50;
avalue->callback(input_cb);
PopupMenu* choice = new PopupMenu(5,y,200,21); x+=40;
choice->type(PopupMenu::POPUP3);
choice->begin();
(new Item("rgb"))->callback(mode_cb,M_RGB);
(new Item("byte"))->callback(mode_cb,M_BYTE);
(new Item("hex"))->callback(mode_cb,M_HEX);
(new Item("hsv"))->callback(mode_cb,M_HSV);
choice->end();
choice->value(0);
choice->tooltip("Right-click to change type of data entered here");
y += 26;
ok_color = new Widget(5, y, 95, 21);
ok_color->box(ENGRAVED_BOX);
ok_button = new ReturnButton(5, y+21, 95, 21, ok);
ok_button->callback(ok_cb);
cancel_color = new Widget(110, y, 95, 21);
cancel_color->box(ENGRAVED_BOX);
cancel_button = new Button(110, y+21, 95, 21, cancel);
cancel_button->callback(cancel_cb);
// window->size_range(210, 240); // minimum usable size?
window->resizable(chooser);
window->end();
}
window->label(name);
chooser->rgb(r,g,b);
if (alpha) {avalue->show(); chooser->a(a);}
else {chooser->hide_a(); avalue->hide();}
set_valuators(chooser);
ok_color->color(chooser->value());
cancel_color->color(chooser->value());
window->hotspot(window);
return window->exec();
}
/*! \addtogroup dialogs
\{
*/
/*!
\image html fl_color_chooser.jpg
fltk::color_chooser() pops up a window to let the user pick an
arbitrary RGB color. They can pick the hue and saturation in the
"hue box" on the left (hold down CTRL to just change the
saturation), and the brighness using the vertical slider. Or they
can type the 8-bit numbers into the RGB fltk::ValueInput fields, or
drag the mouse across them to adjust them. The pull-down menu lets
the user set the input fields to show RGB, HSV, or 8-bit RGB (0 to
255).
This returns non-zero if the user picks ok, and updates the RGB
values. If the user picks cancel or closes the window this returns
zero and leaves RGB unchanged.
This version takes and returns numbers in the 0-1 range.
There is also a class fltk::ColorChooser which you can use to imbed
a color chooser into another control panel.
*/
bool fltk::color_chooser(const char* name, float& r, float& g, float& b) {
if (!run_it(name, false, r,g,b,1)) return false;
r = chooser->r();
g = chooser->g();
b = chooser->b();
return true;
}
/*! Same but user can also select an alpha value. Currently the color
chips do not remember or set the alpha! */
bool fltk::color_chooser(const char* name, float&r, float&g, float&b, float&a) {
if (!run_it(name, true, r,g,b,a)) return false;
r = chooser->r();
g = chooser->g();
b = chooser->b();
a = chooser->a();
return true;
}
/*! Same but it takes and returns 8-bit numbers for the rgb arguments. */
bool fltk::color_chooser(const char* name, uchar& r, uchar& g, uchar& b) {
if (!run_it(name,false,r/255.0f, g/255.0f, b/255.0f, 1)) return false;
r = uchar(255*chooser->r()+.5f);
g = uchar(255*chooser->g()+.5f);
b = uchar(255*chooser->b()+.5f);
return true;
}
/*! Same but with 8-bit alpha chosen by the user. */
bool fltk::color_chooser(const char* name, uchar&r, uchar&g, uchar&b, uchar&a) {
if (!run_it(name,true,r/255.0f, g/255.0f, b/255.0f, a/255.0f)) return false;
r = uchar(255*chooser->r()+.5f);
g = uchar(255*chooser->g()+.5f);
b = uchar(255*chooser->b()+.5f);
a = uchar(255*chooser->a()+.5f);
return true;
}
/*! Same but it takes and returns an fltk::Color number. No alpha. */