1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2011-2017. 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_tv_table). 21 22-export([start_link/2]). 23 24%% wx_object callbacks 25-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3, 26 handle_event/2, handle_sync_event/3, handle_cast/2]). 27 28-export([format/1]). 29 30-include("observer_defs.hrl"). 31-import(observer_lib, [to_str/1]). 32 33-behaviour(wx_object). 34-include_lib("wx/include/wx.hrl"). 35-include("observer_tv.hrl"). 36 37-define(ID_TABLE_INFO, 400). 38-define(ID_REFRESH, 401). 39-define(ID_REFRESH_INTERVAL, 402). 40-define(ID_EDIT, 403). 41-define(ID_DELETE, 404). 42-define(ID_SEARCH, 405). 43 44-define(SEARCH_ENTRY, 420). 45-define(GOTO_ENTRY, 421). 46 47-define(DEFAULT_COL_WIDTH, 150). 48 49-record(state, 50 { 51 parent, 52 frame, 53 grid, 54 status, 55 sizer, 56 search, 57 selected, 58 node=node(), 59 columns, 60 pid, 61 source, 62 tab, 63 attrs, 64 timer={false, 30} 65 }). 66 67-record(opt, 68 { 69 sort_key=2, 70 sort_incr=true 71 }). 72 73-record(search, 74 {enable=true, % Subwindow is enabled 75 win, % Sash Sub window obj 76 name, % name 77 78 search, % Search input ctrl 79 goto, % Goto input ctrl 80 radio, % Radio buttons 81 82 find % Search string 83 }). 84 85-record(find, {start, % start pos 86 strlen, % Found 87 found % false 88 }). 89 90start_link(Parent, Opts) -> 91 wx_object:start_link(?MODULE, [Parent, Opts], []). 92 93init([Parent, Opts]) -> 94 Source = proplists:get_value(type, Opts), 95 Table = proplists:get_value(table, Opts), 96 Node = proplists:get_value(node, Opts), 97 Title0 = atom_to_list(Table#tab.name) ++ " @ " ++ atom_to_list(Node), 98 Title = case Source of 99 ets -> "TV Ets: " ++ Title0; 100 mnesia -> "TV Mnesia: " ++ Title0 101 end, 102 Scale = observer_wx:get_scale(), 103 Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {Scale * 800, Scale * 600}}]), 104 IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"), 105 Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]), 106 wxFrame:setIcon(Frame, Icon), 107 wxIcon:destroy(Icon), 108 MenuBar = wxMenuBar:new(), 109 create_menus(MenuBar), 110 wxFrame:setMenuBar(Frame, MenuBar), 111 %% wxFrame:setAcceleratorTable(Frame, AccelTable), 112 wxMenu:connect(Frame, command_menu_selected), 113 114 StatusBar = wxFrame:createStatusBar(Frame, []), 115 try 116 TabId = table_id(Table), 117 ColumnNames = column_names(Node, Source, TabId), 118 KeyPos = key_pos(Node, Source, TabId), 119 Panel = wxPanel:new(Frame), 120 Attrs = observer_lib:create_attrs(Panel), 121 122 Self = self(), 123 Holder = spawn_link(fun() -> 124 init_table_holder(Self, Table, Source, 125 length(ColumnNames), Node, Attrs) 126 end), 127 128 Sizer = wxBoxSizer:new(?wxVERTICAL), 129 Style = ?wxLC_REPORT bor ?wxLC_VIRTUAL bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES, 130 Grid = wxListCtrl:new(Panel, [{style, Style}, 131 {onGetItemText, 132 fun(_, Item,Col) -> get_row(Holder, Item, Col+1) end}, 133 {onGetItemAttr, 134 fun(_, Item) -> get_attr(Holder, Item) end} 135 ]), 136 wxListCtrl:connect(Grid, command_list_item_activated), 137 wxListCtrl:connect(Grid, command_list_item_selected), 138 wxListCtrl:connect(Grid, command_list_col_click), 139 wxListCtrl:connect(Grid, size, [{skip, true}]), 140 wxWindow:setFocus(Grid), 141 142 Search = search_area(Panel), 143 wxSizer:add(Sizer, Grid, 144 [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]), 145 wxSizer:add(Sizer, Search#search.win, 146 [{flag,?wxEXPAND bor ?wxLEFT bor ?wxRIGHT bor 147 ?wxRESERVE_SPACE_EVEN_IF_HIDDEN}, 148 {border, 5}]), 149 wxWindow:setSizer(Panel, Sizer), 150 wxSizer:hide(Sizer, Search#search.win), 151 152 Cols = add_columns(Grid, 0, ColumnNames), 153 wxFrame:show(Frame), 154 {Panel, #state{frame=Frame, grid=Grid, status=StatusBar, search=Search, 155 sizer = Sizer, 156 parent=Parent, columns=Cols, 157 pid=Holder, source=Source, tab=Table#tab{keypos=KeyPos}, 158 attrs=Attrs}} 159 catch node_or_table_down -> 160 wxFrame:destroy(Frame), 161 stop 162 end. 163 164add_columns(Grid, Start, ColumnNames) -> 165 Li = wxListItem:new(), 166 AddListEntry = fun(Name, Col) -> 167 wxListItem:setText(Li, to_str(Name)), 168 wxListItem:setAlign(Li, ?wxLIST_FORMAT_LEFT), 169 wxListCtrl:insertColumn(Grid, Col, Li), 170 wxListCtrl:setColumnWidth(Grid, Col, ?DEFAULT_COL_WIDTH), 171 Col + 1 172 end, 173 Cols = lists:foldl(AddListEntry, Start, ColumnNames), 174 wxListItem:destroy(Li), 175 Cols. 176 177create_menus(MB) -> 178 File = wxMenu:new(), 179 wxMenu:append(File, ?ID_TABLE_INFO, "Table Information\tCtrl-I"), 180 wxMenu:append(File, ?wxID_CLOSE, "Close"), 181 wxMenuBar:append(MB, File, "File"), 182 Edit = wxMenu:new(), 183 wxMenu:append(Edit, ?ID_EDIT, "Edit Object"), 184 wxMenu:append(Edit, ?ID_DELETE, "Delete Object\tCtrl-D"), 185 wxMenu:appendSeparator(Edit), 186 wxMenu:append(Edit, ?ID_SEARCH, "Search\tCtrl-S"), 187 wxMenu:appendSeparator(Edit), 188 wxMenu:append(Edit, ?ID_REFRESH, "Refresh\tCtrl-R"), 189 wxMenu:append(Edit, ?ID_REFRESH_INTERVAL, "Refresh interval..."), 190 wxMenuBar:append(MB, Edit, "Edit"), 191 Help = wxMenu:new(), 192 wxMenu:append(Help, ?wxID_HELP, "Help"), 193 wxMenuBar:append(MB, Help, "Help"), 194 ok. 195 196search_area(Parent) -> 197 HSz = wxBoxSizer:new(?wxHORIZONTAL), 198 wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Find:"), 199 [{flag,?wxALIGN_CENTER_VERTICAL}]), 200 TC1 = wxTextCtrl:new(Parent, ?SEARCH_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]), 201 wxSizer:add(HSz, TC1, [{proportion,3}, {flag, ?wxEXPAND}]), 202 Nbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Next"), 203 wxRadioButton:setValue(Nbtn, true), 204 wxSizer:add(HSz,Nbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]), 205 Pbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Previous"), 206 wxSizer:add(HSz,Pbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]), 207 Cbtn = wxCheckBox:new(Parent, ?wxID_ANY, "Match Case"), 208 wxSizer:add(HSz,Cbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]), 209 wxSizer:add(HSz, 15,15, [{proportion,1}, {flag, ?wxEXPAND}]), 210 wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Goto Entry:"), 211 [{flag,?wxALIGN_CENTER_VERTICAL}]), 212 TC2 = wxTextCtrl:new(Parent, ?GOTO_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]), 213 wxSizer:add(HSz, TC2, [{proportion,0}, {flag, ?wxEXPAND}]), 214 wxTextCtrl:connect(TC1, command_text_updated), 215 wxTextCtrl:connect(TC1, command_text_enter), 216 wxTextCtrl:connect(TC1, kill_focus), 217 wxTextCtrl:connect(TC2, command_text_enter), 218 wxWindow:connect(Parent, command_button_clicked), 219 220 #search{name='Search Area', win=HSz, 221 search=TC1,goto=TC2,radio={Nbtn,Pbtn,Cbtn}}. 222 223edit(Index, #state{pid=Pid, frame=Frame}) -> 224 Str = get_row(Pid, Index, all_multiline), 225 case observer_lib:user_term_multiline(Frame, "Edit object:", Str) of 226 cancel -> ok; 227 {ok, Term} -> Pid ! {edit, Index, Term}; 228 Err = {error, _} -> self() ! Err 229 end. 230 231handle_event(#wx{id=?ID_REFRESH},State = #state{pid=Pid}) -> 232 Pid ! refresh, 233 {noreply, State}; 234 235handle_event(#wx{event=#wxList{type=command_list_col_click, col=Col}}, 236 State = #state{pid=Pid, grid=Grid, selected=OldSel}) -> 237 SelObj = case OldSel of 238 undefined -> undefined; 239 _ -> get_row(Pid, OldSel, term) 240 end, 241 Pid ! {sort, Col+1}, 242 case SelObj =/= undefined andalso search(Pid, SelObj, -1, true, term) of 243 false when is_integer(OldSel) -> 244 wxListCtrl:setItemState(Grid, OldSel, 0, ?wxLIST_STATE_SELECTED), 245 {noreply, State#state{selected=undefined}}; 246 false -> 247 {noreply, State#state{selected=undefined}}; 248 Row -> 249 wxListCtrl:setItemState(Grid, Row, 16#FFFF, ?wxLIST_STATE_SELECTED), 250 {noreply, State#state{selected=Row}} 251 end; 252 253handle_event(#wx{event=#wxSize{size={W,_}}}, State=#state{grid=Grid}) -> 254 observer_lib:set_listctrl_col_size(Grid, W), 255 {noreply, State}; 256 257handle_event(#wx{event=#wxList{type=command_list_item_selected, itemIndex=Index}}, 258 State = #state{pid=Pid, grid=Grid, status=StatusBar}) -> 259 N = wxListCtrl:getItemCount(Grid), 260 Str = get_row(Pid, Index, all), 261 wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w: ~ts",[N, Str])), 262 {noreply, State#state{selected=Index}}; 263 264handle_event(#wx{event=#wxList{type=command_list_item_activated, itemIndex=Index}}, 265 State) -> 266 edit(Index, State), 267 {noreply, State}; 268 269handle_event(#wx{id=?ID_EDIT}, State = #state{selected=undefined}) -> 270 {noreply, State}; 271handle_event(#wx{id=?ID_EDIT}, State = #state{selected=Index}) -> 272 edit(Index, State), 273 {noreply, State}; 274 275handle_event(#wx{id=?ID_DELETE}, State = #state{selected=undefined}) -> 276 {noreply, State}; 277handle_event(#wx{id=?ID_DELETE}, 278 State = #state{grid=Grid, pid=Pid, status=StatusBar, selected=Index}) -> 279 Str = get_row(Pid, Index, all), 280 Pid ! {delete, Index}, 281 wxStatusBar:setStatusText(StatusBar, io_lib:format("Deleted object: ~ts",[Str])), 282 wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), 283 {noreply, State#state{selected=undefined}}; 284 285handle_event(#wx{id=?wxID_CLOSE}, State = #state{frame=Frame}) -> 286 wxFrame:destroy(Frame), 287 {stop, normal, State}; 288 289handle_event(Help = #wx{id=?wxID_HELP}, State) -> 290 observer ! Help, 291 {noreply, State}; 292 293handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}}, 294 State = #state{grid=Grid}) -> 295 try 296 Row0 = list_to_integer(Str), 297 Row1 = max(0, Row0), 298 Row = min(wxListCtrl:getItemCount(Grid)-1,Row1), 299 wxListCtrl:ensureVisible(Grid, Row), 300 ok 301 catch _:_ -> ok 302 end, 303 {noreply, State}; 304 305%% Search functionality 306handle_event(#wx{id=?ID_SEARCH}, 307 State = #state{grid=Grid, sizer=Sz, search=Search, selected=Index}) -> 308 is_integer(Index) andalso 309 wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), 310 wxSizer:show(Sz, Search#search.win), 311 wxWindow:setFocus(Search#search.search), 312 wxSizer:layout(Sz), 313 {noreply, State}; 314handle_event(#wx{id=?SEARCH_ENTRY, event=#wxFocus{}}, 315 State = #state{search=Search, pid=Pid}) -> 316 Pid ! {mark_search_hit, false}, 317 {noreply, State#state{search=Search#search{find=undefined}}}; 318handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=""}}, 319 State = #state{search=Search, pid=Pid}) -> 320 Pid ! {mark_search_hit, false}, 321 {noreply, State#state{search=Search#search{find=undefined}}}; 322handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{type=command_text_enter,cmdString=Str}}, 323 State = #state{grid=Grid, pid=Pid, status=SB, 324 search=Search=#search{radio={Next0, _, Case0}, 325 find=Find}}) 326 when Find =/= undefined -> 327 Dir = wxRadioButton:getValue(Next0) xor wx_misc:getKeyState(?WXK_SHIFT), 328 Case = wxCheckBox:getValue(Case0), 329 Pos = if Find#find.found, Dir -> %% Forward Continuation 330 Find#find.start+1; 331 Find#find.found -> %% Backward Continuation 332 Find#find.start-1; 333 Dir -> %% Forward wrap 334 0; 335 true -> %% Backward wrap 336 wxListCtrl:getItemCount(Grid)-1 337 end, 338 Pid ! {mark_search_hit, false}, 339 case search(Pid, Str, Pos, Dir, Case) of 340 false -> 341 wxStatusBar:setStatusText(SB, io_lib:format("Not found (regexp): ~ts",[Str])), 342 Pid ! {mark_search_hit, Find#find.start}, 343 wxListCtrl:refreshItem(Grid, Find#find.start), 344 {noreply, State#state{search=Search#search{find=Find#find{found=false}}}}; 345 Row -> 346 wxListCtrl:ensureVisible(Grid, Row), 347 wxListCtrl:refreshItem(Grid, Row), 348 Status = "Found: (Hit Enter for next, Shift-Enter for previous)", 349 wxStatusBar:setStatusText(SB, Status), 350 {noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}} 351 end; 352handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=Str}}, 353 State = #state{grid=Grid, pid=Pid, status=SB, 354 search=Search=#search{radio={Next0, _, Case0}, 355 find=Find}}) -> 356 try 357 Dir = wxRadioButton:getValue(Next0), 358 Case = wxCheckBox:getValue(Case0), 359 Start = case Dir of 360 true -> 0; 361 false -> wxListCtrl:getItemCount(Grid)-1 362 end, 363 Cont = case Find of 364 undefined -> 365 #find{start=Start, strlen=length(Str)}; 366 #find{strlen=Old} when Old < length(Str) -> 367 Find#find{start=Start, strlen=length(Str)}; 368 _ -> 369 Find#find{strlen=length(Str)} 370 end, 371 372 Pid ! {mark_search_hit, false}, 373 case search(Pid, Str, Cont#find.start, Dir, Case) of 374 false -> 375 wxStatusBar:setStatusText(SB, io_lib:format("Not found (regexp): ~ts",[Str])), 376 {noreply, State}; 377 Row -> 378 wxListCtrl:ensureVisible(Grid, Row), 379 wxListCtrl:refreshItem(Grid, Row), 380 Status = "Found: (Hit Enter for next, Shift-Enter for previous)", 381 wxStatusBar:setStatusText(SB, Status), 382 {noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}} 383 end 384 catch _:_ -> {noreply, State} 385 end; 386 387handle_event(#wx{id=?ID_TABLE_INFO}, 388 State = #state{frame=Frame, node=Node, source=Source, tab=Table}) -> 389 observer_tv_wx:display_table_info(Frame, Node, Source, Table), 390 {noreply, State}; 391 392handle_event(#wx{id=?ID_REFRESH_INTERVAL}, 393 State = #state{grid=Grid, timer=Timer0}) -> 394 Timer = observer_lib:interval_dialog(Grid, Timer0, 10, 5*60), 395 {noreply, State#state{timer=Timer}}; 396 397handle_event(_Event, State) -> 398 %io:format("~p:~p, handle event ~tp\n", [?MODULE, ?LINE, Event]), 399 {noreply, State}. 400 401handle_sync_event(_Event, _Obj, _State) -> 402 %io:format("~p:~p, handle sync_event ~tp\n", [?MODULE, ?LINE, Event]), 403 ok. 404 405handle_call(_Event, _From, State) -> 406 %io:format("~p:~p, handle call (~p) ~tp\n", [?MODULE, ?LINE, From, Event]), 407 {noreply, State}. 408 409handle_cast(_Event, State) -> 410 %io:format("~p:~p, handle cast ~tp\n", [?MODULE, ?LINE, Event]), 411 {noreply, State}. 412 413handle_info({no_rows, N}, State = #state{grid=Grid, status=StatusBar}) -> 414 wxListCtrl:setItemCount(Grid, N), 415 wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w",[N])), 416 {noreply, State}; 417 418handle_info({new_cols, New}, State = #state{grid=Grid, columns=Cols0}) -> 419 Cols = add_columns(Grid, Cols0, New), 420 {noreply, State#state{columns=Cols}}; 421 422handle_info({refresh, Min, Min}, State = #state{grid=Grid}) -> 423 wxListCtrl:refreshItem(Grid, Min), %% Avoid assert in wx below if Max is 0 424 {noreply, State}; 425handle_info({refresh, Min, Max}, State = #state{grid=Grid}) -> 426 Max > 0 andalso wxListCtrl:refreshItems(Grid, Min, Max), 427 {noreply, State}; 428 429handle_info(refresh_interval, State = #state{pid=Pid}) -> 430 Pid ! refresh, 431 {noreply, State}; 432 433handle_info({error, Error}, State = #state{frame=Frame}) -> 434 ErrorStr = 435 try io_lib:format("~ts", [Error]), Error 436 catch _:_ -> io_lib:format("~tp", [Error]) 437 end, 438 Dlg = wxMessageDialog:new(Frame, ErrorStr), 439 wxMessageDialog:showModal(Dlg), 440 wxMessageDialog:destroy(Dlg), 441 {noreply, State}; 442 443handle_info(_Event, State) -> 444 %% io:format("~p:~p, handle info ~tp\n", [?MODULE, ?LINE, _Event]), 445 {noreply, State}. 446 447terminate(_Event, #state{pid=Pid, attrs=Attrs}) -> 448 %% ListItemAttr are not auto deleted 449 #attrs{odd=Odd, even=Even, deleted=D, searched=S, 450 changed_odd=Ch1, changed_even=Ch2, 451 new_odd=New1, new_even=New2 452 } = Attrs, 453 wxListItemAttr:destroy(Odd), wxListItemAttr:destroy(Even), 454 wxListItemAttr:destroy(D), 455 wxListItemAttr:destroy(Ch1),wxListItemAttr:destroy(Ch2), 456 wxListItemAttr:destroy(New1),wxListItemAttr:destroy(New2), 457 wxListItemAttr:destroy(S), 458 unlink(Pid), 459 exit(Pid, window_closed), 460 ok. 461 462code_change(_, _, State) -> 463 State. 464 465%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 466%% Table holder needs to be in a separate process otherwise 467%% the callback get_row/3 may deadlock if the process do 468%% wx calls when callback is invoked. 469get_row(Table, Item, Column) -> 470 Ref = erlang:monitor(process, Table), 471 Table ! {get_row, self(), Item, Column}, 472 receive 473 {'DOWN', Ref, _, _, _} -> ""; 474 {Table, Res} -> 475 erlang:demonitor(Ref), 476 Res 477 end. 478 479get_attr(Table, Item) -> 480 Ref = erlang:monitor(process, Table), 481 Table ! {get_attr, self(), Item}, 482 receive 483 {'DOWN', Ref, _, _, _} -> wx:null(); 484 {Table, Res} -> 485 erlang:demonitor(Ref), 486 Res 487 end. 488 489search(Table, Str, Row, Dir, Case) -> 490 Ref = erlang:monitor(process, Table), 491 Table ! {search, [Str, Row, Dir, Case]}, 492 receive 493 {'DOWN', Ref, _, _, _} -> ""; 494 {Table, Res} -> 495 erlang:demonitor(Ref), 496 Res 497 end. 498 499-record(holder, {node, parent, pid, 500 table=array:new(), n=0, columns, 501 temp=[], 502 search, 503 source, tabid, 504 sort, 505 key, 506 type, 507 attrs 508 }). 509 510init_table_holder(Parent, Table, MnesiaOrEts, Cols, Node, Attrs) -> 511 TabId = case Table#tab.id of 512 ignore -> Table#tab.name; 513 Id -> Id 514 end, 515 self() ! refresh, 516 table_holder(#holder{node=Node, parent=Parent, 517 source=MnesiaOrEts, tabid=TabId, columns=Cols, 518 sort=#opt{sort_key=Table#tab.keypos, sort_incr=true}, 519 type=Table#tab.type, key=Table#tab.keypos, 520 attrs=Attrs}). 521 522table_holder(S0 = #holder{parent=Parent, pid=Pid, table=Table}) -> 523 receive 524 {get_attr, From, Row} -> 525 get_attr(From, Row, S0), 526 table_holder(S0); 527 {get_row, From, Row, Col} -> 528 get_row(From, Row, Col, Table), 529 table_holder(S0); 530 {Pid, Data} -> 531 S1 = handle_new_data_chunk(Data, S0), 532 table_holder(S1); 533 {sort, Col} -> 534 Parent ! {refresh, 0, S0#holder.n-1}, 535 table_holder(sort(Col, S0)); 536 {search, Data} -> 537 table_holder(search(Data, S0)); 538 {mark_search_hit, Row} -> 539 Old = S0#holder.search, 540 is_integer(Old) andalso (Parent ! {refresh, Old, Old}), 541 table_holder(S0#holder{search=Row}); 542 refresh when is_pid(Pid) -> 543 %% Already getting the table... 544 %% io:format("ignoring refresh", []), 545 table_holder(S0); 546 refresh -> 547 GetTab = rpc:call(S0#holder.node, observer_backend, get_table, 548 [self(), S0#holder.tabid, S0#holder.source]), 549 table_holder(S0#holder{pid=GetTab}); 550 {delete, Row} -> 551 delete_row(Row, S0), 552 table_holder(S0); 553 {edit, Row, Term} -> 554 edit_row(Row, Term, S0), 555 table_holder(S0); 556 What -> 557 io:format("Table holder got ~tp~n",[What]), 558 Parent ! {refresh, 0, S0#holder.n-1}, 559 table_holder(S0) 560 end. 561 562handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) -> 563 S1 = #holder{n=N,columns=NewCols} = handle_new_data_chunk2(Data, S0), 564 Parent ! {no_rows, N}, 565 Parent ! {refresh, 0, N-1}, 566 case NewCols =:= Cols of 567 true -> S1; 568 false -> 569 Parent ! {new_cols, lists:seq(Cols+1, NewCols)}, 570 S1 571 end. 572 573handle_new_data_chunk2('$end_of_table', 574 S0 = #holder{sort=Opt0, key=Key, 575 table=Old, temp=New}) -> 576 Merged = merge(array:to_list(Old), New, Key), 577 {Opt,Sorted} = sort(Opt0#opt.sort_key, Opt0#opt{sort_key = undefined}, Merged), 578 SortedA = array:from_list(Sorted), 579 S0#holder{sort=Opt, table=SortedA, n=array:size(SortedA), temp=[], pid=undefined}; 580handle_new_data_chunk2(Data, S0 = #holder{columns=Cols0, source=ets, temp=Tab0}) -> 581 {Tab, Cols} = parse_ets_data(Data, Cols0, Tab0), 582 S0#holder{columns=Cols, temp=Tab}; 583handle_new_data_chunk2(Data, S0 = #holder{source=mnesia, temp=Tab}) -> 584 S0#holder{temp=(Data ++ Tab)}. 585 586parse_ets_data([[Rec]|Rs], C, Tab) -> 587 parse_ets_data(Rs, max(tuple_size(Rec), C), [Rec|Tab]); 588parse_ets_data([Recs|Rs], C0, Tab0) -> 589 {Tab, Cols} = parse_ets_data(Recs, C0, Tab0), 590 parse_ets_data(Rs, Cols, Tab); 591parse_ets_data([], Cols, Tab) -> 592 {Tab, Cols}. 593 594sort(Col, S=#holder{sort=Opt0, table=Table0}) -> 595 {Opt, Table} = sort(Col, Opt0, array:to_list(Table0)), 596 S#holder{sort=Opt, table=array:from_list(Table)}. 597 598sort(Col, Opt = #opt{sort_key=Col, sort_incr=Bool}, Table) -> 599 {Opt#opt{sort_incr=not Bool}, lists:reverse(Table)}; 600sort(Col, S=#opt{sort_incr=true}, Table) -> 601 {S#opt{sort_key=Col}, keysort(Col, Table)}; 602sort(Col, S=#opt{sort_incr=false}, Table) -> 603 {S#opt{sort_key=Col}, lists:reverse(keysort(Col, Table))}. 604 605keysort(Col, Table) -> 606 Sort = fun([A0|_], [B0|_]) -> 607 A = try element(Col, A0) catch _:_ -> [] end, 608 B = try element(Col, B0) catch _:_ -> [] end, 609 case A == B of 610 true -> A0 =< B0; 611 false -> A < B 612 end; 613 (A0, B0) when is_tuple(A0), is_tuple(B0) -> 614 A = try element(Col, A0) catch _:_ -> [] end, 615 B = try element(Col, B0) catch _:_ -> [] end, 616 case A == B of 617 true -> A0 =< B0; 618 false -> A < B 619 end 620 end, 621 lists:sort(Sort, Table). 622 623search([Term, -1, true, term], S=#holder{parent=Parent, table=Table}) -> 624 Search = fun(Idx, [Tuple|_]) -> 625 Tuple =:= Term andalso throw(Idx), 626 Tuple 627 end, 628 try array:map(Search, Table) of 629 _ -> Parent ! {self(), false} 630 catch Index -> 631 Parent ! {self(), Index} 632 end, 633 S; 634search([Str, Row, Dir0, CaseSens], 635 S=#holder{parent=Parent, n=N, table=Table}) -> 636 Opt = case CaseSens of 637 true -> []; 638 false -> [caseless] 639 end, 640 Dir = case Dir0 of 641 true -> 1; 642 false -> -1 643 end, 644 Res = case re:compile(Str, [unicode|Opt]) of 645 {ok, Re} -> re_search(Row, Dir, N, Re, Table); 646 {error, _} -> false 647 end, 648 Parent ! {self(), Res}, 649 S#holder{search=Res}. 650 651re_search(Row, Dir, N, Re, Table) when Row >= 0, Row < N -> 652 [Term|_] = array:get(Row, Table), 653 Str = format(Term), 654 Res = re:run(Str, Re), 655 case Res of 656 nomatch -> re_search(Row+Dir, Dir, N, Re, Table); 657 {match,_} -> 658 Row 659 end; 660re_search(_, _, _, _, _) -> 661 false. 662 663get_row(From, Row, Col, Table) -> 664 case array:get(Row, Table) of 665 [Object|_] when Col =:= all -> 666 From ! {self(), format(Object)}; 667 [Object|_] when Col =:= all_multiline -> 668 From ! {self(), io_lib:format("~tp", [Object])}; 669 [Object|_] when Col =:= term -> 670 From ! {self(), Object}; 671 [Object|_] when tuple_size(Object) >= Col -> 672 From ! {self(), format(element(Col, Object))}; 673 _ -> 674 From ! {self(), ""} 675 end. 676 677get_attr(From, Row, #holder{attrs=Attrs, search=Row}) -> 678 What = Attrs#attrs.searched, 679 From ! {self(), What}; 680get_attr(From, Row, #holder{table=Table, attrs=Attrs}) -> 681 Odd = (Row rem 2) > 0, 682 What = case array:get(Row, Table) of 683 [_|deleted] -> Attrs#attrs.deleted; 684 [_|changed] when Odd -> Attrs#attrs.changed_odd; 685 [_|changed] -> Attrs#attrs.changed_even; 686 [_|new] when Odd -> Attrs#attrs.new_odd; 687 [_|new] -> Attrs#attrs.new_even; 688 _ when Odd -> Attrs#attrs.odd; 689 _ -> Attrs#attrs.even 690 end, 691 From ! {self(), What}. 692 693merge([], New, _Key) -> 694 [[N] || N <- New]; %% First time 695merge(Old, New, Key) -> 696 merge2(keysort(Key, Old), keysort(Key, New), Key). 697 698-dialyzer({no_improper_lists, merge2/3}). 699merge2([[Obj|_]|Old], [Obj|New], Key) -> 700 [[Obj]|merge2(Old, New, Key)]; 701merge2([[A|Op]|Old], [B|New], Key) 702 when element(Key, A) == element(Key, B) -> 703 case Op of 704 deleted -> 705 [[B|new]|merge2(Old, New, Key)]; 706 _ -> 707 [[B|changed]|merge2(Old, New, Key)] 708 end; 709merge2([[A|Op]|Old], New = [B|_], Key) 710 when element(Key, A) < element(Key, B) -> 711 case Op of 712 deleted -> merge2(Old, New, Key); 713 _ -> [[A|deleted]|merge2(Old, New, Key)] 714 end; 715merge2(Old = [[A|_]|_], [B|New], Key) 716 when element(Key, A) > element(Key, B) -> 717 [[B|new]|merge2(Old, New, Key)]; 718merge2([], New, _Key) -> 719 [[N|new] || N <- New]; 720merge2(Old, [], _Key) -> 721 lists:foldl(fun([_O|deleted], Acc) -> Acc; 722 ([O|_], Acc) -> [[O|deleted]|Acc] 723 end, [], Old). 724 725 726delete_row(Row, S0 = #holder{parent=Parent}) -> 727 case delete(Row, S0) of 728 ok -> 729 self() ! refresh; 730 {error, Err} -> 731 Parent ! {error, "Could not delete object: " ++ Err} 732 end. 733 734 735delete(Row, #holder{tabid=Id, table=Table, 736 source=Source, node=Node}) -> 737 [Object|_] = array:get(Row, Table), 738 try 739 case Source of 740 ets -> 741 true = rpc:call(Node, ets, delete_object, [Id, Object]); 742 mnesia -> 743 ok = rpc:call(Node, mnesia, dirty_delete_object, [Id, Object]) 744 end, 745 ok 746 catch _:_Error -> 747 {error, "node or table is not available"} 748 end. 749 750edit_row(Row, Term, S0 = #holder{parent=Parent}) -> 751 case delete(Row, S0) of 752 ok -> 753 case insert(Term, S0) of 754 ok -> self() ! refresh; 755 Err -> Parent ! {error, Err} 756 end; 757 {error, Err} -> 758 Parent ! {error, "Could not edit object: " ++ Err} 759 end. 760 761insert(Object, #holder{tabid=Id, source=Source, node=Node}) -> 762 try 763 case Source of 764 ets -> 765 true = rpc:call(Node, ets, insert, [Id, Object]); 766 mnesia -> 767 ok = rpc:call(Node, mnesia, dirty_write, [Id, Object]) 768 end, 769 ok 770 catch _:_Error -> 771 {error, "node or table is not available"} 772 end. 773 774%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 775 776column_names(Node, Type, Table) -> 777 case Type of 778 ets -> [1, 2]; 779 mnesia -> 780 Attrs = rpc:call(Node, mnesia, table_info, [Table, attributes]), 781 is_list(Attrs) orelse throw(node_or_table_down), 782 ["Record Name"|Attrs] 783 end. 784 785table_id(#tab{id=ignore, name=Name}) -> Name; 786table_id(#tab{id=Id}) -> Id. 787 788key_pos(_, mnesia, _) -> 2; 789key_pos(Node, ets, TabId) -> 790 KeyPos = rpc:call(Node, ets, info, [TabId, keypos]), 791 is_integer(KeyPos) orelse throw(node_or_table_down), 792 KeyPos. 793 794%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 795 796format(Tuple) when is_tuple(Tuple) -> 797 [${ |format_tuple(Tuple, 1, tuple_size(Tuple))]; 798format(List) when is_list(List) -> 799 format_list(List); 800format(Bin) when is_binary(Bin), byte_size(Bin) > 100 -> 801 io_lib:format("<<#Bin:~w>>", [byte_size(Bin)]); 802format(Bin) when is_binary(Bin) -> 803 try 804 true = io_lib:printable_list(unicode:characters_to_list(Bin)), 805 io_lib:format("<<\"~ts\">>", [Bin]) 806 catch _:_ -> 807 io_lib:format("~w", [Bin]) 808 end; 809format(Float) when is_float(Float) -> 810 io_lib:format("~.3g", [Float]); 811format(Term) -> 812 io_lib:format("~tw", [Term]). 813 814format_tuple(Tuple, I, Max) when I < Max -> 815 [format(element(I, Tuple)), $,|format_tuple(Tuple, I+1, Max)]; 816format_tuple(Tuple, Max, Max) -> 817 [format(element(Max, Tuple)), $}]; 818format_tuple(_Tuple, 1, 0) -> 819 [$}]. 820 821format_list([]) -> "[]"; 822format_list(List) -> 823 case io_lib:printable_list(List) of 824 true -> io_lib:format("\"~ts\"", [map_printable_list(List)]); 825 false -> [$[ | make_list(List)] 826 end. 827 828make_list([Last]) -> 829 [format(Last), $]]; 830make_list([Head|Tail]) when is_list(Tail) -> 831 [format(Head), $,|make_list(Tail)]; 832make_list([Head|Tail]) -> 833 [format(Head), $|, format(Tail), $]]. 834 835map_printable_list([$\n|Cs]) -> 836 [$\\, $n|map_printable_list(Cs)]; 837map_printable_list([$\r|Cs]) -> 838 [$\\, $r|map_printable_list(Cs)]; 839map_printable_list([$\t|Cs]) -> 840 [$\\, $t|map_printable_list(Cs)]; 841map_printable_list([$\v|Cs]) -> 842 [$\\, $v|map_printable_list(Cs)]; 843map_printable_list([$\b|Cs]) -> 844 [$\\, $b|map_printable_list(Cs)]; 845map_printable_list([$\f|Cs]) -> 846 [$\\, $f|map_printable_list(Cs)]; 847map_printable_list([$\e|Cs]) -> 848 [$\\, $e|map_printable_list(Cs)]; 849map_printable_list([]) -> []; 850map_printable_list([C|Cs]) -> 851 [C|map_printable_list(Cs)]. 852