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