Files
2021-10-31 21:20:46 +02:00

779 lines
22 KiB
C++

/*
* This plugin demonstrates how one can create a graph viewer, and
* manipulate the graph that is being displayed (by adding nodes &
* edges.)
*
* To illustrate this functionality, we will be showing the a subset
* of the "family tree" of the elvish characters described in JRR
* Tolkien's "Lord of the rings" books.
*
* That "family tree" was found at:
* https://i.pinimg.com/originals/aa/36/0b/aa360b00a0f309f56e6b7f48ff92de2d.jpg
* and please note that we didn't check that it is actually correct
* since this is not exactly relevant to the functionality being
* showcased.
* We'll limit ourselves to 4 "generations" of characters, and provide
* the graph user the ability to show, or hide generations (thus
* modifying the underlying graph.)
*
* The following actions will be available in the graph's
* context menu:
* - change the layout type
* - show & hide generations
* - modify the character's name
* - modify the character name's background color
*/
#include <ida.hpp>
#include <idp.hpp>
#include <graph.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
//
// Dramatis personae
//
#define Finarfin "Finarfin"
#define Earwen "Earwen"
#define Eldalote "Eldalote"
#define Finrod "Finrod"
#define Angrod "Angrod"
#define Aegnor "Aegnor"
#define Galadriel "Galadriel"
#define Celeborn "Celeborn"
#define Orodreth "Orodreth"
#define Unknown_name "Purple dress lady\n(name unknown)"
#define Celebrian "Celebrian"
#define Gil_Galad "Gil-Galad"
#define Finduilas "Finduilas"
static const char *characters[] =
{
// Generation 0
Finarfin, Earwen,
// Generation 1
Eldalote, Finrod, Angrod, Aegnor, Galadriel, Celeborn,
// Generation 2
Orodreth, Unknown_name, Celebrian,
// Generation 3
Gil_Galad, Finduilas,
};
static const size_t ncharacters = qnumber(characters);
static const size_t nlevels = 4;
static const size_t levels_offsets[] =
{
0,
2,
2 + 6,
2 + 6 + 3,
2 + 6 + 3 + 2,
};
CASSERT(qnumber(levels_offsets) == nlevels+1);
//-------------------------------------------------------------------------
static int character_to_node_number(const char *name)
{
for ( size_t i = 0; i < ncharacters; ++i )
if ( streq(characters[i], name) )
return int(i);
INTERR(0); // This shouldn't happen
}
//-------------------------------------------------------------------------
static const char *node_number_to_character(int n)
{
QASSERT(0, n >= 0 && n < ncharacters);
return characters[n];
}
//-------------------------------------------------------------------------
// Relationship between the characters
struct parenthood_t
{
const char *parent0;
const char *parent1;
const char *child;
};
DECLARE_TYPE_AS_MOVABLE(parenthood_t);
static const parenthood_t parenthood_data[] =
{
// level 0
{ Finarfin, Earwen, Finrod },
{ Finarfin, Earwen, Angrod },
{ Finarfin, Earwen, Aegnor },
{ Finarfin, Earwen, Galadriel },
// level 1
{ Eldalote, Angrod, Orodreth },
{ Galadriel, Celeborn, Celebrian },
// level 2
{ Orodreth, Unknown_name, Gil_Galad },
{ Orodreth, Unknown_name, Finduilas },
};
static const size_t nparenthood = qnumber(parenthood_data);
//-------------------------------------------------------------------------
// Optional coloring of character names' text
struct character_name_decoration_t
{
const char *name;
const char *color;
const char *name_subset;
};
DECLARE_TYPE_AS_MOVABLE(character_name_decoration_t);
static const character_name_decoration_t character_name_decorations[] =
{
{ Finarfin, SCOLOR_MACRO, "Fin" },
{ Finrod, SCOLOR_MACRO, "Fin" },
{ Finduilas, SCOLOR_MACRO, "Fin" },
{ Earwen, SCOLOR_CNAME, nullptr },
{ Unknown_name, SCOLOR_ERROR, "name unknown" },
{ Galadriel, SCOLOR_IMPNAME, "Gal" },
{ Gil_Galad, SCOLOR_IMPNAME, "Gal" },
{ Celeborn, SCOLOR_DNUM, "Celeb" },
{ Celebrian, SCOLOR_DNUM, "Celeb" },
};
//--------------------------------------------------------------------------
static void gen_character_name(qstring *out, const char *name)
{
*out = name;
for ( size_t i = 0; i < qnumber(character_name_decorations); ++i )
{
if ( streq(character_name_decorations[i].name, name) )
{
const character_name_decoration_t *d = &character_name_decorations[i];
qstring token;
token.append(SCOLOR_ON);
token.append(d->color);
const char *token_s = d->name_subset != nullptr ? d->name_subset : d->name;
token.append(token_s);
token.append(SCOLOR_OFF);
token.append(d->color);
out->replace(token_s, token.c_str());
break;
}
}
}
//-------------------------------------------------------------------------
struct graph_data_t
{
// Currently shown nodes data. Since we want to have some
// nodes show a special color, we keep a 'live' version of
// their text, which contains that information.
qstrvec_t live;
size_t levels_shown = 4;
void refresh(mutable_graph_t *g);
};
//-------------------------------------------------------------------------
void graph_data_t::refresh(mutable_graph_t *g)
{
QASSERT(0, levels_shown < qnumber(levels_offsets));
// Clear nodes & edges information
g->clear();
// Add nodes
const size_t nnodes = levels_offsets[levels_shown];
g->resize(nnodes);
// Add edges
for ( size_t i = 0; i < nparenthood; ++i )
{
const parenthood_t &p = parenthood_data[i];
int c_n = character_to_node_number(p.child);
if ( c_n >= g->size() )
break; // means we are not showing this (and the following) level(s)
int p0_n = character_to_node_number(p.parent0);
int p1_n = character_to_node_number(p.parent1);
g->add_edge(p0_n, c_n, nullptr);
g->add_edge(p1_n, c_n, nullptr);
}
// Generate names w/ possible colors
live.resize(nnodes);
for ( size_t i = 0; i < nnodes; ++i )
gen_character_name(&live[i], node_number_to_character(i));
// Clear previously-registered custom text & background color.
// (We could be smarter and move those from old nodes to new
// nodes, but this would only bring little benefit in the
// context of this sample.)
for ( size_t i = 0; i < nnodes; ++i )
del_node_info(g->gid, i);
}
//-------------------------------------------------------------------------
struct plugin_ctx_t;
//-------------------------------------------------------------------------
// A base action handler, ensuring the action is only available on the
// right widget, and possibly only if a (or more) node(s) is(are)
// selected.
struct base_ugraph_ah_t : public action_handler_t
{
plugin_ctx_t &plg;
bool requires_node;
base_ugraph_ah_t(
plugin_ctx_t &_plg,
bool _requires_node=false)
: plg(_plg),
requires_node(_requires_node) {}
virtual action_state_t idaapi update(action_update_ctx_t *ctx) override;
struct node_visitor_t
{
virtual ~node_visitor_t() {}
virtual bool on_node(int node, node_info_t &ni) newapi = 0;
};
protected:
bool get_nodes(
intvec_t *out,
const action_ctx_base_t &ctx) const;
bool for_each_node(
const action_ctx_base_t &ctx,
node_visitor_t &visitor);
};
//-------------------------------------------------------------------------
struct change_layout_ah_t : public base_ugraph_ah_t
{
change_layout_ah_t(plugin_ctx_t &_plg)
: base_ugraph_ah_t(_plg) {}
virtual int idaapi activate(action_activation_ctx_t *ctx) override;
};
//-------------------------------------------------------------------------
struct modify_levels_ah_t : public base_ugraph_ah_t
{
int inc;
modify_levels_ah_t(plugin_ctx_t &_plg, int _inc)
: base_ugraph_ah_t(_plg),
inc(_inc) {}
virtual int idaapi activate(action_activation_ctx_t *ctx) override;
virtual action_state_t idaapi update(action_update_ctx_t *ctx) override;
private:
size_t compute_levels_to_show() const;
};
//-------------------------------------------------------------------------
struct set_custom_text_ah_t : public base_ugraph_ah_t
{
set_custom_text_ah_t(plugin_ctx_t &_plg)
: base_ugraph_ah_t(_plg, /*_requires_node=*/ true) {}
virtual int idaapi activate(action_activation_ctx_t *ctx) override;
};
//-------------------------------------------------------------------------
struct set_custom_bgcolor_ah_t : public base_ugraph_ah_t
{
set_custom_bgcolor_ah_t(plugin_ctx_t &_plg)
: base_ugraph_ah_t(_plg, /*_requires_node=*/ true) {}
virtual int idaapi activate(action_activation_ctx_t *ctx) override;
};
#define ANAME_CHANGE_LAYOUT "ugraph:ChangeLayout"
#define ANAME_INC_LEVELS "ugraph:IncVisibleLevels"
#define ANAME_DEC_LEVELS "ugraph:DecVisibleLevels"
#define ANAME_SET_CUSTOM_TEXT "ugraph:SetCustomText"
#define ANAME_SET_CUSTOM_BGCOLOR "ugraph:SetCustomBgcolor"
//--------------------------------------------------------------------------
struct plugin_ctx_t : public plugmod_t, public event_listener_t
{
change_layout_ah_t change_layout_ah = change_layout_ah_t(*this);
const action_desc_t change_layout_desc = ACTION_DESC_LITERAL_PLUGMOD(
ANAME_CHANGE_LAYOUT,
"Change layout type",
&change_layout_ah,
this,
NULL,
NULL,
-1);
modify_levels_ah_t inc_levels_ah = modify_levels_ah_t(*this, 1);
const action_desc_t inc_levels_desc = ACTION_DESC_LITERAL_PLUGMOD(
ANAME_INC_LEVELS,
"Add level",
&inc_levels_ah,
this,
NULL,
NULL,
-1);
modify_levels_ah_t dec_levels_ah = modify_levels_ah_t(*this, -1);
const action_desc_t dec_levels_desc = ACTION_DESC_LITERAL_PLUGMOD(
ANAME_DEC_LEVELS,
"Remove level",
&dec_levels_ah,
this,
NULL,
NULL,
-1);
set_custom_text_ah_t set_custom_text_ah = set_custom_text_ah_t(*this);
const action_desc_t set_custom_text_desc = ACTION_DESC_LITERAL_PLUGMOD(
ANAME_SET_CUSTOM_TEXT,
"Custom text",
&set_custom_text_ah,
this,
NULL,
NULL,
-1);
set_custom_bgcolor_ah_t set_custom_bgcolor_ah = set_custom_bgcolor_ah_t(*this);
const action_desc_t set_custom_bgcolor_desc = ACTION_DESC_LITERAL_PLUGMOD(
ANAME_SET_CUSTOM_BGCOLOR,
"Custom background color",
&set_custom_bgcolor_ah,
this,
NULL,
NULL,
-1);
graph_data_t data;
graph_viewer_t *gv = nullptr;
plugin_ctx_t()
{
hook_event_listener(HT_VIEW, this);
}
~plugin_ctx_t()
{
// listeners are uninstalled automatically
// when the owner module is unloaded
}
virtual bool idaapi run(size_t) override;
virtual ssize_t idaapi on_event(ssize_t code, va_list va) override;
static ssize_t idaapi gr_callback(void *ud, int code, va_list va);
};
//--------------------------------------------------------------------------
ssize_t idaapi plugin_ctx_t::gr_callback(void *ud, int code, va_list va)
{
// Please refer to the SDK's graph.hpp for an explanation
// of the notifications, and their parameters
plugin_ctx_t &ctx = *(plugin_ctx_t *)ud;
ssize_t result = 0;
switch ( code )
{
case grcode_calculating_layout:
msg("calculating graph layout...\n");
break;
case grcode_clicked:
{
graph_viewer_t *v = va_arg(va, graph_viewer_t *); qnotused(v);
selection_item_t *it = va_arg(va, selection_item_t *); qnotused(it);
graph_item_t *m = va_arg(va, graph_item_t *);
msg("clicked on ");
switch ( m->type )
{
case git_none:
msg("background\n");
break;
case git_edge:
msg("edge (%d, %d)\n", m->e.src, m->e.dst);
break;
case git_node:
msg("node %d\n", m->n);
break;
case git_tool:
msg("toolbutton %d\n", m->b);
break;
case git_text:
msg("text (x,y)=(%d,%d)\n", m->p.x, m->p.y);
break;
case git_elp:
msg("edge layout point (%d, %d) #%d\n", m->elp.e.src, m->elp.e.dst, m->elp.pidx);
break;
}
}
break;
case grcode_dblclicked:
{
graph_viewer_t *v = va_arg(va, graph_viewer_t *);
selection_item_t *s = va_arg(va, selection_item_t *);
msg("%p: dblclicked on ", v);
if ( s == NULL )
msg("background\n");
else if ( s->is_node )
msg("node %d\n", s->node);
else
msg("edge (%d, %d) layout point #%d\n", s->elp.e.src, s->elp.e.dst, s->elp.pidx);
}
break;
case grcode_creating_group:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
intvec_t &nodes = *va_arg(va, intvec_t *);
msg("%p: creating group", g);
for ( intvec_t::iterator p=nodes.begin(); p != nodes.end(); ++p )
msg(" %d", *p);
msg("...\n");
}
break;
case grcode_deleting_group:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
int group = va_argi(va, int);
msg("%p: deleting group %d\n", g, group);
}
break;
case grcode_group_visibility:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
int group = va_argi(va, int);
bool expand = va_argi(va, bool);
msg("%p: %scollapsing group %d\n", g, expand ? "un" : "", group);
}
break;
case grcode_gotfocus:
{
graph_viewer_t *g = va_arg(va, graph_viewer_t *);
msg("%p: got focus\n", g);
}
break;
case grcode_lostfocus:
{
graph_viewer_t *g = va_arg(va, graph_viewer_t *);
msg("%p: lost focus\n", g);
}
break;
case grcode_user_refresh:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
msg("%p: refresh\n", g);
ctx.data.refresh(g);
result = true;
}
break;
case grcode_user_text:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
int node = va_arg(va, int);
const char **text = va_arg(va, const char **);
bgcolor_t *bgcolor = va_arg(va, bgcolor_t *);
*text = ctx.data.live[node].c_str();
if ( bgcolor != NULL )
*bgcolor = DEFCOLOR;
result = true;
qnotused(g);
}
break;
case grcode_user_size:
// result is 0 -> ida will calculate the node size based on the node text
break;
case grcode_user_title:
// result is 0 -> ida will draw the node title itself
break;
case grcode_user_draw:
// result is 0 -> ida will draw the node text itself
break;
case grcode_user_hint:
{
mutable_graph_t *g = va_arg(va, mutable_graph_t *);
int mousenode = va_argi(va, int);
int mouseedge_src = va_argi(va, int);
int mouseedge_dst = va_argi(va, int);
char **hint = va_arg(va, char **);
char buf[MAXSTR];
buf[0] = '\0';
if ( mousenode != -1 )
qsnprintf(buf, sizeof(buf), "My fancy hint for node %d", mousenode);
else if ( mouseedge_src != -1 )
qsnprintf(buf, sizeof(buf), "Hovering on (%d,%d)", mouseedge_src, mouseedge_dst);
if ( buf[0] != '\0' )
*hint = qstrdup(buf);
result = true; // use our hint
qnotused(g);
}
break;
}
return result;
}
//-------------------------------------------------------------------------
ssize_t idaapi plugin_ctx_t::on_event(ssize_t code, va_list va)
{
if ( code == view_close )
{
TWidget *view = va_arg(va, TWidget *);
if ( view == (TWidget *)gv )
gv = nullptr;
}
return 0;
}
//-------------------------------------------------------------------------
action_state_t idaapi base_ugraph_ah_t::update(action_update_ctx_t *ctx)
{
if ( ctx->widget != (TWidget *) plg.gv )
return AST_DISABLE_FOR_WIDGET;
if ( requires_node )
{
// If this requires nodes, we want to be called again as
// soon as something (i.e., the selection) changes
return get_nodes(nullptr, *ctx) ? AST_ENABLE : AST_DISABLE;
}
else
{
return AST_ENABLE_FOR_WIDGET;
}
}
//-------------------------------------------------------------------------
bool base_ugraph_ah_t::get_nodes(
intvec_t *out,
const action_ctx_base_t &ctx) const
{
screen_graph_selection_t *s = ctx.graph_selection;
if ( s == nullptr )
return false;
intvec_t tmp;
size_t nitems = s->size();
for ( size_t i = 0; i < nitems; ++i )
{
const selection_item_t &item = s->at(i);
if ( item.is_node )
tmp.push_back(item.node);
}
bool ok = !tmp.empty();
if ( out != nullptr )
out->swap(tmp);
return ok;
}
//-------------------------------------------------------------------------
bool base_ugraph_ah_t::for_each_node(
const action_ctx_base_t &ctx,
node_visitor_t &visitor)
{
mutable_graph_t *g = get_viewer_graph(plg.gv);
intvec_t nodes;
bool ok = get_nodes(&nodes, ctx);
if ( ok )
{
size_t nnodes = nodes.size();
for ( size_t i = 0; i < nnodes; ++i )
{
int node = nodes[i];
node_info_t ni;
get_node_info(&ni, g->gid, node);
visitor.on_node(node, ni);
uint32 niflags = ni.get_flags_for_valid();
if ( niflags != 0 )
set_node_info(g->gid, node, ni, niflags);
else
del_node_info(g->gid, node);
}
}
return ok;
}
//-------------------------------------------------------------------------
int idaapi change_layout_ah_t::activate(action_activation_ctx_t *)
{
mutable_graph_t *g = get_viewer_graph(plg.gv);
int code = ask_buttons(
"Circle", "Tree", "Digraph", 1, "Please select layout type");
g->current_layout = code + 2;
g->circle_center = point_t(200, 200);
g->circle_radius = 200;
refresh_viewer(plg.gv);
return 1;
}
//-------------------------------------------------------------------------
int idaapi modify_levels_ah_t::activate(action_activation_ctx_t *)
{
plg.data.levels_shown = compute_levels_to_show();
refresh_viewer(plg.gv);
return 1;
}
//-------------------------------------------------------------------------
action_state_t idaapi modify_levels_ah_t::update(action_update_ctx_t *ctx)
{
action_state_t state = base_ugraph_ah_t::update(ctx);
if ( !is_action_enabled(state) )
return state;
const size_t next = compute_levels_to_show();
return next > 0 && next <= nlevels ? AST_ENABLE : AST_DISABLE;
}
//-------------------------------------------------------------------------
size_t modify_levels_ah_t::compute_levels_to_show() const
{
return plg.data.levels_shown + inc;
}
//-------------------------------------------------------------------------
int idaapi set_custom_text_ah_t::activate(action_activation_ctx_t *ctx)
{
struct ida_local visitor_t : public node_visitor_t
{
qstring text;
virtual ~visitor_t() {}
virtual bool on_node(int, node_info_t &ni) override
{
ni.text = text;
return true;
}
};
visitor_t visitor;
return ask_text(&visitor.text, 256, nullptr, "Please enter node custom text")
&& for_each_node(*ctx, visitor);
}
//-------------------------------------------------------------------------
int idaapi set_custom_bgcolor_ah_t::activate(action_activation_ctx_t *ctx)
{
struct ida_local visitor_t : public node_visitor_t
{
bgcolor_t bg_color = DEFCOLOR;
virtual ~visitor_t() {}
virtual bool on_node(int, node_info_t &ni) override
{
ni.bg_color = bg_color;
return true;
}
};
visitor_t visitor;
static const char form[] =
"Please pick a color\n"
"\n"
"<~C~olor:K::6::>\n";
CASSERT(sizeof(visitor.bg_color) == sizeof(bgcolor_t));
return ask_form(form, &visitor.bg_color)
&& for_each_node(*ctx, visitor);
}
//--------------------------------------------------------------------------
static plugmod_t *idaapi init()
{
if ( !is_idaq() )
return nullptr;
return new plugin_ctx_t;
}
//--------------------------------------------------------------------------
static const char wanted_title[] = "Sample graph";
bool idaapi plugin_ctx_t::run(size_t)
{
TWidget *widget = find_widget(wanted_title);
if ( widget == nullptr )
{
// get a unique graph id
netnode id;
id.create("$ ugraph sample");
gv = create_graph_viewer(wanted_title, id, gr_callback, this, 0);
if ( gv != nullptr )
{
display_widget(gv, WOPN_DP_TAB);
viewer_fit_window(gv);
register_action(change_layout_desc);
register_action(inc_levels_desc);
register_action(dec_levels_desc);
register_action(set_custom_text_desc);
register_action(set_custom_bgcolor_desc);
widget = find_widget(wanted_title);
attach_action_to_popup(widget, nullptr, change_layout_desc.name);
attach_action_to_popup(widget, nullptr, inc_levels_desc.name);
attach_action_to_popup(widget, nullptr, dec_levels_desc.name);
attach_action_to_popup(widget, nullptr, set_custom_text_desc.name, "Set/");
attach_action_to_popup(widget, nullptr, set_custom_bgcolor_desc.name, "Set/");
}
}
else
{
close_widget(widget, 0);
}
return true;
}
//--------------------------------------------------------------------------
static const char comment[] = "This is a sample graph plugin.";
static const char help[] =
"A sample graph plugin module\n"
"\n"
"This module shows you how to create a graph viewer.";
//--------------------------------------------------------------------------
// This is the preferred name of the plugin module in the menu system
// The preferred name may be overridden in plugins.cfg file
static const char wanted_name[] = "Create sample graph view";
// This is the preferred hotkey for the plugin module
// The preferred hotkey may be overridden in plugins.cfg file
static const char wanted_hotkey[] = "";
//--------------------------------------------------------------------------
//
// PLUGIN DESCRIPTION BLOCK
//
//--------------------------------------------------------------------------
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
PLUGIN_MULTI, // The plugin can work with multiple idbs in parallel
init, // initialize
nullptr,
nullptr, // invoke plugin
comment, // long comment about the plugin
// it could appear in the status line
// or as a hint
help, // multiline help about the plugin
wanted_name, // the preferred short name of the plugin
wanted_hotkey // the preferred hotkey to run the plugin
};