%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-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%
%%
-module(appmon_a).
-compile([{nowarn_deprecated_function,{gs,config,2}},
{nowarn_deprecated_function,{gs,create,3}},
{nowarn_deprecated_function,{gs,destroy,1}},
{nowarn_deprecated_function,{gs,read,2}},
{nowarn_deprecated_function,{gs,start,1}}]).
%%----------------------------------------------------------------------
%%
%% Monitors an application, i.e its supervision tree.
%%
%%----------------------------------------------------------------------
%%
%%
%% INTRODUCTION
%% ------------
%%
%% This file contains a description of the files involved
%% and the communication between the appmon_a display
%% manager and the appmon_a2 information gatherer. Further
%% information on the placement algorithm can be found in
%% the place.erl file.
%%
%%
%% FILES
%% -----
%%
%% The supervision tree graphical software consists of
%% the following files:
%%
%% appmon_a Gen server driving the process display window.
%% Responsible for assigning gs identifiers to all
%% processes and process link
%% appmon_a2 The process information gathering routines.
%% Works by following the process links from application
%% master once every second
%% dg The process database is implemented as a shared
%% digraph (see manual pages for digraph) and this is
%% the routines handling this digraph. Since the digraph
%% is shared appmon_a2 will put some info into it that the
%% appmon_a later will modify. The structures used are
%% described in dg.hrl
%% place Places a tree, decides the x and y coordinates (not
%% necessarily corresponding to window coordinates) of
%% processes (or vertices to be specific). Note that
%% special routines are used to transform the possibly
%% cyclic digraph into a strict tree before trying to
%% place it.
%%
%%
%%
%% IMPLEMENTATION DETAIL
%% ---------------------
%%
%% The appmon_a module will follow links between processes,
%% starting with the application master. A unique
%% reference is used to prevent infinite recursion. Note
%% that this process and link gathering is done in the
%% live digraph so that already known processes are
%% updated with the reference and new ones are added to
%% the digraph. After all processes and links have been
%% added or updated a search is made for those processes
%% and links that have an old reference. These are those
%% processes and links that are not present in the
%% application any more. Those are extracted from the
%% digraph and then deleted and the extracts are then
%% used (by appmon_a) to delete the appropriate gs
%% objects. The responsibilities of appmon_a is thus 1) add
%% all new processes and links to the digraph and 2) make
%% a list of all those objects from the digraph that have
%% been deleted.
%%
%% When appmon_a2 has gathered all necessary information it
%% notifies the appmon_a display manager. Note that this is
%% implemented as a call (as opposed to a cast) to
%% prevent appmon_a2 from changing the digraph while appmon_a
%% uses it. appmon_a places all processes using the place
%% module. place will place the processes in the x y
%% planes, hopefully in a nice way, re-forming the
%% digraph during the process into a strict tree using
%% some simple heuristics, some links that makes the
%% graph cyclic will be considered secondary and later
%% coloured red. Note that the process links are not
%% placed since their coordinates are those of the
%% processes that they are links between. The place
%% module is only concerned at a fairly high level of
%% abstraction. Currently its x coordinates are used as
%% real coordinates while the y coordinates must be
%% scaled to correct values, thus the x plane is
%% continous and the y plane is disctrete.
%%
%% Having placed processes the new ones are drawn on the
%% display along with all new process links, then all
%% processes and process links are moved to their
%% possibly new positions. The place module is not
%% sensitive to changes in position and therefore has no
%% concept of which nodes will have to be moved. hence
%% all nodes are moved (but most of them probably to the
%% same position as before)
%%
%%
%%
%%
%%----------------------------------------------------------------------
-export([start/2, start/3, stop/0]).
-record(astate, {app, name, client, digraph}).
-import(lists, [foreach/2]).
%% gen server stuff
-behaviour(gen_server).
-export([init/1, handle_cast/2, handle_info/2, terminate/2]).
-export([handle_call/3, code_change/3]).
-define(APPSPACE, 10). % The space between apps
-define(NODEAREA_H, 90). % The height of a node
-define(BUTTAREA_H, 80). % The button area height
-define(APPBUTT_H, 20). % Height of appl button
-define(EDITORW, 260).
-define(MAXWIDTH, 800).
-define(MINWIDTH, 382).
-define(MAXHEIGHT, 450).
-define(MINHEIGHT, 325).
-define(SUPVIEWTXT, "Sup. view").
-define(PROCVIEWTXT, "Proc. view").
-define(CLOSETXT, "Close").
-define(REFRESHTXT, "Refresh").
-define(SAVEOPTSTXT, "Save options").
-define(HELPTXT, "Help").
-define(CHARWIDTH, 7). %Should use GS primitives
-define( darkkhaki, {189, 183, 107}).
-define( palegoldenrod, {238, 232, 170}).
-define( peachpuff4, {139, 119, 101}).
-define( red, red).
-define( darkgrey, {169, 169, 169}).
-define( lightgrey, {211, 211, 211}).
-define( royalblue, {65, 105, 225}).
-define( aquamarine4, {69, 139, 116}).
-define( palegreen4, {84, 139, 84}).
-define( darkseagreen, {105, 139, 105}).
-define( f_line_col, {150, 150, 255}).
-include("appmon_dg.hrl").
%%------------------------------------------------------------
%%------------------------------------------------------------
start(NodeName, AppName) ->
gen_server:start_link(?MODULE, {NodeName, AppName, AppName}, []).
start(NodeName, AppName, AppId) ->
gen_server:start_link(?MODULE, {NodeName, AppName, AppId}, []).
stop() ->
ok.
%%------------------------------------------------------------
%% Public interface
%%------------------------------------------------------------
%% Administration
%% AppName is the name of the application, usually an atom like sasl
%% or kernel, AppId is the application pid or the application name,
%% either goes.
init({NodeName, AppName, AppId}) ->
process_flag(trap_exit, true),
{ok, Client} = appmon_info:start_link(NodeName, self(), []),
init_ref(),
init_foreign_places(),
DG = digraph:new([cyclic, private]),
State = #astate{app=AppId, name=AppName, client=Client, digraph=DG},
refresh(State),
setup_base_win(NodeName, AppName),
{ok, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
handle_call(norequest, _From, State) ->
{reply, null, State}.
%%------------------------------------------------------------
%% handle casts
handle_cast({ping, _Node, _From}, State) ->
{noreply, State};
handle_cast(_Other, State) ->
{noreply, State}.
%%------------------------------------------------------------
%% handle info
handle_info({gs, _, click, _, [?CLOSETXT|_]}, State) ->
{stop, normal, State};
handle_info({gs, _, destroy, _, _}, State) ->
{stop, normal, State};
handle_info({gs, _, click, _, [?REFRESHTXT|_]}, State) ->
refresh(State),
{noreply, State};
handle_info({gs, _, click, _, [?HELPTXT|_]}, State) ->
HelpFile = filename:join([code:lib_dir(appmon),
"doc", "html", "part_frame.html"]),
tool_utils:open_help(win(), HelpFile),
{noreply, State};
handle_info({gs, Id, click, {mode, Mode}, _}, State) ->
%%io:format("handle_info: Setting mode: ~p~n", [Mode]),
set_mode(Id, Mode),
{noreply, State};
handle_info({gs, _, click, _, [?SUPVIEWTXT|_]}, State) ->
refresh(State, [{info_type, sup}]),
{noreply, State};
handle_info({gs, _, click, _, [?PROCVIEWTXT|_]}, State) ->
refresh(State, [{info_type, link}]),
{noreply, State};
handle_info({gs, Id, buttonpress, _,[1, X, Y|_]}, State) ->
%%io:format("Id clicked: ~p~n", [gs:read(Id, {find, {X, Y}})]),
catch find_pid(State, Id, X, Y),
set_default_mode(),
{noreply, State};
handle_info({gs, Win, configure, _Data, [W, H|_]}, State) ->
case win() of Win -> user_driven_resize(W, H);
_-> ok
end,
{noreply, State};
handle_info({delivery, _S, pinfo, _N, Res}, State) ->
appmon_txt:print(Res),
{noreply, State};
handle_info({delivery, S, app, N, Res}, State) ->
{delivery, _Serv, app, _Name, {Root, Vs, Ls, SecLs}} =
flush({delivery, S, app, N, Res}),
update2(Vs, Root, Ls, SecLs, State),
{noreply, State};
handle_info({kill}, State) ->
{stop, normal, State};
handle_info({state}, State) ->
{noreply, State};
handle_info({'EXIT', _Pid, _Reason}, State) ->
{noreply, State};
handle_info(_Other, State) ->
{noreply, State}.
%% Refresh sets new options for the request and forces an update of
%% the screen ant status.
refresh(State) ->
refresh(State, []).
refresh(State, Opts) ->
appmon_info:app(State#astate.client,
State#astate.name, true, Opts).
%% find_pid finds the pid of the clicked object. The scenario is that
%% the user clicks on an item in his window, that ObjId is searched
%% for among all nodes (vertices) and if found action is taken
%% depending on the current mode (see handle_info)
find_pid(State, Id, X, Y) ->
%% Try to manage both versions of GS, remove first case later.
ObjList = case gs:read(Id, {find, {X, Y}}) of
{error, _} ->
gs:read(Id, {hit, {X, Y}}); % Try new format
Num when is_integer(Num) -> [Num];
_Other -> []
end,
DG = State#astate.digraph,
All = appmon_dg:get(all, DG),
find_pid2(ObjList, All, DG, State).
find_pid2([Id | Ids], All, DG, State) ->
case search_for_pid(All, DG, Id) of
{ok, _KeyStr, Pid} ->
handle_proc_press(mode(), Pid, State);
_ -> find_pid2(Ids, All, DG, State)
end;
find_pid2([], _All, _DG, _State) -> ok.
search_for_pid([V|Vs], DG, ObjId) ->
VD = appmon_dg:get(data, DG, V),
if ObjId==VD#vdata.txt_obj ->
{ok, V, VD#vdata.type};
true -> search_for_pid(Vs, DG, ObjId)
end;
search_for_pid([], _DG, _ObjId) -> false.
%%
%% called when a process has been clicked on.
%%
handle_proc_press(info, Pid, State) ->
appmon_info:pinfo(State#astate.client, Pid, true,
[{timeout, at_most_once}]);
handle_proc_press(send, Pid, _State) ->
{P, RawStr} = two_entries(winroot(), 250, 70,
"Send", "To: ", "Msg: ",
pid_to_list(Pid), "", bg()),
Str = case lists:last(RawStr) of
46 -> RawStr;
_ -> RawStr++"."
end,
case erl_scan:string(Str) of
{ok, Tokens, _} ->
case erl_parse:parse_term(Tokens) of
{ok, Term} ->
case catch list_to_pid(P) of
To when is_pid(To) -> To ! Term;
_ -> error
end;
_Error -> error
end;
_Error -> error
end;
handle_proc_press(trace, Pid, _State) ->
case trace_state(Pid) of
true ->
io:format("Removing trace on ~p~n", [Pid]),
sys:trace(Pid, false),
set_trace_state(Pid, false);
_Other ->
io:format("Putting trace on ~p~n", [Pid]),
sys:trace(Pid, true, 1000),
set_trace_state(Pid, true)
end;
handle_proc_press(kill, Pid, _State) ->
exit(Pid, kill).
trace_state(Pid) -> get({trace_state, Pid}).
set_trace_state(Pid, State) -> put({trace_state, Pid}, State).
set_default_mode() ->
{Id, Mode} = get(default_mode),
case mode() of
Mode -> true;
_Other -> set_mode(Id, Mode)
end.
set_default_mode(Id, Mode) ->
put(default_mode, {Id, Mode}),
select(Id), %Dirty workaround
set_default_mode().
set_mode(Id, Mode) ->
%%io:format("mode=~p~n", [Mode]),
set_mode(Mode),
deselect(),
select(Id).
set_mode(Mode) -> put(mode, Mode).
mode() -> get(mode).
flush({delivery, S, A, N, R}) ->
receive
{delivery, S, A, N, R2} ->
flush({delivery, S, A, N, R2})
after 0 ->
{delivery, S, A, N, R}
end.
%%------------------------------------------------------------
%% Real stuff
%%
update2(Vs, Root, Ls, SecLs, State) ->
DG = State#astate.digraph,
Ref = mk_ref(),
Added = add_procs(Vs, DG, Ref),
AddedLs = add_links(Ls, DG, Ref, primary),
AddedLs2 = add_links(SecLs, DG, Ref, secondary),
DelLs = del_links(appmon_dg:eget(all, DG), DG, Ref),
Dels = del_procs(appmon_dg:get(all, DG), DG, Ref),
LastX = appmon_place:place(DG, Root),
Width = lists:max(LastX),
Height = length(LastX),
%% Delete things from screen
del(Dels), del(DelLs),
%% Add vertices to screen
foreach(fun(V) -> draw_node(DG, V) end, Added),
%% Add edges to screen
foreach(fun(E) -> draw_edge(DG, E) end, AddedLs),
foreach(fun(E) -> draw_edge(DG, E) end, AddedLs2),
%% Move vertices on screen
foreach(fun(V) -> move_vertex(DG, V) end, appmon_dg:get(all, DG)),
tree_driven_resize(Width, Height),
gs:config(win(), {map, true}), %Make win visible
ok.
%% Make an integer reference, could have used make_ref BIF but didn't
mk_ref() -> put(reference, get(reference)+1).
init_ref() -> put(reference, 0).
%% Add processes (vertices) to digraph, use the string repr of pid as
%% key in digraph.
add_procs([{Pid, Str}|Vs], DG, Ref) ->
case appmon_dg:add(DG, Str, mk_vdata(Str, Pid, Ref), Ref) of
known -> add_procs(Vs, DG, Ref);
updated -> add_procs(Vs, DG, Ref);
_ ->
case lists:member(32, Str) of
true ->
appmon_dg:set(x, DG, Str, foreign), % UNHOLY!
add_procs(Vs, DG, Ref); % Don't add foreign
_ -> [Str | add_procs(Vs, DG, Ref)]
end
end;
add_procs([], _DG, _Ref) -> [].
%% Add links to digraph. NOTE that foreign links get a special weight
%% and that no link is added if it goes to a process not in the set of
%% vertices.
%%
%% OTP-1970: Check that linked-to processes really exist.
%%
add_links([{V1, V2}|Ls], DG, Ref, Weight) ->
L = case lists:member(32, V2) of
true -> {V1, V2, foreign};
_ -> {V1, V2, Weight}
end,
case appmon_dg:get(data, DG, V2) of
false -> add_links(Ls, DG, Ref, Weight);
VD ->
if VD#vdata.ref == Ref -> % OTP-1970
case appmon_dg:eadd(DG, L, mk_edata(L, Ref), Ref) of
known -> add_links(Ls, DG, Ref, Weight);
updated -> add_links(Ls, DG, Ref, Weight);
_Other -> [L | add_links(Ls, DG, Ref, Weight)]
end;
true ->
add_links(Ls, DG, Ref, Weight)
end
end;
add_links([], _DG, _Ref, _Weight) -> [].
%% Make an edge data structure
mk_edata(_L, Ref) ->
#edata{ref=Ref}.
%% Make a vertex data structure, note that pid can be either a pid or
%% a port, we're not picky here.
mk_vdata(P, Pid, Ref) ->
#vdata{ref=Ref, type=Pid, txt=P, width=width(P)}.
width(Txt) -> ?CHARWIDTH*length(Txt)+10. % Should use GS stuff instead
%% Delete those processes that have the wrong reference from the
%% digraph. Returns a list of deleted procs and their data (to be able
%% to erase things on screen).
del_procs([V|Vs], DG, Ref) ->
VD = appmon_dg:get(data, DG, V),
if VD#vdata.ref /= Ref -> appmon_dg:del(DG, V),
[{V, VD} | del_procs(Vs, DG, Ref)];
true -> del_procs(Vs, DG, Ref)
end;
del_procs([], _DG, _Ref) -> [].
%% Deletes links that have the wrong reference from the digraph, note
%% that the weight of the edge is not considered here. Returns a list
%% of deleted links and their data (to be able to erase things on
%% screen).
del_links([L | Ls], DG, Ref) ->
ED = appmon_dg:eget(data, DG, L),
if ED#edata.ref /= Ref -> appmon_dg:edel(DG, L),
[{L, ED} | del_links(Ls, DG, Ref)];
true -> del_links(Ls, DG, Ref)
end;
del_links([], _DG, _Ref) -> [].
%% Del deletes the GS objects of the list of should-be-deleted
%% items. Returns nothing particular.
del(L) ->
lists:foreach(fun({{V1, V2, Weight}, D}) when is_record(D, edata) ->
if Weight== foreign ->
dealloc_foreign({V1, V2, Weight});
true -> ok end,
destroy(D#edata.line);
({_I, D}) when is_record(D, vdata) ->
destroy(D#vdata.sym_obj),
destroy(D#vdata.txt_obj)
end, L).
move_vertex(DG, V) ->
VData = appmon_dg:get(data, DG, V),
%% io:format("Vertex ~p data: x:~p, oldx:~p, y:~p, oldy:~p offs:~p~n",
%% [V, VData#vdata.x, VData#vdata.origx,
%% VData#vdata.y, VData#vdata.origy, offsetx()]),
if VData#vdata.x == foreign -> ok;
true ->
X = VData#vdata.x,
Y = scaley(VData#vdata.y),
OldX = VData#vdata.origx,
OldY = scaley(VData#vdata.origy),
if X==OldX, Y==OldY -> true;
true ->
%%io:format("Moving vertex: ~p~n", [V]),
W = VData#vdata.width,
{BoxC, TxtC} = calc_box_coords(X, Y, W),
%% move the symbol and text
gs:config(VData#vdata.sym_obj, [{coords, BoxC}]),
gs:config(VData#vdata.txt_obj, [{coords, TxtC}]),
foreach(fun(E) -> move_edge(DG, E) end,
appmon_dg:get(edges, DG, V))
end
end.
move_edge(DG, E) ->
{{V1, V2, Weight}, V1, V2, ED} = appmon_dg:eget(edge, DG, E),
VD1 = appmon_dg:get(data, DG, V1),
VD2 = appmon_dg:get(data, DG, V2),
Line = ED#edata.line,
move_line(DG, VD1, VD2, Line, Weight).
move_line(DG, VD1, VD2, Line, Weight) when is_list(Line) ->
move_line(DG, VD1, VD2, hd(Line), Weight);
move_line(_DG, VD1, VD2, Line, Weight) ->
Coords = calc_coords(VD1, VD2, Weight),
gs:config(Line, [{coords, Coords} | line_opts(Weight)]).
%% Draw the vertex on the canvas
draw_node(DG, V) ->
%%io:format("Drawing~n",[]),
Data = appmon_dg:get(data, DG, V),
X = Data#vdata.x,
Y = scaley(Data#vdata.y),
{Sym, Txt} = draw(rectangle, sup_col(), Data#vdata.txt, X, Y,
Data#vdata.width),
appmon_dg:av(DG, V, Data#vdata{sym_obj=Sym, txt_obj=Txt}),
true.
%% Draws a symbol (rectangle for instance) on the canvas.
draw(Symbol, Col, Txt, X, Y, W) ->
{BoxC, TxtC} = calc_box_coords(X, Y, W),
Box = gs:create(Symbol, canvas(), [{coords, BoxC}, {fill, Col}]),
TxtObj = gs:create(text, canvas(), [{coords, TxtC},
{anchor, c},
%%{buttonpress, true},
{text, Txt}]),
{Box, TxtObj}.
%% Returns {BoxCoords, TextCoords}
calc_box_coords(X, Y, W) ->
{[{X, Y-radius()}, {X+W, Y+radius()}], [{X+trunc(W/2), Y}]}.
%% Draw a line on the canvas
draw_edge(DG, E) ->
{V1, V2, Weight} = E,
Line = draw_line(DG, V1, V2, Weight),
%% io:format("Line: ~p~n", [Line]),
appmon_dg:eset(line, DG, E, Line).
%% From is parent, To is child. If To is not a record then we are
%% dealing with a link to a process on another node. Find a suitable
%% place at the left margin and write the process name there and draw
%% a line to it.
%%
draw_line(DG, From, To, foreign) ->
VD1 = appmon_dg:get(data, DG, From),
Y = find_foreign_place(VD1#vdata.y+0.5, foreign_places()),
add_foreign_place({From, To, foreign}, Y),
%% io:format("New Y: ~p~n", [Y]),
appmon_dg:set(x, DG, To, 0),
appmon_dg:set(y, DG, To, Y),
VD2 = appmon_dg:get(data, DG, To),
Coords = calc_coords(VD1, VD2, foreign),
%% io:format("Node coords: ~p~n", [Coords]),
L = gs:create(line, canvas(), [{coords, Coords} | line_opts(foreign)]),
T = gs:create(text, canvas(), [{coords, [{0, 5+scaley(Y)}]},
{anchor, nw}, {fg, f_line_col()},
%% {font, {screen, 10}},
{text, To}]),
[L, T];
draw_line(DG, From, To, Weight) ->
VD1 = appmon_dg:get(data, DG, From),
VD2 = appmon_dg:get(data, DG, To),
Coords = calc_coords(VD1, VD2, Weight),
gs:create(line, canvas(), [{coords, Coords} | line_opts(Weight)]).
%%----------------------------------------------------------------------
%%
%% Line coordinate calculation
%%
%% Calculate coordinates for edges (links, lines). Primaries have a
%% nice knee and secondaries are oriented differently. If weight is
%% foreign then we will calculate a nice line to the left margin.
%%
calc_coords(From, To, foreign) ->
Y = scaley(To#vdata.y),
X1 = From#vdata.x+trunc(From#vdata.width/2),
Y1 = scaley(From#vdata.y)+radius(),
[{0, Y}, {X1-40, Y}, {X1, Y1}];
calc_coords(From, To, primary) ->
X1 = From#vdata.x+trunc(From#vdata.width/2),
Y1 = scaley(From#vdata.y)+radius(),
X2 = To#vdata.x+trunc(To#vdata.width/2),
Y2 = scaley(To#vdata.y)-radius(),
Y3 = trunc((Y1+Y2)/2),
[{X1, Y1}, {X1, Y3}, {X2, Y3}, {X2, Y2}];
calc_coords(V1, V2, _Weight) ->
Y1 = scaley(V1#vdata.y),
X1 = V1#vdata.x,
X1w = X1+V1#vdata.width,
Y2 = scaley(V2#vdata.y),
X2 = V2#vdata.x,
X2w = X2+V2#vdata.width,
if Y1 == Y2 -> calc_u(X1, X1w, Y1, X2, X2w);
X1w < X2 -> calc_s(X1w, Y1, X2, Y2);
X2w < X1 -> calc_s(X1, Y1, X2w, Y2);
true ->
D1 = abs(X1-X2), D2 = abs(X1w-X2w),
if D1 > D2 -> calc_rbrack(X1w, Y1, X2w, Y2);
true -> calc_lbrack(X1, Y1, X2, Y2)
end
end.
%% Calculates line coordinates that will go from bottom of one node to
%% bottom of another on the same level. The line will form a nice "U".
calc_u(X1, X1w, Y, X2, X2w) ->
X3 = trunc((X1+X1w)/2),
X4 = trunc((X2+X2w)/2),
Y2 = Y+radius(),
Y3 = Y2+20,
[{X3, Y2}, {X3, Y3}, {X4, Y3}, {X4, Y2}].
%% Calculates line coordinates that will go from right (or left) side
%% to right (or left) side, thus forming a "[" or a "]" (bracket).
calc_rbrack(X1, Y1, X2, Y2) ->
X3 = 20 + if X1 > X2 -> X1;
true -> X2
end,
[{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}].
calc_lbrack(X1, Y1, X2, Y2) ->
X3 = -20 + if X1 < X2 -> X1;
true -> X2
end,
[{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}].
%% Calculates line coordinates that will form a nice "S"
calc_s(X1, Y1, X2, Y2) ->
X3 = trunc((X1+X2)/2),
[{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}].
%% Options for lines (edges, links)
line_opts(foreign) -> [{width, 2}, {smooth, true}, {fg, f_line_col()}];
line_opts(primary) -> [{width, 2}, {smooth, false}, {fg, line_col()}];
line_opts(_) -> [{width, 2}, {smooth, true}, {fg, sec_line_col()}].
%%----------------------------------------------------------------------
%%
%% Handling of links to foreign processes
%%
%%----------------------------------------------------------------------
dealloc_foreign(L) ->
%% io:format("deallocing foreign: ~p~n", [L]),
put(foreign_places, lists:keydelete(L, 1, foreign_places())).
add_foreign_place(V, Y) ->
%% io:format("Adding foreign: ~p~n", [V]),
put(foreign_places, [{V, Y} | foreign_places()]).
foreign_places() ->
get(foreign_places).
init_foreign_places() ->
put(foreign_places, []).
%% Find a good place for the foreign node
find_foreign_place(StartY, L) ->
case lists:keysearch(StartY, 2, L) of
{value, _} -> find_foreign_place(StartY + 1, L);
_ -> StartY
end.
%%------------------------------------------------------------
%%
%% Graphical stuff
%%
setup_base_win(NodeName, AppName) ->
set_winroot(gs:start([{kernel,true}])),
W = ?MINWIDTH, H = ?MINHEIGHT,
Name = "APPMON: " ++ atom_to_list(AppName) ++ " on " ++
atom_to_list(NodeName),
set_win(gs:create(window, winroot(), [{title, Name}, %%{bg, red},
{x, 250}, {y, 100},
{width, W}, {bg, win_col()},
{height, H+?BUTTAREA_H}])),
%% standard buttons
mk_std_butts(win(), W),
set_canvas(gs:create(canvas, win(),[{x,0}, {y,?BUTTAREA_H},
{width, W}, {height, H},
{bg, bg()},
{buttonpress, true}])),
set_old_win_size(width, gs:read(win(), width)),
set_old_win_size(height, gs:read(win(), height)),
%% gs:config(win(), {map, true}), %Make win visible
ok.
nice_line_coords(W, H) ->
[{0,H-10}, {W,H-10}].
%%------------------------------
%% Button stuff
mk_butt_area(Win, W) ->
H = ?BUTTAREA_H,
F = gs:create(frame, Win,[{x,0}, {y,0}, %%{bg, frame_col()},
{width,W}, {height,H}]),
C = gs:create(canvas,F,[{x,0}, {y,0}, {width, W}, {height, H-9},
{bg, bg()}]),
L = gs:create(line,C,[{coords,nice_line_coords(W, H)}]),
MB = gs:create(menubar, Win, []),
FMB = gs:create(menubutton, MB, [{label, {text, "File"}}]),
FM = gs:create(menu, FMB, []),
gs:create(menuitem, FM, [{label, {text, ?CLOSETXT}}]),
OMB = gs:create(menubutton, MB, [{label, {text, "Options"}}]),
OM = gs:create(menu, OMB, []),
gs:create(menuitem, OM, [{label, {text, ?REFRESHTXT}}]),
Group = now(),
gs:create(menuitem, OM, [{itemtype, separator}]),
gs:create(menuitem, OM, [{label, {text, ?SUPVIEWTXT}}, {itemtype, radio},
{group, Group}]),
gs:create(menuitem, OM, [{label, {text, ?PROCVIEWTXT}}, {select, true},
{group, Group}, {itemtype, radio}]),
HMB = gs:create(menubutton, MB, [{label, {text, "Help"}}, {side, right}]),
HM = gs:create(menu, HMB, []),
gs:create(menuitem, HM, [{label, {text, ?HELPTXT}}]),
{F, C, L}.
mk_std_butts(Win, W) ->
{F, C, L} = mk_butt_area(Win, W),
set_bframe(F), set_bcanvas(C), set_bline(L),
IButt = mk_mode_butt({text, "Info"}, {mode, info}, 10),
mk_mode_butt({text, "Send"}, {mode, send}, 90),
mk_mode_butt({text, "Trace"}, {mode, trace}, 170),
mk_mode_butt({text, "Kill"}, {mode, kill}, 250),
set_default_mode(IButt, info),
true.
select(Id) ->
gs:config(Id, {bg, sel_col()}),
set_selected(Id).
deselect() ->
gs:config(selected(), {bg, de_sel_col()}).
mk_mode_butt(Label, Data, X) ->
gs:create(button, bframe(), [{label, Label}, {x, X}, {y, 35},
{data, Data}, {width, 70}, {height, 25}]).
%%------------------------------------------------------------
%% Graphical utilities
mk_frame(P, X, Y, W, H, BG) ->
gs:create(frame, P, [{x, X}, {y, Y}, {width, W}, {height, H}, {bg, BG}]).
mk_butt(P, X, Y, W, H, Txt) ->
gs:create(button, P, [{x, X}, {y, Y}, {height, H}, {width, W},
{label, {text, Txt}}]).
mk_butt(P, X, Y, Txt) ->
mk_butt(P, X, Y, 70, 20, Txt).
mk_label(P, X, Y, W, H, Txt, BG) ->
gs:create(label, P, [{x, X}, {y, Y}, {height, H}, {width, W},
{label, {text, Txt}}, {bg, BG}]).
mk_entry(P, X, Y, W, H, Txt, BG) ->
gs:create(entry, P, [{x, X}, {y, Y}, {height, H}, {width, W}, {text, Txt},
{bg, BG}, {keypress, true}]).
two_entries(Root, W, H, Name, LTxt1, LTxt2, StartTxt1, StartTxt2, BG) ->
Win = gs:create(window, Root, [{title, Name}, %%{bg, red},
%%{x, X}, {y, Y},
{width, W}, {bg, BG},
{height, H}]),
F = mk_frame(Win, 0, 0, W, H, BG),
mk_label(F, 10, 10, 30, 20, LTxt1, BG),
mk_label(F, 10, 40, 30, 20, LTxt2, BG),
E1 = mk_entry(F, 40, 10, 120, 20, StartTxt1, BG),
E2 = mk_entry(F, 40, 40, 120, 20, StartTxt2, BG),
Ok = mk_butt(F, 170, 10, "Ok"),
Cn = mk_butt(F, 170, 40, "Cancel"),
gs:config(Win, {map, true}),
Ret = case catch two_entries_loop(E1, E2, Ok, Cn) of
{P2, Msg} -> {P2, Msg};
_Other ->
false
end,
gs:destroy(Win),
Ret.
two_entries_loop(E1, E2, Ok, Cn) ->
receive
{gs, Ok, click, _, _} ->
{gs:read(E1, text),
gs:read(E2, text)};
{gs, E1, keypress, _, ['Return'|_]} ->
{gs:read(E1, text),
gs:read(E2, text)};
{gs, E2, keypress, _, ['Return'|_]} ->
{gs:read(E1, text),
gs:read(E2, text)};
{gs, _, keypress, _, _} ->
two_entries_loop(E1, E2, Ok, Cn);
{gs, Cn, click, _, _} ->
true
end.
%%--------------------------------------------------------------------
%%
%% Resizing routines.
%%
%% Resizing deals with a number of different interdependent
%% sizes. Top size is the window size. From window size all other
%% sizes are calculated, we call this the "leader" size. The
%% canvas is usually the same size as the window, except for the
%% row of buttons at the top of the windoww. The canvas is also
%% displaced when the tree is smaller than the minimum window
%% size.
%%
%%
%% Window size - the size of the outer window. Note that
%% provisions must be made for the button area at the top of the
%% window, this is called WinAdj. this is the only item taht
%% changes when the user manually resizes the window.
%%
%% Canvas size - The size of the canvas, should be equal to
%% window size less the button area. Must be adjusted when the
%% window has been manually resized. The canvas also has a
%% scrollregion which must be maintained. Note that we could have
%% used the canvas size as "leading" size, but this did not work
%% since the canvas doesn't fill the complete window when the
%% tree is smaller than the window.
%%
%% Tree size - The size of the tree. This may change whenever a
%% new tree is delivered from the info routine.
%%
%% Dim - All these size adjustments are done in some dimension
%% (width or height).
%%
%% Max, Min - The outmost window may not become larger than Max
%% size or smaller than Min size when resized by the tree
%% size. The user resizing is not restricted to these sizes.
%%
%% Scrollbars:
%%
%% Scrollbars are used whenever necessary, whenever the tree size
%% is bigger than canvas size (in any dimension).
%%
%% Invariants:
%%
%% The three sizes are not varied at the same time. When the
%% window is resized because of a new tree, then window and
%% canvas must be updated. When the user has resized, then only
%% the canvas must be changed (to fit in the window)
%%
%% Tree driven resize
%%
%% This occurs when the tree has been updated. The window may
%% grow and shrink to fit the tree, but may not be smaller than
%% Min and not bigger than Max (scrollbars will be used instead)
%%
tree_driven_resize(TWidth, THeight) ->
gs:config(win(), {configure, false}),
Width = TWidth+20,
Height = scaley(THeight+1),
put({width, tree}, Width),
put({height, tree}, Height),
adjust_win(width, Width),
adjust_win(height, Height),
fit_tree_to_win(width, Width),
fit_tree_to_win(height, Height),
check_scroll_region(Width, Height, gs:read(canvas(), scrollregion)),
gs:config(win(), {configure, true}),
ok.
%% Will adjust the window size to the tree size (given the max and min
%% restrictions.
adjust_win(Dim, TreeSize) ->
case get({Dim, user_resize}) of
true -> ok;
_ ->
WinSize = gs:read(win(), Dim),%%get_dim(Dim, win()),
case get_wanted_winsize(Dim, TreeSize) + winadj(Dim) of
WinSize -> ok;
NewSize ->
%%set(Dim, win(), NewSize+winadj(Dim))
set_old_win_size(Dim, NewSize),
gs:config(win(), {Dim, NewSize})
end
end.
get_wanted_winsize(Dim, Size) ->
Max = maxsize(Dim), Min = minsize(Dim),
if Size > Max -> Max;
Size < Min -> Min;
true -> Size
end.
set_old_win_size(Dim, Size) -> put({Dim, winsize}, Size).
old_win_size(Dim) -> get({Dim, winsize}).
%%--------------------------------------------------------------------
%%
%% user_driven_resize
%%
%% This is when the user drags the window to some size. This is
%% basically the same as a tree resize, only this time the window
%% itself must not be fiddled with. When the window has been
%% resized this way then normal tree driven resize is not allow
%% to alter the size in that dimension. User overrides.
%%
user_driven_resize(W, H) ->
gs:config(win(), {configure, false}),
check_user_resize(width, W),
check_user_resize(height, H),
check_scroll_region(get({width, tree}), get({height, tree}),
gs:read(canvas(), scrollregion)),
gs:config(win(), {configure, true}).
check_user_resize(Dim, Size) ->
case old_win_size(Dim) of
Size -> false;
_ ->
put({Dim, user_resize}, true),
set_old_win_size(Dim, Size),
fit_tree_to_win(Dim, get({Dim, tree}))
end.
%%--------------------------------------------------------------------
%%
%% General resizing routines
%%
%% fit_tree_to_win - Will fit the canvas into a pre-sized window in
%% one dimension.
%%
fit_tree_to_win(Dim, TreeSize) ->
Size = gs:read(win(), Dim) - winadj(Dim),
set_canvas_offset(Dim, Size, TreeSize),
set_button_width(Dim, Size),
if TreeSize > Size ->
gs:config(canvas(), {trans_dim2vh(Dim), trans_dim2enable(Dim)});
TreeSize < Size ->
gs:config(canvas(), {trans_dim2vh(Dim), false});
true ->
gs:config(canvas(), {trans_dim2vh(Dim), false})
end.
%%------------------------------
%% Set the canvas width and displacement in x.
set_canvas_offset(height, Size, _) ->
gs:config(canvas(), {height, Size});
set_canvas_offset(width, Size, Size) ->
gs:config(canvas(), [{x, 0}, {width, Size}]);
set_canvas_offset(width, Size, TreeSize) when Size<TreeSize ->
gs:config(canvas(), [{x, 0}, {width, Size}]);
set_canvas_offset(width, Size, TreeSize) when Size>TreeSize->
Val = trunc((Size-TreeSize)/2),
gs:config(canvas(), [{x, Val}, {width, Size-Val}]).
%%------------------------------
%% Set the button area width
set_button_width(height,_) -> ok;
set_button_width(width, W) ->
gs:config(bcanvas(), [{width, W}]),
gs:config(bframe(), [{width, W}]),
gs:config(bline(), [{coords, nice_line_coords(W, ?BUTTAREA_H)}]).
%%------------------------------
%% Update the scrollregion size if needed.
check_scroll_region(W, H, {_, _, W, H}) -> ok;
check_scroll_region(W, H, {_, _, _, _}) ->
gs:config(canvas(), {scrollregion, {0, 0, W, H}}).
%% Window sizing primitives
winadj(width) -> 0;
winadj(height) -> ?BUTTAREA_H.
maxsize(width) -> ?MAXWIDTH;
maxsize(height) -> ?MAXHEIGHT.
minsize(width) -> ?MINWIDTH;
minsize(height) -> ?MINHEIGHT.
trans_dim2vh(width) -> hscroll;
trans_dim2vh(height) -> vscroll.
trans_dim2enable(width) -> bottom;
trans_dim2enable(height) -> right.
%%------------------------------------------------------------
%% Global Window info
winroot() -> get(winroot).
win() -> get(win).
canvas() -> get(canvas).
bframe() -> get(bframe).
bcanvas() -> get(bcanvas).
bline() -> get(bline).
set_winroot(X) -> put(winroot, X).
set_win(X) -> put(win, X).
set_canvas(X) -> put(canvas, X).
set_bframe(X) -> put(bframe, X).
set_bcanvas(X) -> put(bcanvas, X).
set_bline(X) -> put(bline, X).
sup_col() -> ?darkkhaki.
%%work_col() -> ?orange.
bg() -> ?palegoldenrod.
line_col() -> ?peachpuff4. %% saddlebrown.darkgoldenrod
f_line_col() -> ?royalblue. %% saddlebrown.darkgoldenrod
sec_line_col() -> ?red.
win_col() -> bg(). %%darkolivegreen.
sel_col() -> ?darkgrey.
de_sel_col() -> ?lightgrey.
set_selected(Id)-> put(selected, Id).
selected() -> get(selected).
scaley(Y) -> 55*Y.
radius() -> 10.
destroy(undefined) -> true;
destroy(L) when is_list(L) -> lists:foreach(fun(X) -> destroy(X) end , L);
destroy(Win) -> gs:destroy(Win).