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