1%%
2%%  ww_color_slider.erl --
3%%
4%%     A color slider
5%%
6%%  Copyright (c) 2014 Dan Gudmundsson
7%%
8%%  See the file "license.terms" for information on usage and redistribution
9%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
10%%
11
12-module(ww_color_slider).
13-behaviour(wx_object).
14%% Callbacks
15-export([init/1, terminate/2, code_change/3,
16	 handle_sync_event/3, handle_event/2, handle_cast/2, handle_info/2,
17	 handle_call/3]).
18
19%% API
20-export([new/3, new/4, getColor/1, setColor/2, connect/2, connect/3]).
21
22-ifdef(DEBUG).
23-export([test/0]). % Test
24-endif.
25
26new(Parent, Id, Col) ->
27    new(Parent, Id, Col, []).
28new(Parent, Id, Col, Opts) ->
29    wx_object:start(?MODULE, [Parent, Id, Col, Opts], []).
30
31getColor(Ctrl) ->
32    wx_object:call(Ctrl, get_color).
33
34setColor(Ctrl, RGB) ->
35    wx_object:cast(Ctrl, {set_color, RGB}).
36
37connect(Ctrl, Msg) ->
38    connect(Ctrl, Msg, []).
39
40connect(Ctrl, col_changed, Opts) ->
41    wx_object:call(Ctrl, {connect, Opts});
42connect(Ctrl, What, Opts) ->
43    wxPanel:connect(Ctrl, What, Opts).
44
45
46%% Callbacks
47
48-include_lib("wx/include/wx.hrl").
49
50-record(state, {self, this, curr, mode,
51		c1, c2,
52		bmp, bgb,
53		focus=false,
54		capture=false,
55		fpen,
56		handlers=[] %% Listeners or callbacks
57	       }).
58-define(PANEL_MIN_SIZE, {150, 20}).
59-define(SLIDER_MIN_HEIGHT, 10).
60-define(SLIDER_OFFSET, {8, 5}).
61
62-define(wxGC, wxGraphicsContext).
63
64init([Parent, Id, Col, Opts0]) ->
65    {Mode, Opts1} = default(color, Opts0, rgb),
66    {Style0, Opts} = default(style, Opts1, 0),
67    Style =  Style0 bor ?wxFULL_REPAINT_ON_RESIZE
68	bor ?wxCLIP_CHILDREN bor ?wxTAB_TRAVERSAL,
69    Panel = wxPanel:new(Parent, [{winid, Id}, {style, Style}|Opts]),
70    wxWindow:setMinSize(Panel, ?PANEL_MIN_SIZE),
71    Bmp = slider_bitmap(),
72    wxPanel:connect(Panel, paint, [callback]),
73    wxPanel:connect(Panel, erase_background), %% WIN32 only?
74    BGC = wxPanel:getBackgroundColour(Parent),
75    case os:type() of
76	{win32,_} ->
77	    wxPanel:setBackgroundColour(Panel, BGC);
78	_ -> ignore
79    end,
80
81    Brush = wxBrush:new(BGC),
82    wxPanel:connect(Panel, left_down),
83    wxPanel:connect(Panel, left_up),
84    wxPanel:connect(Panel, motion),
85    wxPanel:connect(Panel, set_focus),
86    wxPanel:connect(Panel, kill_focus),
87    wxPanel:connect(Panel, char_hook, [callback]),
88    wxPanel:connect(Panel, key_down,  [callback]),
89
90    {Curr,SCol,ECol}  = get_col_range(Mode, Col),
91
92    FPen = wxPen:new(wxSystemSettings:getColour(?wxSYS_COLOUR_HIGHLIGHT)),
93
94    {Panel, #state{self=self(), this=Panel,
95		   curr=Curr, mode=Mode,
96		   c1=SCol, c2=ECol,
97		   bmp=Bmp, bgb=Brush,
98		   fpen=FPen}}.
99
100%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
101%% Redraw the control
102handle_sync_event(#wx{obj=Panel, event=#wxPaint{}}, _,
103		  #state{this=Panel, curr=Curr, mode=Mode,
104			 c1=C1, c2=C2,
105			 bmp=Bmp, bgb=BGB,
106			 focus=Focus, fpen=FPen
107			}) ->
108    DC = case os:type() of
109	     {win32, _} -> %% Flicker on windows
110		 BDC = wx:typeCast(wxBufferedPaintDC:new(Panel), wxPaintDC),
111		 wxDC:setBackground(BDC, BGB),
112		 wxDC:clear(BDC),
113		 BDC;
114	     _ ->
115		 wxPaintDC:new(Panel)
116	 end,
117    {_,_, W0,H0} = wxPanel:getRect(Panel),
118    %% Draw focus rectangle
119    if Focus ->
120	    wxDC:setPen(DC, FPen),
121	    wxDC:setBrush(DC, ?wxTRANSPARENT_BRUSH),
122	    wxDC:drawRoundedRectangle(DC, {1,1,W0-2,H0-2}, 3);
123       true -> ignore
124    end,
125    %% Draw background
126    {XOFF,YOFF} = ?SLIDER_OFFSET,
127    {_, HMIN} = ?PANEL_MIN_SIZE,
128    Y0 = (H0 - HMIN) div 2,
129    X = get_curr(Mode, Curr),
130    case Mode of
131	hue ->
132	    Width = W0-2*XOFF,
133	    PartW = round(Width*60/360),
134	    Draw = fun(Hue, {X0, C00, C11, S, V}) ->
135			   DW = if Hue =:= 300 -> XOFF+Width - X0;
136				   true -> PartW
137				end,
138			   wxDC:gradientFillLinear(DC, {X0, Y0+YOFF, DW, ?SLIDER_MIN_HEIGHT},
139						   C00, C11, [{nDirection, ?wxRIGHT}]),
140			   {X0+PartW, C11, rgb256(hsv_to_rgb(Hue+120,S,V)), S,V}
141		   end,
142	    {_H, S, V} = Curr,
143   	    lists:foldl(Draw, {XOFF,rgb256(C1),rgb256(C2),S,V}, lists:seq(0, 300, 60));
144	_ ->
145	    wxDC:gradientFillLinear(DC, {XOFF, Y0+YOFF, W0-2*XOFF, ?SLIDER_MIN_HEIGHT},
146				    rgb256(C1), rgb256(C2), [{nDirection, ?wxRIGHT}])
147    end,
148    %% Draw selector
149    Pos = XOFF + (W0-2*XOFF)*X,
150    wxDC:drawBitmap(DC, Bmp, {trunc(Pos-7),Y0}),
151    wxPaintDC:destroy(DC),
152    ok;
153
154%%% Key events must be handled sync'ed so we can call skip for TAB traversal
155handle_sync_event(#wx{event=#wxKey{keyCode=Key}}, Event,
156		  #state{self=Self, curr=Curr, mode=Mode}) ->
157    Prev = get_curr(Mode, Curr),
158    Move = case Key of
159	       ?WXK_LEFT     -> -0.01;
160	       ?WXK_RIGHT    ->  0.01;
161	       ?WXK_PAGEUP   -> -0.10;
162	       ?WXK_PAGEDOWN ->  0.10;
163	       ?WXK_HOME     -> -Prev;
164	       ?WXK_END      ->  1.0-Prev;
165	       ?WXK_NUMPAD_LEFT     -> -0.01;
166	       ?WXK_NUMPAD_RIGHT    ->  0.01;
167	       ?WXK_NUMPAD_PAGEUP   -> -0.10;
168	       ?WXK_NUMPAD_PAGEDOWN ->  0.10;
169	       ?WXK_NUMPAD_HOME     -> -Prev;
170	       ?WXK_NUMPAD_END      ->  1.0-Prev;
171	       _ -> false
172	   end,
173    %% io:format("Key ~p ~p~n",[Key, Move]),
174    case Move of
175	false -> wxEvent:skip(Event);
176	_ -> Self ! {move, Move}
177    end,
178    ok.
179
180%% Other events
181handle_event(#wx{event=#wxMouse{type=motion, x=X}},
182	     #state{this=This, mode=Mode, curr=Curr, capture=true} = State0) ->
183    State = State0#state{curr=slider_pos(This, X, Mode, Curr)},
184    self() ! apply_cb,
185    {noreply, State};
186
187handle_event(#wx{event=#wxMouse{type=left_down, x=X}},
188	     #state{this=This, mode=Mode, curr=Curr, capture=false} = State0) ->
189    %% wxPanel:setFocus(This),  %% crashes on win64 when in autouv..
190    wxPanel:captureMouse(This),
191    State = State0#state{curr=slider_pos(This, X, Mode, Curr), capture=true},
192    self() ! apply_cb,
193    {noreply, State#state{focus=true}};
194handle_event(#wx{event=#wxMouse{type=left_up}},
195	     #state{this=This, capture=Captured} = State) ->
196    Captured andalso wxPanel:releaseMouse(This),
197    wxWindow:refresh(This),
198    {noreply, State#state{capture=false}};
199handle_event(#wx{event=#wxFocus{type=What}}, #state{this=This} = State) ->
200    wxWindow:refresh(This),
201    {noreply, State#state{focus=What=:=set_focus}};
202
203handle_event(_Ev, State) ->
204    %% io:format("Skip Ev ~p~n",[_Ev]),
205    {noreply, State}.
206
207handle_call({connect, Opts}, From, #state{handlers=Curr} = State) ->
208    case proplists:get_value(callback, Opts) of
209	undefined ->
210	    {reply, ok, State#state{handlers=[From|Curr]}};
211	CB when is_function(CB) ->
212	    {reply, ok, State#state{handlers=[CB|Curr]}};
213	Bad ->
214	    {reply, {error, {badarg, Bad}}, State}
215    end;
216
217handle_call(get_color, _From, State) ->
218    {reply, get_curr_color(State), State}.
219
220handle_cast({set_color, Col}, State = #state{this=This, mode=Mode}) ->
221    {Curr, SCol, ECol} = get_col_range(Mode, Col),
222    wxWindow:refresh(This),
223    {noreply, State#state{curr=Curr, c1=SCol, c2=ECol}}.
224
225terminate(_Reason, #state{this=_This, bmp=Bmp, bgb=BGB, fpen=Fpen}) ->
226    wxBrush:destroy(BGB),
227    wxBitmap:destroy(Bmp),
228    wxPen:destroy(Fpen),
229    %% wxPanel:destroy(This), %% Is destroyed by the owner
230    ok.
231
232handle_info({move,Move}, State0 = #state{this=This, mode=Mode, curr=Prev}) ->
233    V = get_curr(Mode, Prev),
234    State = State0#state{curr=set_curr(Mode, max(0.0, min(1.0, V+Move)), Prev)},
235    [apply_callback(H, get_curr_color(State)) || H <- State#state.handlers],
236    wxWindow:refresh(This),
237    {noreply, State};
238handle_info(apply_cb, State) ->
239    fun Flush () ->
240            receive apply_cb -> Flush()
241            after 0 -> ok
242            end
243    end(),
244    [apply_callback(H, get_curr_color(State)) || H <- State#state.handlers],
245    {noreply, State};
246handle_info(_Msg, State) ->
247    io:format("~p:~p: Unexpected message: ~p~n", [?MODULE, ?LINE, _Msg]),
248    {noreply, State}.
249
250code_change(_, _, State) -> State.
251
252default(Key, Opts, Def) ->
253    {proplists:get_value(Key, Opts, Def),
254     proplists:delete(Key,Opts)}.
255
256slider_pos(This, X, Mode, Curr) ->
257    wxWindow:refresh(This),
258    {W, _} = wxPanel:getSize(This),
259    {X0,_Y0} = ?SLIDER_OFFSET,
260    Value = max(0.0, min(1.0, (X-X0)/(W-X0*2))),
261    set_curr(Mode, Value, Curr).
262
263rgb_to_hsv({R,G,B}) ->
264    rgb_to_hsv(R, G, B).
265
266rgb_to_hsv(R,G,B) ->
267    {H,S,V} = wings_color:rgb_to_hsv(R,G,B),
268    {round(H),S,V}.
269
270hsv_to_rgb({H,S,V}) ->
271    hsv_to_rgb(H, S, V).
272
273hsv_to_rgb(H, S, V) ->
274    wings_color:hsv_to_rgb(H, S, V).
275
276get_curr(rgb, {_H,_S,V}) -> V;
277get_curr(red, {R, _G, _B}) -> R;
278get_curr(green, {_R, G, _B}) -> G;
279get_curr(blue, {_R, _G, B}) -> B;
280get_curr(hue, {H, _S, _V}) -> H / 360;
281get_curr(sat, {_H, S, _V}) -> S;
282get_curr(val, {_H, _S, V}) -> V.
283
284set_curr(rgb, V, {H,S,_V}) -> {H,S,V};
285set_curr(red, R, {_R, G, B}) -> {R,G,B};
286set_curr(green, G, {R, _G, B}) -> {R,G,B};
287set_curr(blue, B, {R, G, _B}) -> {R,G,B};
288set_curr(hue, H, {_H, S, V}) -> {H*360.0, S,V};
289set_curr(sat, S, {H, _S, V}) -> {H,S,V};
290set_curr(val, V, {H, S, _V}) -> {H,S,V}.
291
292get_curr_color(#state{mode=Mode, curr=Curr}) ->
293    get_curr_color(Mode, Curr).
294
295get_curr_color(C, RGB) when C =:= red; C =:= blue; C =:= green -> RGB;
296get_curr_color(_Other, HSV) -> hsv_to_rgb(HSV).
297
298get_col_range(rgb, {R,G,B}) ->
299    HSV  = {Hue,S,_V} = rgb_to_hsv(R, G, B),
300    SCol = hsv_to_rgb(Hue, S, 0.0),
301    ECol = hsv_to_rgb(Hue, S, 1.0),
302    {HSV,SCol,ECol};
303get_col_range(red, RGB={_R,G,B}) ->
304    {RGB,{0,G,B},{1,G,B}};
305get_col_range(green, RGB={R,_G,B}) ->
306    {RGB,{R,0,B},{R,1,B}};
307get_col_range(blue, RGB={R,G,_B}) ->
308    {RGB,{R,G,0},{R,G,1}};
309get_col_range(sat, RGB) ->
310    HSV={H,_S,V} = rgb_to_hsv(RGB),
311    S0 = hsv_to_rgb(H,0.0,V),
312    S1 = hsv_to_rgb(H,1.0,V),
313    {HSV,S0,S1};
314get_col_range(val, RGB) ->
315    HSV={H,S,_V} = rgb_to_hsv(RGB),
316    V0 = hsv_to_rgb(H,S,0.0),
317    V1 = hsv_to_rgb(H,S,1.0),
318    {HSV,V0,V1};
319get_col_range(hue, RGB) ->
320    HSV={_H,S,V} = rgb_to_hsv(RGB),
321    V0 = hsv_to_rgb(0.0,S,V),
322    V1 = hsv_to_rgb(60.0,S,V),
323    {HSV,V0,V1}.
324
325rgb256({R,G,B}) -> {round(R*255),round(G*255),round(B*255)};
326rgb256({R,G,B,_A}) -> {round(R*255),round(G*255),round(B*255)}.
327
328apply_callback(Pid, Col) when is_pid(Pid) ->
329    Pid ! {col_changed, Col};
330apply_callback(CB, Col) when is_function(CB) ->
331    CB({col_changed, Col}).
332
333%% Image / icon data
334
335slider_bitmap() ->
336    I = wxImage:new(15, 20, rgb()), %alpha(), [{static_data, false}]), doesn't work...
337    wxImage:setAlpha(I, alpha()),
338    Bmp = wxBitmap:new(I),
339    wxImage:destroy(I),
340    Bmp.
341
342rgb() ->
343    <<0,0,0,0,0,0,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,0,0,0,0,0,0,0,0,0,240,240,240,193,193,193,141,141,141,112,112,112,112,112,112,112,112,112,112,112,112,112,112,112,112,112,112,141,141,141,193,193,193,240,240,240,240,240,240,0,0,0,0,0,0,240,240,240,141,141,141,221,221,221,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,221,221,221,134,134,134,232,232,232,240,240,240,0,0,0,0,0,0,240,240,240,112,112,112,252,252,252,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,252,252,252,112,112,112,211,211,211,240,240,240,0,0,0,0,0,0,240,240,240,112,112,112,251,251,251,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,251,251,251,112,112,112,198,198,198,240,240,240,0,0,0,0,0,0,240,240,240,112,112,112,251,251,251,240,240,240,240,240,240,0,0,0,240,240,240,240,240,240,240,240,240,251,251,251,112,112,112,195,195,195,240,240,240,0,0,0,240,240,240,240,240,240,112,112,112,251,251,251,239,239,239,239,239,239,239,239,239,0,0,0,239,239,239,239,239,239,251,251,251,112,112,112,195,195,195,250,250,250,0,0,0,0,0,0,240,240,240,112,112,112,251,251,251,238,238,238,238,238,238,0,0,0,0,0,0,238,238,238,238,238,238,251,251,251,112,112,112,188,190,190,250,250,250,0,0,0,0,0,0,240,240,240,112,112,112,250,250,250,236,236,236,236,236,236,0,0,0,0,0,0,236,236,236,236,236,236,250,250,250,112,112,112,188,190,190,255,255,255,0,0,0,0,0,0,231,234,234,112,112,112,250,250,250,235,235,235,235,235,235,0,0,0,235,235,235,235,235,235,235,235,235,250,250,250,112,112,112,188,190,190,255,255,255,0,0,0,0,0,0,252,252,252,112,112,112,246,246,246,219,219,219,219,219,219,0,0,0,0,0,0,219,219,219,219,219,219,246,246,246,112,112,112,188,190,190,255,255,255,0,0,0,0,0,0,240,240,240,112,112,112,245,245,245,217,217,217,217,217,217,0,0,0,0,0,0,217,217,217,217,217,217,245,245,245,112,112,112,195,195,195,255,255,255,0,0,0,0,0,0,240,240,240,112,112,112,245,245,245,215,215,215,215,215,215,0,0,0,0,0,0,215,215,215,215,215,215,245,245,245,112,112,112,195,195,195,240,240,240,0,0,0,0,0,0,240,240,240,112,112,112,245,245,245,218,218,218,214,214,214,214,214,214,214,214,214,214,214,214,218,218,218,245,245,245,112,112,112,195,195,195,240,240,240,0,0,0,0,0,0,240,240,240,165,165,165,182,182,182,244,244,244,217,217,217,212,212,212,212,212,212,217,217,217,244,244,244,182,182,182,114,114,114,200,200,200,240,240,240,0,0,0,0,0,0,240,240,240,225,225,225,155,155,155,180,180,180,244,244,244,215,215,215,215,215,215,244,244,244,180,180,180,104,104,104,149,149,149,216,216,216,240,240,240,0,0,0,0,0,0,240,240,240,240,240,240,222,222,222,151,151,151,180,180,180,243,243,243,243,243,243,180,180,180,105,105,105,145,145,145,205,205,205,234,234,234,240,240,240,0,0,0,0,0,0,240,240,240,240,240,240,240,240,240,222,222,222,151,151,151,178,178,178,180,180,180,106,106,106,145,145,145,205,205,205,234,234,234,240,240,240,240,240,240,0,0,0,0,0,0,0,0,0,240,240,240,240,240,240,240,240,240,224,224,224,158,158,158,138,138,138,160,160,160,205,205,205,234,234,234,240,240,240,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,240,240,240,240,240,240,234,234,234,223,223,223,222,222,222,234,234,234,240,240,240,0,0,0,0,0,0,0,0,0,0,0,0>>.
344
345
346alpha() ->
347    <<0,0,171,188,186,176,198,224,226,226,227,242,132,0,0,0,168,252,255,255,255,255,255,255,255,255,255,219,33,0,0,184,255,255,255,255,255,255,255,255,255,255,253,126,0,0,184,255,255,255,249,182,200,251,255,255,255,255,119,0,0,184,255,255,255,130,5,12,118,255,255,255,255,142,0,0,184,254,255,255,5,0,1,16,255,255,255,255,140,0,4,184,244,255,255,5,1,0,47,255,255,255,254,127,0,0,184,244,255,255,13,0,0,51,255,255,255,255,119,0,0,184,254,255,255,11,0,0,32,255,255,255,254,119,0,0,184,254,255,255,44,0,1,40,255,255,255,255,119,0,0,184,244,255,255,44,0,0,16,255,255,255,255,119,0,0,184,243,255,255,51,0,0,27,255,255,255,255,119,0,0,184,244,255,255,93,0,0,58,255,255,255,255,119,0,0,119,254,255,255,162,1,1,159,255,255,255,255,119,0,0,130,255,255,255,247,167,165,246,255,255,255,255,124,0,0,119,184,255,255,255,255,255,255,255,255,255,255,119,0,0,0,119,184,255,255,255,255,255,255,255,254,184,0,0,0,2,0,119,184,255,255,255,255,255,253,217,0,22,0,0,0,5,0,119,184,255,255,253,226,155,17,0,0,0,0,0,0,0,2,119,184,255,184,94,3,0,0,0,0>>.
348
349-ifdef(DEBUG).
350test() ->
351    process_flag(trap_exit, true),
352    Pid = spawn_link(fun() -> run_test() end),
353    receive {'EXIT', Pid, Msg} -> Msg end.
354
355-spec run_test() -> no_return().
356
357run_test() ->
358    Frame = wxFrame:new(wx:new(), -1, "FOO"),
359    Panel = wxPanel:new(Frame),
360    Sz = wxBoxSizer:new(?wxVERTICAL),
361    wxSizer:add(Sz, wxButton:new(Panel, 42, [{label, "A button"}])),
362    wxSizer:add(Sz, wxStaticText:new(Panel, 43, "Some static text")),
363    wxSizer:add(Sz, wxSlider:new(Panel, 46, 27, 1, 100), [{flag, ?wxEXPAND}]),
364    RGB = fun(What) -> rgb(wxSystemSettings:getColour(What)) end,
365    wxSizer:add(Sz, new(Panel, 45, RGB(?wxSYS_COLOUR_ACTIVECAPTION), []), [{flag, ?wxEXPAND}]),
366    wxSizer:add(Sz, new(Panel, -1, RGB(?wxSYS_COLOUR_HIGHLIGHT), []), [{flag, ?wxEXPAND}]),
367    wxSizer:add(Sz, new(Panel, -1, RGB(?wxSYS_COLOUR_MENUHILIGHT), []), [{flag, ?wxEXPAND}]),
368    wxSizer:add(Sz, new(Panel, -1, RGB(?wxSYS_COLOUR_ACTIVEBORDER), []), [{flag, ?wxEXPAND}]),
369    wxSizer:add(Sz, new(Panel, -1, RGB(?wxSYS_COLOUR_BTNHILIGHT), []), [{flag, ?wxEXPAND}]),
370    wxSizer:add(Sz, new(Panel, -1, RGB(?wxSYS_COLOUR_BACKGROUND), []), [{flag, ?wxEXPAND}]),
371    wxSizer:add(Sz, wxButton:new(Panel, 44, [{label, "B button"}])),
372    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,red}]), [{flag, ?wxEXPAND}]),
373    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,green}]), [{flag, ?wxEXPAND}]),
374    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,blue}]), [{flag, ?wxEXPAND}]),
375
376    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,hue}]), [{flag, ?wxEXPAND}]),
377    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,sat}]), [{flag, ?wxEXPAND}]),
378    wxSizer:add(Sz, new(Panel, -1, {0.5,0.73,0.5}, [{color,val}]), [{flag, ?wxEXPAND}]),
379
380    wxPanel:setSizerAndFit(Panel, Sz),
381    wxSizer:setSizeHints(Sz, Frame),
382    wxFrame:show(Frame),
383    exit(ok).
384
385rgb({R,G,B,_}) -> {R/255, G/255, B/255}.
386-endif.
387