aboutsummaryrefslogblamecommitdiffstats
path: root/lib/observer/src/observer_tv_table.erl
blob: a2797700d8c19040b985fdeb64130932f0003bef (plain) (tree)



























                                                                          
                                  






























                                  

                





















































































































































































































































































































































                                                                                               




                                                                 
























































































































































































                                                                                   



                                                               





















































































































































































































                                                                                             
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2011. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%

-module(observer_tv_table).

-export([start_link/2]).

%% wx_object callbacks
-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
	 handle_event/2, handle_sync_event/3, handle_cast/2]).

-export([get_table/3]).

-import(observer_lib, [to_str/1]).

-behaviour(wx_object).
-include_lib("wx/include/wx.hrl").
-include("observer_tv.hrl").

-define(ID_TABLE_INFO, 400).
-define(ID_REFRESH, 401).
-define(ID_REFRESH_INTERVAL, 402).
-define(ID_EDIT, 403).
-define(ID_DELETE, 404).
-define(ID_SEARCH, 405).

-define(SEARCH_ENTRY, 420).
-define(GOTO_ENTRY,   421).

-define(DEFAULT_COL_WIDTH, 100).

-record(state,
	{
	  parent,
	  frame,
	  grid,
	  status,
	  sizer,
	  search,
	  selected,
	  node=node(),
	  columns,
	  pid,
	  source,
	  tab,
	  attrs,
	  timer
	}).

-record(opt,
	{
	  sort_key=2,
	  sort_incr=true
	}).

-record(attrs, {even, odd, deleted, changed, searched}).

-record(search,
	{enable=true,          %  Subwindow is enabled
	 win,                  %  Sash Sub window obj
	 name,                 %  name

	 search,               %  Search input ctrl
	 goto,                 %  Goto  input ctrl
	 radio,                %  Radio buttons

	 find                  %  Search string
	}).

-record(find, {start,              % start pos
	       strlen,             % Found
	       found               % false
	      }).

start_link(Parent, Opts) ->
    wx_object:start_link(?MODULE, [Parent, Opts], []).

init([Parent, Opts]) ->
    Source = proplists:get_value(type, Opts),
    Table  = proplists:get_value(table, Opts),
    Node   = proplists:get_value(node, Opts),
    Title0 = atom_to_list(Table#tab.name) ++ " @ " ++ atom_to_list(Node),
    Title = case Source of
		ets -> "TV Ets: " ++ Title0;
		mnesia -> "TV Mnesia: " ++ Title0
	    end,
    Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {800, 300}}]),
    IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"),
    Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]),
    wxFrame:setIcon(Frame, Icon),
    wxIcon:destroy(Icon),
    MenuBar = wxMenuBar:new(),
    create_menus(MenuBar),
    wxFrame:setMenuBar(Frame, MenuBar),
    %% wxFrame:setAcceleratorTable(Frame, AccelTable),
    wxMenu:connect(Frame, command_menu_selected),

    StatusBar = wxFrame:createStatusBar(Frame, []),
    try
	TabId = table_id(Table),
	ColumnNames = column_names(Node, Source, TabId),
	KeyPos = key_pos(Node, Source, TabId),

	Attrs = create_attrs(),

	Self = self(),
	Holder = spawn_link(fun() ->
				    init_table_holder(Self, Table, Source,
						      length(ColumnNames), Node, Attrs)
			    end),

	Panel = wxPanel:new(Frame),
	Sizer = wxBoxSizer:new(?wxVERTICAL),
	Style = ?wxLC_REPORT bor ?wxLC_VIRTUAL bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES,
	Grid = wxListCtrl:new(Panel, [{style, Style},
				      {onGetItemText,
				       fun(_, Item,Col) -> get_row(Holder, Item, Col+1) end},
				      {onGetItemAttr,
				       fun(_, Item) -> get_attr(Holder, Item) end}
				     ]),
	wxListCtrl:connect(Grid, command_list_item_activated),
	wxListCtrl:connect(Grid, command_list_item_selected),
	wxListCtrl:connect(Grid, command_list_col_click),
	wxListCtrl:connect(Grid, size, [{skip, true}]),
	wxWindow:setFocus(Grid),

	Search = search_area(Panel),
	wxSizer:add(Sizer, Grid,
		    [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]),
	wxSizer:add(Sizer, Search#search.win,
		    [{flag,?wxEXPAND bor ?wxLEFT bor ?wxRIGHT bor
			  ?wxRESERVE_SPACE_EVEN_IF_HIDDEN},
		     {border, 5}]),
	wxWindow:setSizer(Panel, Sizer),
	wxSizer:hide(Sizer, Search#search.win),

	Cols = add_columns(Grid, 0, ColumnNames),
	wxFrame:show(Frame),
	{Panel, #state{frame=Frame, grid=Grid, status=StatusBar, search=Search,
		       sizer = Sizer,
		       parent=Parent, columns=Cols,
		       pid=Holder, source=Source, tab=Table#tab{keypos=KeyPos},
		       attrs=Attrs}}
    catch node_or_table_down ->
	    wxFrame:destroy(Frame),
	    stop
    end.

add_columns(Grid, Start, ColumnNames) ->
    Li = wxListItem:new(),
    AddListEntry = fun(Name, Col) ->
			   wxListItem:setText(Li, to_str(Name)),
			   wxListItem:setAlign(Li, ?wxLIST_FORMAT_LEFT),
			   wxListCtrl:insertColumn(Grid, Col, Li),
			   wxListCtrl:setColumnWidth(Grid, Col, ?DEFAULT_COL_WIDTH),
			   Col + 1
		   end,
    Cols = lists:foldl(AddListEntry, Start, ColumnNames),
    wxListItem:destroy(Li),
    Cols.

create_menus(MB) ->
    File = wxMenu:new(),
    wxMenu:append(File, ?ID_TABLE_INFO, "Table Information\tCtrl-I"),
    wxMenu:append(File, ?wxID_CLOSE, "Close"),
    wxMenuBar:append(MB, File, "File"),
    Edit = wxMenu:new(),
    wxMenu:append(Edit, ?ID_EDIT, "Edit Object"),
    wxMenu:append(Edit, ?ID_DELETE, "Delete Object\tCtrl-D"),
    wxMenu:appendSeparator(Edit),
    wxMenu:append(Edit, ?ID_SEARCH, "Search\tCtrl-S"),
    wxMenu:appendSeparator(Edit),
    wxMenu:append(Edit, ?ID_REFRESH, "Refresh\tCtrl-R"),
    wxMenu:append(Edit, ?ID_REFRESH_INTERVAL, "Refresh interval..."),
    wxMenuBar:append(MB, Edit, "Edit"),
    Help = wxMenu:new(),
    wxMenu:append(Help, ?wxID_HELP, "Help"),
    wxMenuBar:append(MB, Help, "Help"),
    ok.

search_area(Parent) ->
    HSz = wxBoxSizer:new(?wxHORIZONTAL),
    wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Find:"),
		[{flag,?wxALIGN_CENTER_VERTICAL}]),
    TC1 = wxTextCtrl:new(Parent, ?SEARCH_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]),
    wxSizer:add(HSz, TC1,  [{proportion,3}, {flag, ?wxEXPAND}]),
    Nbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Next"),
    wxRadioButton:setValue(Nbtn, true),
    wxSizer:add(HSz,Nbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
    Pbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Previous"),
    wxSizer:add(HSz,Pbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
    Cbtn = wxCheckBox:new(Parent, ?wxID_ANY, "Match Case"),
    wxSizer:add(HSz,Cbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
    wxSizer:add(HSz, 15,15, [{proportion,1}, {flag, ?wxEXPAND}]),
    wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Goto Entry:"),
		[{flag,?wxALIGN_CENTER_VERTICAL}]),
    TC2 = wxTextCtrl:new(Parent, ?GOTO_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]),
    wxSizer:add(HSz, TC2,  [{proportion,0}, {flag, ?wxEXPAND}]),
    wxTextCtrl:connect(TC1, command_text_updated),
    wxTextCtrl:connect(TC1, command_text_enter),
    wxTextCtrl:connect(TC1, kill_focus),
    wxTextCtrl:connect(TC2, command_text_enter),
    wxWindow:connect(Parent, command_button_clicked),

    #search{name='Search Area', win=HSz,
	    search=TC1,goto=TC2,radio={Nbtn,Pbtn,Cbtn}}.

edit(Index, #state{pid=Pid, frame=Frame}) ->
    Str = get_row(Pid, Index, all),
    Dialog = wxTextEntryDialog:new(Frame, "Edit object:", [{value, Str}]),
    case wxTextEntryDialog:showModal(Dialog) of
	?wxID_OK ->
	    New = wxTextEntryDialog:getValue(Dialog),
	    wxTextEntryDialog:destroy(Dialog),
	    case Str =:= New of
		true -> ok;
		false ->
		    complete_edit(Index, New, Pid)
	    end;
	?wxID_CANCEL ->
	    wxTextEntryDialog:destroy(Dialog)
    end.

complete_edit(Row, New0, Pid) ->
    New = case lists:reverse(New0) of
	      [$.|_] -> New0;
	      _ -> New0 ++ "."
	  end,
    try
	{ok, Tokens, _} = erl_scan:string(New),
	{ok, Term} = erl_parse:parse_term(Tokens),
	Pid ! {edit, Row, Term}
    catch _:{badmatch, {error, {_, _, Err}}} ->
	    self() ! {error, ["Parse error: ", Err]};
	  _Err ->
	    self() ! {error, ["Syntax error in: ", New]}
    end.

handle_event(#wx{id=?ID_REFRESH},State = #state{pid=Pid}) ->
    Pid ! refresh,
    {noreply, State};

handle_event(#wx{event=#wxList{type=command_list_col_click, col=Col}},
	     State = #state{pid=Pid}) ->
    Pid ! {sort, Col+1},
    {noreply, State};

handle_event(#wx{event=#wxSize{size={W,_}}},  State=#state{grid=Grid}) ->
    wx:batch(fun() ->
		     Cols = wxListCtrl:getColumnCount(Grid),
		     Last = lists:foldl(fun(I, Last) ->
						Last - wxListCtrl:getColumnWidth(Grid, I)
					end, W-2, lists:seq(0, Cols - 2)),
		     Size = max(?DEFAULT_COL_WIDTH, Last),
		     wxListCtrl:setColumnWidth(Grid, Cols-1, Size)
	     end),
    {noreply, State};

handle_event(#wx{event=#wxList{type=command_list_item_selected, itemIndex=Index}},
	     State = #state{pid=Pid, grid=Grid, status=StatusBar}) ->
    N = wxListCtrl:getItemCount(Grid),
    Str = get_row(Pid, Index, all),
    wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w: ~s",[N, Str])),
    {noreply, State#state{selected=Index}};

handle_event(#wx{event=#wxList{type=command_list_item_activated, itemIndex=Index}},
	     State) ->
    edit(Index, State),
    {noreply, State};

handle_event(#wx{id=?ID_EDIT}, State = #state{selected=undefined}) ->
    {noreply, State};
handle_event(#wx{id=?ID_EDIT}, State = #state{selected=Index}) ->
    edit(Index, State),
    {noreply, State};

handle_event(#wx{id=?ID_DELETE}, State = #state{selected=undefined}) ->
    {noreply, State};
handle_event(#wx{id=?ID_DELETE},
	     State = #state{pid=Pid, status=StatusBar, selected=Index}) ->
    Str = get_row(Pid, Index, all),
    Pid ! {delete, Index},
    wxStatusBar:setStatusText(StatusBar, io_lib:format("Deleted object: ~s",[Str])),
    {noreply, State};

handle_event(#wx{id=?wxID_CLOSE}, State) ->
    {stop, normal, State};

handle_event(Help = #wx{id=?wxID_HELP}, State = #state{parent=Parent}) ->
    Parent ! Help,
    {noreply, State};

handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}},
	     State = #state{grid=Grid}) ->
    try
	Row0 = list_to_integer(Str),
	Row1 = min(0, Row0),
	Row  = max(wxListCtrl:getItemCount(Grid)-1,Row1),
	wxListCtrl:ensureVisible(Grid, Row),
	ok
    catch _:_ -> ok
    end,
    {noreply, State};

%% Search functionality
handle_event(#wx{id=?ID_SEARCH},
	     State = #state{sizer=Sz, search=Search}) ->
    wxSizer:show(Sz, Search#search.win),
    wxWindow:setFocus(Search#search.search),
    wxSizer:layout(Sz),
    {noreply, State};
handle_event(#wx{id=?SEARCH_ENTRY, event=#wxFocus{}},
	     State = #state{search=Search, pid=Pid}) ->
    Pid ! {mark_search_hit, false},
    {noreply, State#state{search=Search#search{find=undefined}}};
handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=""}},
	     State = #state{search=Search, pid=Pid}) ->
    Pid ! {mark_search_hit, false},
    {noreply, State#state{search=Search#search{find=undefined}}};
handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{type=command_text_enter,cmdString=Str}},
	     State = #state{grid=Grid, pid=Pid, status=SB,
			    search=Search=#search{radio={Next0, _, Case0},
						  find=Find}})
  when Find =/= undefined ->
    Dir  = wxRadioButton:getValue(Next0) xor wx_misc:getKeyState(?WXK_SHIFT),
    Case = wxCheckBox:getValue(Case0),
    Pos = if Find#find.found, Dir ->  %% Forward Continuation
		  Find#find.start+1;
	     Find#find.found ->  %% Backward Continuation
		  Find#find.start-1;
	     Dir ->   %% Forward wrap
		  0;
	     true ->  %% Backward wrap
		  wxListCtrl:getItemCount(Grid)-1
	  end,
    Pid ! {mark_search_hit, false},
    case search(Pid, Str, Pos, Dir, Case) of
	false ->
	    wxStatusBar:setStatusText(SB, "Not found"),
	    Pid ! {mark_search_hit, Find#find.start},
	    wxListCtrl:refreshItem(Grid, Find#find.start),
	    {noreply, State#state{search=Search#search{find=#find{found=false}}}};
	Row ->
	    wxListCtrl:ensureVisible(Grid, Row),
	    wxListCtrl:refreshItem(Grid, Row),
	    Status = "Found: (Hit Enter for next, Shift-Enter for previous)",
	    wxStatusBar:setStatusText(SB, Status),
	    {noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}}
    end;
handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=Str}},
	     State = #state{grid=Grid, pid=Pid, status=SB,
			    search=Search=#search{radio={Next0, _, Case0},
						  find=Find}}) ->
    try
	Dir  = wxRadioButton:getValue(Next0),
	Case = wxCheckBox:getValue(Case0),
	Start = case Dir of
		    true -> 0;
		    false -> wxListCtrl:getItemCount(Grid)-1
		end,
	Cont = case Find of
		   undefined ->
		       #find{start=Start, strlen=length(Str)};
		   #find{strlen=Old} when Old < length(Str) ->
		       Find#find{start=Start, strlen=length(Str)};
		   _ ->
		       Find#find{strlen=length(Str)}
	       end,

	Pid ! {mark_search_hit, false},
	case search(Pid, Str, Cont#find.start, Dir, Case) of
	    false ->
		wxStatusBar:setStatusText(SB, "Not found"),
		{noreply, State};
	    Row ->
		wxListCtrl:ensureVisible(Grid, Row),
		wxListCtrl:refreshItem(Grid, Row),
		Status = "Found: (Hit Enter for next, Shift-Enter for previous)",
		wxStatusBar:setStatusText(SB, Status),
		{noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}}
	end
    catch _:_ -> {noreply, State}
    end;

handle_event(#wx{id=?ID_TABLE_INFO},
	     State = #state{frame=Frame, node=Node, source=Source, tab=Table}) ->
    observer_tv_wx:display_table_info(Frame, Node, Source, Table),
    {noreply, State};

handle_event(#wx{id=?ID_REFRESH_INTERVAL},
	     State = #state{grid=Grid, timer=Timer0}) ->
    Timer = observer_lib:interval_dialog(Grid, Timer0, 10, 5*60),
    {noreply, State#state{timer=Timer}};

handle_event(Event, State) ->
    io:format("~p:~p, handle event ~p\n", [?MODULE, ?LINE, Event]),
    {noreply, State}.

handle_sync_event(Event, _Obj, _State) ->
    io:format("~p:~p, handle sync_event ~p\n", [?MODULE, ?LINE, Event]),
    ok.

handle_call(Event, From, State) ->
    io:format("~p:~p, handle call (~p) ~p\n", [?MODULE, ?LINE, From, Event]),
    {noreply, State}.

handle_cast(Event, State) ->
    io:format("~p:~p, handle cast ~p\n", [?MODULE, ?LINE, Event]),
    {noreply, State}.

handle_info({no_rows, N}, State = #state{grid=Grid, status=StatusBar}) ->
    wxListCtrl:setItemCount(Grid, N),
    wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w",[N])),
    {noreply, State};
handle_info({new_cols, New}, State = #state{grid=Grid, columns=Cols0}) ->
    Cols = add_columns(Grid, Cols0, New),
    {noreply, State#state{columns=Cols}};
handle_info({refresh, Min, Max}, State = #state{grid=Grid}) ->
    wxListCtrl:refreshItems(Grid, Min, Max),
    {noreply, State};
handle_info({error, Error}, State = #state{frame=Frame}) ->
    Dlg = wxMessageDialog:new(Frame, Error),
    wxMessageDialog:showModal(Dlg),
    wxMessageDialog:destroy(Dlg),
    {noreply, State};

handle_info(Event, State) ->
    io:format("~p:~p, handle info ~p\n", [?MODULE, ?LINE, Event]),
    {noreply, State}.

terminate(_Event, #state{pid=Pid, attrs=Attrs}) ->
    %% ListItemAttr are not auto deleted
    #attrs{odd=Odd, deleted=D, changed=Ch, searched=S} = Attrs,
    wxListItemAttr:destroy(Odd),
    wxListItemAttr:destroy(D),
    wxListItemAttr:destroy(Ch),
    wxListItemAttr:destroy(S),
    unlink(Pid),
    exit(Pid, window_closed),
    ok.

code_change(_, _, State) ->
    State.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  Table holder needs to be in a separate process otherwise
%%  the callback get_row/3 may deadlock if the process do
%%  wx calls when callback is invoked.
get_row(Table, Item, Column) ->
    Ref = erlang:monitor(process, Table),
    Table ! {get_row, self(), Item, Column},
    receive
	{'DOWN', Ref, _, _, _} -> "";
	{Table, Res} ->
	    erlang:demonitor(Ref),
	    Res
    end.

get_attr(Table, Item) ->
    Ref = erlang:monitor(process, Table),
    Table ! {get_attr, self(), Item},
    receive
	{'DOWN', Ref, _, _, _} -> "";
	{Table, Res} ->
	    erlang:demonitor(Ref),
	    Res
    end.

search(Table, Str, Row, Dir, Case) ->
    Ref = erlang:monitor(process, Table),
    Table ! {search, [Str, Row, Dir, Case]},
    receive
	{'DOWN', Ref, _, _, _} -> "";
	{Table, Res} ->
	    erlang:demonitor(Ref),
	    Res
    end.

-record(holder, {node, parent, pid,
		 table=[], n=0, columns,
		 temp=[],
		 search,
		 source, tabid,
		 sort,
		 key,
		 type,
		 attrs
		}).

init_table_holder(Parent, Table, MnesiaOrEts, Cols, Node, Attrs) ->
    TabId = case Table#tab.id of
		ignore -> Table#tab.name;
		Id -> Id
	    end,
    self() ! refresh,
    table_holder(#holder{node=Node, parent=Parent,
			 source=MnesiaOrEts, tabid=TabId, columns=Cols,
			 sort=#opt{sort_key=Table#tab.keypos, sort_incr=true},
			 type=Table#tab.type, key=Table#tab.keypos,
			 attrs=Attrs}).

table_holder(S0 = #holder{parent=Parent, pid=Pid, table=Table}) ->
    receive
	{get_attr, From, Row} ->
	    get_attr(From, Row, S0),
	    table_holder(S0);
	{get_row, From, Row, Col} ->
	    get_row(From, Row, Col, Table),
	    table_holder(S0);
	{Pid, Data} ->
	    S1 = handle_new_data_chunk(Data, S0),
	    table_holder(S1);
	{sort, Col} ->
	    table_holder(sort(Col, S0));
	{search, Data} ->
	    table_holder(search(Data, S0));
	{mark_search_hit, Row} ->
	    Old = S0#holder.search,
	    is_integer(Old) andalso (Parent ! {refresh, Old, Old}),
	    table_holder(S0#holder{search=Row});
	refresh when is_pid(Pid) ->
	    %% Already getting the table...
	    %% io:format("ignoring refresh", []),
	    table_holder(S0);
	refresh ->
	    GetTab = rpc:call(S0#holder.node, ?MODULE, get_table,
			      [self(), S0#holder.tabid, S0#holder.source]),
	    table_holder(S0#holder{pid=GetTab});
	{delete, Row} ->
	    delete_row(Row, S0),
	    table_holder(S0);
	{edit, Row, Term} ->
	    edit_row(Row, Term, S0),
	    table_holder(S0);
	What ->
	    io:format("Table holder got ~p~n",[What]),
	    table_holder(S0)
    end.

handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) ->
    S1 = #holder{columns=NewCols} = handle_new_data_chunk2(Data, S0),
    case NewCols =:= Cols of
	true -> S1;
	false ->
	    Parent ! {new_cols, lists:seq(Cols+1, NewCols)},
	    S1
    end.

handle_new_data_chunk2('$end_of_table',
		       S0 = #holder{parent=Parent, sort=Opt,
				    key=Key,
				    table=Old, temp=New}) ->
    Table = merge(Old, New, Key),
    N = length(Table),
    Parent ! {no_rows, N},
    sort(Opt#opt.sort_key, S0#holder{n=N, pid=undefine,
				     sort=Opt#opt{sort_key = undefined},
				     table=Table, temp=[]});
handle_new_data_chunk2(Data, S0 = #holder{columns=Cols0, source=ets, temp=Tab0}) ->
    {Tab, Cols} = parse_ets_data(Data, Cols0, Tab0),
    S0#holder{columns=Cols, temp=Tab};
handle_new_data_chunk2(Data, S0 = #holder{source=mnesia, temp=Tab}) ->
    S0#holder{temp=(Data ++ Tab)}.

parse_ets_data([[Rec]|Rs], C, Tab) ->
    parse_ets_data(Rs, max(tuple_size(Rec), C), [Rec|Tab]);
parse_ets_data([Recs|Rs], C0, Tab0) ->
    {Tab, Cols} = parse_ets_data(Recs, C0, Tab0),
    parse_ets_data(Rs, Cols, Tab);
parse_ets_data([], Cols, Tab) ->
    {Tab, Cols}.

sort(Col, S=#holder{n=N, parent=Parent, sort=Opt0, table=Table0}) ->
    {Opt, Table} = sort(Col, Opt0, Table0),
    Parent ! {refresh, 0, N},
    S#holder{sort=Opt, table=Table}.

sort(Col, Opt = #opt{sort_key=Col, sort_incr=Bool}, Table) ->
    {Opt#opt{sort_incr=not Bool}, lists:reverse(Table)};
sort(Col, S=#opt{sort_incr=true}, Table) ->
    {S#opt{sort_key=Col}, keysort(Col, Table)};
sort(Col, S=#opt{sort_incr=false}, Table) ->
    {S=#opt{sort_key=Col}, lists:reverse(keysort(Col, Table))}.

keysort(Col, Table) ->
    Sort = fun([A0|_], [B0|_]) ->
		   A = try element(Col, A0) catch _:_ -> [] end,
		   B = try element(Col, B0) catch _:_ -> [] end,
		   case A == B of
		       true -> A0 =< B0;
		       false -> A < B
		   end;
	      (A0, B0) when is_tuple(A0), is_tuple(B0) ->
		   A = try element(Col, A0) catch _:_ -> [] end,
		   B = try element(Col, B0) catch _:_ -> [] end,
		   case A == B of
		       true -> A0 =< B0;
		       false -> A < B
		   end
	   end,
    lists:sort(Sort, Table).

search([Str, Row, Dir0, CaseSens],
       S=#holder{parent=Parent, table=Table}) ->
    Opt = case CaseSens of
	      true -> [];
	      false -> [caseless]
	  end,
    {ok, Re} = re:compile(Str, Opt),
    Dir = case Dir0 of
	      true -> 1;
	      false -> -1
	  end,
    Res = search(Row, Dir, Re, Table),
    Parent ! {self(), Res},
    S#holder{search=Res}.

search(Row, Dir, Re, Table) ->
    Res = try lists:nth(Row+1, Table) of
	      Term ->
		  Str = io_lib:format("~w", [Term]),
		  re:run(Str, Re)
	  catch _:_ -> no_more
	  end,
    case Res of
	nomatch -> search(Row+Dir, Dir, Re, Table);
	no_more -> false;
	{match,_} -> Row
    end.

get_row(From, Row, Col, Table) ->
    case lists:nth(Row+1, Table) of
	[Object|_] when Col =:= all ->
	    From ! {self(), io_lib:format("~w", [Object])};
	[Object|_] when tuple_size(Object) >= Col ->
	    From ! {self(), io_lib:format("~w", [element(Col, Object)])};
	_ ->
	    From ! {self(), ""}
    end.

get_attr(From, Row, #holder{attrs=Attrs, search=Row}) ->
    What = Attrs#attrs.searched,
    From ! {self(), What};
get_attr(From, Row, #holder{table=Table, attrs=Attrs}) ->
    What = case lists:nth(Row+1, Table) of
	       [_|deleted]  -> Attrs#attrs.deleted;
	       [_|changed]  -> Attrs#attrs.changed;
	       [_|new]      -> Attrs#attrs.changed;
	       _ when (Row rem 2) > 0 ->
		   Attrs#attrs.odd;
	       _ ->
		   Attrs#attrs.even
	   end,
    From ! {self(), What}.

merge([], New, _Key) ->
    [[N] || N <- New]; %% First time
merge(Old, New, Key) ->
    merge2(keysort(Key, Old), keysort(Key, New), Key).

merge2([[Obj|_]|Old], [Obj|New], Key) ->
    [[Obj]|merge2(Old, New, Key)];
merge2([[A|_]|Old], [B|New], Key)
  when element(Key, A) == element(Key, B) ->
    [[B|changed]|merge2(Old, New, Key)];
merge2([[A|_]|Old], New = [B|_], Key)
  when element(Key, A) < element(Key, B) ->
    [[A|deleted]|merge2(Old, New, Key)];
merge2(Old = [[A|_]|_], [B|New], Key)
  when element(Key, A) > element(Key, B) ->
    [[B|new]|merge2(Old, New, Key)];
merge2([], New, _Key) ->
    [[N|new] || N <- New];
merge2(Old, [], _Key) ->
    [[O|deleted] || [O|_] <- Old].


delete_row(Row, S0 = #holder{parent=Parent}) ->
    case delete(Row, S0) of
	ok ->
	    self() ! refresh;
	{error, Err} ->
	    Parent ! {error, "Could not delete object: " ++ Err}
    end.


delete(Row, #holder{tabid=Id, table=Table,
		    source=Source, node=Node}) ->
    [Object|_] = lists:nth(Row+1, Table),
    try
	case Source of
	    ets ->
		true = rpc:call(Node, ets, delete_object, [Id, Object]);
	    mnesia ->
		ok = rpc:call(Node, mnesia, dirty_delete_object, [Id, Object])
	end,
	ok
    catch _:_Error ->
	    {error, "node or table is not available"}
    end.

edit_row(Row, Term, S0 = #holder{parent=Parent}) ->
    case delete(Row, S0) of
	ok ->
	    case insert(Term, S0) of
		ok -> self() ! refresh;
		Err -> Parent ! {error, Err}
	    end;
	{error, Err} ->
	    Parent ! {error, "Could not edit object: " ++ Err}
    end.

insert(Object, #holder{tabid=Id, source=Source, node=Node}) ->
    try
	case Source of
	    ets ->
		true = rpc:call(Node, ets, insert, [Id, Object]);
	    mnesia ->
		ok = rpc:call(Node, mnesia, dirty_write, [Id, Object])
	end,
	ok
    catch _:_Error ->
	    {error, "node or table is not available"}
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
get_table(Parent, Table, Module) ->
    spawn(fun() ->
		  link(Parent),
		  get_table2(Parent, Table, Module)
	  end).

get_table2(Parent, Table, Type) ->
    Size = case Type of
	       ets -> ets:info(Table, size);
	       mnesia -> mnesia:table_info(Table, size)
	   end,
    case Size > 0 of
	false ->
	    Parent ! {self(), '$end_of_table'},
	    normal;
	true when Type =:= ets ->
	    Mem = ets:info(Table, memory),
	    Average = Mem div Size,
	    NoElements = max(10, 20000 div Average),
	    get_ets_loop(Parent, ets:match(Table, '$1', NoElements));
	true ->
	    Mem = mnesia:table_info(Table, memory),
	    Average = Mem div Size,
	    NoElements = max(10, 20000 div Average),
	    Ms = [{'$1', [], ['$1']}],
	    Get = fun() ->
			  get_mnesia_loop(Parent, mnesia:select(Table, Ms, NoElements, read))
		  end,
	    %% Not a transaction, we don't want to grab locks when inspecting the table
	    mnesia:async_dirty(Get)
    end.

get_ets_loop(Parent, '$end_of_table') ->
    Parent ! {self(), '$end_of_table'};
get_ets_loop(Parent, {Match, Cont}) ->
    Parent ! {self(), Match},
    get_ets_loop(Parent, ets:match(Cont)).

get_mnesia_loop(Parent, '$end_of_table') ->
    Parent ! {self(), '$end_of_table'};
get_mnesia_loop(Parent, {Match, Cont}) ->
    Parent ! {self(), Match},
    get_ets_loop(Parent, mnesia:select(Cont)).

column_names(Node, Type, Table) ->
    case Type of
	ets -> [1, 2];
	mnesia ->
	    Attrs = rpc:call(Node, mnesia, table_info, [Table, attributes]),
	    is_list(Attrs) orelse throw(node_or_table_down),
	    ["Record Name"|Attrs]
    end.

table_id(#tab{id=ignore, name=Name}) -> Name;
table_id(#tab{id=Id}) -> Id.

key_pos(_, mnesia, _) -> 2;
key_pos(Node, ets, TabId) ->
    KeyPos = rpc:call(Node, ets, info, [TabId, keypos]),
    is_integer(KeyPos) orelse throw(node_or_table_down),
    KeyPos.

create_attrs() ->
    Font = wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT),
    Text = wxSystemSettings:getColour(?wxSYS_COLOUR_LISTBOXTEXT),
    #attrs{even = wx:typeCast(wx:null(), wxListItemAttr),
	   odd  = wxListItemAttr:new(Text, {240,240,255}, Font),
	   deleted = wxListItemAttr:new({240,30,30}, {100,100,100}, Font),
	   changed = wxListItemAttr:new(Text, {255,215,0}, Font),
	   searched = wxListItemAttr:new(Text, {235,215,90}, Font)
	  }.