diff options
Diffstat (limited to 'lib/et/src/et_wx_viewer.erl')
-rw-r--r-- | lib/et/src/et_wx_viewer.erl | 2124 |
1 files changed, 2124 insertions, 0 deletions
diff --git a/lib/et/src/et_wx_viewer.erl b/lib/et/src/et_wx_viewer.erl new file mode 100644 index 0000000000..1e2677e216 --- /dev/null +++ b/lib/et/src/et_wx_viewer.erl @@ -0,0 +1,2124 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2000-2009. 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% +%% +%%---------------------------------------------------------------------- +%% Purpose: Displays a sequence chart for trace events (messages/actions) +%%---------------------------------------------------------------------- + +-module(et_wx_viewer). + +-behaviour(gen_server). + +%% External exports +-export([start_link/1]). + +%% gen_server callbacks +-export([init/1, terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2]). + +-include("../include/et.hrl"). +-include("et_internal.hrl"). +-include_lib("wx/include/wx.hrl"). + +-define(unknown, "UNKNOWN"). +-define(initial_x, 10). +-define(incr_x, 60). +-define(initial_y, 15). +-define(incr_y, 15). +-define(detail_level_min, 0). +-define(detail_level_max, 100). + +-record(state, + {parent_pid, % Pid of parent process + auto_shutdown, % Shutdown collector when last subscriber dies + collector_pid, % Pid of collector process + event_order, % Field to be used as primary key + trace_pattern, % Collector trace pattern + active_filter, % Name of the active filter + filters, % List of possible filters + filter_menu, + pending_actor, % Pending actor - move or toggle + first_event, % Key of first event (regardless of visibility) + last_event, % Key of last event (regardless of visibility) + events_per_page, % Maximum number of shown events + events, % Queue containg all event keys (regardless of visibility) + n_events, % Number of events available in the collector + max_actors, % Maximum number of shown actors + actors, % List of known actors + refresh_needed, % Refresh is needed in order to show all actors + detail_level, % Show only events with lesser detail level + hide_actions, % Hide/show events where to == from actor (bool) + hide_actors, % Hide/show events with unknown actor (bool) + display_all, + context, % display | print + title, % GUI: Window title + frame, % GUI: Window object + menubar, % GUI: Menu bar object + packer, % GUI: Packer object + width, % GUI: Window width + height, % GUI: Window height + scale, % GUI: Scaling factor on canvas + normal_font, % GUI: Font to be used on text labels + bold_font, % GUI: Font to be used on text labels + pen, + brush, + print_psdd, + print_d, + canvas_width, % GUI: Canvas width + canvas_height, % GUI: Canvas height + canvas, % GUI: Canvas object + canvas_sizer, + scroll_bar, % GUI: Canvas scroll bar + y_pos, % GUI: Current y position on canvas + menu_data, + checkbox_data, + hide_actions_box, + hide_actors_box, + status_bar, + event_file, + wx_debug, % GUI: WX debug level + trap_exit}). % trap_exit process flag + + +-record(actor, {name, string, include, exclude}). +-record(e, {pos, key, event}). + +%%%---------------------------------------------------------------------- +%%% Client side +%%%---------------------------------------------------------------------- + +start_link(Options) -> + case parse_opt(Options, default_state(), []) of + {ok, S, CollectorOpt} -> + case S#state.collector_pid of + CollectorPid when is_pid(CollectorPid) -> + case gen_server:start_link(?MODULE, [S], []) of + {ok, Pid} when S#state.parent_pid =/= self() -> + unlink(Pid), + {ok, Pid}; + Other -> + Other + end; + undefined -> + case et_collector:start_link([{auto_shutdown, true} | CollectorOpt]) of + {ok, CollectorPid} -> + S2 = S#state{collector_pid = CollectorPid}, + case gen_server:start_link(?MODULE, [S2], []) of + {ok, Pid} when S#state.parent_pid =/= self() -> + unlink(Pid), + {ok, Pid}; + Other -> + Other + end; + {error, Reason} -> + {error, {et_collector, Reason}} + end + end; + {error, Reason} -> + {error, Reason} + end. + +default_state() -> + #state{parent_pid = self(), + collector_pid = undefined, + n_events = 0, + detail_level = ?detail_level_max, + active_filter = ?DEFAULT_FILTER_NAME, + filters = [?DEFAULT_FILTER], + event_order = trace_ts, + events_per_page = 100, + first_event = first, + last_event = first, + events = queue_new(), + max_actors = 5, + actors = [create_actor(?unknown)], + pending_actor = ?unknown, + hide_actions = false, + hide_actors = false, + display_all = true, + context = display, + refresh_needed = false, + scale = 2, + canvas_height = 0, + canvas_width = 0, + width = 800, + height = 600, + event_file = filename:absname("et_viewer.etrace"), + wx_debug = 0, + trap_exit = true}. + +parse_opt([], S, CollectorOpt) -> + {ok, S, [{parent_pid, S#state.parent_pid} | CollectorOpt]}; +parse_opt([H | T], S, CollectorOpt) -> + case H of + {parent_pid, Parent} when is_pid(Parent); Parent =:= undefined -> + parse_opt(T, S#state{parent_pid = Parent}, CollectorOpt); + {wx_debug, Level} -> + parse_opt(T, S#state{wx_debug = Level}, CollectorOpt); + {trap_exit, Bool} when Bool =:= true; Bool =:= false-> + parse_opt(T, S#state{trap_exit = Bool}, CollectorOpt); + {title, Title} -> + parse_opt(T, S#state{title = name_to_string(Title)}, CollectorOpt); + {detail_level, Level} when is_integer(Level), + Level >= ?detail_level_min, + Level =< ?detail_level_max -> + parse_opt(T, S#state{detail_level = Level}, CollectorOpt); + {detail_level, max} -> + parse_opt(T, S#state{detail_level = ?detail_level_max}, CollectorOpt); + {detail_level, min} -> + parse_opt(T, S#state{detail_level = ?detail_level_min}, CollectorOpt); + {scale, Scale} when is_integer(Scale), Scale > 0 -> + parse_opt(T, S#state{scale = Scale}, CollectorOpt); + {width, W} when is_integer(W), W > 0 -> + parse_opt(T, S#state{width = W, canvas_width = W}, CollectorOpt); + {height, WH} when is_integer(WH), WH > 0 -> + parse_opt(T, S#state{height = WH, canvas_height = WH}, CollectorOpt); + {collector_pid, Pid} when is_pid(Pid) -> + parse_opt(T, S#state{collector_pid = Pid}, CollectorOpt); + {collector_pid, undefined} -> + parse_opt(T, S#state{collector_pid = undefined}, CollectorOpt); + {active_filter, Name} when is_atom(Name) -> + parse_opt(T, S#state{active_filter = Name}, CollectorOpt); + {event_order, trace_ts} -> %% BUGBUG: Verify event_order with collector + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{event_order = trace_ts}, CollectorOpt2); + {event_order, event_ts} -> %% BUGBUG: Verify event_order with collector + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{event_order = event_ts}, CollectorOpt2); + {trace_port, _Port} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_max_queue, _Queue} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_pattern, _Pattern} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_global, _Boolean} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {trace_client, _Client} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {dict_insert, {filter, Name}, Fun} -> + if + is_atom(Name), is_function(Fun) -> + F = #filter{name = Name, function = Fun}, + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{filters = Filters ++ [F]}, CollectorOpt2); + true -> + {error, {bad_option, H}} + end; + {dict_insert, {subscriber, Pid}, _Val} -> + if + is_pid(Pid) -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + true -> + {error, {bad_option, H}} + end; + {dict_insert, _Key, _Val} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {dict_delete, {filter, Name}} -> + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S#state{filters = Filters}, CollectorOpt2); + {dict_delete, _Key} -> + CollectorOpt2 = [H | CollectorOpt], + parse_opt(T, S, CollectorOpt2); + {max_events, _Max} -> + %% Kept for backward compatibility + parse_opt(T, S, CollectorOpt); + {max_actors, Max} when is_integer(Max), Max >= 0 -> + parse_opt(T, S#state{max_actors = Max}, CollectorOpt); + {max_actors, Max} when Max =:= infinity -> + parse_opt(T, S#state{max_actors = Max}, CollectorOpt); + {actors, ActorNames} when is_list(ActorNames) -> + ActorNames2 = + case lists:member(?unknown, ActorNames) of + false -> [?unknown | ActorNames]; + true -> ActorNames + end, + Actors = [create_actor(Name) || Name <- ActorNames2], + parse_opt(T, S#state{actors = Actors}, CollectorOpt); + {include, ActorNames} when is_list(ActorNames) -> + Actors = [opt_create_actor(Name, include, S#state.actors) || Name <- ActorNames], + parse_opt(T, S#state{actors = Actors}, CollectorOpt); + {exclude, ActorNames} when is_list(ActorNames) -> + Actors = [opt_create_actor(Name, exclude, S#state.actors) || Name <- ActorNames], + parse_opt(T, S#state{actors = Actors}, CollectorOpt); + {first_event, _FirstKey} -> + %% NYI + parse_opt(T, S, CollectorOpt); + {hide_actors, Bool} when Bool =:= true; Bool =:= false -> + parse_opt(T, S#state{hide_actors = Bool}, CollectorOpt); + {hide_actions, Bool} when Bool =:= true; Bool =:= false -> + parse_opt(T, S#state{hide_actions = Bool}, CollectorOpt); + {hide_unknown, Bool} when Bool =:= true; Bool =:= false -> + %% Kept for backward compatibility + parse_opt(T, S, CollectorOpt); + {display_mode, _Mode} -> + %% Kept for backward compatibility + parse_opt(T, S, CollectorOpt); + Bad -> + {error, {bad_option, Bad}} + end; +parse_opt(BadList, _S, _CollectorOpt) -> + {error, {bad_option_list, BadList}}. + +do_dict_insert({filter, Name}, Fun, S) when is_atom(Name), is_function(Fun) -> + F = #filter{name = Name, function = Fun}, + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + Filters2 = lists:keysort(#filter.name, [F | Filters]), + S2 = create_filter_menu(S, S#state.active_filter, Filters2), + S2#state{filters = Filters2}; +do_dict_insert(_Key, _Val, S) -> + %%ok = error_logger:format("~p(~p): handle_info({et, {dict_insert, ~p, ~p}})~n", + %% [?MODULE, self(), Key, Val]), + S. + +do_dict_delete({filter, Name}, S) when is_atom(Name), Name =/= S#state.active_filter -> + Filters = lists:keydelete(Name, #filter.name, S#state.filters), + S2 = create_filter_menu(S, S#state.active_filter, Filters), + S2#state{filters = Filters}; +do_dict_delete(_Key, S) -> + %% ok = error_logger:format("~p(~p): handle_info({et, {dict_delete, ~p}})~n", + %% [?MODULE, self(), Key]), + S. + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- + +init([S]) when is_record(S, state) -> + process_flag(trap_exit, S#state.trap_exit), + case S#state.parent_pid of + undefined -> ok; + ParentPid -> link(ParentPid) + end, + wx:new(), + wx:debug(S#state.wx_debug), + et_collector:dict_insert(S#state.collector_pid, + {subscriber, self()}, + ?MODULE), + S2 = create_main_window(S), + EventsPerPage = events_per_page(S2, S2#state.height), + S3 = revert_main_window(S2#state{events_per_page = EventsPerPage}), + Timeout = timeout(S3), + {ok, S3, Timeout}. + +%%---------------------------------------------------------------------- +%% Func: handle_call/3 +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_call(get_collector_pid, _From, S) -> + Reply = S#state.collector_pid, + reply(Reply, S); +handle_call(stop, _From, S) -> + wxFrame:destroy(S#state.frame), + opt_unlink(S#state.parent_pid), + {stop, shutdown, ok, S}; +handle_call({open_event, N}, _From, S) when is_integer(N), N > 0-> + Reply = do_open_event(S, N), + reply(Reply, S); +handle_call(Request, From, S) -> + ok = error_logger:format("~p(~p): handle_call(~p, ~p, ~p)~n", + [?MODULE, self(), Request, From, S]), + Reply = {error, {bad_request, Request}}, + reply(Reply, S). + +%%---------------------------------------------------------------------- +%% Func: handle_cast/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_cast(Msg, S) -> + ok = error_logger:format("~p(~p): handle_cast(~p, ~p)~n", + [?MODULE, self(), Msg, S]), + noreply(S). + + +%%---------------------------------------------------------------------- +%% Func: handle_info/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- + +handle_info({et, {more_events, N}}, S) -> + %% io:format("more events: ~p \n", [N]), + S4 = + if + N =:= S#state.n_events -> + S; + true -> + Missing = S#state.events_per_page - queue_length(S#state.events), + if + Missing =:= 0 -> + update_scroll_bar(S#state{n_events = N}); + Missing > 0 -> + OldEvents = queue_to_list(S#state.events), + {S2, NewEvents} = + collect_more_events(S#state{n_events = N}, + S#state.last_event, + Missing), + S3 = replace_events(S2, OldEvents ++ NewEvents), + refresh_main_window(S3) + end + end, + noreply(S4); +handle_info({et, {insert_actors, ActorNames}}, S) when is_list(ActorNames) -> + Fun = fun(N, Actors) -> + case lists:keymember(N, #actor.name, Actors) of + true -> Actors; + false -> Actors ++ [create_actor(N)] + end + end, + Actors = lists:foldl(Fun, S#state.actors, ActorNames), + S2 = refresh_main_window(S#state{actors = Actors}), + noreply(S2); +handle_info({et, {delete_actors, ActorNames}}, S) when is_list(ActorNames)-> + Fun = fun(N, Actors) when N =:= ?unknown -> + Actors; + (N, Actors) -> + lists:keydelete(N, #actor.name, Actors) + end, + Actors = lists:foldl(Fun, S#state.actors, ActorNames), + S2 = refresh_main_window(S#state{actors = Actors}), + noreply(S2); +handle_info({et, {dict_insert, Key, Val}}, S) -> + S2 = do_dict_insert(Key, Val, S), + noreply(S2); +handle_info({et, {dict_delete, Key}}, S) -> + S2 = do_dict_delete(Key, S), + noreply(S2); +handle_info({et, first}, S) -> + S2 = scroll_first(S), + noreply(S2); +handle_info({et, prev}, S) -> + S2 = scroll_prev(S), + noreply(S2); +handle_info({et, next}, S) -> + S2 = scroll_next(S), + noreply(S2); +handle_info({et, last}, S) -> + S2 = scroll_last(S), + noreply(S2); +handle_info({et, refresh}, S) -> + S2 = revert_main_window(S), + noreply(S2); +handle_info({et, {display_mode, _Mode}}, S) -> + %% Kept for backward compatibility + noreply(S); +handle_info({et, close}, S) -> + wxFrame:destroy(S#state.frame), + opt_unlink(S#state.parent_pid), + {stop, shutdown, S}; +handle_info(#wx{id=?wxID_HELP}, S) -> + HelpString = + "Vertical scroll:\n" + "\tUse mouse wheel and up/down arrows to scroll little.\n" + "\tUse page up/down and home/end buttons to scroll more.\n\n" + "Display details of an event:\n" + "\tLeft mouse click on the event label or the arrow.\n\n" + "Highlight actor (toggle):\n" + "\tLeft mouse click on the actor name tag.\n" + "\tThe actor name will be enclosed in square brackets [].\n\n" + "Exclude actor (toggle):\n" + "\tRight mouse click on the actor name tag.\n" + "\tThe actor name will be enclosed in round brackets ().\n\n" + "Move actor:\n" + "\tLeft mouse button drag and drop on actor name tag.\n\n" + "Display all (reset settings for hidden and/or highlighted actors):\n" + "\tPress the 'a' button.", + Dialog = + wxMessageDialog:new(S#state.frame, HelpString, + [{style, 0 + bor ?wxOK + bor ?wxICON_INFORMATION + bor ?wxSTAY_ON_TOP}, + {caption, "Help"}]), + wxMessageDialog:showModal(Dialog), + noreply(S); +handle_info(#wx{id=Id, event = #wxCommand{type = command_menu_selected}}, S=#state{filter_menu = {_,Data}}) -> + CollectorPid = S#state.collector_pid, + case get_value(Id, 3, S#state.menu_data) of + close -> + wxFrame:destroy(S#state.frame), + opt_unlink(S#state.parent_pid), + {stop, shutdown, S}; + up -> + S2 = scroll_up(S), + noreply(S2); + down -> + S2 = scroll_down(S), + noreply(S2); + first -> + S2 = scroll_first(S), + noreply(S2); + prev -> + S2 = scroll_prev(S), + noreply(S2); + next -> + S2 = scroll_next(S), + noreply(S2); + last -> + S2 = scroll_last(S), + noreply(S2); + refresh -> + S2 = revert_main_window(S), + noreply(S2); + {display_mode, _Mode} -> + %% Kept for backward compatibility + noreply(S); + display_all -> + S2 = display_all(S), + noreply(S2); + close_all -> + close_all(S); + close_all_others -> + close_all_others(S); + first_all -> + et_collector:multicast(CollectorPid, first), + noreply(S); + prev_all -> + et_collector:multicast(CollectorPid, prev), + noreply(S); + next_all -> + et_collector:multicast(CollectorPid, next), + noreply(S); + last_all -> + et_collector:multicast(CollectorPid, last), + noreply(S); + refresh_all -> + et_collector:multicast(CollectorPid, refresh), + noreply(S); + clear_all -> + et_collector:clear_table(CollectorPid), + et_collector:multicast(CollectorPid, refresh), + noreply(S); + load_all -> + Style = ?wxFD_OPEN bor ?wxFD_OVERWRITE_PROMPT, + Msg = "Select a file to load events from", + S2 = + case select_file(S#state.frame, Msg, S#state.event_file, Style) of + {ok, NewFile} -> + et_collector:start_trace_client(CollectorPid, event_file, NewFile), + S#state{event_file = NewFile}; + cancel -> + S + end, + noreply(S2); + save_all -> + Style = ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT, + Msg = "Select a file to save events to", + S2 = + case select_file(S#state.frame, Msg, S#state.event_file, Style) of + {ok, NewFile} -> + et_collector:save_event_file(CollectorPid, NewFile, [existing, write, keep]), + S#state{event_file = NewFile}; + cancel -> + S + end, + noreply(S2); + print_setup -> + S2 = print_setup(S), + noreply(S2); + print_one_page = Scope -> + S2 = print(S, Scope), + noreply(S2); + print_all_pages = Scope -> + S2 = print(S, Scope), + noreply(S2); + {open_viewer, Scale} -> + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + + _ -> + case get_value(Id, 3, Data) of + {data, F=#filter{}, Scale} -> + open_viewer(S#state.scale+Scale, F#filter.name, [?unknown], S); + {data, F=#filter{}} -> + open_viewer(S#state.scale, F#filter.name, [?unknown], S); + false -> + ok + end, + noreply(S) + end; +handle_info(#wx{event = #wxCommand{type = command_slider_updated, commandInt = Level}}, S) -> + if + Level >= ?detail_level_min, + Level =< ?detail_level_max -> + S2 = S#state{detail_level = Level}, + S3 = revert_main_window(S2), + noreply(S3); + + true -> + noreply(S) + end; +handle_info(#wx{id = Id, event = #wxCommand{type = command_checkbox_clicked, commandInt = Int}}, S) -> + case get_value(Id, 2, S#state.checkbox_data) of + hide_actions -> + case Int of + 1 -> + S2 = S#state{hide_actions = true}, + S3 = revert_main_window(S2), + noreply(S3); + 0 -> + S2 = S#state{hide_actions = false}, + S3 = revert_main_window(S2), + noreply(S3) + end; + hide_actors -> + case Int of + 1 -> + S2 = S#state{hide_actors = true}, + S3 = revert_main_window(S2), + noreply(S3); + 0 -> + S2 = S#state{hide_actors = false}, + S3 = revert_main_window(S2), + noreply(S3) + end; + false -> + noreply(S) + end; +handle_info(#wx{event = #wxMouse{type = left_down, x = X, y = Y}}, S) -> + S3 = + case y_to_n(Y, S) of + actor -> + %% Actor click + case S#state.actors of + [] -> + S; + Actors -> + N = x_to_n(X, S), + A = lists:nth(N, Actors), + S#state{pending_actor = A} + end; + {event, N} -> + %% Event click + do_open_event(S, N), + S + end, + noreply(S3); +handle_info(#wx{event = #wxMouse{type = left_up}}, S) when S#state.pending_actor =:= undefined -> + noreply(S); +handle_info(#wx{event = #wxMouse{type = left_up, x = X, y = Y}}, S) -> + S3 = + case y_to_n(Y, S) of + actor -> + %% Actor click + case S#state.actors of + [] -> + S; + Actors -> + N = x_to_n(X, S), + A = lists:nth(N, Actors), + Pending = S#state.pending_actor, + if + A#actor.name =:= Pending#actor.name -> + %% Toggle include actor + A2 = A#actor{include = not A#actor.include}, + %% io:format("include ~p: ~p -> ~p\n", + %% [A#actor.name, A#actor.include, A2#actor.include]), + Actors2 = lists:keyreplace(A#actor.name, #actor.name, Actors, A2), + DisplayAll = not lists:keymember(true, #actor.include, Actors2), + S2 = S#state{actors = Actors2, display_all = DisplayAll}, + revert_main_window(S2); + true -> + move_actor(Pending, A, Actors, S) + end + end; + {event, _N} -> + %% Event click ignored + S + end, + noreply(S3#state{pending_actor = undefined}); +handle_info(#wx{event = #wxMouse{type = right_up, x = X, y = Y}}, S) -> + S3 = + case y_to_n(Y, S) of + actor -> + %% Actor click + case S#state.actors of + [] -> + S; + Actors -> + %% Toggle exclude actor + N = x_to_n(X, S), + A = lists:nth(N, Actors), + A2 = A#actor{exclude = not A#actor.exclude}, + Actors2 = lists:keyreplace(A#actor.name, #actor.name, Actors, A2), + S2 = S#state{actors = Actors2}, + revert_main_window(S2) + end; + {event, _N} -> + %% Event click ignored + S + end, + noreply(S3#state{pending_actor = undefined}); +handle_info(#wx{event = #wxKey{keyCode = KeyCode, shiftDown = SD}}, S) -> + case KeyCode of + $C when SD =:= true -> + close_all(S); + $c -> + close_all_others(S); + ?WXK_HOME -> + S2 = scroll_first(S), + noreply(S2); + ?WXK_END -> + S2 = scroll_last(S), + noreply(S2); + ?WXK_UP -> + S2 = scroll_up(S), + noreply(S2); + ?WXK_DOWN -> + S2 = scroll_down(S), + noreply(S2); + ?WXK_PAGEUP -> + S2 = scroll_prev(S), + noreply(S2); + ?WXK_PAGEDOWN -> + S2 = scroll_next(S), + noreply(S2); + $F when SD =:= true -> + et_collector:multicast(S#state.collector_pid, first), + noreply(S); + $F -> + S2 = scroll_first(S), + noreply(S2); + $P when SD =:= true -> + et_collector:multicast(S#state.collector_pid, prev), + noreply(S); + $P -> + S2 = scroll_prev(S), + noreply(S2); + $N when SD =:= true -> + et_collector:multicast(S#state.collector_pid, next), + noreply(S); + $N -> + S2 = scroll_next(S), + noreply(S2); + $L when SD =:= true -> + et_collector:multicast(S#state.collector_pid, last), + noreply(S); + $L -> + S2 = scroll_last(S), + noreply(S2); + $R when SD =:= true -> + et_collector:multicast(S#state.collector_pid, refresh), + noreply(S); + $R -> + S2 = revert_main_window(S), + noreply(S2); + $A -> + S2 = display_all(S), + noreply(S2); + $= -> + Scale = S#state.scale, + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + Int when Int =:= $+; Int =:= ?WXK_NUMPAD_ADD -> + Scale = S#state.scale + 1, + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale, S#state.active_filter, Actors, S), + noreply(S); + Int when Int =:= $-; Int =:= ?WXK_NUMPAD_SUBTRACT -> + case S#state.scale of + 1 -> + ignore; + Scale -> + Actors = [A#actor.name || A <- S#state.actors], + open_viewer(Scale - 1, S#state.active_filter, Actors, S) + end, + noreply(S); + $0 -> + case lists:keysearch(?DEFAULT_FILTER_NAME, #filter.name, S#state.filters) of + {value, F} when is_record(F, filter) -> + open_viewer(S#state.scale, F#filter.name, [?unknown], S); + false -> + ok + end, + noreply(S); + Int when is_integer(Int), Int > $0, Int =< $9 -> + case catch lists:nth(Int-$0, S#state.filters) of + F when is_record(F, filter) -> + open_viewer(S#state.scale, F#filter.name, [?unknown], S); + {'EXIT', _} -> + ok + end, + noreply(S); + + _ -> + noreply(S) + end; +handle_info(#wx{event = #wxScroll{type = scroll_changed}} = Wx, S) -> + get_latest_scroll(Wx), + Pos = wxScrollBar:getThumbPosition(S#state.scroll_bar), + {_, LineTopY, LineBotY} = calc_y(S), + Range = LineBotY - LineTopY, + N = round(S#state.n_events * Pos / Range), + Diff = + case N - event_pos(S) of + D when D < 0 -> D - 1; + D -> D + 1 + end, + S2 = scroll_changed(S, Diff), + noreply(S2); +handle_info(timeout, S) -> + noreply(S); +handle_info({'EXIT', Pid, Reason}, S) -> + if + Pid =:= S#state.collector_pid -> + io:format("collector died: ~p\n\n", [Reason]), + wxFrame:destroy(S#state.frame), + {stop, Reason, S}; + Pid =:= S#state.parent_pid -> + wxFrame:destroy(S#state.frame), + {stop, Reason, S}; + true -> + noreply(S) + end; +handle_info(#wx{event = #wxClose{}}, S) -> + opt_unlink(S#state.parent_pid), + {stop, shutdown, S}; +handle_info(#wx{event = #wxMouse{type = mousewheel, wheelRotation = Rot}}, S) when Rot > 0 -> + S2 = scroll_up(S), + noreply(S2); +handle_info(#wx{event = #wxMouse{type = mousewheel, wheelRotation = Rot}}, S) when Rot < 0 -> + S2 = scroll_down(S), + noreply(S2); +handle_info(#wx{event = #wxSize{size = {OldW, OldH}}} = Wx, S) -> + #wx{event = #wxSize{type = size, size = {W, H}}} = get_latest_resize(Wx), + S2 = S#state{width = W, height = H, canvas_width = W, canvas_height = H}, + EventsPerPage = events_per_page(S, H), + Diff = EventsPerPage - S#state.events_per_page, + S6 = + if + OldW =:= W, OldH =:= H, S2#state.events_per_page =:= EventsPerPage -> + S2; + Diff =:= 0 -> + refresh_main_window(S2); + Diff > 0 -> + OldEvents = queue_to_list(S2#state.events), + {S3, NewEvents} = collect_more_events(S2, S2#state.last_event, Diff), + S4 = S3#state{events_per_page = EventsPerPage}, + S5 = replace_events(S4, OldEvents ++ NewEvents), + refresh_main_window(S5); + Diff < 0 -> + OldEvents = queue_to_list(S2#state.events), + RevEvents = delete_n(lists:reverse(OldEvents), abs(Diff)), + S3 = S2#state{events_per_page = EventsPerPage}, + S4 = replace_events(S3, lists:reverse(RevEvents)), + refresh_main_window(S4) + end, + noreply(S6); +handle_info(#wx{event = #wxFocus{}}, S) -> + wxWindow:setFocus(S#state.canvas), % Get keyboard focus + noreply(S); +handle_info(#wx{event = #wxMouse{type = enter_window}}, S) -> + wxWindow:setFocus(S#state.canvas), % Get keyboard focus + noreply(S); +handle_info(#wx{event = #wxPaint{}}, S) -> + S2 = refresh_main_window(S), + noreply(S2); +handle_info(#wx{event = #wxMouse{type = T, x=X,y=Y}}, S) -> + io:format("~p ~p\n", [T, {X,Y}]), + noreply(S); +handle_info(Info, S) -> + ok = error_logger:format("~p(~p): handle_info(~p, ~p)~n", + [?MODULE, self(), Info, S]), + noreply(S). + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%%---------------------------------------------------------------------- + +terminate(_Reason, _S) -> + ignore. + +%%---------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%%---------------------------------------------------------------------- + +code_change(_OldVsn, S, _Extra) -> + {ok, S}. + +%%%---------------------------------------------------------------------- +%%% Handle stuff +%%%---------------------------------------------------------------------- + +reply(Reply, S) -> + Timeout = timeout(S), + {reply, Reply, S, Timeout}. + +noreply(S) -> + Timeout = timeout(S), + {noreply, S, Timeout}. + +timeout(_S) -> + infinity. + +scroll_first(S) -> + EventsPerPage = S#state.events_per_page, + {S2, NewEvents} = + collect_more_events(S, first, EventsPerPage), + S3 = + case NewEvents of + [] -> + S2; + [FirstE | _] -> + S2#state{first_event = FirstE} + end, + S4 = replace_events(S3, NewEvents), + refresh_main_window(S4). + +scroll_last(S) -> + case collect_more_events(S, last, -1) of + {_, []} -> + scroll_first(S); + {S2, NewEvents} -> + [FirstE | _] = NewEvents, + S3 = replace_events(S2#state{first_event = FirstE}, NewEvents), + refresh_main_window(S3) + end. + +scroll_prev(S) -> + scroll_up(S, S#state.events_per_page). + +scroll_next(S) -> + scroll_down(S, S#state.events_per_page). + +scroll_up(S) -> + scroll_up(S, calc_scroll(S)). + +scroll_up(S, Expected) -> + N = queue_length(S#state.events), + EventsPerPage = S#state.events_per_page, + Expected2 = adjust_expected(Expected, N, EventsPerPage), + OldEvents = queue_to_list(S#state.events), + case collect_more_events(S, S#state.first_event, -Expected2) of + {_, []} -> + S; + {S2, NewEvents} -> + NewN = length(NewEvents), + if + N + NewN > EventsPerPage -> + RevAllEvents = lists:reverse(OldEvents, lists:reverse(NewEvents)), + TooMany = N + NewN - EventsPerPage, + case delete_n(RevAllEvents, TooMany) of + [] -> + S; + [LastE | _] = RevEvents -> + Events = lists:reverse(RevEvents), + S3 = replace_events(S2#state{last_event = LastE}, Events), + refresh_main_window(S3) + end; + true -> + Events = NewEvents ++ OldEvents, + LastE = lists:last(Events), + S3 = replace_events(S2#state{last_event = LastE}, Events), + refresh_main_window(S3) + end + end. + +scroll_down(S) -> + scroll_down(S, calc_scroll(S)). + +scroll_down(S, Expected) -> + N = queue_length(S#state.events), + EventsPerPage = S#state.events_per_page, + Expected2 = adjust_expected(Expected, N, EventsPerPage), + OldEvents = queue_to_list(S#state.events), + case collect_more_events(S, S#state.last_event, Expected2) of + {_, []} -> + case collect_more_events(S, S#state.first_event, N - EventsPerPage) of + {_, []} -> + S; + {S2, NewEvents} -> + Events = NewEvents ++ OldEvents, + [FirstE | _] = Events, + S3 = replace_events(S2#state{first_event = FirstE}, Events), + refresh_main_window(S3) + end; + {S2, NewEvents} -> + AllEvents = OldEvents ++ NewEvents, + case delete_n(AllEvents, length(NewEvents)) of + [] -> + scroll_first(S); + Events -> + [FirstE | _] = Events, + S3 = replace_events(S2#state{first_event = FirstE}, Events), + refresh_main_window(S3) + end + end. + +scroll_changed(S, Expected) -> + if + Expected =:= 0 -> + refresh_main_window(S); + Expected < 0 -> + %% Up + OldPos = event_pos(S), + NewPos = lists:max([OldPos + Expected, 0]), + case S#state.first_event of + #e{key = Key, pos = OldPos} -> + jump_up(S, Key, OldPos, NewPos); + first -> + scroll_first(S); + last -> + scroll_last(S) + end; + true -> + %% Down + OldPos = event_pos(S), + NewPos = lists:min([OldPos + Expected, S#state.n_events]), + case S#state.first_event of + #e{key = Key, pos = OldPos} -> + jump_down(S, Key, OldPos, NewPos); + first = Key -> + jump_down(S, Key, 0, NewPos); + last -> + scroll_last(S) + end + end. + +jump_up(S, OldKey, OldPos, NewPos) -> + Try = NewPos - OldPos, + Order = S#state.event_order, + Fun = fun(Event, #e{pos = P}) when P >= NewPos -> + Key = et_collector:make_key(Order, Event), + #e{event = Event, key = Key, pos = P - 1}; + (_, Acc) -> + Acc + end, + PrevE = et_collector:iterate(S#state.collector_pid, + OldKey, + Try, + Fun, + #e{key = OldKey, pos = OldPos}), + case collect_more_events(S, PrevE, S#state.events_per_page) of + {_, []} -> + S; + {S2, Events} -> + [FirstE | _] = Events, + S3 = replace_events(S2#state{first_event = FirstE}, Events), + refresh_main_window(S3) + end. + +jump_down(S, OldKey, OldPos, NewPos) -> + Try = NewPos - OldPos, + Order = S#state.event_order, + Fun = fun(Event, #e{pos = P}) when P < NewPos -> + Key = et_collector:make_key(Order, Event), + #e{event = Event, key = Key, pos = P + 1}; + (_, Acc) -> + Acc + end, + PrevE = et_collector:iterate(S#state.collector_pid, + OldKey, + Try, + Fun, + #e{key = OldKey, pos = OldPos}), + case collect_more_events(S, PrevE, S#state.events_per_page) of + {_, []} -> + S; + {S2, Events} -> + [FirstE | _] = Events, + S3 = replace_events(S2#state{first_event = FirstE}, Events), + refresh_main_window(S3) + end. + +adjust_expected(Expected, N, EventsPerPage) -> + if + N < EventsPerPage -> + EventsPerPage - N; + Expected < EventsPerPage -> + Expected; + true -> + EventsPerPage + end. + +calc_scroll(S) -> + lists:max([S#state.events_per_page div 3, 1]). + +revert_main_window(S) -> + {S2, Events} = revert(S), + S3 = replace_events(S2, Events), + refresh_main_window(S3). + +revert(S) -> + EventsPerPage = S#state.events_per_page, + %% Find previous event + case collect_more_events(S, S#state.first_event, -1) of + {_, []} -> + collect_more_events(S, first, EventsPerPage); + {S2, [_PrevEvent]} -> + collect_more_events(S, S2#state.first_event, EventsPerPage) + end. + +delete_n(List, 0) -> + List; +delete_n([], _) -> + []; +delete_n([_ | Tail], N) when N > 0 -> + delete_n(Tail, N - 1). + +pick_n(Rest, 0, Acc) -> + {lists:reverse(Acc), Rest}; +pick_n([], _N, Acc) -> + {lists:reverse(Acc), []}; +pick_n([Head | Tail], N, Acc) when N > 0 -> + pick_n(Tail, N - 1, [Head | Acc]). + +close_all(S) -> + close_all_others(S), + wxFrame:destroy(S#state.frame), + opt_unlink(S#state.parent_pid), + {stop, shutdown, S}. + +close_all_others(S) -> + Fun = + fun({{subscriber, Pid}, _}) -> + if + Pid =:= self() -> + ignore; + true -> + unlink(Pid), + Pid ! {et, close} + end + end, + All = et_collector:dict_match(S#state.collector_pid, + {{subscriber, '_'}, '_'}), + lists:foreach(Fun, All), + noreply(S). + +opt_unlink(Pid) -> + if + Pid =:= undefined -> + ignore; + true -> + unlink(Pid) + end. + +%%%---------------------------------------------------------------------- +%%% Clone viewer +%%%---------------------------------------------------------------------- + +open_viewer(Scale, FilterName, Actors, S) -> + Filters = [{dict_insert, {filter, F#filter.name}, F#filter.function} + || F <- S#state.filters], + Options = + [{parent_pid, S#state.parent_pid}, + {title, S#state.title}, + {collector_pid, S#state.collector_pid}, + {detail_level, S#state.detail_level}, + {active_filter, FilterName}, + {event_order, S#state.event_order}, + {first_event, S#state.first_event}, + {max_actors, S#state.max_actors}, + {hide_actions, S#state.hide_actions}, + {hide_actors, S#state.hide_actors}, + {actors, Actors}, + {scale, Scale}, + {width, S#state.width}, + {height, S#state.height} | Filters], + case start_link(Options) of + {ok, _ViewerPid} -> + %% unlink(ViewerPid), + ok; + {error, Reason} -> + ok = error_logger:format("~p: Failed to start a new window: ~p~n", + [?MODULE, Reason]) + end. + +%%%---------------------------------------------------------------------- +%%% Handle graphics +%%%---------------------------------------------------------------------- + +create_main_window(S) -> + {NormalFont, BoldFont} = select_fonts(S#state.scale), + Name = name_to_string(S#state.active_filter), + Title = case S#state.title of + undefined -> atom_to_list(?MODULE); + Explicit -> name_to_string(Explicit) + end, + Frame = wxFrame:new(wx:null(), + ?wxID_ANY, + Title ++ " (filter: " ++ Name ++ ")", + [{size, {S#state.width, S#state.height}}]), + StatusBar = wxFrame:createStatusBar(Frame), + + Panel = wxPanel:new(Frame, []), + Bar = wxMenuBar:new(), + wxFrame:setMenuBar(Frame,Bar), + MainSizer = wxBoxSizer:new(?wxVERTICAL), + + MenuData = lists:flatten([create_file_menu(Bar), + create_viewer_menu(Bar), + create_collector_menu(Bar)]), + FilterMenu = wxMenu:new([]), + S2 = create_filter_menu(S#state{filter_menu = {FilterMenu,[]}}, + S#state.active_filter, + S#state.filters), + wxMenuBar:append(Bar, FilterMenu, "Filters and scaling"), + create_help_menu(Bar), + + OptSizer = wxBoxSizer:new(?wxHORIZONTAL), + CheckSizer = wxBoxSizer:new(?wxVERTICAL), + HideActions = wxCheckBox:new(Panel, ?wxID_ANY, "Hide From=To"), + wxCheckBox:setValue(HideActions, S#state.hide_actions), + HideActors = wxCheckBox:new(Panel, ?wxID_ANY, "Hide (excluded actors)"), + wxCheckBox:setValue(HideActors, S#state.hide_actors), + CheckBoxData = [{wxCheckBox:getId(HideActions), hide_actions}, + {wxCheckBox:getId(HideActors), hide_actors}], + wxPanel:connect(Panel, command_checkbox_clicked), + wxSizer:add(CheckSizer, HideActions), + wxSizer:add(CheckSizer,HideActors), + wxSizer:add(OptSizer, CheckSizer, [{border, 10}, {flag, ?wxALL}]), + DetailLevelBox = wxStaticBoxSizer:new(?wxHORIZONTAL, + Panel, + [{label, "Detail level"}]), + DetailLevel = wxSlider:new(Panel, ?wxID_ANY, + S#state.detail_level, + ?detail_level_min, + ?detail_level_max, + [{style, ?wxSL_LABELS}, + {size, {200,-1}}]), + wxStatusBar:setStatusText(StatusBar, where_text(S)), + wxFrame:connect(Frame, command_slider_updated), + wxSizer:add(DetailLevelBox, DetailLevel), + wxSizer:add(OptSizer, DetailLevelBox, [{border, 10}, {flag, ?wxALL}]), + wxSizer:addStretchSpacer(OptSizer), + wxSizer:add(MainSizer, OptSizer), + wxSizer:add(MainSizer, + wxStaticLine:new(Panel, [{style, ?wxLI_HORIZONTAL}]), + [{flag, ?wxEXPAND}]), + + CanvasSizer = wxBoxSizer:new(?wxHORIZONTAL), + Canvas = wxPanel:new(Panel, []), + {CanvasW,CanvasH} = wxPanel:getSize(Canvas), + ScrollBar = wxScrollBar:new(Panel, ?wxID_ANY, [{style, ?wxSB_VERTICAL}]), + + wxSizer:add(CanvasSizer, Canvas, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxSizer:add(CanvasSizer, ScrollBar, [{flag, ?wxEXPAND}]), + wxSizer:add(MainSizer, CanvasSizer, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:connect(Canvas, left_down), + wxPanel:connect(Canvas, left_up), + wxPanel:connect(Canvas, right_up), + wxPanel:connect(Canvas, size), + wxPanel:connect(Canvas, paint), + wxPanel:connect(Canvas, key_down), + wxPanel:connect(Canvas, kill_focus), + wxPanel:connect(Canvas, enter_window, [{skip, true}]), + wxFrame:connect(Frame, command_menu_selected), + wxFrame:connect(Frame, close_window), + wxFrame:connect(ScrollBar, scroll_changed), + wxPanel:setSize(Panel, {S#state.width, S#state.height}), + wxPanel:setSizer(Panel, MainSizer), + wxFrame:show(Frame), + wxPanel:setFocus(Canvas), + wxPanel:connect(Canvas, mousewheel), + + S3 = S2#state{title = Title, + frame = Frame, packer = Panel, + normal_font = NormalFont, bold_font = BoldFont, + canvas_width = CanvasW, canvas_height = CanvasH, + canvas = Canvas, + canvas_sizer = CanvasSizer, + scroll_bar = ScrollBar, + y_pos = ?initial_y * S#state.scale, + pen = wxPen:new(), + brush = wxBrush:new(), + print_d = undefined, + print_psdd = undefined, + menu_data = MenuData, + checkbox_data = CheckBoxData, + hide_actions_box = HideActions, + hide_actors_box = HideActors, + status_bar = StatusBar}, + DC = wxClientDC:new(Canvas), + S4 = draw_all_actors(S3, DC), + wxClientDC:destroy(DC), + S4. + +where_text(#state{n_events = N} = S) -> + Pos = event_pos(S), + lists:concat([Pos, " (", N, ")"]). + +event_pos(#state{first_event = E, events = Events, n_events = Last}) -> + case E of + #e{pos = Pos} -> + Pos; + first -> + case queue_length(Events) of + 0 -> + 0; + _ -> + 1 + end; + last -> + Last + end. + +init_printers(#state{print_d = undefined, print_psdd = undefined} = S) -> + PD = wxPrintData:new(), + PSDD = wxPageSetupDialogData:new(PD), + wxPrintData:setPaperId(PD, ?wxPAPER_A4), + wxPageSetupDialogData:setMarginTopLeft(PSDD, {15,15}), + wxPageSetupDialogData:setMarginBottomRight(PSDD, {15,15}), + S#state{print_d = PD, print_psdd = PSDD}; +init_printers(#state{} = S) -> + S. + +select_fonts(Scale) when is_integer(Scale) -> + Size = + case Scale of + 1 -> 5; + 2 -> 10; + 3 -> 14; + 4 -> 20; + S -> S*6 + end, + {wxFont:new(Size, ?wxFONTFAMILY_TELETYPE, ?wxNORMAL, ?wxNORMAL,[]), + wxFont:new(Size, ?wxFONTFAMILY_TELETYPE, ?wxNORMAL, ?wxBOLD,[])}. + +get_value(Key, Pos, TupleList) when is_list(TupleList)-> + case lists:keysearch(Key, 1, TupleList) of + {value, Tuple} when is_tuple(Tuple)-> + element(Pos, Tuple); + false -> + false + end. + +menuitem(Menu, Id, Text, UserData) -> + Item = wxMenu:append(Menu, Id, Text), + {wxMenuItem:getId(Item), Item, UserData}. + +create_file_menu(Bar) -> + Menu = wxMenu:new([]), + Data = [ + menuitem(Menu, ?wxID_ANY, "Clear all events in the Collector", clear_all), + menuitem(Menu, ?wxID_ANY, "Load events to the Collector from file", load_all), + menuitem(Menu, ?wxID_ANY, "Save all events in the Collector to file", save_all), + + menuitem(Menu, ?wxID_PRINT_SETUP, "Print setup", print_setup), + menuitem(Menu, ?wxID_ANY, "Print current page", print_one_page), + menuitem(Menu, ?wxID_PRINT, "Print all pages", print_all_pages), + + menuitem(Menu, ?wxID_ANY, "Close this Viewer", close), + menuitem(Menu, ?wxID_ANY, "Close all other Viewers, but this (c)", close_all_others), + menuitem(Menu, ?wxID_ANY, "Close all Viewers and the Collector) (C) ", close_all) + ], + wxMenu:insertSeparator(Menu, 3), + wxMenu:insertSeparator(Menu, 7), + wxMenuBar:append(Bar, Menu, "File"), + Data. + +create_viewer_menu(Bar) -> + Menu = wxMenu:new([]), + wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Scroll this Viewer"), + [{enable, false}]), + wxMenu:appendSeparator(Menu), + D1 = [ + menuitem(Menu, ?wxID_ANY, "First (f)", first), + menuitem(Menu, ?wxID_ANY, "Last (l)", last), + menuitem(Menu, ?wxID_ANY, "Prev (p)", prev), + menuitem(Menu, ?wxID_ANY, "Next (n)", next), + menuitem(Menu, ?wxID_ANY, "Refresh (r)", refresh) + ], + wxMenu:appendSeparator(Menu), + D2 = [ + menuitem(Menu, ?wxID_ANY, "Up 5 (Up)", up), + menuitem(Menu, ?wxID_ANY, "Down 5 (Down)", down) + ], + wxMenu:appendSeparator(Menu), + wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Actor visibility in this Viewer"), + [{enable, false}]), + wxMenu:appendSeparator(Menu), + D3 = [menuitem(Menu, ?wxID_ANY, "Display all actors (a)", display_all)], + wxMenuBar:append(Bar, Menu, "Viewer"), + [D1,D2,D3]. + +create_collector_menu(Bar) -> + Menu = wxMenu:new([]), + wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Scroll all Viewers"), + [{enable, false}]), + wxMenu:appendSeparator(Menu), + Data = [ + menuitem(Menu, ?wxID_ANY, "First (F)", first_all), + menuitem(Menu, ?wxID_ANY, "Last (L)", last_all), + menuitem(Menu, ?wxID_ANY, "Prev (P)", prev_all), + menuitem(Menu, ?wxID_ANY, "Next (N)", next_all), + menuitem(Menu, ?wxID_ANY, "Refresh (R)", refresh_all) + ], + wxMenuBar:append(Bar, Menu, "Collector"), + Data. + +create_filter_menu(S=#state{filter_menu = {Menu,Data}}, ActiveFilterName, Filters) -> + wx:foreach(fun({_,I,_}) -> + wxMenu:delete(Menu,I); + (I) -> + try + wxMenu:delete(Menu,I) + catch + _:Reason -> + io:format("Could not delete item: ~p, because ~p.\n", [I, Reason]) + end + end, + Data), + Item = fun(F, {N, Acc}) when F#filter.name =:= all -> + Label = lists:concat([pad_string(F#filter.name, 20), "(0)"]), + {N+1, [menuitem(Menu, ?wxID_ANY, Label, {data, F})|Acc]}; + (F, {N, Acc}) -> + Label = lists:concat([pad_string(F#filter.name, 20), "(", N, ")"]), + {N+1, [menuitem(Menu, ?wxID_ANY, Label, {data, F})|Acc]} + end, + D1 = [I1 = wxMenu:append(Menu, ?wxID_ANY, "Same Filter New Scale"), + wxMenu:appendSeparator(Menu)], + wxMenuItem:enable(I1, [{enable,false}]), + {value, Filter} = lists:keysearch(ActiveFilterName, #filter.name, Filters), + Same = lists:concat([pad_string(ActiveFilterName, 20), "(=) same scale"]), + Larger = lists:concat([pad_string(ActiveFilterName, 20), "(+) bigger scale"]), + Smaller = lists:concat([pad_string(ActiveFilterName, 20), "(-) smaller scale"]), + D2 = [ + menuitem(Menu, ?wxID_ANY, Same, {data, Filter, 0}), + menuitem(Menu, ?wxID_ANY, Smaller, {data, Filter, -1}), + menuitem(Menu, ?wxID_ANY, Larger, {data, Filter, 1}), + wxMenu:appendSeparator(Menu), + I2 = wxMenu:append(Menu, ?wxID_ANY, "New Filter Same Scale"), + wxMenu:appendSeparator(Menu) + ], + wxMenuItem:enable(I2, [{enable,false}]), + {_,D3} = lists:foldl(Item, {1,[]}, Filters), + S#state{filter_menu = {Menu, lists:flatten([D1,D2,D3])}}. + +create_help_menu(Bar) -> + Menu = wxMenu:new([]), + menuitem(Menu, ?wxID_HELP, "Info", help), + wxMenuBar:append(Bar, Menu, "Help"). + +clear_canvas(S) -> + DC = wxClientDC:new(S#state.canvas), + wxDC:clear(DC), + {CanvasW, CanvasH} = wxPanel:getSize(S#state.canvas), + wxSizer:recalcSizes(S#state.canvas_sizer), + S2 = S#state{refresh_needed = false, + y_pos = ?initial_y * S#state.scale, + canvas_width = CanvasW, + canvas_height = CanvasH, + events = queue_new()}, + S3 = draw_all_actors(S2, DC), + wxClientDC:destroy(DC), + S3. + +replace_events(S, []) -> + S#state{first_event = first, + last_event = first, + events = queue_new()}; +replace_events(S, Events) -> + Queue = lists:foldl(fun(E, Q) -> queue_in(E, Q) end, queue_new(), Events), + S#state{events = Queue}. + +refresh_main_window(S) -> + wx:batch(fun() -> + S2 = clear_canvas(S), + S3 = update_scroll_bar(S2), + display_events(S3, queue_to_list(S#state.events)) + end). + +display_events(S, []) -> + S; +display_events(S, Events) -> + DC = wxClientDC:new(S#state.canvas), + S2 = lists:foldl(fun(E, State) -> display_event(E, State, DC) end, S, Events), + wxClientDC:destroy(DC), + S2. + +collect_more_events(S, PrevKey = first, Try) -> + PrevE = #e{event = undefined, key = PrevKey, pos = 0}, + S2 = S#state{first_event = PrevE, last_event = PrevE}, + do_collect_more_events(S2, Try, PrevE, []); +collect_more_events(S, PrevKey = last, Try) -> + PrevE = #e{event = undefined, key = PrevKey, pos = S#state.n_events}, + S2 = S#state{first_event = PrevE, last_event = PrevE}, + do_collect_more_events(S2, Try, PrevE, []); +collect_more_events(S, #e{} = PrevE, Try) -> + do_collect_more_events(S, Try, PrevE, []). + +do_collect_more_events(#state{collector_pid = Collector, + event_order = Order, + active_filter = Active, + filters = Filters} = S, + Try, + PrevE, + Acc) -> + Incr = + if + Try < 0 -> -1; + true -> 1 + end, + PrevKey = PrevE#e.key, + {value, #filter{function = FilterFun}} = + lists:keysearch(Active, #filter.name, Filters), + {_S, _Incr, _Order, _Active, _FilterFun, LastE, NewEvents} = + et_collector:iterate(Collector, + PrevKey, + Try, + fun collect_event/2, + {S, Incr, Order, Active, FilterFun, PrevE, []}), + Expected = abs(Try), + Actual = length(NewEvents), + Missing = Expected - Actual, + {S2, Acc2, Try2} = + if + Try < 0 -> + {S#state{first_event = LastE}, NewEvents ++ Acc, -Missing}; + true -> + TmpEvents = lists:reverse(NewEvents), + {S#state{last_event = LastE}, Acc ++ TmpEvents, Missing} + end, + if + Missing =/= 0, PrevKey =/= LastE#e.key -> + do_collect_more_events(S2, Try2, LastE, Acc2); + true -> + {S2, Acc2} + end. + +collect_event(Event, {S, Incr, Order, Active, FilterFun, #e{pos = PrevPos}, Events}) -> + Key = et_collector:make_key(Order, Event), + E = #e{event = Event, key = Key, pos = PrevPos + Incr}, + {LastE, Events2} = + case catch FilterFun(Event) of + true -> + case is_hidden(Event#event.from, Event#event.to, S) of + true -> + {E, Events}; + false -> + {E, [E | Events]} + end; + {true, Event2} -> + Key2 = et_collector:make_key(Order, Event2), + E2 = E#e{event = Event2, key = Key2}, + case is_hidden(Event2#event.from, Event2#event.to, S) of + true -> + {E2, Events}; + false -> + {E2, [E2 | Events]} + end; + false -> + {E, Events}; + Bad -> + Contents = {bad_filter, S#state.active_filter, Bad, Event}, + Event2 = Event#event{contents = Contents, + from = bad_filter, + to = bad_filter}, + E2 = E#e{event = Event2}, + {E2, [E2 | Events]} + end, + {S, Incr, Order, Active, FilterFun, LastE, Events2}. + +display_event(#e{event = Event} = E, S, DC) + when Event#event.detail_level < S#state.detail_level -> + {FromRefresh, From} = ensure_actor(Event#event.from, S, DC), + {FromName, FromPos, S2} = From, + {ToRefresh, To} = ensure_actor(Event#event.to, S2, DC), + {ToName, ToPos, S3} = To, + S4 = + if + FromRefresh =/= false, ToRefresh =/= false -> + S3#state{refresh_needed = true, + events = queue_in(E, S3#state.events)}; + FromName =:= ToName -> + case S#state.hide_actions of + true -> + S3; + false -> + Label = name_to_string(Event#event.label), + draw_named_arrow(Label, FromName, ToName, FromPos, ToPos, E, S3, DC) + end; + true -> + Label = name_to_string(Event#event.label), + draw_named_arrow(Label, FromName, ToName, FromPos, ToPos, E, S3, DC) + end, + S4; +display_event(#e{}, S, _DC) -> + S. + +draw_named_arrow(Label, FromName, ToName, FromPos, ToPos, E, S, DC) -> + case S#state.y_pos + (?incr_y * S#state.scale) of + _ when S#state.hide_actors =:= true, FromName =:= ?unknown -> + S; + _ when S#state.hide_actors =:= true, ToName =:= ?unknown -> + S; + Y when Y > S#state.canvas_height -> + S#state{refresh_needed = true, + events = queue_in(E, S#state.events)}; + Y -> + S2 = S#state{y_pos = Y, events = queue_in(E, S#state.events)}, + S3 = draw_arrow(FromPos, ToPos, S2, DC), + draw_label(Label, FromName, ToName, FromPos, ToPos, S3, DC) + end. + +draw_arrow(Pos, Pos, S, _DC) -> + S; +draw_arrow(FromPos, ToPos, S, DC) -> + Y = S#state.y_pos, + wxPen:setColour(S#state.pen, ?wxBLACK), + wxDC:setPen(DC, S#state.pen), + wxDC:drawLine(DC, {FromPos , Y}, {ToPos, Y}), + + %% Draw arrow head + Radians = calc_angle({FromPos, Y}, {ToPos, Y}), + Len = 5, + Radians2 = Radians + 3.665191429188092, + Radians3 = Radians + 2.617993877991494, + {X3, Y3} = calc_point({ToPos, Y}, Len, Radians2), + {X4, Y4} = calc_point({ToPos, Y}, Len, Radians3), + Points = [{round(ToPos), round(Y)}, + {round(X3), round(Y3)}, + {round(X4), round(Y4)}], + wxBrush:setColour(S#state.brush, ?wxBLACK), + wxDC:setBrush(DC, S#state.brush), + wxDC:drawPolygon(DC, Points, []), + S. + + %% Calclulate angle in radians for a line between two points +calc_angle({X1, Y1}, {X2, Y2}) -> + math:atan2((Y2 - Y1), (X2 - X1)). + + %% Calc new point at a given distance and angle from another point +calc_point({X, Y}, Length, Radians) -> + X2 = round(X + Length * math:cos(Radians)), + Y2 = round(Y + Length * math:sin(Radians)), + {X2, Y2}. + +draw_label(Label, FromName, ToName, FromPos, ToPos, S, DC) -> + Color = + if + FromName =:= ?unknown, + ToName =:= ?unknown -> {2, 71, 254};% blue + FromName =:= ?unknown -> {255,126,0}; % orange + ToName =:= ?unknown -> {255,126,0}; % orange + FromPos =:= ToPos -> {2, 71, 254};% blue + true -> {227,38, 54} % red + end, + Scale = S#state.scale, + X = lists:min([FromPos, ToPos]) + (6 * Scale), + Y = S#state.y_pos, + write_text(Label, X, Y, Color, S#state.normal_font, S, DC), + S. + +draw_all_actors(S, DC) -> + Scale = S#state.scale, + Fun = fun(A, X) -> + case draw_actor(A, X, S, DC) of + true -> + X + (?incr_x * Scale); + false -> + X + end + end, + lists:foldl(Fun, ?initial_x * Scale, S#state.actors), + S. + +%% Returns: {NeedsRefreshBool, {ActorPos, NewsS, NewActors}} +ensure_actor(Name, S, DC) -> + do_ensure_actor(Name, S, S#state.actors, 0, DC). + +do_ensure_actor(Name, S, [H | _], N, _DC) when H#actor.name =:= Name -> + Pos = (?initial_x + (N * ?incr_x)) * S#state.scale, + {false, {Name, Pos, S}}; +do_ensure_actor(Name, S, [H | T], N, DC) -> + if + S#state.hide_actors, H#actor.exclude -> + do_ensure_actor(Name, S, T, N, DC); + true -> + do_ensure_actor(Name, S, T, N + 1, DC) + end; +do_ensure_actor(Name, S, [], N, DC) -> + %% A brand new actor, let's see if it does fit + Pos = (?initial_x + (N * ?incr_x)) * S#state.scale, + MaxActors = S#state.max_actors, + if + is_integer(MaxActors), N > MaxActors -> + %% Failed on max_actors limit, put into unknown + %% Assume that unknown always is in actor list + ensure_actor(?unknown, S, DC); + Pos > (S#state.canvas_width - ((?initial_x - 15) * S#state.scale)) -> + %% New actor does not fit in canvas, refresh needed + A = create_actor(Name), + draw_actor(A, Pos, S, DC), + {true, {Name, Pos, S#state{actors = S#state.actors ++ [A]}}}; + true -> + %% New actor fits in canvas. Draw the new actor. + A = create_actor(Name), + draw_actor(A, Pos, S, DC), + {false, {Name, Pos, S#state{actors = S#state.actors ++ [A]}}} + end. + +draw_actor(A, LineX, S, DC) -> + if + S#state.hide_actors, A#actor.exclude -> + false; + true -> + Scale = S#state.scale, + TextX = LineX - (5 * Scale), + {TextY, LineTopY, LineBotY} = calc_y(S), + Color = + case A#actor.name of + ?unknown -> {255,126,0};% orange + _ -> {227,38,54} % red + end, + {String, Font} = + if + S#state.context =:= display, A#actor.exclude -> + {"(" ++ A#actor.string ++ ")", S#state.normal_font}; + S#state.context =:= display, A#actor.include -> + {"[" ++ A#actor.string ++ "]", S#state.bold_font}; + true -> + {A#actor.string, S#state.normal_font} + end, + write_text(String, TextX, TextY, Color, Font, S, DC), + wxPen:setColour(S#state.pen, Color), + wxDC:setPen(DC, S#state.pen), + wxDC:drawLines(DC, [{LineX, LineTopY}, {LineX, LineBotY}]), + true + end. + +calc_y(#state{canvas_height = Height, scale = Scale}) -> + TextY = ?initial_y * Scale, + LineTopY = round(TextY + ((?incr_y / 2) * Scale)), + LineBotY = Height, + %% LineBotY = round(Height - ((?incr_y / 2) * Scale)), + {TextY, LineTopY, LineBotY}. + +display_all(S) -> + Actors = S#state.actors, + Actors2 = [A#actor{include = false, exclude = false} || A <- Actors], + S2 = S#state{actors = Actors2, + display_all = true, + hide_actions = false, + hide_actors = false}, + wxCheckBox:setValue(S2#state.hide_actions_box, S2#state.hide_actions), + wxCheckBox:setValue(S2#state.hide_actors_box, S2#state.hide_actors), + revert_main_window(S2). + +is_hidden(A, S) -> + case S#state.display_all of + true -> + A#actor.exclude; + false -> + A#actor.exclude orelse not A#actor.include + end. + +is_hidden(From, To, S) -> + Actors = S#state.actors, + DisplayAll = S#state.display_all, + FromMatch = lists:keysearch(From, #actor.name, Actors), + ToMatch = lists:keysearch(To, #actor.name, Actors), + case {FromMatch, ToMatch} of + {false, false} -> + not DisplayAll; + {false, {value, T}} -> + is_hidden(T, S); + {{value, F}, false} -> + is_hidden(F, S); + {{value, F}, {value, T}} when DisplayAll -> + is_hidden(F, S) orelse is_hidden(T, S); + {{value, F}, {value, T}} when F#actor.include; T#actor.include -> + F#actor.exclude orelse T#actor.exclude; + {{value, _F}, {value, _T}}-> + true + end. + +move_actor(From, To, Actors, S) -> + Pos = #actor.name, + ToName = To#actor.name, + FromName = From#actor.name, + ToIx = actor_index(ToName, Pos, Actors), + FromIx = actor_index(FromName, Pos, Actors), + if + FromIx =/= 0, ToIx =/= 0, ToIx > FromIx -> + Actors2 = lists:keydelete(FromName, Pos, Actors), + Actors3 = insert_actor_after(From, To, Actors2), + S2 = S#state{actors = Actors3}, + refresh_main_window(S2); + FromIx =/= 0, ToIx =/= 0 -> + Actors2 = lists:keydelete(FromName, Pos, Actors), + Actors3 = insert_actor_before(From, To, Actors2), + S2 = S#state{actors = Actors3}, + refresh_main_window(S2); + true -> + %% Ignore + S + end. + +insert_actor_after(From, To, [H | T]) -> + case To#actor.name =:= H#actor.name of + true -> [H, From | T]; + false -> [H | insert_actor_after(From, To, T)] + end; +insert_actor_after(_From, _To, []) -> + []. + +insert_actor_before(From, To, [H | T]) -> + case To#actor.name =:= H#actor.name of + true -> [From, H | T]; + false -> [H | insert_actor_before(From, To, T)] + end; +insert_actor_before(_From, _To, []) -> + []. + +actor_index(_Key, _Pos, []) -> + 0; +actor_index(Key, Pos, [H | T]) -> + case Key =:= element(Pos, H) of + false -> actor_index(Key, Pos, T) + 1; + true -> 1 + end. + +y_to_n(Y, S) -> + Y2 = ((Y / S#state.scale) - ?initial_y + (?incr_y / 2)), + N = round(Y2 / ?incr_y - 0.2), + MaxN = queue_length(S#state.events), + if + N =< 0 -> actor; + N > MaxN -> actor; + true -> {event, N} + end. + +x_to_n(X, S) -> + Scale = S#state.scale, + Len = length(S#state.actors), + X2 = X - (?initial_x * Scale), + N = X2 / (?incr_x * Scale), + N2 = trunc(N + 1.5), + if + N2 > Len -> Len; + N2 < 1 -> 1; + true -> N2 + end. + +write_text(Text, X, Y, Color, Font, S, DC) -> + wxDC:setFont(DC, Font), + wxDC:setTextForeground(DC, Color), + wxDC:drawText(DC, Text, {X, round(Y - (?incr_y * S#state.scale / 2))-3}). + +do_open_event(S, N) -> + Events = queue_to_list(S#state.events), + S2 = S#state{events = list_to_queue(Events)}, + case catch lists:nth(N, Events) of + {'EXIT', _} -> + {error, {no_such_event, N}}; + #e{key = Key} -> + Pid = S#state.collector_pid, + Fun = fun create_contents_window/2, + Prev = et_collector:iterate(Pid, Key, -1), + {S2, Res} = + if + Prev =:= Key -> + et_collector:iterate(Pid, first, 1, Fun, {S2, []}); + true -> + et_collector:iterate(Pid, Prev, 1, Fun, {S2, []}) + end, + case Res of + [] -> + {error, no_contents_viewer_started}; + [Single] -> + Single; + Multi -> + {error, {too_many, Multi}} + end + end. + +create_contents_window(Event, {S, Res}) -> + Options = [{viewer_pid, self()}, + {event, Event}, + {event_order, S#state.event_order}, + {active_filter, S#state.active_filter}, + {wx_debug, S#state.wx_debug} + | S#state.filters], + case catch et_wx_contents_viewer:start_link(Options) of + {ok, Pid} -> + {S, [{ok, Pid} | Res]}; + {error, Reason} -> + ok = error_logger:format("~p(~p): create_contents_window(~p) ->~n ~p~n", + [?MODULE, self(), Options, Reason]), + {S, [{error, Reason} | Res]}; + Stuff -> + {S, [{error, {stuff, Stuff}} | Res]} + end. + +print_setup(S) -> + S2 = #state{print_psdd = PSDD0, print_d = PD0} = init_printers(S), + + wxPageSetupDialogData:setPrintData(PSDD0, PD0), + PSD = wxPageSetupDialog:new(S#state.frame, [{data,PSDD0}]), + wxPageSetupDialog:showModal(PSD), + + PSDD1 = wxPageSetupDialog:getPageSetupData(PSD), + PD1 = wxPageSetupDialogData:getPrintData(PSDD1), + + %% Create new objects using copy constructor + PD = wxPrintData:new(PD1), + PsDD = wxPageSetupDialogData:new(PSDD1), + wxPageSetupDialog:destroy(PSD), + wxPageSetupDialogData:destroy(PSDD0), + wxPrintData:destroy(PD0), + S2#state{print_psdd=PsDD, print_d=PD}. + +print(#state{print_d = undefined, print_psdd = undefined} = S, Scope) -> + S2 = print_setup(S), + print(S2, Scope); +print(#state{print_psdd = PSDD, print_d = PD} = S, Scope) -> + PDD = wxPrintDialogData:new(PD), + wxPrintDialogData:enablePrintToFile(PDD, true), + wxPrintDialogData:enablePageNumbers(PDD, true), + wxPrintDialogData:enableSelection(PDD, true), + Tab = ets:new(?MODULE, [public]), + GetPageInfo = + fun(This) -> + {_, _, PW, PH} = wxPrintout:getPaperRectPixels(This), + PrinterS = S#state{context = printer, + canvas_width = PW, + canvas_height = PH}, + EventsPerPage = events_per_page(PrinterS, PH), + PagedEvents = paged_events(PrinterS, Scope, EventsPerPage), + [ets:insert(Tab, PE) || PE <- PagedEvents], + ets:insert(Tab, PrinterS), + NumPages = length(PagedEvents), + {1, NumPages, 1, NumPages} + end, + HasPage = + fun(_This, Page) -> + Size = ets:info(Tab, size), + NumPages = Size - 1, + (Page >= 1) andalso (Page =< NumPages) + end, + OnPrintPage = + fun(This, Page) -> + wxPrintout:mapScreenSizeToPageMargins(This, PSDD), + [PrinterS] = ets:lookup(Tab, state), + Events = ets:lookup_element(Tab, Page, 2), + DC = wxPrintout:getDC(This), + PrinterS2 = draw_all_actors(PrinterS, DC), + PrinterS3 = PrinterS2#state{y_pos = ?initial_y * PrinterS2#state.scale}, + lists:foldl(fun(E, State) -> display_event(E, State, DC) end, + PrinterS3, + Events), + true + end, + Printout1 = wxPrintout:new("Print", OnPrintPage, + [{getPageInfo, GetPageInfo}, {hasPage, HasPage}]), + Printout2 = wxPrintout:new("Print", OnPrintPage, + [{getPageInfo, GetPageInfo}, {hasPage, HasPage}]), + Preview = wxPrintPreview:new(Printout1, [{printoutForPrinting, Printout2}, {data,PDD}]), + case wxPrintPreview:isOk(Preview) of + true -> + PF = wxPreviewFrame:new(Preview, S#state.frame, []), + wxPreviewFrame:centre(PF, [{dir, ?wxBOTH}]), + wxPreviewFrame:initialize(PF), + wxPreviewFrame:centre(PF), + wxPreviewFrame:show(PF), + OnClose = fun(_Wx, EventRef) -> ets:delete(Tab), wxEvent:skip(EventRef) end, + wxPreviewFrame:connect(PF, close_window, [{callback, OnClose}]); + false -> + io:format("Could not create preview window.\n" + "Perhaps your current printer is not set correctly?~n", []), + wxPrintPreview:destroy(Preview), + ets:delete(Tab) + end, + S. + +paged_events(S, Scope, EventsPerPage) -> + {_, Events} = + case Scope of + print_one_page -> + revert(S#state{events_per_page = EventsPerPage}); + print_all_pages -> + collect_more_events(S, first, S#state.n_events) + end, + split_list(Events, EventsPerPage). + +split_list(List, N) when is_integer(N), N > 0 -> + do_split_list(List, N, 1, []). + +do_split_list([], _N, _Page, Acc) -> + lists:reverse(Acc); +do_split_list(List, N, Page, Acc) -> + {Items, Rest} = pick_n(List, N, []), + do_split_list(Rest, N, Page + 1, [{Page, Items} | Acc]). + +get_latest_resize(#wx{obj = ObjRef, event = #wxSize{}} = Wx) -> + receive + #wx{obj = ObjRef, event = #wxSize{}} = Wx2 -> + get_latest_resize(Wx2) + after 100 -> + Wx + end. + +get_latest_scroll(#wx{obj = ObjRef, event = #wxScroll{type = scroll_changed}} = Wx) -> + receive + #wx{obj = ObjRef, event = #wxScroll{type = scroll_changed}} = Wx2 -> + get_latest_scroll(Wx2) + after 100 -> + Wx + end. + +update_scroll_bar(#state{scroll_bar = ScrollBar, + status_bar = StatusBar, + events_per_page = EventsPerPage, + n_events = N} = S) -> + Opts = [{refresh, true}], + {_, LineTopY, LineBotY} = calc_y(S), + Range = LineBotY - LineTopY, + EventPos = + case event_pos(S) of + 1 -> 0; + P -> P + end, + if + N =/= 0, + EventsPerPage =/= 0 -> + PixelsPerEvent = Range / EventsPerPage, + Share = EventsPerPage / N, + wxScrollBar:setScrollbar(ScrollBar, + trunc(EventPos * Share * PixelsPerEvent), + round(Share * Range), + Range, + round(Share * Range), + Opts); + true -> + wxScrollBar:setScrollbar(ScrollBar, + 0, + Range, + Range, + Range, + Opts) + end, + wxStatusBar:setStatusText(StatusBar, where_text(S)), + S. + +events_per_page(S, PageHeight) -> + EventsPerPage = ((PageHeight - (?initial_y * S#state.scale)) div (?incr_y * S#state.scale)), + lists:max([1, EventsPerPage]). + +select_file(Frame, Message, DefaultFile, Style) -> + Dialog = wxFileDialog:new(Frame, + [{message, Message}, + {defaultDir, filename:dirname(DefaultFile)}, + {defaultFile, filename:basename(DefaultFile)}, + {style, Style}]), + Choice = + case wxMessageDialog:showModal(Dialog) of + ?wxID_CANCEL -> cancel; + ?wxID_OK -> {ok, wxFileDialog:getPath(Dialog)} + end, + wxFileDialog:destroy(Dialog), + Choice. + +%%%---------------------------------------------------------------------- +%%% String padding of actors +%%%---------------------------------------------------------------------- + +opt_create_actor(Name, Tag, S) -> + Actors = S#state.actors, + New = + case lists:keysearch(Name, #actor.name, Actors) of + {value, Old} -> Old; + false -> create_actor(Name) + end, + case Tag of + include -> New#actor{include = true}; + exclude -> New#actor{exclude = true} + end. + +create_actor(Name) -> + String = name_to_string(Name), + %% PaddedString = pad_string(String, 8), + #actor{name = Name, string = String, include = false, exclude = false}. + +name_to_string(Name) -> + case catch io_lib:format("~s", [Name]) of + {'EXIT', _} -> lists:flatten(io_lib:format("~w", [Name])); + GoodString -> lists:flatten(GoodString) + end. + +pad_string(Atom, MinLen) when is_atom(Atom) -> + pad_string(atom_to_list(Atom), MinLen); +pad_string(String, MinLen) when is_integer(MinLen), MinLen >= 0 -> + Len = length(String), + case Len >= MinLen of + true -> + String; + false -> + String ++ lists:duplicate(MinLen - Len, $ ) + end. + +%%%---------------------------------------------------------------------- +%%% Queue management +%%%---------------------------------------------------------------------- + +queue_new() -> + {0, [], []}. + +queue_in(X, {Size, In, Out}) -> + {Size + 1, [X | In], Out}. + +%% queue_out(Q) -> +%% case Q of +%% {Size, In, [H | Out]} -> {{value, H}, {Size - 1, In, Out}}; +%% {Size, [], []} -> {empty, {Size, [], []}}; +%% {Size, In, _} -> queue_out({Size, [], lists:reverse(In)}) +%% end. + +queue_to_list({_Size, [], Out}) -> + Out; +queue_to_list({_Size, In, Out}) -> + Out ++ lists:reverse(In). + +queue_length({Size, _In, _Out}) -> + Size. + +list_to_queue(List) when is_list(List) -> + {length(List), [], List}. |