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