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