%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2011-2017. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions 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([format/1]). -include("observer_defs.hrl"). -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, 150). -record(state, { parent, frame, grid, status, sizer, search, selected, node=node(), columns, pid, source, tab, attrs, timer={false, 30} }). -record(opt, { sort_key=2, sort_incr=true }). -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, Scale = observer_wx:get_scale(), Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {Scale * 800, Scale * 600}}]), 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), Panel = wxPanel:new(Frame), Attrs = observer_lib:create_attrs(Panel), Self = self(), Holder = spawn_link(fun() -> init_table_holder(Self, Table, Source, length(ColumnNames), Node, Attrs) end), 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_multiline), case observer_lib:user_term_multiline(Frame, "Edit object:", Str) of cancel -> ok; {ok, Term} -> Pid ! {edit, Index, Term}; Err = {error, _} -> self() ! Err 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, grid=Grid, selected=OldSel}) -> SelObj = case OldSel of undefined -> undefined; _ -> get_row(Pid, OldSel, term) end, Pid ! {sort, Col+1}, case SelObj =/= undefined andalso search(Pid, SelObj, -1, true, term) of false when is_integer(OldSel) -> wxListCtrl:setItemState(Grid, OldSel, 0, ?wxLIST_STATE_SELECTED), {noreply, State#state{selected=undefined}}; false -> {noreply, State#state{selected=undefined}}; Row -> wxListCtrl:setItemState(Grid, Row, 16#FFFF, ?wxLIST_STATE_SELECTED), {noreply, State#state{selected=Row}} end; handle_event(#wx{event=#wxSize{size={W,_}}}, State=#state{grid=Grid}) -> observer_lib:set_listctrl_col_size(Grid, W), {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: ~ts",[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{grid=Grid, pid=Pid, status=StatusBar, selected=Index}) -> Str = get_row(Pid, Index, all), Pid ! {delete, Index}, wxStatusBar:setStatusText(StatusBar, io_lib:format("Deleted object: ~ts",[Str])), wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), {noreply, State#state{selected=undefined}}; handle_event(#wx{id=?wxID_CLOSE}, State = #state{frame=Frame}) -> wxFrame:destroy(Frame), {stop, normal, State}; handle_event(Help = #wx{id=?wxID_HELP}, State) -> observer ! Help, {noreply, State}; handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}}, State = #state{grid=Grid}) -> try Row0 = list_to_integer(Str), Row1 = max(0, Row0), Row = min(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{grid=Grid, sizer=Sz, search=Search, selected=Index}) -> is_integer(Index) andalso wxListCtrl:setItemState(Grid, Index, 0, ?wxLIST_STATE_FOCUSED), 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, io_lib:format("Not found (regexp): ~ts",[Str])), Pid ! {mark_search_hit, Find#find.start}, wxListCtrl:refreshItem(Grid, Find#find.start), {noreply, State#state{search=Search#search{find=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, io_lib:format("Not found (regexp): ~ts",[Str])), {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 ~tp\n", [?MODULE, ?LINE, Event]), {noreply, State}. handle_sync_event(_Event, _Obj, _State) -> %io:format("~p:~p, handle sync_event ~tp\n", [?MODULE, ?LINE, Event]), ok. handle_call(_Event, _From, State) -> %io:format("~p:~p, handle call (~p) ~tp\n", [?MODULE, ?LINE, From, Event]), {noreply, State}. handle_cast(_Event, State) -> %io:format("~p:~p, handle cast ~tp\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, Min}, State = #state{grid=Grid}) -> wxListCtrl:refreshItem(Grid, Min), %% Avoid assert in wx below if Max is 0 {noreply, State}; handle_info({refresh, Min, Max}, State = #state{grid=Grid}) -> Max > 0 andalso wxListCtrl:refreshItems(Grid, Min, Max), {noreply, State}; handle_info(refresh_interval, State = #state{pid=Pid}) -> Pid ! refresh, {noreply, State}; handle_info({error, Error}, State = #state{frame=Frame}) -> ErrorStr = try io_lib:format("~ts", [Error]), Error catch _:_ -> io_lib:format("~tp", [Error]) end, Dlg = wxMessageDialog:new(Frame, ErrorStr), wxMessageDialog:showModal(Dlg), wxMessageDialog:destroy(Dlg), {noreply, State}; handle_info(_Event, State) -> %% io:format("~p:~p, handle info ~tp\n", [?MODULE, ?LINE, _Event]), {noreply, State}. terminate(_Event, #state{pid=Pid, attrs=Attrs}) -> %% ListItemAttr are not auto deleted #attrs{odd=Odd, even=Even, deleted=D, searched=S, changed_odd=Ch1, changed_even=Ch2, new_odd=New1, new_even=New2 } = Attrs, wxListItemAttr:destroy(Odd), wxListItemAttr:destroy(Even), wxListItemAttr:destroy(D), wxListItemAttr:destroy(Ch1),wxListItemAttr:destroy(Ch2), wxListItemAttr:destroy(New1),wxListItemAttr:destroy(New2), 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, _, _, _} -> wx:null(); {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=array:new(), 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} -> Parent ! {refresh, 0, S0#holder.n-1}, 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, observer_backend, 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 ~tp~n",[What]), Parent ! {refresh, 0, S0#holder.n-1}, table_holder(S0) end. handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) -> S1 = #holder{n=N,columns=NewCols} = handle_new_data_chunk2(Data, S0), Parent ! {no_rows, N}, Parent ! {refresh, 0, N-1}, 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{sort=Opt0, key=Key, table=Old, temp=New}) -> Merged = merge(array:to_list(Old), New, Key), {Opt,Sorted} = sort(Opt0#opt.sort_key, Opt0#opt{sort_key = undefined}, Merged), SortedA = array:from_list(Sorted), S0#holder{sort=Opt, table=SortedA, n=array:size(SortedA), temp=[], pid=undefined}; 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{sort=Opt0, table=Table0}) -> {Opt, Table} = sort(Col, Opt0, array:to_list(Table0)), S#holder{sort=Opt, table=array:from_list(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([Term, -1, true, term], S=#holder{parent=Parent, table=Table}) -> Search = fun(Idx, [Tuple|_]) -> Tuple =:= Term andalso throw(Idx), Tuple end, try array:map(Search, Table) of _ -> Parent ! {self(), false} catch Index -> Parent ! {self(), Index} end, S; search([Str, Row, Dir0, CaseSens], S=#holder{parent=Parent, n=N, table=Table}) -> Opt = case CaseSens of true -> []; false -> [caseless] end, Dir = case Dir0 of true -> 1; false -> -1 end, Res = case re:compile(Str, [unicode|Opt]) of {ok, Re} -> re_search(Row, Dir, N, Re, Table); {error, _} -> false end, Parent ! {self(), Res}, S#holder{search=Res}. re_search(Row, Dir, N, Re, Table) when Row >= 0, Row < N -> [Term|_] = array:get(Row, Table), Str = format(Term), Res = re:run(Str, Re), case Res of nomatch -> re_search(Row+Dir, Dir, N, Re, Table); {match,_} -> Row end; re_search(_, _, _, _, _) -> false. get_row(From, Row, Col, Table) -> case array:get(Row, Table) of [Object|_] when Col =:= all -> From ! {self(), format(Object)}; [Object|_] when Col =:= all_multiline -> From ! {self(), io_lib:format("~tp", [Object])}; [Object|_] when Col =:= term -> From ! {self(), Object}; [Object|_] when tuple_size(Object) >= Col -> From ! {self(), format(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}) -> Odd = (Row rem 2) > 0, What = case array:get(Row, Table) of [_|deleted] -> Attrs#attrs.deleted; [_|changed] when Odd -> Attrs#attrs.changed_odd; [_|changed] -> Attrs#attrs.changed_even; [_|new] when Odd -> Attrs#attrs.new_odd; [_|new] -> Attrs#attrs.new_even; _ when Odd -> 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). -dialyzer({no_improper_lists, merge2/3}). merge2([[Obj|_]|Old], [Obj|New], Key) -> [[Obj]|merge2(Old, New, Key)]; merge2([[A|Op]|Old], [B|New], Key) when element(Key, A) == element(Key, B) -> case Op of deleted -> [[B|new]|merge2(Old, New, Key)]; _ -> [[B|changed]|merge2(Old, New, Key)] end; merge2([[A|Op]|Old], New = [B|_], Key) when element(Key, A) < element(Key, B) -> case Op of deleted -> merge2(Old, New, Key); _ -> [[A|deleted]|merge2(Old, New, Key)] end; 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) -> lists:foldl(fun([_O|deleted], Acc) -> Acc; ([O|_], Acc) -> [[O|deleted]|Acc] end, [], 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|_] = array:get(Row, 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. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% format(Tuple) when is_tuple(Tuple) -> [${ |format_tuple(Tuple, 1, tuple_size(Tuple))]; format(List) when is_list(List) -> format_list(List); format(Bin) when is_binary(Bin), byte_size(Bin) > 100 -> io_lib:format("<<#Bin:~w>>", [byte_size(Bin)]); format(Bin) when is_binary(Bin) -> try true = io_lib:printable_list(unicode:characters_to_list(Bin)), io_lib:format("<<\"~ts\">>", [Bin]) catch _:_ -> io_lib:format("~w", [Bin]) end; format(Float) when is_float(Float) -> io_lib:format("~.3g", [Float]); format(Term) -> io_lib:format("~tw", [Term]). format_tuple(Tuple, I, Max) when I < Max -> [format(element(I, Tuple)), $,|format_tuple(Tuple, I+1, Max)]; format_tuple(Tuple, Max, Max) -> [format(element(Max, Tuple)), $}]; format_tuple(_Tuple, 1, 0) -> [$}]. format_list([]) -> "[]"; format_list(List) -> case io_lib:printable_list(List) of true -> io_lib:format("\"~ts\"", [map_printable_list(List)]); false -> [$[ | make_list(List)] end. make_list([Last]) -> [format(Last), $]]; make_list([Head|Tail]) when is_list(Tail) -> [format(Head), $,|make_list(Tail)]; make_list([Head|Tail]) -> [format(Head), $|, format(Tail), $]]. map_printable_list([$\n|Cs]) -> [$\\, $n|map_printable_list(Cs)]; map_printable_list([$\r|Cs]) -> [$\\, $r|map_printable_list(Cs)]; map_printable_list([$\t|Cs]) -> [$\\, $t|map_printable_list(Cs)]; map_printable_list([$\v|Cs]) -> [$\\, $v|map_printable_list(Cs)]; map_printable_list([$\b|Cs]) -> [$\\, $b|map_printable_list(Cs)]; map_printable_list([$\f|Cs]) -> [$\\, $f|map_printable_list(Cs)]; map_printable_list([$\e|Cs]) -> [$\\, $e|map_printable_list(Cs)]; map_printable_list([]) -> []; map_printable_list([C|Cs]) -> [C|map_printable_list(Cs)].