1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2011-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19
20-module(observer_lib).
21
22-export([get_wx_parent/1,
23	 display_info_dialog/2, display_yes_no_dialog/1,
24	 display_progress_dialog/3,
25         destroy_progress_dialog/0, sync_destroy_progress_dialog/0,
26	 wait_for_progress/0, report_progress/1,
27	 user_term/3, user_term_multiline/3,
28	 interval_dialog/4, start_timer/1, start_timer/2, stop_timer/1, timer_config/1,
29	 display_info/2, display_info/3,
30	 fill_info/2, fill_info/3, update_info/2,
31	 to_str/1,
32	 create_menus/3, create_menu_item/3,
33	 is_darkmode/1, colors/1, create_attrs/1,
34	 set_listctrl_col_size/2, mix/3,
35	 create_status_bar/1,
36	 html_window/1, html_window/2,
37         make_obsbin/2,
38         add_scroll_entries/2
39	]).
40
41-include_lib("wx/include/wx.hrl").
42-include("observer_defs.hrl").
43
44-define(SINGLE_LINE_STYLE, ?wxBORDER_NONE bor ?wxTE_READONLY bor ?wxTE_RICH2).
45-define(MULTI_LINE_STYLE, ?SINGLE_LINE_STYLE bor ?wxTE_MULTILINE).
46
47-define(NUM_SCROLL_ITEMS,8).
48
49-define(pulse_timeout,50).
50
51get_wx_parent(Window) ->
52    Parent = wxWindow:getParent(Window),
53    case wx:is_null(Parent) of
54	true -> Window;
55	false -> get_wx_parent(Parent)
56    end.
57
58interval_dialog(Parent0, {Timer, Value}, Min, Max) ->
59    Parent = get_wx_parent(Parent0),
60    Dialog = wxDialog:new(Parent, ?wxID_ANY, "Update Interval",
61			  [{style, ?wxDEFAULT_DIALOG_STYLE bor
62				?wxRESIZE_BORDER}]),
63    Panel = wxPanel:new(Dialog),
64    Check = wxCheckBox:new(Panel, ?wxID_ANY, "Periodical refresh"),
65    wxCheckBox:setValue(Check, Timer /= false),
66    Style = ?wxSL_HORIZONTAL bor ?wxSL_AUTOTICKS bor ?wxSL_LABELS,
67    Slider = wxSlider:new(Panel, ?wxID_ANY, Value, Min, Max,
68			  [{style, Style}, {size, {200, -1}}]),
69    wxWindow:enable(Slider, [{enable, Timer /= false}]),
70    InnerSizer = wxBoxSizer:new(?wxVERTICAL),
71    Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
72    Flags = [{flag, ?wxEXPAND bor ?wxALL}, {border, 2}],
73    wxSizer:add(InnerSizer, Check,  Flags),
74    wxSizer:add(InnerSizer, Slider, Flags),
75    wxPanel:setSizer(Panel, InnerSizer),
76    TopSizer = wxBoxSizer:new(?wxVERTICAL),
77    wxSizer:add(TopSizer, Panel, [{flag, ?wxEXPAND bor ?wxALL}, {border, 5}]),
78    wxSizer:add(TopSizer, Buttons, [{flag, ?wxEXPAND}]),
79    wxWindow:setSizerAndFit(Dialog, TopSizer),
80    wxSizer:setSizeHints(TopSizer, Dialog),
81    wxCheckBox:connect(Check, command_checkbox_clicked,
82		       [{callback, fun(#wx{event=#wxCommand{commandInt=Enable0}},_) ->
83					   Enable = Enable0 > 0,
84					   wxWindow:enable(Slider, [{enable, Enable}])
85				   end}]),
86    Res = case wxDialog:showModal(Dialog) of
87	      ?wxID_OK ->
88		  Enabled = wxCheckBox:isChecked(Check),
89		  setup_timer(Enabled, {Timer, wxSlider:getValue(Slider)});
90	      ?wxID_CANCEL ->
91		  {Timer, Value}
92	  end,
93    wxDialog:destroy(Dialog),
94    Res.
95
96stop_timer(Timer = {false, _}) -> Timer;
97stop_timer(Timer = {true, _}) -> Timer;
98stop_timer(Timer = {_, Intv}) ->
99    setup_timer(false, Timer),
100    {true, Intv}.
101
102start_timer(#{interval:=Intv}, _Def) ->
103    setup_timer(true, {false, Intv});
104start_timer(_, Def) ->
105    setup_timer(true, {false, Def}).
106
107start_timer(Intv) when is_integer(Intv) ->
108    setup_timer(true, {true, Intv});
109start_timer(Timer) ->
110    setup_timer(true, Timer).
111
112setup_timer(false, {Timer, Value})
113  when is_boolean(Timer) ->
114    {false, Value};
115setup_timer(true,  {false, Value}) ->
116    {ok, Timer} = timer:send_interval(Value * 1000, refresh_interval),
117    {Timer, Value};
118setup_timer(Bool, {Timer, Old}) ->
119    timer:cancel(Timer),
120    setup_timer(Bool, {false, Old}).
121
122timer_config({_, Interval}) ->
123    #{interval=>Interval};
124timer_config(#{}=Config) ->
125    Config.
126
127display_info_dialog(Parent,Str) ->
128    display_info_dialog(Parent,"",Str).
129display_info_dialog(Parent,Title,Str) ->
130    Dlg = wxMessageDialog:new(Parent, Str, [{caption,Title}]),
131    wxMessageDialog:showModal(Dlg),
132    wxMessageDialog:destroy(Dlg),
133    ok.
134
135display_yes_no_dialog(Str) ->
136    Dlg = wxMessageDialog:new(wx:null(), Str, [{style,?wxYES_NO}]),
137    R = wxMessageDialog:showModal(Dlg),
138    wxMessageDialog:destroy(Dlg),
139    R.
140
141%% display_info(Parent, [{Title, [{Label, Info}]}]) -> {Panel, Sizer, InfoFieldsToUpdate}
142display_info(Frame, Info) ->
143    Panel = wxPanel:new(Frame),
144    wxWindow:setBackgroundStyle(Panel, ?wxBG_STYLE_SYSTEM),
145    Sizer = wxBoxSizer:new(?wxVERTICAL),
146    InfoFs = display_info(Panel, Sizer, Info),
147    wxWindow:setSizerAndFit(Panel, Sizer),
148    {Panel, Sizer, InfoFs}.
149
150display_info(Panel, Sizer, Info) ->
151    wxSizer:addSpacer(Sizer, 5),
152    Add = fun(BoxInfo) ->
153		  case create_box(Panel, BoxInfo) of
154		      {Box, InfoFs} ->
155			  wxSizer:add(Sizer, Box,
156				      [{flag, ?wxEXPAND bor ?wxALL},
157				       {border, 5}]),
158			  wxSizer:addSpacer(Sizer, 5),
159			  InfoFs;
160		      undefined ->
161			  []
162		  end
163	  end,
164    [Add(I) || I <- Info].
165
166fill_info(Fields, Data) ->
167    fill_info(Fields, Data, undefined).
168
169fill_info([{dynamic, Key}|Rest], Data, Default)
170  when is_atom(Key); is_function(Key) ->
171    %% Special case used by crashdump_viewer when the value decides
172    %% which header to use
173    case get_value(Key, Data, Default) of
174	undefined   -> [undefined | fill_info(Rest, Data, Default)];
175	{Str,Value} -> [{Str, Value} | fill_info(Rest, Data, Default)]
176    end;
177%% This crap is simply to make it unique, see above.
178fill_info([{socket, Str, {Level, Opt} = Key}|Rest], Data, Default)
179  when is_list(Str) andalso is_atom(Level) andalso is_atom(Opt) ->
180    %% d("fill_info(2) -> entry with"
181    %%   "~n   Str: ~s", [Str]),
182    Key = {Level, Opt},
183    case get_value(Key, Data, Default) of
184	undefined ->
185	    %% d("fill_info -> not found"),
186	    [undefined | fill_info(Rest, Data, Default)];
187	Value ->
188	    %% d("fill_info -> found: "
189	    %%   "~n   Value: ~p", [Value]),
190	    [{Str, Value} | fill_info(Rest, Data, Default)]
191    end;
192fill_info([{Str, Key}|Rest], Data, Default)
193  when is_atom(Key); is_function(Key) ->
194    %% d("fill_info(3) -> entry with"
195    %%   "~n   Str: ~s", [Str]),
196    case get_value(Key, Data, Default) of
197	undefined ->
198	    [undefined | fill_info(Rest, Data, Default)];
199	Value ->
200	    [{Str, Value} | fill_info(Rest, Data, Default)]
201    end;
202fill_info([{Str, Attrib, Key}|Rest], Data, Default)
203  when is_atom(Key); is_function(Key) ->
204    %% d("fill_info(4) -> entry with"
205    %%   "~n   Str: ~s", [Str]),
206    case get_value(Key, Data, Default) of
207	undefined ->
208	    [undefined | fill_info(Rest, Data, Default)];
209	Value ->
210	    [{Str,Attrib,Value} | fill_info(Rest, Data, Default)]
211    end;
212fill_info([{Str, {Format, Key}}|Rest], Data, Default)
213  when is_atom(Key); is_function(Key) ->
214    %% d("fill_info(5) -> entry with"
215    %%   "~n   Str: ~s", [Str]),
216    case get_value(Key, Data, Default) of
217	undefined -> [undefined | fill_info(Rest, Data, Default)];
218	Value -> [{Str, {Format, Value}} | fill_info(Rest, Data, Default)]
219    end;
220fill_info([{Str, Attrib, {Format, Key}}|Rest], Data, Default)
221  when is_atom(Key); is_function(Key) ->
222    %% d("fill_info(6) -> entry with"
223    %%   "~n   Str:    ~s"
224    %%   "~n   Attrib: ~p"
225    %%   "~n   Format: ~p"
226    %%   "~n   Key:    ~p", [Str, Attrib, Format, Key]),
227    case get_value(Key, Data, Default) of
228	undefined -> [undefined | fill_info(Rest, Data, Default)];
229	Value -> [{Str, Attrib, {Format, Value}} |
230		  fill_info(Rest, Data, Default)]
231    end;
232fill_info([{Str, SubStructure}|Rest], Data, Default)
233  when is_list(SubStructure) ->
234    %% d("fill_info(7) -> entry with"
235    %%   "~n   Str: ~s", [Str]),
236    [{Str, fill_info(SubStructure, Data, Default)}|
237     fill_info(Rest, Data, Default)];
238fill_info([{Str, Attrib, SubStructure}|Rest], Data, Default) ->
239    %% d("fill_info(8) -> entry with"
240    %%   "~n   Str: ~s", [Str]),
241    [{Str, Attrib, fill_info(SubStructure, Data, Default)}|
242     fill_info(Rest, Data, Default)];
243fill_info([{Str, Key = {K,N}}|Rest], Data, Default)
244  when is_atom(K), is_integer(N) ->
245    %% d("fill_info(9) -> entry with"
246    %%   "~n   Str: ~s", [Str]),
247    case get_value(Key, Data, Default) of
248	undefined -> [undefined | fill_info(Rest, Data, Default)];
249	Value -> [{Str, Value} | fill_info(Rest, Data, Default)]
250    end;
251fill_info([], _, _Default) ->
252    [].
253
254get_value(Fun, Data, _Default) when is_function(Fun) ->
255    Fun(Data);
256get_value(Key, Data, Default) ->
257    proplists:get_value(Key, Data, Default).
258
259update_info([Fields|Fs], [{_Header, SubStructure}| Rest]) ->
260    update_info2(Fields, SubStructure),
261    update_info(Fs, Rest);
262update_info([Fields|Fs], [{_Header, _Attrib, SubStructure}| Rest]) ->
263    update_info2(Fields, SubStructure),
264    update_info(Fs, Rest);
265update_info([], []) ->
266    ok.
267
268update_info2([undefined|Fs], [_|Rest]) ->
269    update_info2(Fs, Rest);
270update_info2([Scroll = {_, _, _}|Fs], [{_, NewInfo}|Rest]) ->
271    update_scroll_boxes(Scroll, NewInfo),
272    update_info2(Fs, Rest);
273update_info2([Field|Fs], [{_Str, {click, Value}}|Rest]) ->
274    wxStaticText:setLabel(Field, to_str(Value)),
275    update_info2(Fs, Rest);
276update_info2([Field|Fs], [{_Str, Value}|Rest]) ->
277    wxStaticText:setLabel(Field, to_str(Value)),
278    update_info2(Fs, Rest);
279update_info2([Field|Fs], [undefined|Rest]) ->
280    wxStaticText:setLabel(Field, ""),
281    update_info2(Fs, Rest);
282update_info2([], []) -> ok.
283
284update_scroll_boxes({_, _, 0}, {_, []}) -> ok;
285update_scroll_boxes({Win, Sizer, _}, {Type, List}) ->
286    [wxSizerItem:deleteWindows(Child) ||  Child <- wxSizer:getChildren(Sizer)],
287    Cursor = wxCursor:new(?wxCURSOR_HAND),
288    add_entries(Type, List, Win, Sizer, Cursor),
289    wxCursor:destroy(Cursor),
290    wxSizer:recalcSizes(Sizer),
291    wxWindow:refresh(Win),
292    ok.
293
294to_str(Value) when is_atom(Value) ->
295    atom_to_list(Value);
296to_str({Unit, X}) when (Unit==bytes orelse Unit==time_ms) andalso is_list(X) ->
297    try list_to_integer(X) of
298	B -> to_str({Unit,B})
299    catch error:badarg -> X
300    end;
301to_str({bytes, B}) ->
302    KB = B div 1024,
303    MB = KB div 1024,
304    GB = MB div 1024,
305    if
306	GB > 10 -> integer_to_list(GB) ++ " GB";
307	MB > 10 -> integer_to_list(MB) ++ " MB";
308	KB >  0 -> integer_to_list(KB) ++ " kB";
309	true -> integer_to_list(B) ++ " B"
310    end;
311to_str({{words,WSz}, Sz}) ->
312    to_str({bytes, WSz*Sz});
313to_str({time_ms, MS}) ->
314    S = MS div 1000,
315    Min = S div 60,
316    Hours = Min div 60,
317    Days = Hours div 24,
318    if
319	Days > 0 -> integer_to_list(Days) ++ " Days";
320	Hours > 0 -> integer_to_list(Hours) ++ " Hours";
321	Min > 0 -> integer_to_list(Min) ++ " Mins";
322	true -> integer_to_list(S) ++ " Secs"
323    end;
324
325to_str({func, {F,A}}) when is_atom(F), is_integer(A) ->
326    lists:concat([F, "/", A]);
327to_str({func, {F,'_'}}) when is_atom(F) ->
328    atom_to_list(F);
329to_str({inet, Addr}) ->
330    case inet:ntoa(Addr) of
331        {error,einval} -> to_str(Addr);
332        AddrStr -> AddrStr
333    end;
334to_str({{format,Fun},Value}) when is_function(Fun) ->
335    Fun(Value);
336to_str({A, B}) when is_atom(A), is_atom(B) ->
337    lists:concat([A, ":", B]);
338to_str({M,F,A}) when is_atom(M), is_atom(F), is_integer(A) ->
339    lists:concat([M, ":", F, "/", A]);
340to_str(Value) when is_list(Value) ->
341    case lists:all(fun(X) -> is_integer(X) end, Value) of
342	true -> Value;
343	false ->
344	    lists:foldl(fun(X, Acc) ->
345				to_str(X) ++ " " ++ Acc end,
346			"", Value)
347    end;
348to_str(Port) when is_port(Port) ->
349    erlang:port_to_list(Port);
350to_str(Pid) when is_pid(Pid) ->
351    pid_to_list(Pid);
352to_str(No) when is_integer(No) ->
353    integer_to_list(No);
354to_str(Float) when is_float(Float) ->
355    io_lib:format("~.3f", [Float]);
356to_str({trunc, Float}) when is_float(Float) ->
357    float_to_list(Float, [{decimals,0}]);
358to_str(Term) ->
359    io_lib:format("~tw", [Term]).
360
361create_menus([], _MenuBar, _Type) -> ok;
362create_menus(Menus, MenuBar, Type) ->
363    Add = fun({Tag, Ms}, Index) ->
364		  create_menu(Tag, Ms, Index, MenuBar, Type)
365	  end,
366    [{First, _}|_] = Menus,
367    Index = if Type =:= default -> 0;
368	       First =:= "File" -> 0;
369	       true -> 1
370	    end,
371    wx:foldl(Add, Index, Menus),
372    ok.
373
374create_menu("File", MenuItems, Index, MenuBar, Type) ->
375    if
376	Type =:= plugin ->
377	    MenuId = wxMenuBar:findMenu(MenuBar, "File"),
378	    Menu = wxMenuBar:getMenu(MenuBar, MenuId),
379	    lists:foldl(fun(Record, N) ->
380				create_menu_item(Record, Menu, N)
381			end, 0, MenuItems),
382	    Index + 1;
383	true ->
384	    Menu = wxMenu:new(),
385	    lists:foldl(fun(Record, N) ->
386				create_menu_item(Record, Menu, N)
387			end, 0, MenuItems),
388	    wxMenuBar:insert(MenuBar, Index, Menu, "File"),
389	    Index+1
390    end;
391create_menu(Name, MenuItems, Index, MenuBar, _Type) ->
392    Menu = wxMenu:new(),
393    lists:foldl(fun(Record, N) ->
394			create_menu_item(Record, Menu, N)
395		end, 0, MenuItems),
396    wxMenuBar:insert(MenuBar, Index, Menu, Name),
397    Index+1.
398
399create_menu_item(#create_menu{id = ?wxID_HELP=Id}, Menu, Index) ->
400    wxMenu:insert(Menu, Index, Id),
401    Index+1;
402create_menu_item(#create_menu{id=Id, text=Text, help=Help, type=Type, check=Check},
403		 Menu, Index) ->
404    Opts = case Help of
405	       [] -> [];
406	       _ -> [{help, Help}]
407	   end,
408    case Type of
409	append ->
410	    wxMenu:insert(Menu, Index, Id,
411			  [{text, Text}|Opts]);
412	check ->
413	    wxMenu:insertCheckItem(Menu, Index, Id, Text, Opts),
414	    wxMenu:check(Menu, Id, Check);
415	radio ->
416	    wxMenu:insertRadioItem(Menu, Index, Id, Text, Opts),
417	    wxMenu:check(Menu, Id, Check);
418	separator ->
419	    wxMenu:insertSeparator(Menu, Index)
420    end,
421    Index+1;
422create_menu_item(separator, Menu, Index) ->
423    wxMenu:insertSeparator(Menu, Index),
424    Index+1.
425
426colors(Window) ->
427    DarkMode = is_darkmode(wxWindow:getBackgroundColour(Window)),
428    Text = case wxSystemSettings:getColour(?wxSYS_COLOUR_LISTBOXTEXT) of
429               {255,255,255,_} when not DarkMode -> {10,10,10}; %% Is white on Mac for some reason
430               Color -> Color
431           end,
432    Even = wxSystemSettings:getColour(?wxSYS_COLOUR_LISTBOX),
433    Odd = mix(Even, wxSystemSettings:getColour(?wxSYS_COLOUR_HIGHLIGHT), 0.8),
434    #colors{fg=rgb(Text), even=rgb(Even), odd=rgb(Odd)}.
435
436create_attrs(Window) ->
437    Font = wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT),
438    #colors{fg=Text, even=Even, odd=Odd} = colors(Window),
439    #attrs{even = wxListItemAttr:new(Text, Even, Font),
440           odd  = wxListItemAttr:new(Text, Odd, Font),
441           deleted = wxListItemAttr:new(?FG_DELETED, ?BG_DELETED, Font),
442           changed_even = wxListItemAttr:new(Text, mix(?BG_CHANGED, ?BG_EVEN, 0.9), Font),
443           changed_odd  = wxListItemAttr:new(Text, mix(?BG_CHANGED, ?BG_ODD, 0.9), Font),
444           new_even = wxListItemAttr:new(Text, mix(?BG_NEW, ?BG_EVEN, 0.9), Font),
445           new_odd  = wxListItemAttr:new(Text, mix(?BG_NEW, ?BG_ODD, 0.9), Font),
446           searched = wxListItemAttr:new(Text, ?BG_SEARCHED, Font)
447          }.
448
449rgb({R,G,B,_}) -> {R,G,B};
450rgb({_,_,_}=RGB) -> RGB.
451
452mix(RGB,{MR,MG,MB,_}, V) ->
453    mix(RGB, {MR,MG,MB}, V);
454mix({R,G,B,_}, RGB, V) ->
455    mix({R,G,B}, RGB, V);
456mix({R,G,B},{MR,MG,MB}, V) when V =< 1.0 ->
457    {min(255, round(R*V+MR*(1.0-V))),
458     min(255, round(G*V+MG*(1.0-V))),
459     min(255, round(B*V+MB*(1.0-V)))}.
460
461
462is_darkmode({R,G,B,_}) ->
463    ((R+G+B) div 3) < 100.
464
465%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
466
467get_box_info({Title, List}) when is_list(List) -> {Title, ?wxALIGN_LEFT, List};
468get_box_info({Title, left, List}) -> {Title, ?wxALIGN_LEFT, List};
469get_box_info({Title, right, List}) -> {Title, ?wxALIGN_RIGHT, List}.
470
471add_box(Panel, OuterBox, Cursor, Title, Proportion, {Format, List}) ->
472    NumStr = " ("++integer_to_list(length(List))++")",
473    Box = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, Title ++ NumStr}]),
474    Scroll = wxScrolledWindow:new(Panel),
475    wxScrolledWindow:enableScrolling(Scroll,true,true),
476    wxScrolledWindow:setScrollbars(Scroll,1,1,0,0),
477    ScrollSizer  = wxBoxSizer:new(?wxVERTICAL),
478    wxScrolledWindow:setSizer(Scroll, ScrollSizer),
479    wxWindow:setBackgroundStyle(Scroll, ?wxBG_STYLE_SYSTEM),
480    Entries = add_entries(Format, List, Scroll, ScrollSizer, Cursor),
481    wxSizer:add(Box,Scroll,[{proportion,1},{flag,?wxEXPAND}]),
482    wxSizer:add(OuterBox,Box,[{proportion,Proportion},{flag,?wxEXPAND}]),
483    {Scroll,ScrollSizer,length(Entries)}.
484
485add_entries(click, List, Scroll, ScrollSizer, Cursor) ->
486    Add = fun(Link) ->
487		  TC = link_entry(Scroll, Link, Cursor),
488                  wxWindow:setBackgroundStyle(TC, ?wxBG_STYLE_SYSTEM),
489		  wxSizer:add(ScrollSizer,TC, [{flag,?wxEXPAND}])
490	  end,
491    if length(List) > ?NUM_SCROLL_ITEMS ->
492            {List1,Rest} = lists:split(?NUM_SCROLL_ITEMS,List),
493            LinkEntries = [Add(Link) || Link <- List1],
494            NStr = integer_to_list(length(Rest)),
495            TC = link_entry2(Scroll,
496                             {{more,{Rest,Scroll,ScrollSizer}},"more..."},
497                             Cursor,
498                             "Click to see " ++ NStr ++ " more entries"),
499            wxWindow:setBackgroundStyle(TC, ?wxBG_STYLE_SYSTEM),
500            E = wxSizer:add(ScrollSizer,TC, [{flag,?wxEXPAND}]),
501            LinkEntries ++ [E];
502       true ->
503            [Add(Link) || Link <- List]
504    end;
505add_entries(plain, List, Scroll, ScrollSizer, _) ->
506    Add = fun(String) ->
507		  TC = wxStaticText:new(Scroll, ?wxID_ANY, String),
508		  wxSizer:add(ScrollSizer,TC,[{flag,?wxEXPAND}])
509	  end,
510    [Add(String) || String <- List].
511
512add_scroll_entries(MoreEntry,{List, Scroll, ScrollSizer}) ->
513    wx:batch(
514      fun() ->
515              wxSizer:remove(ScrollSizer,?NUM_SCROLL_ITEMS),
516              wxStaticText:destroy(MoreEntry),
517              Cursor = wxCursor:new(?wxCURSOR_HAND),
518              Add = fun(Link) ->
519                            TC = link_entry(Scroll, Link, Cursor),
520                            wxWindow:setBackgroundStyle(TC, ?wxBG_STYLE_SYSTEM),
521                            wxSizer:add(ScrollSizer,TC, [{flag,?wxEXPAND}])
522                    end,
523              Entries = [Add(Link) || Link <- List],
524              wxCursor:destroy(Cursor),
525              wxSizer:layout(ScrollSizer),
526              wxSizer:setVirtualSizeHints(ScrollSizer,Scroll),
527              Entries
528      end).
529
530create_box(_Panel, {scroll_boxes,[]}) ->
531    undefined;
532create_box(Panel, {scroll_boxes,Data}) ->
533    OuterBox = wxBoxSizer:new(?wxHORIZONTAL),
534    Cursor = wxCursor:new(?wxCURSOR_HAND),
535    AddBox = fun({Title,Proportion,Format = {_,_}}) ->
536		     add_box(Panel, OuterBox, Cursor, Title, Proportion, Format);
537		({Title, Format = {_,_}}) ->
538		     add_box(Panel, OuterBox, Cursor, Title, 1, Format);
539		(undefined) ->
540		     undefined
541	     end,
542    Boxes = [AddBox(Entry) || Entry <- Data],
543    wxCursor:destroy(Cursor),
544
545    MaxL = lists:foldl(fun({_,_,L},Max) when L>Max -> L;
546			  (_,Max) -> Max
547		       end,
548		       0,
549		       Boxes),
550
551    Dummy = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?SINGLE_LINE_STYLE}]),
552    {_,H} = wxWindow:getSize(Dummy),
553    wxTextCtrl:destroy(Dummy),
554
555    MaxH = if MaxL > ?NUM_SCROLL_ITEMS -> ?NUM_SCROLL_ITEMS*H;
556	      true -> MaxL*H
557	   end,
558    [wxWindow:setMinSize(B,{0,MaxH}) || {B,_,_} <- Boxes],
559    wxSizer:layout(OuterBox),
560    {OuterBox, Boxes};
561
562create_box(Parent, Data) ->
563    {Title, _Align, Info} = get_box_info(Data),
564    Top = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, Title}]),
565    Panel = wxPanel:new(Parent),
566    Box = wxBoxSizer:new(?wxVERTICAL),
567    LeftSize = 30 + get_max_width(Panel,Info),
568    RightProportion = [{flag, ?wxEXPAND}],
569    AddRow = fun({Desc0, Value0}) ->
570		     Desc = Desc0++":",
571		     Line = wxBoxSizer:new(?wxHORIZONTAL),
572		     Label = wxStaticText:new(Panel, ?wxID_ANY, Desc),
573		     wxSizer:add(Line, 5, 0),
574		     wxSizer:add(Line, Label),
575		     wxSizer:setItemMinSize(Line, Label, LeftSize, -1),
576		     Field =
577			 case Value0 of
578			     {click,"unknown"} ->
579				 wxStaticText:new(Panel, ?wxID_ANY,"unknown");
580			     {click,Value} ->
581				 link_entry(Panel,Value);
582			     _ ->
583				 Value = to_str(Value0),
584                                 case string:nth_lexeme(lists:sublist(Value, 80),1, [$\n]) of
585                                     Value ->
586                                         %% Short string, no newlines - show all
587					 wxStaticText:new(Panel, ?wxID_ANY, Value);
588                                     Shown ->
589                                         %% Long or with newlines,
590                                         %% use tooltip to show all
591					 TCtrl = wxStaticText:new(Panel, ?wxID_ANY, [Shown,"..."]),
592					 wxWindow:setToolTip(TCtrl,wxToolTip:new(Value)),
593					 TCtrl
594				 end
595			 end,
596		     wxSizer:add(Line, 10, 0), % space of size 10 horisontally
597		     wxSizer:add(Line, Field, RightProportion),
598		     wxSizer:add(Box, Line, [{proportion,1}]),
599		     Field;
600		(undefined) ->
601		     undefined
602	     end,
603    InfoFields = [AddRow(Entry) || Entry <- Info],
604    wxWindow:setSizer(Panel, Box),
605    wxSizer:add(Top, Panel, [{proportion,1},{flag,?wxEXPAND}]),
606    {Top, InfoFields}.
607
608link_entry(Panel, Link) ->
609    Cursor = wxCursor:new(?wxCURSOR_HAND),
610    TC = link_entry(Panel, Link, Cursor),
611    wxCursor:destroy(Cursor),
612    TC.
613link_entry(Panel, Link, Cursor) ->
614    link_entry2(Panel,to_link(Link),Cursor).
615
616link_entry2(Panel,{Target,Str},Cursor) ->
617    link_entry2(Panel,{Target,Str},Cursor,"Click to see properties for " ++ Str).
618link_entry2(Panel,{Target,Str},Cursor,ToolTipText) ->
619    TC = wxStaticText:new(Panel, ?wxID_ANY, Str),
620    wxWindow:setForegroundColour(TC,?wxBLUE),
621    wxWindow:setCursor(TC, Cursor),
622    wxWindow:connect(TC, left_down, [{userData,Target}]),
623    wxWindow:connect(TC, enter_window),
624    wxWindow:connect(TC, leave_window),
625    ToolTip = wxToolTip:new(ToolTipText),
626    wxWindow:setToolTip(TC, ToolTip),
627    TC.
628
629to_link(RegName={Name, Node}) when is_atom(Name), is_atom(Node) ->
630    Str = io_lib:format("{~tp,~p}", [Name, Node]),
631    {RegName, Str};
632to_link(TI = {_Target, _Identifier}) ->
633    TI;
634to_link(Target0) ->
635    Target=to_str(Target0),
636    {Target, Target}.
637
638html_window(Panel) ->
639    Win = wxHtmlWindow:new(Panel, [{style, ?wxHW_SCROLLBAR_AUTO}]),
640    %% wxHtmlWindow:setFonts(Win, "", FixedName),
641    wxHtmlWindow:connect(Win,command_html_link_clicked),
642    Win.
643
644html_window(Panel, Html) ->
645    Win = html_window(Panel),
646    wxHtmlWindow:setPage(Win, Html),
647    Win.
648
649get_max_width(Parent,Info) ->
650    lists:foldl(fun({Desc,_}, Max) ->
651			{W, _, _, _} = wxWindow:getTextExtent(Parent, Desc),
652			max(W,Max);
653		   (_, Max) -> Max
654		end, 0, Info).
655
656%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
657set_listctrl_col_size(LCtrl, Total) ->
658    wx:batch(fun() -> calc_last(LCtrl, Total) end).
659
660calc_last(LCtrl, _Total) ->
661    Cols = wxListCtrl:getColumnCount(LCtrl),
662    {Total, _} = wxWindow:getClientSize(LCtrl),
663    SBSize = scroll_size(LCtrl),
664    Last = lists:foldl(fun(I, Last) ->
665			       Last - wxListCtrl:getColumnWidth(LCtrl, I)
666		       end, Total-SBSize, lists:seq(0, Cols - 2)),
667    Size = max(150, Last),
668    wxListCtrl:setColumnWidth(LCtrl, Cols-1, Size).
669
670scroll_size(LCtrl) ->
671    case os:type() of
672	{win32, nt} -> 0;
673	{unix, darwin} -> 0; %% Always 0 in wxWidgets-3.0
674	_ ->
675	    case wxWindow:hasScrollbar(LCtrl, ?wxVERTICAL) of
676		true -> wxSystemSettings:getMetric(?wxSYS_VSCROLL_X);
677		false -> 0
678	    end
679    end.
680
681
682user_term(Parent, Title, Default) ->
683    Dialog = wxTextEntryDialog:new(Parent, Title, [{value, Default}]),
684    case wxTextEntryDialog:showModal(Dialog) of
685	?wxID_OK ->
686	    Str = wxTextEntryDialog:getValue(Dialog),
687	    wxTextEntryDialog:destroy(Dialog),
688	    parse_string(ensure_last_is_dot(Str));
689	?wxID_CANCEL ->
690	    wxTextEntryDialog:destroy(Dialog),
691	    cancel
692    end.
693
694user_term_multiline(Parent, Title, Default) ->
695    Dialog = wxDialog:new(Parent, ?wxID_ANY, Title,
696			  [{style, ?wxDEFAULT_DIALOG_STYLE bor
697			    ?wxRESIZE_BORDER}]),
698    Panel = wxPanel:new(Dialog),
699
700    TextCtrl = wxTextCtrl:new(Panel, ?wxID_ANY,
701			      [{value, Default},
702			       {style, ?wxDEFAULT bor ?wxTE_MULTILINE}]),
703    Line = wxStaticLine:new(Panel, [{style, ?wxLI_HORIZONTAL}]),
704
705    Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
706
707    InnerSizer = wxBoxSizer:new(?wxVERTICAL),
708    wxSizer:add(InnerSizer, TextCtrl,
709		[{flag, ?wxEXPAND bor ?wxALL},{proportion, 1},{border, 5}]),
710    wxSizer:add(InnerSizer, Line,
711		[{flag, ?wxEXPAND},{proportion, 0},{border, 5}]),
712    wxPanel:setSizer(Panel, InnerSizer),
713
714    TopSizer = wxBoxSizer:new(?wxVERTICAL),
715    wxSizer:add(TopSizer, Panel,
716		[{flag, ?wxEXPAND bor ?wxALL},{proportion, 1},{border, 5}]),
717    wxSizer:add(TopSizer, Buttons,
718		[{flag, ?wxEXPAND bor ?wxBOTTOM bor ?wxRIGHT},{border, 10}]),
719
720    % calculate the size of TopSizer when the whole user_term
721    % fits in the TextCtrl
722    DC = wxClientDC:new(Panel),
723    W = wxDC:getCharWidth(DC),
724    H = wxDC:getCharHeight(DC),
725    {EW, EH} = wxDC:getMultiLineTextExtent(DC, Default),
726    wxSizer:setItemMinSize(InnerSizer, 0, EW+2*W, EH+H),
727    TopSize = wxSizer:getMinSize(TopSizer),
728    % reset min size of TextCtrl to 40 chararacters * 4 lines
729    wxSizer:setItemMinSize(InnerSizer, 0, 40*W, 4*H),
730
731    wxWindow:setSizerAndFit(Dialog, TopSizer),
732    wxSizer:setSizeHints(TopSizer, Dialog),
733
734    wxWindow:setClientSize(Dialog, TopSize),
735
736    case wxDialog:showModal(Dialog) of
737	?wxID_OK ->
738	    Str = wxTextCtrl:getValue(TextCtrl),
739	    wxDialog:destroy(Dialog),
740	    parse_string(ensure_last_is_dot(Str));
741	?wxID_CANCEL ->
742	    wxDialog:destroy(Dialog),
743	    cancel
744    end.
745
746parse_string(Str) ->
747    try
748	Tokens = case erl_scan:string(Str, 1, [text]) of
749		     {ok, Ts, _} -> Ts;
750		     {error, {_SLine, SMod, SError}, _} ->
751			 throw(io_lib:format("~ts", [SMod:format_error(SError)]))
752		 end,
753	case erl_eval:extended_parse_term(Tokens) of
754	    {error, {_PLine, PMod, PError}} ->
755		throw(io_lib:format("~ts", [PMod:format_error(PError)]));
756	    Res -> Res
757	end
758    catch
759	throw:ErrStr ->
760	    {error, ErrStr};
761	_:_Err ->
762	    {error, ["Syntax error in: ", Str]}
763    end.
764
765ensure_last_is_dot([]) ->
766    ".";
767ensure_last_is_dot(String) ->
768    case lists:last(String) =:= $. of
769	true ->
770	    String;
771	false ->
772	    String ++ "."
773    end.
774
775%%%-----------------------------------------------------------------
776%%% Status bar for warnings
777create_status_bar(Panel) ->
778    StatusStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY bor ?wxTE_RICH2,
779    Red = wxTextAttr:new(?wxRED),
780
781    %% wxTextCtrl:setSize/3 does not work, so we must create a dummy
782    %% text ctrl first to get the size of the text, then set it when
783    %% creating the real text ctrl.
784    Dummy = wxTextCtrl:new(Panel, ?wxID_ANY,[{style,StatusStyle}]),
785    {X,Y,_,_} = wxTextCtrl:getTextExtent(Dummy,"WARNING"),
786    wxTextCtrl:destroy(Dummy),
787    StatusBar = wxTextCtrl:new(Panel, ?wxID_ANY,
788			 [{style,StatusStyle},
789			  {size,{X,Y+2}}]), % Y+2 to avoid scrollbar
790    wxTextCtrl:setDefaultStyle(StatusBar,Red),
791    wxTextAttr:destroy(Red),
792    StatusBar.
793
794%%%-----------------------------------------------------------------
795%%% Progress dialog
796-define(progress_handler,cdv_progress_handler).
797display_progress_dialog(Parent,Title,Str) ->
798    Caller = self(),
799    Env = wx:get_env(),
800    spawn_link(fun() ->
801		       progress_handler(Caller,Env,Parent,Title,Str)
802	       end),
803    ok.
804
805wait_for_progress() ->
806    receive
807	continue ->
808	    ok;
809	Error ->
810	    Error
811    end.
812
813destroy_progress_dialog() ->
814    report_progress(finish).
815
816sync_destroy_progress_dialog() ->
817    Ref = erlang:monitor(process,?progress_handler),
818    destroy_progress_dialog(),
819    receive {'DOWN',Ref,process,_,_} -> ok end.
820
821report_progress(Progress) ->
822    case whereis(?progress_handler) of
823	Pid when is_pid(Pid) ->
824	    Pid ! {progress,Progress},
825	    ok;
826	_ ->
827	    ok
828    end.
829
830progress_handler(Caller,Env,Parent,Title,Str) ->
831    register(?progress_handler,self()),
832    wx:set_env(Env),
833    PD = progress_dialog(Env,Parent,Title,Str),
834    try progress_loop(Title,PD,Caller,infinity)
835    catch closed -> normal end.
836
837progress_loop(Title,PD,Caller,Pulse) ->
838    receive
839	{progress,{ok,done}} -> % to make wait_for_progress/0 return
840	    Caller ! continue,
841	    progress_loop(Title,PD,Caller,Pulse);
842        {progress,{ok,start_pulse}} ->
843            update_progress_pulse(PD),
844            progress_loop(Title,PD,Caller,?pulse_timeout);
845        {progress,{ok,stop_pulse}} ->
846            progress_loop(Title,PD,Caller,infinity);
847	{progress,{ok,Percent}} when is_integer(Percent) ->
848	    update_progress(PD,Percent),
849	    progress_loop(Title,PD,Caller,Pulse);
850	{progress,{ok,Msg}} ->
851	    update_progress_text(PD,Msg),
852	    progress_loop(Title,PD,Caller,Pulse);
853	{progress,{error, Reason}} ->
854            {Dialog,_,_} = PD,
855            Parent = wxWindow:getParent(Dialog),
856	    finish_progress(PD),
857	    FailMsg =
858		if is_list(Reason) -> Reason;
859		   true -> file:format_error(Reason)
860		end,
861	    display_info_dialog(Parent,"Crashdump Viewer Error",FailMsg),
862	    Caller ! error,
863	    unregister(?progress_handler),
864	    unlink(Caller);
865	{progress,finish} ->
866	    finish_progress(PD),
867	    unregister(?progress_handler),
868	    unlink(Caller)
869    after Pulse ->
870            update_progress_pulse(PD),
871            progress_loop(Title,PD,Caller,?pulse_timeout)
872    end.
873
874progress_dialog(_Env,Parent,Title,Str) ->
875    progress_dialog_new(Parent,Title,Str).
876
877update_progress(PD,Value) ->
878    try progress_dialog_update(PD,Value)
879    catch _:_ -> throw(closed) %% Port or window have died
880    end.
881update_progress_text(PD,Text) ->
882    try progress_dialog_update(PD,Text)
883    catch _:_ -> throw(closed) %% Port or window have died
884    end.
885update_progress_pulse(PD) ->
886    try progress_dialog_pulse(PD)
887    catch _:_ -> throw(closed) %% Port or window have died
888    end.
889finish_progress(PD) ->
890    try progress_dialog_update(PD,100)
891    catch _:_ -> ok
892    after progress_dialog_destroy(PD)
893    end.
894
895progress_dialog_new(Parent,Title,Str) ->
896    Dialog = wxDialog:new(Parent, ?wxID_ANY, Title,
897                          [{style,?wxDEFAULT_DIALOG_STYLE}]),
898    Panel = wxPanel:new(Dialog),
899    Sizer = wxBoxSizer:new(?wxVERTICAL),
900    Message = wxStaticText:new(Panel, 1, Str,[{size,{220,-1}}]),
901    Gauge = wxGauge:new(Panel, 2, 100, [{style, ?wxGA_HORIZONTAL}]),
902    SizerFlags = ?wxEXPAND bor ?wxLEFT bor ?wxRIGHT bor ?wxTOP,
903    wxSizer:add(Sizer, Message, [{flag,SizerFlags},{border,15}]),
904    wxSizer:add(Sizer, Gauge, [{flag, SizerFlags bor ?wxBOTTOM},{border,15}]),
905    wxPanel:setSizer(Panel, Sizer),
906    wxSizer:setSizeHints(Sizer, Dialog),
907    wxDialog:show(Dialog),
908    {Dialog,Message,Gauge}.
909
910progress_dialog_update({_,_,Gauge},Value) when is_integer(Value) ->
911    wxGauge:setValue(Gauge,Value);
912progress_dialog_update({_,Message,Gauge},Text) when is_list(Text) ->
913    wxGauge:setValue(Gauge,0),
914    wxStaticText:setLabel(Message,Text).
915progress_dialog_pulse({_,_,Gauge}) ->
916    wxGauge:pulse(Gauge).
917progress_dialog_destroy({Dialog,_,_}) ->
918    wxDialog:destroy(Dialog).
919
920make_obsbin(Bin,Tab) ->
921    Size = byte_size(Bin),
922    {Preview,PreviewBitSize} =
923        try
924            %% The binary might be a unicode string, in which case we
925            %% don't want to split it in the middle of a grapheme
926            %% cluster - thus trying string:length and slice.
927            PL1 = min(string:length(Bin), 10),
928            PB1 = string:slice(Bin,0,PL1),
929            PS1 = byte_size(PB1) * 8,
930            <<P1:PS1>> = PB1,
931            {P1,PS1}
932        catch _:_ ->
933                %% Probably not a string, so just split anywhere
934                PS2 = min(Size, 10) * 8,
935                <<P2:PS2, _/binary>> = Bin,
936                {P2,PS2}
937        end,
938    Hash = erlang:phash2(Bin),
939    Key = {Preview, Size, Hash},
940    ets:insert(Tab, {Key,Bin}),
941    ['#OBSBin',Preview,PreviewBitSize,Size,Hash].
942
943
944%% d(F) ->
945%%     d(F, []).
946
947%% d(Debug, F) when is_boolean(Debug) andalso is_list(F) ->
948%%     d(Debug, F, []);
949%% d(F, A) when is_list(F) andalso is_list(A) ->
950%%     d(get(debug), F, A).
951
952%% d(true, F, A) ->
953%%    io:format("[ol] " ++ F ++ "~n", A);
954%% d(_, _, _) ->
955%%    ok.
956
957
958