aboutsummaryrefslogtreecommitdiffstats
path: root/lib/et/src/et_wx_viewer.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/et/src/et_wx_viewer.erl')
-rw-r--r--lib/et/src/et_wx_viewer.erl2124
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}.