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