From 5b7b5519d955cd0c99d094ba140514e0a2b73083 Mon Sep 17 00:00:00 2001 From: Charles McGarvey Date: Wed, 27 Aug 2014 22:00:43 -0600 Subject: [PATCH] add plugin engine (supports C and Perl plugins) --- .gitignore | 2 + Makefile.am | 10 +- bootstrap | 3 +- configure.ac | 45 +- data/icons/Makefile.am | 2 + .../hicolor_status_22x22_prf-plugins.png | Bin 0 -> 1537 bytes .../hicolor_status_48x48_prf-plugins.png | Bin 0 -> 2269 bytes plugins/Makefile.am | 12 + plugins/hello.pl | 281 +++++ plugins/native.c | 57 + plugins/transfer-matcher.pl | 28 + src/HomeBank.pm | 335 ++++++ src/Makefile.am | 29 +- src/dsp_mainwindow.c | 33 +- src/ext-native.c | 192 +++ src/ext-perl.xs | 1042 +++++++++++++++++ src/ext-value.c | 82 ++ src/ext-value.h | 64 + src/ext.c | 327 ++++++ src/ext.h | 46 + src/hb-account.c | 18 +- src/hb-archive.c | 11 +- src/hb-assign.c | 9 +- src/hb-category.c | 11 +- src/hb-payee.c | 9 +- src/hb-preferences.c | 59 +- src/hb-preferences.h | 4 + src/hb-tag.c | 9 +- src/hb-transaction.c | 17 +- src/hb-xml.c | 8 + src/homebank.c | 48 +- src/homebank.h | 1 + src/refcount.h | 54 + src/typemap | 60 + src/ui-pref.c | 255 +++- src/ui-pref.h | 16 +- 36 files changed, 3125 insertions(+), 54 deletions(-) create mode 100644 data/icons/hicolor_status_22x22_prf-plugins.png create mode 100644 data/icons/hicolor_status_48x48_prf-plugins.png create mode 100644 plugins/Makefile.am create mode 100644 plugins/hello.pl create mode 100644 plugins/native.c create mode 100644 plugins/transfer-matcher.pl create mode 100644 src/HomeBank.pm create mode 100644 src/ext-native.c create mode 100644 src/ext-perl.xs create mode 100644 src/ext-value.c create mode 100644 src/ext-value.h create mode 100644 src/ext.c create mode 100644 src/ext.h create mode 100644 src/refcount.h create mode 100644 src/typemap diff --git a/.gitignore b/.gitignore index 894d66d..67b5baf 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,9 @@ /po/POTFILES /po/stamp-it /src/.deps +/src/ext-perl.c /src/homebank +/src/perlxsi.c /stamp-h1 Makefile Makefile.in diff --git a/Makefile.am b/Makefile.am index e91e4b7..ac9cc1c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,9 @@ # HomeBank Makefile.am +ACLOCAL_AMFLAGS = -I m4 + #SUBDIRS = src -SUBDIRS = src data images mime po doc +SUBDIRS = src data images mime po doc plugins # don't forget to do a 'make check' @@ -18,3 +20,9 @@ DISTCLEANFILES = ... intltool-extract \ intltool-merge \ intltool-update \ po/.intltool-merge-cache + +run: all + PERL5LIB=src src/homebank + +debug: all + PERL5LIB=src gdb src/homebank diff --git a/bootstrap b/bootstrap index 83328e3..616d23a 100755 --- a/bootstrap +++ b/bootstrap @@ -1,6 +1,7 @@ #!/bin/sh -aclocal \ +libtoolize \ +&& aclocal \ && autoheader \ && automake --gnu --add-missing \ && autoconf diff --git a/configure.ac b/configure.ac index 601558b..dfaa66d 100644 --- a/configure.ac +++ b/configure.ac @@ -8,6 +8,10 @@ AM_CONFIG_HEADER(config.h) AM_INIT_AUTOMAKE([1.9 foreign]) +LT_PREREQ([2.2]) +LT_INIT([dlopen]) +AC_CONFIG_MACRO_DIR([m4]) + # If the source code has changed at all, increment REVISION # If any interfaces have been added, removed, or changed, increment CURRENT, and set REVISION to 0. # If any interfaces have been added since the last public release, then increment AGE. @@ -21,7 +25,7 @@ AC_PROG_INSTALL AC_PROG_INTLTOOL # Checks for libraries. -PKG_CHECK_MODULES(DEPS, gtk+-2.0 >= 2.24 glib-2.0 >= 2.28) +PKG_CHECK_MODULES(DEPS, gtk+-2.0 >= 2.24 glib-2.0 >= 2.28 gmodule-2.0 >= 2.28) AC_SUBST(DEPS_CFLAGS) AC_SUBST(DEPS_LIBS) AC_CHECK_LIB(m, pow) @@ -58,7 +62,7 @@ then then AC_CHECK_LIB(ofx, ofx_set_status_cb, OFX_0_7="-DOFX_ENABLE") DEPS_LIBS="-lofx ${DEPS_LIBS}" - CFLAGS="${CFLAGS} $OFX_0_7" + CPPFLAGS="${CPPFLAGS} $OFX_0_7" else noofx=true AC_MSG_RESULT([Libofx header missing. Check your libofx installation]) @@ -70,6 +74,41 @@ else fi AM_CONDITIONAL(NOOFX, test x$noofx = xtrue) +AC_ARG_WITH(perl, + [ --with-perl build with perl plug-in support [default=without]], + [build_perl=$withval], + [build_perl=no] +) +if test x$build_perl != xno +then + test x$build_perl != xyes -a -x "$build_perl" && PERL=$build_perl + AC_PATH_PROG(PERL, perl, perl) + AC_MSG_CHECKING(if perl can be embedded) + if $PERL -MExtUtils::Embed -e "use v5.8" >/dev/null 2>&1 + then + AC_MSG_RESULT(yes) + CPPFLAGS="${CPPFLAGS} -DPERL_ENABLE" + PERL_CPPFLAGS="`$PERL -MExtUtils::Embed -e ccopts`" + PERL_OBJS="ext-perl.o perlxsi.o" + PERL_PRIVLIBEXP="`$PERL -MConfig -e 'print $Config{privlibexp}'`" + PERL_SITELIBEXP="`$PERL -MConfig -e 'print $Config{sitelibexp}'`" + DEPS_LIBS="`$PERL -MExtUtils::Embed -e ldopts` ${DEPS_LIBS}" + if test -e "$PERL_SITELIBEXP/ExtUtils/xsubpp" + then + XSUBPP="$PERL $PERL_SITELIBEXP/ExtUtils/xsubpp" + else + XSUBPP="$PERL $PERL_PRIVLIBEXP/ExtUtils/xsubpp" + fi + else + AC_MSG_ERROR([no working perl found, or perl not version >= 5.8]) + fi +fi +AC_SUBST(PERL_CPPFLAGS) +AC_SUBST(PERL_OBJS) +AC_SUBST(PERL_PRIVLIBEXP) +AC_SUBST(PERL_SITELIBEXP) +AC_SUBST(XSUBPP) + # Checks for header files. AC_HEADER_STDC AC_CHECK_HEADERS([libintl.h locale.h stdlib.h string.h]) @@ -104,6 +143,7 @@ mime/Makefile po/Makefile.in doc/Makefile doc/images/Makefile +plugins/Makefile ]) AC_OUTPUT @@ -116,6 +156,7 @@ echo $PACKAGE $VERSION echo echo Compiler................ : $CC echo Build with OFX support.. : $build_ofx +echo Build with perl support. : $build_perl if test "x$noofx" = "xtrue" ; then echo ........................ : **error** libofx header is missing, ofx feature will be disabled. Check your libofx installation fi diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am index 266287e..e7880ab 100644 --- a/data/icons/Makefile.am +++ b/data/icons/Makefile.am @@ -114,6 +114,8 @@ private_icons = \ hicolor_status_48x48_prf-report.png \ hicolor_status_22x22_prf-import.png \ hicolor_status_48x48_prf-import.png \ + hicolor_status_22x22_prf-plugins.png \ + hicolor_status_48x48_prf-plugins.png \ $(NULL) EXTRA_DIST = \ diff --git a/data/icons/hicolor_status_22x22_prf-plugins.png b/data/icons/hicolor_status_22x22_prf-plugins.png new file mode 100644 index 0000000000000000000000000000000000000000..64cdca6be2d37c543065d174e5aa0d4956632160 GIT binary patch literal 1537 zcmV+c2LAbpP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x1oKHmK~y-)m6c0uT}K_pe{;^cPy5=}xhK8YPFy>766b+w zT1AOUX)0POD6D7@T~I};1naVa0urh~0;D1qSyXD-AhF=FDN<=gQxqXV6>!?rP2wtU zh||Q+>t}s@ALpET{8`*2iW{@&NVAyNZ$>lU_l^3>_fL-~qzH=7Qba^N&zsf8uAe>q z{5|8r@x#M^`@-IVvgdskuwtX$RPTK7(Mt!0`hIox^u05J{%SZpIM9!XY#A_4W1PiT z9vMG-4>*hyCa356A3kz$+pU9EYKWTp4p=Egl&)}L?Nurr9;?lSLtUrXSr|gZ-B}nX znp9h&mf0R0r-pibH`r&nxlX~K=ArO&yjNSGR$rp125@oTYhu}n02t01o&(O3JG}45fxksrMM~PF5F@{#0&>wbx`GJG`e!jEP zv*+^EwKG%G?u8&VhSUhP`ZT*M@ACQHDo%t{mq|^7Jw06-#Y?{lpL*i4pN#AqJ)X%G7)tch3$@yKpp7N54r3f^(E_zX69KY*7T@!T^(Gsw zRTQwY5uuGCwT{#}8jVJ{e|YfOzFoVIB?Po$O=meQbd^e93aoW#YpIron2R3e(wrq~ zMTDg*92w~5>SDz8$VI95CLm#ci^16 zWeA5(9SVn730f6uOIOkAG;hbY(exBvU1*3JEg7P}My0X3f zyS>g*7b1dE3Z)eJVsZA`iVZ(hCV2bmpKr*j)OE-eJU?~}{t5!aSoM@Mm; zbiMxO4<2t{X4>Gn=YC3%$t)EMg*2DTAoZ|kIr^Pbr~dT7u}At`kmL4$ps*+f`Fw7B@Z`k1 zv$05UK!Gc7SOEif`vGBY|fH##vjEig7ZFfbJ*Kav0d03~!qSaf7z zbY(hiZ)9m^c>ppnF*7YOF)c7MR5CLR?VhFG|91xdp zICvh2&w&DvEDrz#5DNpte+GLKZH5Q0zYd58Kz469c$RGa009Jw0fx6`p!EdT%gXZZ7XU`ry98^{F!0R)VJe+<9>{v%1#yc;qW^!v;f7WG4;dMfk~{zqK;UBZ52-QmmjP?h2lfCtF+f2Q0tg^*5+b!2 z{Ri>@!(Xi8dPOEqd;s68?I0^m5676em6*F9X zy#Y_CVfR4wVI{2U^+f|1g4KG7PC`zZZP`GX4l`X1p7;P41|~*Eh8;)FFdRB_rR3em zPdq^Rl6}*wz5)agBnGf};KK6_41EE)3<0`!SS{UtaXyCF-OAP2+dG#JF%)f>%;2xm zLsSenrnfi(Jyq^tX29cQYr$~z_8V{kp{}mU@Z;AXhV2JWg#Ksvw;!l<6+i&7{QC!r z0n88p2`TdbW!QFc4$(;gms&+ZWw3dFaU_+0XySi<|BBI2QINHvQeq;)kDfmB0(k%+fN&YL>o10|*^&&91c6zMu1bB*AjHiEwgA@b`1SWM!-ucG8D4(+fz?6DVZIbbFo4zGVC6B`1UKqrNVrv3}#6j|;f)^iukkttSxg8*Yfc3y% z(wo2kaKykr2D*WNz?J~W?EnEpatwf~bbPH*ko-S-43+`2LLq_x}S26hHv6klGgKEA!T9e)1kKEvymFPwpV0W~dMyn4QV{l>t|!uA6ofLKUPLimbUvXe8=4cB)r zE(WpHU88%RKDZr%&jUbDGJFC0;K%0=*8u{EiIVOd)y9Q@ZUE+^hamj(%ZD}h?%n-x zL1;?28^{oub5a^-U0*=I0-Q^kv2&E_RTAXub)0)mE-2)V-OY*BfCfT;P}>U zs!rj1p54C{qNQtq*^GGimf_{|rwo67em{Np@PaOdV#hA$uAGrW2I z8mk%+QBejSenAEmb!`R}4K0#A0E~st3g(_`?%uv-#kGI8l#R1HirOdlZZSN4^5os` z?_Uo9)&2qqAaEXFW+q(p?cceD;mY|lpwi;W-`_tke0}?T^NTA-QATAYbgYeke0p!p z%EjY#@74{OXHOn7IJ)}~l?RZ)!&5u9$y@m7T{?TR;OEaDT&_L=z`}xw;mq;F4A(B4 zW%%{|>z?OtlL&6*%%?E3BMxgVZ4E?=;Mb?SeA`@Hzmi~CbP zJihfD#0Lly*_AoDJ~+Ia1y1U9S#sc;IUCQHa5J9+ zo zLGYpN>liLvzUrG(QYjDe1Zsu!r(#M^yPkorHp7*-;4*@N2%{+)UIz#ua7|1ofRMqB zj|>b#O4^`6)58*wdLTKF8dA*sM?oF{2q173ym`%X22egD7XQQ(;~+Uw%mUd>2>=Kn zMsYp+nJk>VY2;{ra$++hGGE5Rm#oD6=lkbbFD@QPCr2+p0D&9;8kCVFuO0vYELvO0 rn9P8DPEJJ$0ssNTIMkQh00bBSvoE=Blm +# WEBSITE: http://acme.tld/ +# (These comments are read, before the plugin is executed, to provide some +# information to HomeBank and the user about what this plugin is.) + +eval { HomeBank->version } or die "Cannot run outside of HomeBank"; + +use warnings; +use strict; + +use Scalar::Util qw/weaken/; + +#use Moose; + +#has "cool_beans", + #is => 'rw', + #isa => 'Str', + #lazy => 1, + #default => "Booya!!!"; + + +our $counter = 0; +our $temp; + +my $ACC; + +sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + + $self->on(account_inserted => sub { + my $acc = shift; + print "account inserted: ", Dumper($acc); + print "account name is ", $acc->name, " and balance is ", $acc->bank_balance, "\n"; + #$acc->name("FOOOOBAR!"); + if ($acc->name eq 'Vacation') { + $acc->remove; + $ACC = $acc; + } + print Dumper($acc->is_inserted); + if ($acc->is_inserted) { + print "IT IS INSERTED\n"; + } else { + print "not inserted\n"; + } + print Dumper($acc->transactions); + }); + + #print $self->cool_beans, "\n"; + #$self->cool_beans(123); + #print $self->cool_beans, "\n"; + + $self->create_menuitem; + + $self; +} + +sub on_create_main_window { + my $self = shift; + my $window = shift; + + if (!$window) { + require Gtk2; + $window = HomeBank->main_window; + } + + Dump($window); + print Dumper($window); + $window->set_title("foo bar baz"); + print $window->get_title, "\n"; + + HomeBank->hook("my_hook", $window); + + $self->create_menuitem; +} + +my $test_win; + +sub on_test { + my $self = shift; + require Gtk2; + + my $window = Gtk2::Window->new('toplevel'); + use Devel::Peek; + Dump($window); + print Dumper($window); + $window->set_title("Hello World"); + #$window->signal_connect(delete_event => sub { Gtk2->main_quit }); + $window->signal_connect(delete_event => sub { undef $test_win }); + + my $button = Gtk2::Button->new('Click Me!'); + Dump($button); + print Dumper($button); + $button->signal_connect(clicked => sub { + print "Hello Gtk2-Perl: $counter (perl plugin: $self)\n"; + $counter++; + #if ($temp->is_inserted) { + #print "$temp is inserted\n"; + #} else { + #print "$temp is NOT inserted\n"; + #} + #if ($counter == 5) { + #$temp = undef; + #} + my $acc = HomeBank::Account->get(rand(10)); + print "Changin account named ", $acc->name, " to ", $acc->name($acc), "\n"; + HomeBank->main_window->queue_draw; + + }); + $window->add($button); + + $window->show_all; + $test_win = $window; + + weaken $self; +} + +sub on_enter_main_loop { + my $self = shift; + + use Data::Dumper; + print Dumper(\@_); + my $t = HomeBank::Transaction->new; + print "Transaction:::::::: $t: ", $t->amount, "\n"; + + $temp = HomeBank::Account->get(7); + print "retained account: ", $temp->name, "\n"; + + #require Gtk2; + # + my $txn = HomeBank::Transaction->new; + $txn->amount(12.3456); + print Dumper($txn), $txn->amount, "\n"; + #$txn->open; + + my @ret = HomeBank->hook("my_hook", @_, $temp, [qw/foo bar baz/, $txn], { asf => 42, quux => \1, meh => HomeBank->main_window }); + #my @ret = HomeBank->hook("my_hook", @_, HomeBank->main_window, { + #foo => 'bar', baz => 42 + #}); + print Dumper(\@ret); + + print "adding back account...\n"; + $ACC->name("vacation with a different name"); + $ACC->insert; + HomeBank::Account->compute_balances; + print "account name is ", $ACC->name, " and balance is ", $ACC->balance, "\n"; + print Dumper($ACC->transactions); + + my $cloned = $ACC->clone; + $cloned->name("vacation copy"); + $cloned->insert; + #my $asdf = $cloned->open; + #$asdf->set_title("this is a new friggin account"); + + #my $z = HomeBank::Account->get_by_name('Checking'); + for my $xc (HomeBank::File->transactions) { + use DateTime; + my $num = $xc->date; + my $date = DateTime->new($xc->date)->datetime; + print "transaction of amount: ", $xc->amount, "\t", $xc->wording, ", ", $xc->info, ", $num, $date\n"; + } + + HomeBank::File->owner('Billy Murphy'); + #HomeBank::File->anonymize; + print HomeBank::File->owner, "\n"; + + HomeBank::File->baz($ACC); +} + +sub on_deep_hook_recursion { + my $self = shift; + my $level = shift; + print STDERR "recursion is too deep ($level)\n"; + exit -2; +} + +sub on_my_hook { + my $self = shift; + print "This is MY HOOK!!!!!!\n"; + print Dumper(\@_); + + print Dumper($_[2]); + Dump($_[2]); + if ($_[2]) { + print "meh\n"; + } + if ($_[2]->isa('HomeBank::Boolean')) { + print "it is a home;;boolean\n"; + } + if ($_[2]->isa('Types::Serialiser::Boolean')) { + print "it is a types serialiser thingy\n"; + } + if ($_[2]->isa('HomeBank::BooleanBase')) { + print "it is a base bool\n"; + } + + my $win = $_[6]; + if ($win && ref($win) eq 'HASH') { + my $w = $win->{meh}; + if ($w) { + $w->set_title("this is MY HOOK setting a window title"); + } + } + #print Dumper($acc); + #print "transferred account: ", $acc->name, "\n"; + + #my $fff = HomeBank::File->foo({foo => 'asdf', bar => 123456789}); + my $fff = HomeBank::File->meh([qw/hello this is a test 82/, \1, {foo => 'bar'}, 48]); + print Dumper($fff); + + print "my hook done\n"; +} + +sub on_unhandled { + my ($self, $hook) = @_; + warn "Unhandled hook '$hook'\n"; + #HomeBank->warn($hook, 'Hook not handled.'); +} + +sub on_main_window_disposal { + my $self = shift; + print "main window disposed so forgetting about merge id et al.\n"; + delete $self->{merge_id}; + delete $self->{action_group}; +} + +sub DESTROY { + my $self = shift; + print "DESTROYING HELLO WORLD!!!!!!\n"; + if ($test_win) { + print "there is a test_win...\n"; + } + $test_win->destroy if $test_win; + + $self->destroy_menuitem; +} + +sub destroy_menuitem { + my $self = shift; + + return unless $self->{merge_id}; + + my $ui_manager = HomeBank->main_ui_manager; + $ui_manager->remove_action_group($self->{action_group}); + $ui_manager->remove_ui($self->{merge_id}); +} + +sub create_menuitem { + my $self = shift; + + return if $self->{merge_id}; + + require Gtk2; + + my $ui_manager = HomeBank->main_ui_manager; + print Dumper($ui_manager); + return unless $ui_manager; + + $self->{merge_id} = $ui_manager->new_merge_id; + $self->{action_group} = Gtk2::ActionGroup->new('HelloActionGroup'); + + my $action = Gtk2::Action->new(name => 'HelloPlugin', label => 'Booyah!', stock_id => 'prf-plugins', tooltip => 'blaaaargh'); + $action->signal_connect(activate => sub { print "hello!!!!!!!!\n" }); + $self->{action_group}->add_action($action); + + $ui_manager->insert_action_group($self->{action_group}, -1); + $ui_manager->add_ui($self->{merge_id}, 'ui/MenuBar/PluginMenu', 'HelloPlugin', 'HelloPlugin', 'auto', ''); + #$self->{merge_id} = $ui_manager->new_merge_id; + $ui_manager->add_ui($self->{merge_id}, 'ui/ToolBar', 'HelloPluginTool', 'HelloPlugin', 'auto', ''); +} + +sub EXECUTE { + print "the perl plugin is being configured.....\n"; + HomeBank->info("Hello Prefs", "YEEEEEARGGH!!!!!"); +} + +#__PACKAGE__->meta->make_immutable; diff --git a/plugins/native.c b/plugins/native.c new file mode 100644 index 0000000..c0a5461 --- /dev/null +++ b/plugins/native.c @@ -0,0 +1,57 @@ + +#include + +#include "ext.h" + + +const gchar* metadata[] = { +"NAME: Some Native Plugin", +"VERSION: 0.0105", +"ABSTRACT: Native plugins are also possible.", +"AUTHOR: Charles McGarvey ", +"WEBSITE: http://acme.tld/", +}; + + +G_MODULE_EXPORT void load(void); +G_MODULE_EXPORT void unload(void); +G_MODULE_EXPORT void execute(void); + +G_MODULE_EXPORT void on_create_main_window(GList* args); +G_MODULE_EXPORT void on_enter_main_loop(GList* args); + + +G_MODULE_EXPORT void load() +{ + g_print("loading native plugin....... %p\n", load); +} + +G_MODULE_EXPORT void unload() +{ + g_print("destroy native plugin....... %p\n", unload); +} + +G_MODULE_EXPORT void execute() +{ + g_print("Configuring that native plugin!!!\n"); +} + +static GtkWidget* win = NULL; + +G_MODULE_EXPORT void on_create_main_window(GList* args) +{ + GList* it = g_list_first(args); + win = g_value_get_object(it->data); + /*gtk_window_set_title(GTK_WINDOW(GLOBALS->mainwindow), "This is the native hello-world plugin!");*/ +} + +G_MODULE_EXPORT void on_enter_main_loop(GList* args) +{ + g_print("setting main window title.....\n"); + if (win) { + gtk_window_set_title(GTK_WINDOW(win), "This is the native hello-world plugin!"); + } else { + g_printerr("the main window is not set :(\n"); + } +} + diff --git a/plugins/transfer-matcher.pl b/plugins/transfer-matcher.pl new file mode 100644 index 0000000..d173990 --- /dev/null +++ b/plugins/transfer-matcher.pl @@ -0,0 +1,28 @@ + +# NAME: Transfer Matcher +# VERSION: 0.01 +# ABSTRACT: Automatically find and pair together internal transfers. +# AUTHOR: Charles McGarvey +# WEBSITE: http://www.homebank.free.fr/ + +eval { HomeBank->version } or die "Cannot run outside of HomeBank"; + +use warnings FATAL => 'all'; +use strict; + +my $days = 3; + +sub on_transaction_inserted { + my ($self, $txn) = @_; + + my @match = grep { + $txn->account_num != $_->account_num && + $txn->amount == -$_->amount && + abs($txn->date - $_->date) <= $days + } HomeBank::File->transactions; + + return unless @match; + + $txn->pair_with(@match); +} + diff --git a/src/HomeBank.pm b/src/HomeBank.pm new file mode 100644 index 0000000..e976610 --- /dev/null +++ b/src/HomeBank.pm @@ -0,0 +1,335 @@ +package HomeBank; + +use warnings FATAL => 'all'; +use strict; + +use Symbol qw/delete_package/; + +=head1 NAME + +HomeBank - Perl plugin bindings for C + +=head1 SYNOPSIS + + # NAME: Example Plugin + + sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + + $self->on( + terminate => sub { + print "Terminating...\n"; + }, + ); + + $self; + } + + sub on_unhandled { + my ($self, $hook_id) = @_; + print "An unhandled hook named '$hook_id' was called.\n"; + } + +=head1 DESCRIPTION + +The C class provides the infrastructure for loading plugins and handling the registration and calling of +hooks. + +=head1 VARIABLES + +=head2 %plugins + +Contains all of the information about each loaded perl plugin. Plugins probably shouldn't mess around with this. + +=cut + +our %plugins; + +=head1 METHODS + +=head2 load_plugin $filepath + +Load a plugin with the given name. Dies if a plugin with the given name cannot be found or if the plugin couldn't +successfully be eval'd. L calls this to load enabled plugins; plugins themselves probably shouldn't ever use +this. + +=cut + +sub load_plugin { + my $filepath = shift; + + my $package = _valid_package_name($filepath); + $plugins{$package} ||= {}; + + my $mtime = -M $filepath; + if (defined $plugins{$package}->{mtime} && $plugins{$package}->{mtime} <= $mtime) { + warn "Already loaded $filepath"; + } else { + delete_package $package if exists $plugins{$package}->{mtime}; + + open my $fh, $filepath or die "Open '$filepath' failed ($!)"; + binmode $fh, 'utf8'; + local $/ = undef; + my $code = <$fh>; + close $fh; + + my $eval = qq/# line 1 "$filepath"\npackage $package; use base 'HomeBank::Plugin'; $code/; + { + my (%plugins, $mtime, $package); + eval "$eval; 1" or die $@; + } + + $plugins{$package}->{mtime} = $mtime; + } + if (!exists $plugins{$package}->{instance}) { + $plugins{$package}->{instance} = $package->new or die "Plugin instantiation failed"; + } +} + +=head2 unload_plugin $filepath + +The opposite of L. + +=cut + +sub unload_plugin { + my $filepath = shift; + my $package = _valid_package_name($filepath); + + return unless exists $plugins{$package}; + + if ($package->can('delete_package_on_unload') && $package->delete_package_on_unload) { + delete $plugins{$package}; + delete_package $package; + } else { + delete $plugins{$package}->{instance}; + delete $plugins{$package}->{hooks}; + } +} + +=head2 execute_action $filepath + +Allow the plugin specified by C<$filepath> to perform an action. This is called when the plugin is "activated" by the +user. Most plugins should run a modal dialog to allow the user to see and edit plugin preferences. + +=cut + +sub execute_action { + my $filepath = shift; + my $package = _valid_package_name($filepath); + + return unless exists $plugins{$package}; + + my $instance = $plugins{$package}->{instance}; + $instance->EXECUTE if $instance && $instance->can('EXECUTE'); +} + +=head2 read_metadata $filepath + +Get the metadata for a plugin without evaluating it. Plugin metadata should be in the first 100 lines of the plugin file +and should look something like this: + + # NAME: Foobar + # VERSION: 0.01 + # ABSTRACT: This plugin does something. + # AUTHOR: John Doe + # WEBSITE: http://acme.tld/ + +=cut + +sub read_metadata { + my $filepath = shift; + + my $package = _valid_package_name($filepath); + $plugins{$package} ||= {}; + + return $plugins{$package}->{metadata} if exists $plugins{$package}->{metadata}; + + my @keywords = qw/name version abstract author website/; + my $keywords = join('|', @keywords); + + my $metadata = {}; + open my $fh, $filepath or die "Open '$filepath' failed ($!)"; + my $count = 0; + for my $line (<$fh>) { + last if 100 < ++$count; + my ($key, $val) = $line =~ /^#[ \t]*($keywords)[ \t]*[=:](.*)/i; + if ($key && $val) { + $val =~ s/^\s*//; + $val =~ s/\s*$//; + $metadata->{lc $key} = $val; + } + } + close $fh; + + $plugins{$package}->{metadata} = $metadata; +} + +=head2 call_hook $hook_id, ... + +Invoke each perl plugins' hook handlers for the given hook. Additional arguments are passed through to each handler. +Plugins shouldn't use this. + +=cut + +sub call_hook { + my $hook = shift; + + $hook =~ s/[.-]/_/g; + + for my $package (keys %plugins) { + my $hooks = ($plugins{$package} ||= {})->{hooks} ||= {}; + my $count = 0; + for my $cb (@{$hooks->{$hook} ||= []}) { + eval { $cb->(@_); 1 } or warn $@; + $count++; + } + if ($count == 0) { + for my $cb (@{$hooks->{unhandled} ||= []}) { + eval { $cb->($hook, @_); 1 } or warn $@; + } + } + } +} + +=head2 register_method_hooks $plugin + +Register hooks defined as methods that begin with `on_'. + +=cut + +sub register_method_hooks { + my $plugin = shift; + my $package = ref $plugin; + + no strict 'refs'; + my %subs = map { $_ =~ /^on_(.+)/ ? ($1 => $_) : () } keys %{"${package}::"}; + use strict 'refs'; + + register_hooks($plugin, %subs); +} + +=head2 register_hooks $plugin, %hooks + +Register hooks for a plugin. + +=cut + +sub register_hooks { + my ($plugin, %hooks) = @_; + my $package = ref $plugin; + + my $hooks = ($plugins{$package} ||= {})->{hooks} ||= {}; + for my $hook (keys %hooks) { + if (!ref($hooks{$hook}) && defined &{"${package}::$hooks{$hook}"}) { + push @{$hooks->{$hook} ||= []}, sub { unshift @_, $plugin; goto &{"${package}::$hooks{$hook}"} }; + } elsif (ref($hooks{$hook}) eq 'CODE') { + push @{$hooks->{$hook} ||= []}, $hooks{$hook}; + } else { + warn "Hook callback is unusable"; + } + } +} + +=head2 unregister_hooks $package, [@hooks] + +Unregister hooks for a package. If no hooks are specified, B hooks will be unregistered. + +=cut + +sub unregister_hooks { + my ($package, @hooks) = @_; + + if (@hooks) { + for my $hook (@hooks) { + (($plugins{$package} ||= {})->{hooks} ||= {})->{$hook} = []; + } + } else { + ($plugins{$package} ||= {})->{hooks} = {}; + } +} + +=head2 _valid_package_name $string + +Turn a string into a valid name of a package. + +=cut + +sub _valid_package_name { + my $str = shift; + $str =~ s|.*?([^/\\]+)\.pl$|$1|; + $str =~ s|([^A-Za-z0-9\/_])|sprintf("_%2x",unpack("C",$1))|eg; + $str =~ s|/(\d)|sprintf("/_%2x",unpack("C",$1))|eg; + $str =~ s|[/_]|::|g; + "HomeBank::Plugin::$str"; +} + + +package HomeBank::Boolean; + +use overload + '0+' => sub { ${$_[0]} }, + '++' => sub { $_[0] = ${$_[0]} + 1 }, + '--' => sub { $_[0] = ${$_[0]} - 1 }, + fallback => 1; + +package Types::Serialiser::Boolean; +@HomeBank::Boolean::ISA = Types::Serialiser::Boolean::; + + +package HomeBank::Plugin; + +sub new { + my ($class, $self) = (shift, shift || {}); + bless $self, $class; + HomeBank::register_method_hooks($self); + $self; +} + +sub on { + goto &HomeBank::register_hooks; +} + +sub off { + goto &HomeBank::unregister_hooks; +} + + +package HomeBank::Transaction; + +sub datetime { + require DateTime; + require DateTime::Format::Strptime; + my $dt = DateTime->new(shift->date); + $dt->set_formatter(DateTime::Format::Strptime->new(pattern => '%Y-%m-%d')); + $dt; +} + + +=head1 AUTHOR + +Charles McGarvey + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2013 Charles McGarvey. + +This file is part of HomeBank. + +HomeBank is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +HomeBank is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +=cut + +1; diff --git a/src/Makefile.am b/src/Makefile.am index 4115add..71c57f1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,7 +1,8 @@ common_defines = \ -DSHARE_DIR=\""$(pkgdatadir)"\" \ - -DDATA_DIR=\""$(datadir)"\" + -DDATA_DIR=\""$(datadir)"\" \ + -DPKGLIB_DIR=\""$(pkglibdir)"\" bin_PROGRAMS = homebank @@ -108,11 +109,33 @@ homebank_SOURCES = \ ui-widgets.c \ ui-widgets.h \ gtk-chart-colors.c \ - gtk-chart-colors.h + gtk-chart-colors.h \ + refcount.h \ + ext.c \ + ext.h \ + ext-value.c \ + ext-value.h \ + ext-native.c \ + ext-perl.xs -homebank_LDADD = $(DEPS_LIBS) +EXTRA_homebank_DEPENDENCIES = $(PERL_OBJS) + +homebank_LDADD = $(PERL_OBJS) $(DEPS_LIBS) AM_CPPFLAGS = \ $(DEPS_CFLAGS) \ $(common_defines) +$(PERL_OBJS): CPPFLAGS += $(PERL_CPPFLAGS) + +ext-perl.c: ext-perl.xs typemap + $(XSUBPP) -typemap $(PERL_PRIVLIBEXP)/ExtUtils/typemap -typemap typemap $< >$@ + +perlxsi.c: Makefile + $(PERL) -MExtUtils::Embed -e xsinit -- -std HomeBank + +CLEANFILES = ext-perl.c perlxsi.c + +pluginsupportdir = $(pkglibdir) +pluginsupport_DATA = HomeBank.pm + diff --git a/src/dsp_mainwindow.c b/src/dsp_mainwindow.c index 4ada4ac..98c5ea1 100644 --- a/src/dsp_mainwindow.c +++ b/src/dsp_mainwindow.c @@ -22,6 +22,8 @@ #include "dsp_mainwindow.h" +#include "ext.h" + #include "list_account.h" #include "list_upcoming.h" #include "list_topspending.h" @@ -104,6 +106,8 @@ static void ui_mainwindow_action_budget(void); static void ui_mainwindow_action_balance(void); static void ui_mainwindow_action_vehiclecost(void); +static void ui_mainwindow_action_pluginprefs(void); + static void ui_mainwindow_action_import(void); static void ui_mainwindow_action_export(void); static void ui_mainwindow_action_anonymize(void); @@ -137,6 +141,8 @@ void ui_mainwindow_recent_add (struct hbfile_data *data, const gchar *path); static void ui_mainwindow_scheduled_populate(GtkWidget *widget, gpointer user_data); void ui_mainwindow_scheduled_postall(GtkWidget *widget, gpointer user_data); +static void ui_mainwindow_showprefs(gint page); + extern gchar *CYA_ACC_TYPE[]; @@ -150,6 +156,7 @@ static GtkActionEntry entries[] = { { "ManageMenu" , NULL, N_("_Manage"), NULL, NULL, NULL }, { "TransactionMenu", NULL, N_("_Transactions"), NULL, NULL, NULL }, { "ReportMenu" , NULL, N_("_Reports"), NULL, NULL, NULL }, + { "PluginMenu" , NULL, N_("_Plugins"), NULL, NULL, NULL }, { "HelpMenu" , NULL, N_("_Help"), NULL, NULL, NULL }, // { "Import" , NULL, N_("Import") }, @@ -197,6 +204,8 @@ static GtkActionEntry entries[] = { { "RBalance" , HB_STOCK_REP_BALANCE, N_("Balance...") , NULL, N_("Open the Balance report"), G_CALLBACK (ui_mainwindow_action_balance) }, { "RVehiculeCost" , HB_STOCK_REP_CAR , N_("_Vehicle cost...") , NULL, N_("Open the Vehicle cost report"), G_CALLBACK (ui_mainwindow_action_vehiclecost) }, + { "PluginPreferences", "prf-plugins", N_("_Plugins..."), "U", N_("Configure plugin preferences"), G_CALLBACK(ui_mainwindow_action_pluginprefs) }, + /* HelpMenu */ { "Contents" , GTK_STOCK_HELP , N_("_Contents") , "F1", N_("Documentation about HomeBank"), G_CALLBACK (ui_mainwindow_action_help) }, { "Welcome" , NULL , N_("Show welcome dialog...") , NULL, NULL , G_CALLBACK (ui_mainwindow_action_help_welcome) }, @@ -277,6 +286,11 @@ static const gchar *ui_info = " " " " " " +" " +" " +" " +" " +" " " " " " " " @@ -310,6 +324,7 @@ static const gchar *ui_info = " " " " " " +" " " " ""; @@ -394,7 +409,8 @@ GdkPixbuf *pixbuf; static const gchar *authors[] = { "Lead developer:\n" \ "Maxime DOYEN", - "\nContributor:\n" \ + "\nContributors:\n" \ + "Charles MCGARVEY (Plugin system, Perl support)\n" \ "Ga\xc3\xabtan LORIDANT (Maths formulas for charts)\n", NULL }; @@ -628,10 +644,15 @@ static void ui_mainwindow_action_defassign(void) static void ui_mainwindow_action_preferences(void) +{ + ui_mainwindow_showprefs(PREF_GENERAL); +} + +static void ui_mainwindow_showprefs(gint page) { struct hbfile_data *data = g_object_get_data(G_OBJECT(GLOBALS->mainwindow), "inst_data"); - defpref_dialog_new(); + defpref_dialog_new(page); if(!PREFS->euro_active) { GtkToggleAction *action = (GtkToggleAction *)gtk_ui_manager_get_action(data->manager, "/MenuBar/ViewMenu/AsMinor"); @@ -745,6 +766,11 @@ static void ui_mainwindow_action_vehiclecost(void) repcost_window_new(); } +static void ui_mainwindow_action_pluginprefs(void) +{ + ui_mainwindow_showprefs(PREF_PLUGINS); +} + static void ui_mainwindow_action_import(void) { ui_import_window_new(); @@ -2131,6 +2157,9 @@ struct hbfile_data *data = user_data; struct WinGeometry *wg; gboolean retval = FALSE; + GValue widget_value = G_VALUE_INIT; + ext_hook("main_window_disposal", EXT_OBJECT(&widget_value, widget), NULL); + DB( g_print("\n[ui-mainwindow] dispose\n") ); //store position and size diff --git a/src/ext-native.c b/src/ext-native.c new file mode 100644 index 0000000..fce6315 --- /dev/null +++ b/src/ext-native.c @@ -0,0 +1,192 @@ + +#include +#include + +#include "ext.h" + + +static gint ext_native_init(int* argc, char** argv[], char** env[]); +static void ext_native_term(void); +static gboolean ext_native_check_file(const gchar* plugin_filename); +static GHashTable* ext_native_read_plugin_metadata(const gchar* plugin_filepath); +static gint ext_native_load_plugin(const gchar* plugin_filepath); +static void ext_native_unload_plugin(const gchar* plugin_filepath); +static void ext_native_execute_action(const gchar* plugin_filepath); +static void ext_native_call_hook(const gchar* hook_id, GList* args); + +static gchar* _read_data_for_keyword(const gchar* keyword, const gchar* bytes, gsize len); + + +static GHashTable* _loaded_plugins = NULL; + + +static gint ext_native_init(int* argc, char** argv[], char** env[]) +{ + if (!_loaded_plugins) { + _loaded_plugins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_module_close); + } + return 0; +} + +static void ext_native_term(void) +{ + if (_loaded_plugins) { + ext_native_call_hook("unload", NULL); + g_hash_table_unref(_loaded_plugins); + _loaded_plugins = NULL; + } +} + +static gboolean ext_native_check_file(const gchar* plugin_filename) +{ + if (g_str_has_suffix(plugin_filename, "."G_MODULE_SUFFIX)) { + return TRUE; + } + if (g_str_has_suffix(plugin_filename, ".la")) { + // allow a .la file only if no actual native plugin is found + gboolean check = FALSE; + gchar* copy = g_strdup(plugin_filename); + gchar* ext = g_strrstr(copy, ".la"); + if (ext) { + *ext = '\0'; + gchar* native_filename = g_strconcat(copy, "."G_MODULE_SUFFIX, NULL); + gchar* native_filepath = ext_find_plugin(native_filename); + check = !native_filepath; + g_free(native_filepath); + g_free(native_filename); + } + g_free(copy); + return check; + } + return FALSE; +} + +static GHashTable* ext_native_read_plugin_metadata(const gchar* plugin_filepath) +{ + GMappedFile* file = g_mapped_file_new(plugin_filepath, FALSE, NULL); + if (!file) { + g_printerr("mapping plugin file at %s failed\n", plugin_filepath); + return NULL; + } + + gchar* bytes = g_mapped_file_get_contents(file); + gsize len = g_mapped_file_get_length(file); + if (len == 0 || !bytes) { + g_mapped_file_unref(file); + g_printerr("no data in plugin file at %s failed\n", plugin_filepath); + return NULL; + } + + GHashTable* table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + + const gchar* keywords[] = { "name", "version", "abstract", "author", "website", NULL }; + const gchar** it; + for (it = keywords; *it; ++it) { + gchar* value = _read_data_for_keyword(*it, bytes, len); + g_hash_table_insert(table, g_strdup(*it), value); + } + + g_mapped_file_unref(file); + + return table; +} + +static gint ext_native_load_plugin(const gchar* plugin_filepath) +{ + if (g_hash_table_contains(_loaded_plugins, plugin_filepath)) { + return 0; + } + + GModule* module = g_module_open(plugin_filepath, G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL); + if (!module) { + g_printerr("Could not load native plugin: %s\n", g_module_error()); + return -1; + } + + g_hash_table_insert(_loaded_plugins, g_strdup(plugin_filepath), module); + + void (*symbol)(); + if (g_module_symbol(module, "load", (gpointer)&symbol)) { + symbol(); + } + + return 0; +} + +static void ext_native_unload_plugin(const gchar* plugin_filepath) +{ + GModule* module = g_hash_table_lookup(_loaded_plugins, plugin_filepath); + if (module) { + void (*symbol)(); + if (g_module_symbol(module, "unload", (gpointer)&symbol)) { + symbol(); + } + } + + g_hash_table_remove(_loaded_plugins, plugin_filepath); +} + +static void ext_native_execute_action(const gchar* plugin_filepath) +{ + GModule* module = g_hash_table_lookup(_loaded_plugins, plugin_filepath); + if (module) { + void (*symbol)(); + if (g_module_symbol(module, "execute", (gpointer)&symbol)) { + symbol(); + } + } +} + +static void ext_native_call_hook(const gchar* hook_id, GList* args) +{ + gchar* symbol_name = g_strconcat("on_", hook_id, NULL); + void (*symbol)(GList*); + + GHashTableIter it; + g_hash_table_iter_init(&it, _loaded_plugins); + GModule* module; + + while (g_hash_table_iter_next(&it, NULL, (gpointer*)&module)) { + if (g_module_symbol(module, symbol_name, (gpointer)&symbol)) { + symbol(args); + } + } + + g_free(symbol_name); +} + + +static gchar* _read_data_for_keyword(const gchar* keyword, const gchar* bytes, gsize len) +{ + gchar* value = NULL; + + gchar* pattern = g_strdup_printf("[\\x00\\t\\n ]%s\\s*[=:]\\s*([^\\x00]+)", keyword); + GRegex* r = g_regex_new(pattern, G_REGEX_CASELESS, 0, NULL); + g_free(pattern); + + GMatchInfo* match = NULL; + if (g_regex_match_full(r, bytes, len, 0, 0, &match, NULL)) { + value = g_match_info_fetch(match, 1); + } + + g_match_info_free(match); + g_regex_unref(r); + + return value; +} + + +static void _register(void) __attribute__((constructor)); +static void _register() +{ + ext_register("native", + ext_native_init, + ext_native_term, + ext_native_check_file, + ext_native_read_plugin_metadata, + ext_native_load_plugin, + ext_native_unload_plugin, + ext_native_execute_action, + ext_native_call_hook); +} + diff --git a/src/ext-perl.xs b/src/ext-perl.xs new file mode 100644 index 0000000..8002047 --- /dev/null +++ b/src/ext-perl.xs @@ -0,0 +1,1042 @@ + +#include +#include +#include + +#include + +#undef _ +#include "homebank.h" +#include "ext.h" +#include "refcount.h" + +extern struct HomeBank *GLOBALS; +#include "dsp_mainwindow.h" +#include "dsp_account.h" +#include "ui-transaction.h" + + +static gint ext_perl_init(int* argc, char** argv[], char** env[]); +static void ext_perl_term(void); +static gboolean ext_perl_check_file(const gchar* plugin_filepath); +static GHashTable* ext_perl_read_plugin_metadata(const gchar* plugin_filepath); +static gint ext_perl_load_plugin(const gchar* plugin_filepath); +static void ext_perl_unload_plugin(const gchar* plugin_filepath); +static void ext_perl_execute_action(const gchar* plugin_filepath); +static void ext_perl_call_hook(const gchar* hook_id, GList* args); + +static SV* val_to_sv(GValue* val); +static GValue* sv_to_val(SV* sv); + +static gboolean gperl_value_from_sv(GValue* value, SV* sv); +static SV* gperl_sv_from_value(const GValue* value, gboolean copy_boxed); + + +static inline GValue* EXT_SV(GValue* v, SV* sv, GType type) +{ + g_value_init(v, type); + gperl_value_from_sv(v, sv); + return v; +} + + +#define EXT_P2C_OBJECT(PKG, ARG, VAR, TYP) \ +if (sv_derived_from(ARG, PKG)) { \ + IV iv = SvIV((SV*)SvRV(ARG)); \ + VAR = INT2PTR(TYP, iv); \ +} else { \ + croak(#VAR" is not of type "PKG); \ +} + +#define EXT_C2P_OBJECT(PKG, ARG, VAR) \ +sv_setref_pv(ARG, PKG, (void*)VAR) + + +static inline GPtrArray* SvGptrarray(const SV* sv) +{ + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVAV) { + AV* av = (AV*)sv; + int i; + int top = av_len(av); + GPtrArray* array = g_ptr_array_new(); + for (i = 0; i <= top; ++i) { + SV** item = av_fetch(av, i, 0); + if (!item) continue; + g_ptr_array_add(array, sv_to_val(*item)); + } + return array; + // TODO- leaking + } else { + croak("var is not an array"); + } +} + +static inline SV* newSVgptrarray(const GPtrArray* a) +{ + if (a) { + AV* av = newAV(); + int i; + for (i = 0; i < a->len; ++i) { + GValue* item = g_ptr_array_index(a, i); + av_push(av, val_to_sv(item)); + } + return newRV((SV*)av); + } + return &PL_sv_undef; +} + + +static inline GHashTable* SvGhashtable(const SV* sv) +{ + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVHV) { + HV* hv = (HV*)sv; + hv_iterinit(hv); + gchar* key; + I32 len; + SV* item; + GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal); + while ((item = hv_iternextsv(hv, &key, &len))) { + g_hash_table_insert(hash, key, sv_to_val(item)); + } + return hash; + // TODO- leaking + } else { + croak("var is not a hash"); + } +} + +static inline SV* newSVghashtable(GHashTable* h) +{ + if (h) { + HV* hv = newHV(); + GHashTableIter it; + g_hash_table_iter_init(&it, h); + gchar* key = NULL; + GValue* item = NULL; + while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&item)) { + hv_store(hv, key, -g_utf8_strlen(key, -1), val_to_sv(item), 0); + } + return newRV((SV*)hv); + } + return &PL_sv_undef; +} + + +static inline gboolean SvGboolean(SV* sv) +{ + if (!sv) { + return FALSE; + } + if (SvROK(sv)) { + return !!SvIV(SvRV(sv)); + } else { + return SvTRUE(sv); + } +} + +static inline SV* newSVgboolean(gboolean b) +{ + return sv_setref_iv(newSV(0), "HomeBank::Boolean", !!b); +} + + +static inline gchar* SvGchar_ptr(SV* sv) +{ + return SvPVutf8_nolen(sv); +} + +static inline SV* newSVgchar_ptr(const gchar* str) +{ + if (!str) return &PL_sv_undef; + + SV* sv = newSVpv(str, 0); + SvUTF8_on(sv); + return sv; +} + + +static inline GObject* SvGobject(const SV* sv) +{ + GObject* (*func)(const SV*) = ext_symbol_lookup("gperl_get_object"); + if (func) { + return func(sv); + } + return NULL; +} + +static inline SV* newSVgobject(const GObject* o) +{ + SV* (*func)(const GObject*, gboolean) = ext_symbol_lookup("gperl_new_object"); + if (func) { + return func(o, FALSE); + } + return &PL_sv_undef; +} + + +static PerlInterpreter* context = NULL; + + +static gint ext_perl_init(int* argc, char** argv[], char** env[]) +{ + int ret = 0; + + PERL_SYS_INIT3(argc, argv, env); + context = perl_alloc(); + perl_construct(context); + + PL_exit_flags |= PERL_EXIT_DESTRUCT_END; + PL_origalen = 1; + PL_perl_destruct_level = 1; + + gchar* bootstrap = g_strdup_printf("-e" + "use lib '%s';" + "use HomeBank;" + "HomeBank->bootstrap;", + homebank_app_get_pkglib_dir()); + char *args[] = { "", bootstrap }; + + EXTERN_C void xs_init(pTHX); + if (perl_parse(context, xs_init, 2, args, NULL) || perl_run(context)) { + ext_perl_term(); + ret = -1; + } + + g_free(bootstrap); + return ret; +} + +static void ext_perl_term(void) +{ + if (context) { + perl_destruct(context); + perl_free(context); + context = NULL; + } + PERL_SYS_TERM(); +} + +static gboolean ext_perl_check_file(const gchar* plugin_filepath) +{ + if (g_str_has_suffix(plugin_filepath, ".pl")) { + return TRUE; + } + return FALSE; +} + +static GHashTable* ext_perl_read_plugin_metadata(const gchar* plugin_filepath) +{ + GHashTable* table = NULL; + + if (!context) return NULL; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + + int ret = call_pv("HomeBank::read_metadata", G_SCALAR | G_EVAL); + + SPAGAIN; + + if (ret == 1) { + table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + SV* sv = POPs; + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVHV) { + HV* hv = (HV*)sv; + hv_iterinit(hv); + gchar* key; + I32 len; + SV* item; + while ((item = hv_iternextsv(hv, &key, &len))) { + if (SvPOK(item)) { + gchar* val = SvPVutf8_nolen(item); + g_hash_table_insert(table, g_strdup(key), g_strdup(val)); + } + } + } + } + + PUTBACK; + FREETMPS; + LEAVE; + + return table; +} + +static gint ext_perl_load_plugin(const gchar* plugin_filepath) +{ + if (!context) return -1; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::load_plugin", G_DISCARD | G_EVAL); + SPAGAIN; + + gint ret = 0; + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + ret = -1; + } + + PUTBACK; + FREETMPS; + LEAVE; + + return ret; +} + +static void ext_perl_unload_plugin(const gchar* plugin_filepath) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::unload_plugin", G_DISCARD | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + } + + PUTBACK; + FREETMPS; + LEAVE; +} + +static void ext_perl_execute_action(const gchar* plugin_filepath) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::execute_action", G_DISCARD | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + } + + PUTBACK; + FREETMPS; + LEAVE; +} + +static void ext_perl_call_hook(const gchar* hook_id, GList* args) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(hook_id)); + + GList *list = g_list_first(args); + while (list) { + GValue* val = list->data; + XPUSHs(sv_2mortal(val_to_sv(val))); + list = g_list_next(list); + } + + PUTBACK; + call_pv("HomeBank::call_hook", G_ARRAY); + SPAGAIN; + POPi; + PUTBACK; + FREETMPS; + LEAVE; +} + + +static SV* val_to_sv(GValue* val) +{ + if (!val || !G_IS_VALUE(val) || G_VALUE_TYPE(val) == G_TYPE_NONE) { + return &PL_sv_undef; + } + if (G_VALUE_TYPE(val) == G_TYPE_BOOLEAN) { + return newSVgboolean(g_value_get_boolean(val)); + } + if (G_VALUE_TYPE(val) == G_TYPE_PTR_ARRAY) { + return newSVgptrarray((GPtrArray*)g_value_get_boxed(val)); + } + if (G_VALUE_TYPE(val) == G_TYPE_HASH_TABLE) { + return newSVghashtable((GHashTable*)g_value_get_boxed(val)); + } +#define obj(CTYPE, _2, PART, GTYPE, _5) \ + if (G_VALUE_TYPE(val) == GTYPE) { \ + SV* sv = newSV(0); \ + CTYPE* ptr = (CTYPE*)g_value_get_##PART(val); \ + EXT_C2P_OBJECT("HomeBank::"#CTYPE, sv, rc_ref(ptr)); \ + return sv; \ + } +#include "ext-value.h" +#undef obj + return gperl_sv_from_value(val, FALSE); +} + +static GValue* sv_to_val(SV* sv) +{ + GValue* val = g_new0(GValue, 1); + + if (SvUOK(sv)) return EXT_SV(val, sv, G_TYPE_UINT); + if (SvIOK(sv)) return EXT_SV(val, sv, G_TYPE_INT); + if (SvNOK(sv)) return EXT_SV(val, sv, G_TYPE_DOUBLE); + if (SvPOK(sv)) return EXT_SV(val, sv, G_TYPE_STRING); + if (sv_isobject(sv)) { + if (sv_derived_from(sv, "HomeBank::Boolean")) { + return EXT_BOOLEAN(val, SvGboolean(sv)); + } +#define obj(CTYPE, NAME, _3, _4, _5) \ + if (sv_derived_from(sv, "HomeBank::"#CTYPE)) { \ + CTYPE* ptr; \ + EXT_P2C_OBJECT("HomeBank::"#CTYPE, sv, ptr, CTYPE*); \ + return EXT_##NAME(val, ptr); \ + } +#include "ext-value.h" +#undef obj + return EXT_SV(val, sv, G_TYPE_OBJECT); + } + if (SvROK(sv)) { + sv = SvRV(sv); + switch (SvTYPE(sv)) { + case SVt_IV: + return EXT_BOOLEAN(val, SvGboolean(sv)); + case SVt_PVAV: + return EXT_ARRAY(val, SvGptrarray(sv)); + case SVt_PVHV: + return EXT_HASH_TABLE(val, SvGhashtable(sv)); + default: + break; + } + } + switch (SvTYPE(sv)) { + case SVt_PVAV: + return EXT_ARRAY(val, SvGptrarray(sv)); + case SVt_PVHV: + return EXT_HASH_TABLE(val, SvGhashtable(sv)); + default: + break; + } + + g_free(val); + return NULL; +} + + +static gboolean gperl_value_from_sv(GValue* value, SV* sv) +{ + gboolean (*func)(GValue*, SV*) = ext_symbol_lookup("gperl_value_from_sv"); + if (func) return func(value, sv); + + GType type = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(value)); + if (!SvOK(sv)) return TRUE; + switch (type) { + case G_TYPE_CHAR: + { + gchar *tmp = SvGchar_ptr(sv); + g_value_set_schar(value, (gint8)(tmp ? tmp[0] : 0)); + break; + } + case G_TYPE_UCHAR: + { + char *tmp = SvPV_nolen(sv); + g_value_set_uchar(value, (guchar)(tmp ? tmp[0] : 0)); + break; + } + case G_TYPE_BOOLEAN: + g_value_set_boolean(value, SvTRUE(sv)); + break; + case G_TYPE_INT: + g_value_set_int(value, SvIV(sv)); + break; + case G_TYPE_UINT: + g_value_set_uint(value, SvIV(sv)); + break; + case G_TYPE_LONG: + g_value_set_long(value, SvIV(sv)); + break; + case G_TYPE_ULONG: + g_value_set_ulong(value, SvIV(sv)); + break; + case G_TYPE_FLOAT: + g_value_set_float(value, (gfloat)SvNV(sv)); + break; + case G_TYPE_DOUBLE: + g_value_set_double(value, SvNV(sv)); + break; + case G_TYPE_STRING: + g_value_set_string(value, SvGchar_ptr(sv)); + break; + } + return TRUE; +} + +static SV* gperl_sv_from_value(const GValue* value, gboolean copy_boxed) +{ + SV* (*func)(const GValue*, gboolean) = ext_symbol_lookup("gperl_sv_from_value"); + if (func) return func(value, copy_boxed); + + GType type = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(value)); + switch (type) { + case G_TYPE_CHAR: + return newSViv(g_value_get_schar(value)); + case G_TYPE_UCHAR: + return newSVuv(g_value_get_uchar(value)); + case G_TYPE_BOOLEAN: + return newSViv(g_value_get_boolean(value)); + case G_TYPE_INT: + return newSViv(g_value_get_int(value)); + case G_TYPE_UINT: + return newSVuv(g_value_get_uint(value)); + case G_TYPE_LONG: + return newSViv(g_value_get_long(value)); + case G_TYPE_ULONG: + return newSVuv(g_value_get_ulong(value)); + case G_TYPE_FLOAT: + return newSVnv(g_value_get_float(value)); + case G_TYPE_DOUBLE: + return newSVnv(g_value_get_double(value)); + case G_TYPE_STRING: + return newSVgchar_ptr(g_value_get_string(value)); + } + return &PL_sv_undef; +} + + +static void _register(void) __attribute__((constructor)); +static void _register() +{ + ext_register("perl", + ext_perl_init, + ext_perl_term, + ext_perl_check_file, + ext_perl_read_plugin_metadata, + ext_perl_load_plugin, + ext_perl_unload_plugin, + ext_perl_execute_action, + ext_perl_call_hook); +} + + +MODULE = HomeBank PACKAGE = HomeBank + +PROTOTYPES: ENABLE + +const gchar* +version(void) + CODE: + RETVAL = VERSION; + OUTPUT: + RETVAL + +const gchar* +config_dir(void) + CODE: + RETVAL = homebank_app_get_config_dir(); + OUTPUT: + RETVAL + +gboolean +has(const gchar* CLASS, ...) + PREINIT: + int i; + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = TRUE; + for (i = 1; i < items; ++i) { + gchar* feature = SvGchar_ptr(ST(i)); + if (!feature || !ext_has(feature)) { + RETVAL = FALSE; + break; + } + } + OUTPUT: + RETVAL + +GObject* +main_window(void) + CODE: + RETVAL = G_OBJECT(GLOBALS->mainwindow); + OUTPUT: + RETVAL + +GObject* +main_ui_manager(void) + PREINIT: + struct hbfile_data *data; + CODE: + RETVAL = NULL; + if (GLOBALS->mainwindow) { + data = g_object_get_data(G_OBJECT(gtk_widget_get_ancestor(GLOBALS->mainwindow, GTK_TYPE_WINDOW)), "inst_data"); + if (data) { + RETVAL = G_OBJECT(data->manager); + } + } + OUTPUT: + RETVAL + +void +info(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "info"); + +void +warn(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "warn"); + +void +error(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "error"); + +void +hook(const gchar* CLASS, const gchar* hook_name, ...) + PREINIT: + int i; + GList* list = NULL; + CODE: + PERL_UNUSED_ARG(CLASS); + for (i = 2; i < items; ++i) { + SV* sv = ST(i); + GValue *val = sv_to_val(sv); + list = g_list_append(list, val); + } + CLEANUP: + ext_vhook(hook_name, list); + g_list_free(list); + // TODO free all the things + +GObject* +open_prefs(const gchar* CLASS) + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = G_OBJECT(defpref_dialog_new(PREF_GENERAL)); + OUTPUT: + RETVAL + + +MODULE = HomeBank PACKAGE = HomeBank::File + +const gchar* +owner(const gchar* CLASS, ...) + CODE: + PERL_UNUSED_ARG(CLASS); + if (1 < items) { + hbfile_change_owner(g_strdup(SvGchar_ptr(ST(1)))); + } + RETVAL = GLOBALS->owner; + OUTPUT: + RETVAL + +void +transactions(const gchar* CLASS) + PPCODE: + PERL_UNUSED_ARG(CLASS); + GList* list = g_list_first(GLOBALS->ope_list); + for (; list; list = g_list_next(list)) { + GValue val = G_VALUE_INIT; + SV* sv = val_to_sv(EXT_TRANSACTION(&val, list->data)); + mXPUSHs(sv); + } + +void +anonymize(void) + CODE: + hbfile_anonymize(); + +void +baz(const gchar* CLASS, Account* account) + CODE: + PERL_UNUSED_ARG(CLASS); + g_print("hello: %s\n", account->name); + +GPtrArray* +meh(const gchar* CLASS, GPtrArray* asdf) + CODE: + PERL_UNUSED_ARG(CLASS); + g_print("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW\n"); + if (asdf) { + ; + } else { + g_print("the array is nil\n"); + } + RETVAL = asdf; + OUTPUT: + RETVAL + CLEANUP: + g_ptr_array_unref(asdf); + +GHashTable* +foo(const gchar* CLASS, GHashTable* asdf) + CODE: + PERL_UNUSED_ARG(CLASS); + g_print("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW\n"); + if (asdf) { + GHashTableIter it; + g_hash_table_iter_init(&it, asdf); + gchar* key = NULL; + GValue* item = NULL; + while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&item)) { + g_print("hash with key: %s\n", key); + } + } else { + g_print("the hash is nil\n"); + } + RETVAL = asdf; + OUTPUT: + RETVAL + CLEANUP: + g_hash_table_unref(asdf); + + +MODULE = HomeBank PACKAGE = HomeBank::Account + +void +compute_balances(const gchar* CLASS) + CODE: + PERL_UNUSED_ARG(CLASS); + account_compute_balances(); + +Account* +new(void) + CODE: + RETVAL = da_acc_malloc(); + OUTPUT: + RETVAL + +Account* +clone(Account* SELF) + CODE: + RETVAL = da_acc_clone(SELF); + RETVAL->key = 0; + OUTPUT: + RETVAL + +void +DESTROY(Account* SELF) + CODE: + da_acc_free(SELF); + +Account* +get(const gchar* CLASS, guint key) + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = rc_ref(da_acc_get(key)); + OUTPUT: + RETVAL + +Account* +get_by_name(const gchar* CLASS, const gchar* name) + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = rc_ref(da_acc_get_by_name((gchar*)name)); + OUTPUT: + RETVAL + +const gchar* +name(Account* SELF, ...) + CODE: + if (1 < items) { + account_rename(SELF, SvGchar_ptr(ST(1))); + } + RETVAL = SELF->name; + OUTPUT: + RETVAL + +const gchar* +number(Account* SELF, ...) + CODE: + if (1 < items) { + g_free(SELF->number); + SELF->number = g_strdup(SvGchar_ptr(ST(1))); + } + RETVAL = SELF->number; + OUTPUT: + RETVAL + +const gchar* +bankname(Account* SELF, ...) + CODE: + if (1 < items) { + g_free(SELF->bankname); + SELF->bankname = g_strdup(SvGchar_ptr(ST(1))); + } + RETVAL = SELF->bankname; + OUTPUT: + RETVAL + +gdouble +initial(Account* SELF, ...) + CODE: + if (1 < items) { + SELF->initial = SvNV(ST(1)); + } + RETVAL = SELF->initial; + OUTPUT: + RETVAL + +gdouble +minimum(Account* SELF, ...) + CODE: + if (1 < items) { + SELF->minimum = SvNV(ST(1)); + } + RETVAL = SELF->minimum; + OUTPUT: + RETVAL + +guint +cheque1(Account* SELF, ...) + ALIAS: + check1 = 1 + CODE: + PERL_UNUSED_VAR(ix); + if (1 < items) { + SELF->cheque1 = SvUV(ST(1)); + } + RETVAL = SELF->cheque1; + OUTPUT: + RETVAL + +guint +cheque2(Account* SELF, ...) + ALIAS: + check2 = 1 + CODE: + PERL_UNUSED_VAR(ix); + if (1 < items) { + SELF->cheque2 = SvUV(ST(1)); + } + RETVAL = SELF->cheque2; + OUTPUT: + RETVAL + +gdouble +balance(Account* SELF) + ALIAS: + bank_balance = 1 + future_balance = 2 + CODE: + switch (ix) { + case 1: + RETVAL = SELF->bal_bank; + break; + case 2: + RETVAL = SELF->bal_future; + break; + default: + RETVAL = SELF->bal_today; + break; + } + OUTPUT: + RETVAL + +gboolean +is_inserted(Account* SELF) + CODE: + RETVAL = da_acc_get(SELF->key) == SELF; + OUTPUT: + RETVAL + +gboolean +is_used(Account* SELF) + CODE: + RETVAL = account_is_used(SELF->key); + OUTPUT: + RETVAL + +gboolean +insert(Account* SELF) + CODE: + if (SELF->key == 0 || account_is_used(SELF->key)) + RETVAL = da_acc_append(rc_ref(SELF)); + else + RETVAL = da_acc_insert(rc_ref(SELF)); + OUTPUT: + RETVAL + +void +remove(Account* SELF) + CODE: + da_acc_remove(SELF->key); + +void +transactions(Account* SELF) + PPCODE: + GList* list = g_list_first(GLOBALS->ope_list); + for (; list; list = g_list_next(list)) { + Transaction* txn = list->data; + if (txn->kacc == SELF->key) { + GValue val = G_VALUE_INIT; + SV* sv = val_to_sv(EXT_TRANSACTION(&val, txn)); + mXPUSHs(sv); + } + } + +GObject* +open(Account* SELF) + CODE: + RETVAL = G_OBJECT(register_panel_window_new(SELF->key, SELF)); + OUTPUT: + RETVAL + + +MODULE = HomeBank PACKAGE = HomeBank::Transaction + +Transaction* +new(void) + CODE: + RETVAL = da_transaction_malloc(); + OUTPUT: + RETVAL + +void +DESTROY(Transaction* SELF) + CODE: + da_transaction_free(SELF); + +gdouble +amount(Transaction* SELF, ...) + CODE: + if (1 < items) { + SELF->amount = SvNV(ST(1)); + } + RETVAL = SELF->amount; + OUTPUT: + RETVAL + +guint +account_num(Transaction* SELF, ...) + CODE: + if (1 < items) { + SELF->kacc = SvIV(ST(1)); + } + RETVAL = SELF->kacc; + OUTPUT: + RETVAL + +guint +paired_account_num(Transaction* SELF, ...) + CODE: + if (1 < items) { + SELF->kxferacc = SvIV(ST(1)); + } + RETVAL = SELF->kxferacc; + OUTPUT: + RETVAL + +void +date(Transaction* SELF, ...) + PPCODE: + if (1 < items) { + SELF->date = SvIV(ST(1)); + } + if (GIMME_V == G_ARRAY) { + GDate* d = g_date_new_julian(SELF->date); + mXPUSHp("day", 3); + mXPUSHi(g_date_get_day(d)); + mXPUSHp("month", 5); + mXPUSHi(g_date_get_month(d)); + mXPUSHp("year", 4); + mXPUSHi(g_date_get_year(d)); + g_date_free(d); + XSRETURN(6); + } else { + XSRETURN_IV(SELF->date); + } + +const gchar* +wording(Transaction* SELF, ...) + CODE: + if (1 < items) { + if (SELF->wording) g_free(SELF->wording); + SELF->wording = g_strdup(SvGchar_ptr(ST(1))); + } + RETVAL = SELF->wording ? SELF->wording : ""; + OUTPUT: + RETVAL + +const gchar* +info(Transaction* SELF, ...) + CODE: + if (1 < items) { + if (SELF->info) g_free(SELF->info); + SELF->info = g_strdup(SvGchar_ptr(ST(1))); + } + RETVAL = SELF->info ? SELF->info : ""; + OUTPUT: + RETVAL + +GObject* +open(Transaction* SELF) + CODE: + RETVAL = G_OBJECT(create_deftransaction_window(NULL, TRANSACTION_EDIT_MODIFY)); + deftransaction_set_transaction(GTK_WIDGET(RETVAL), SELF); + OUTPUT: + RETVAL + +Transaction* +pair_with(Transaction* SELF, Transaction* other, ...) + PREINIT: + int i; + GList* list = NULL; + CODE: + if (2 < items) { + list = g_list_append(list, other); + for (i = 2; i < items; ++i) { + Transaction* ptr = NULL; + SV* sv = ST(i); + EXT_P2C_OBJECT("HomeBank::Transaction", sv, ptr, Transaction*); + list = g_list_append(list, ptr); + } + other = ui_dialog_transaction_xfer_select_child(list); + } + if (other) { + transaction_xfer_change_to_child(SELF, other); + SELF->paymode = PAYMODE_INTXFER; + } + RETVAL = other; + OUTPUT: + RETVAL + CLEANUP: + g_list_free(list); + +void +dump(Transaction* SELF) + CODE: + g_print("txn: %p (%s) at %u (%d/%d) flags:%d, paymode:%d, kpay:%d, kcat:%d", SELF, + SELF->wording, SELF->date, SELF->kacc, SELF->kxferacc, SELF->flags, SELF->paymode, SELF->kpay, SELF->kcat); + diff --git a/src/ext-value.c b/src/ext-value.c new file mode 100644 index 0000000..47c4829 --- /dev/null +++ b/src/ext-value.c @@ -0,0 +1,82 @@ + +#include + +#include "ext-value.h" + + +const GValue* ext_value_undef() +{ + static GValue v = G_VALUE_INIT; + return &v; +} + +const GValue* ext_value_true() +{ + static GValue v = G_VALUE_INIT; + if (!G_VALUE_HOLDS_BOOLEAN(&v)) EXT_BOOLEAN(&v, TRUE); + return &v; +} + +const GValue* ext_value_false() +{ + static GValue v = G_VALUE_INIT; + if (!G_VALUE_HOLDS_BOOLEAN(&v)) EXT_BOOLEAN(&v, FALSE); + return &v; +} + + +GValue* EXT_LIST(GValue* v, ...) +{ + GPtrArray* a = g_ptr_array_new(); + + va_list ap; + va_start(ap, v); + + for (;;) { + GValue* item = (GValue*)va_arg(ap, GValue*); + if (!item) break; + g_ptr_array_add(a, item); + } + + va_end(ap); + + return EXT_ARRAY(v, a); +} + +GValue* EXT_HASH(GValue* v, ...) +{ + GHashTable* h = g_hash_table_new(g_str_hash, g_str_equal); + + va_list ap; + va_start(ap, v); + + for (;;) { + gchar* key = (gchar*)va_arg(ap, gchar*); + if (!key) break; + GValue* val = (GValue*)va_arg(ap, GValue*); + g_hash_table_insert(h, key, val); + } + + va_end(ap); + + return EXT_HASH_TABLE(v, h); +} + +GValue* EXT_JULIAN(GValue* v, guint32 d) +{ + GDate* date = g_date_new_julian(d); + return EXT_DATE(v, date); +} + + +#define obj(CTYPE, _2, _3, _4, PREFIX) \ +GType PREFIX##get_type() \ +{ \ + static GType type = 0; \ + if (type == 0) \ + type = g_pointer_type_register_static(#CTYPE); \ + return type; \ +} +#include "ext-value.h" +#undef obj + diff --git a/src/ext-value.h b/src/ext-value.h new file mode 100644 index 0000000..a5002bb --- /dev/null +++ b/src/ext-value.h @@ -0,0 +1,64 @@ + +#ifndef __EXT_VALUE_H__ +#define __EXT_VALUE_H__ + +#include "homebank.h" + + +#define DA_TYPE_ACC (da_acc_get_type()) +#define DA_TYPE_TRANSACTION (da_transaction_get_type()) + +#define obj(_1, _2, _3, _4, PREFIX) GType PREFIX##get_type(void); +#include "ext-value.h" +#undef obj + + +#define val(CTYPE, NAME, PART, GTYPE) \ +static inline GValue* EXT_##NAME(GValue* v, CTYPE c) { \ + g_value_init(v, GTYPE); \ + g_value_set_##PART(v, c); \ + return v; \ +} +#define obj(CTYPE, NAME, PART, GTYPE, _5) val(CTYPE*, NAME, PART, GTYPE) +#include "ext-value.h" +#undef val +#undef obj + + +const GValue* ext_value_undef(void); +const GValue* ext_value_true(void); +const GValue* ext_value_false(void); + +#define EXT_UNDEF (ext_value_undef()) +#define EXT_TRUE (ext_value_true()) +#define EXT_FALSE (ext_value_false()) + +GValue* EXT_LIST(GValue* v, ...); +GValue* EXT_HASH(GValue* v, ...); +GValue* EXT_JULIAN(GValue* v, guint32 d); + + +#else + +#ifdef val +// C type, name, fundamental, GType +val(gboolean, BOOLEAN, boolean, G_TYPE_BOOLEAN) +val(gint, INT, int, G_TYPE_INT) +val(guint, UINT, uint, G_TYPE_UINT) +val(gdouble, DOUBLE, double, G_TYPE_DOUBLE) +val(gchar, CHAR, schar, G_TYPE_CHAR) +val(gchar*, STRING, string, G_TYPE_STRING) +val(GPtrArray*, ARRAY, boxed, G_TYPE_PTR_ARRAY) +val(GHashTable*, HASH_TABLE, boxed, G_TYPE_HASH_TABLE) +val(GDate*, DATE, boxed, G_TYPE_DATE) +val(void*, OBJECT, object, G_TYPE_OBJECT) +#endif + +#ifdef obj +// C type, name, fundamental, GType, prefix +obj(Account, ACCOUNT, pointer, DA_TYPE_ACC, da_acc_) +obj(Transaction, TRANSACTION, pointer, DA_TYPE_TRANSACTION, da_transaction_) +#endif + +#endif + diff --git a/src/ext.c b/src/ext.c new file mode 100644 index 0000000..b366491 --- /dev/null +++ b/src/ext.c @@ -0,0 +1,327 @@ + +#include +#include +#include +#include + +#include "ext.h" + +extern struct Preferences *PREFS; + + +const int _hook_recursion_soft_limit = 50; +const int _hook_recursion_hard_limit = 99; + + +struct PluginEngine +{ + const gchar* type; + PluginEngineInitializer init; + PluginEngineTerminator term; + PluginEngineFileChecker check_file; + PluginMetadataReader read_metadata; + PluginLoader load_plugin; + PluginUnloader unload_plugin; + PluginExecutor execute; + PluginHookCaller call_hook; +}; + + +static GList* _engine_list = NULL; +static GHashTable* _loaded_plugins = NULL; + + +void ext_init(int* argc, char** argv[], char** env[]) +{ + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->init(argc, argv, env); + list = g_list_next(list); + } + if (!_loaded_plugins) { + _loaded_plugins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + } +} + +void ext_term(void) +{ + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->term(); + list = g_list_next(list); + } + g_list_free(_engine_list); + _engine_list = NULL; + + if (_loaded_plugins) { + g_hash_table_unref(_loaded_plugins); + _loaded_plugins = NULL; + } +} + +void ext_register(const gchar* type, + PluginEngineInitializer init, + PluginEngineTerminator term, + PluginEngineFileChecker check_file, + PluginMetadataReader read_metadata, + PluginLoader load_plugin, + PluginUnloader unload_plugin, + PluginExecutor execute, + PluginHookCaller call_hook) +{ + struct PluginEngine* engine = g_malloc0(sizeof(struct PluginEngine)); + engine->type = type; + engine->init = init; + engine->term = term; + engine->check_file = check_file; + engine->read_metadata = read_metadata; + engine->load_plugin = load_plugin; + engine->unload_plugin = unload_plugin; + engine->execute = execute; + engine->call_hook = call_hook; + _engine_list = g_list_append(_engine_list, engine); +} + + +static struct PluginEngine* _get_engine_for_plugin(const gchar* plugin_filename) +{ + if (!plugin_filename) { + return NULL; + } + + GList *list = g_list_first(_engine_list); + while (list) { + struct PluginEngine* engine = list->data; + if (engine->check_file(plugin_filename)) { + return engine; + } + list = g_list_next(list); + } + return NULL; +} + +static void _read_directory(const gchar* directory, GHashTable* hash) +{ + GDir* dir = g_dir_open(directory, 0, NULL); + if (!dir) return; + + const gchar* filename; + while ((filename = g_dir_read_name(dir))) { + gchar* full = g_build_filename(directory, filename, NULL); + if (g_file_test(full, G_FILE_TEST_IS_REGULAR) && _get_engine_for_plugin(filename)) { + g_hash_table_insert(hash, g_strdup(filename), NULL); + } + g_free(full); + } + g_dir_close(dir); +} + +gchar** ext_list_plugins() +{ + GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal); + + gchar** it; + for (it = PREFS->ext_path; it && *it; ++it) { + _read_directory(*it, hash); + } + + GList* list = g_list_sort(g_hash_table_get_keys(hash), (GCompareFunc)g_utf8_collate); + g_hash_table_unref(hash); + + guint len = g_list_length(list); + gchar** strv = g_new0(gchar**, len + 1); + int i; + for (i = 0; i < len; ++i) { + strv[i] = g_list_nth_data(list, i); + } + g_list_free(list); + + return strv; +} + +gchar* ext_find_plugin(const gchar* plugin_filename) +{ + if (!plugin_filename) return NULL; + + gchar** it; + for (it = PREFS->ext_path; *it; ++it) { + if (!g_path_is_absolute(*it)) continue; + + gchar* full = g_build_filename(*it, plugin_filename, NULL); + if (g_file_test(full, G_FILE_TEST_IS_REGULAR)) { + return full; + } + g_free(full); + } + + return NULL; +} + +GHashTable* ext_read_plugin_metadata(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return NULL; + + GHashTable* ret = NULL; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->read_metadata) { + ret = engine->read_metadata(full); + } + + g_free(full); + return ret; +} + +gint ext_load_plugin(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return -1; + + gint ret = -1; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->load_plugin && engine->load_plugin(full) == 0) { + g_hash_table_insert(_loaded_plugins, g_strdup(plugin_filename), NULL); + ret = 0; + } + + g_free(full); + return ret; +} + +void ext_unload_plugin(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->unload_plugin) { + engine->unload_plugin(full); + } + + g_free(full); + g_hash_table_remove(_loaded_plugins, plugin_filename); +} + +gboolean ext_is_plugin_loaded(const gchar* plugin_filename) +{ + return g_hash_table_contains(_loaded_plugins, plugin_filename); +} + +void ext_execute_action(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->execute) { + engine->execute(full); + } + + g_free(full); +} + +void ext_hook(const gchar* hook_id, ...) +{ + GList *list = NULL; + + va_list ap; + va_start(ap, hook_id); + for (;;) { + GValue* val = (GValue*)va_arg(ap, GValue*); + if (!val) break; + list = g_list_append(list, val); + } + va_end(ap); + + ext_vhook(hook_id, list); + g_list_free(list); +} + +void ext_vhook(const gchar* hook_id, GList* args) +{ + static int recursion_level = 0; + + if (_hook_recursion_hard_limit <= recursion_level) { + return; + } else if (_hook_recursion_soft_limit <= recursion_level) { + int level = recursion_level; + recursion_level = -1; + GValue val_level = G_VALUE_INIT; + ext_hook("deep_hook_recursion", EXT_INT(&val_level, level), NULL); + recursion_level = level; + } + + ++recursion_level; + + g_print("ext_hook: %s (level %d)\n", hook_id, recursion_level); + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->call_hook(hook_id, args); + list = g_list_next(list); + } + + --recursion_level; +} + +gboolean ext_has(const gchar* feature) +{ +#ifdef OFX_ENABLE + if (0 == g_utf8_collate(feature, "libofx")) { + return TRUE; + } +#endif +#ifdef PERL_ENABLE + if (0 == g_utf8_collate(feature, "perl")) { + return TRUE; + } +#endif + return FALSE; +} + + +void* ext_symbol_lookup(const gchar* symbol) +{ + static GModule* module = NULL; + if (!module) module = g_module_open(NULL, 0); + + void* ptr; + if (module && g_module_symbol(module, symbol, &ptr)) { + return ptr; + } + + return NULL; +} + + +void ext_run_modal(const gchar* title, const gchar* text, const gchar* type) +{ + GtkMessageType t = GTK_MESSAGE_INFO; + if (0 == g_utf8_collate(type, "error")) { + t = GTK_MESSAGE_ERROR; + } + if (0 == g_utf8_collate(type, "warn")) { + t = GTK_MESSAGE_WARNING; + } + if (0 == g_utf8_collate(type, "question")) { + t = GTK_MESSAGE_QUESTION; + } + + GtkWidget* dialog = gtk_message_dialog_new(NULL, + GTK_DIALOG_DESTROY_WITH_PARENT, t, + GTK_BUTTONS_CLOSE, "%s", text); + if (title) { + gtk_window_set_title(GTK_WINDOW(dialog), title); + } + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + diff --git a/src/ext.h b/src/ext.h new file mode 100644 index 0000000..edc3a44 --- /dev/null +++ b/src/ext.h @@ -0,0 +1,46 @@ + +#ifndef __EXT_H__ +#define __EXT_H__ + +#include + +#include "ext-value.h" + + +typedef gint (*PluginEngineInitializer)(int* argc, char** argv[], char** env[]); +typedef void (*PluginEngineTerminator)(); +typedef gboolean (*PluginEngineFileChecker)(const gchar* plugin_filepath); +typedef GHashTable* (*PluginMetadataReader)(const gchar* plugin_filepath); +typedef gint (*PluginLoader)(const gchar* plugin_filepath); +typedef void (*PluginUnloader)(const gchar* plugin_filepath); +typedef void (*PluginExecutor)(const gchar* plugin_filepath); +typedef void (*PluginHookCaller)(const gchar* hook_id, GList* args); + +void ext_init(int* argc, char** argv[], char** env[]); +void ext_term(void); +void ext_register(const gchar* type, + PluginEngineInitializer, + PluginEngineTerminator, + PluginEngineFileChecker, + PluginMetadataReader, + PluginLoader, + PluginUnloader, + PluginExecutor, + PluginHookCaller); + + +gchar** ext_list_plugins(void); +gchar* ext_find_plugin(const gchar* plugin_filename); +GHashTable* ext_read_plugin_metadata(const gchar* plugin_filename); +gint ext_load_plugin(const gchar* plugin_filename); +void ext_unload_plugin(const gchar* plugin_filename); +gboolean ext_is_plugin_loaded(const gchar* plugin_filename); +void ext_execute_action(const gchar* plugin_filename); +void ext_hook(const gchar* hook_id, ...); +void ext_vhook(const gchar* hook_id, GList* args); +gboolean ext_has(const gchar* feature); +void* ext_symbol_lookup(const gchar* symbol); +void ext_run_modal(const gchar* title, const gchar* text, const gchar* type); + +#endif + diff --git a/src/hb-account.c b/src/hb-account.c index da1f30a..c3366e7 100644 --- a/src/hb-account.c +++ b/src/hb-account.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-account.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -41,7 +44,7 @@ extern struct HomeBank *GLOBALS; Account * da_acc_clone(Account *src_item) { -Account *new_item = g_memdup(src_item, sizeof(Account)); +Account *new_item = rc_dup(src_item, sizeof(Account)); DB( g_print("da_acc_clone\n") ); if(new_item) @@ -59,7 +62,7 @@ void da_acc_free(Account *item) { DB( g_print("da_acc_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); @@ -67,7 +70,7 @@ da_acc_free(Account *item) g_free(item->name); g_free(item->number); g_free(item->bankname); - g_free(item); + rc_free(item); } } @@ -76,7 +79,7 @@ Account * da_acc_malloc(void) { DB( g_print("da_acc_malloc\n") ); - return g_malloc0(sizeof(Account)); + return rc_alloc(sizeof(Account)); } @@ -169,6 +172,9 @@ guint32 *new_key; *new_key = item->key; g_hash_table_insert(GLOBALS->h_acc, new_key, item); + GValue item_val = G_VALUE_INIT; + ext_hook("account_inserted", EXT_ACCOUNT(&item_val, item), NULL); + return TRUE; } @@ -204,6 +210,10 @@ guint32 *new_key; DB( g_print(" -> insert id: %d\n", *new_key) ); g_hash_table_insert(GLOBALS->h_acc, new_key, item); + + GValue item_val = G_VALUE_INIT; + ext_hook("account_inserted", EXT_ACCOUNT(&item_val, item), NULL); + return TRUE; } } diff --git a/src/hb-archive.c b/src/hb-archive.c index 0ede270..e3faf5d 100644 --- a/src/hb-archive.c +++ b/src/hb-archive.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-archive.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -40,12 +43,12 @@ extern struct HomeBank *GLOBALS; Archive *da_archive_malloc(void) { - return g_malloc0(sizeof(Archive)); + return rc_alloc(sizeof(Archive)); } Archive *da_archive_clone(Archive *src_item) { -Archive *new_item = g_memdup(src_item, sizeof(Archive)); +Archive *new_item = rc_dup(src_item, sizeof(Archive)); if(new_item) { @@ -57,12 +60,12 @@ Archive *new_item = g_memdup(src_item, sizeof(Archive)); void da_archive_free(Archive *item) { - if(item != NULL) + if(rc_unref(item)) { if(item->wording != NULL) g_free(item->wording); - g_free(item); + rc_free(item); } } diff --git a/src/hb-assign.c b/src/hb-assign.c index ea50aa6..8303436 100644 --- a/src/hb-assign.c +++ b/src/hb-assign.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-assign.h" +#include "ext.h" +#include "refcount.h" + #define MYDEBUG 0 #if MYDEBUG @@ -38,12 +41,12 @@ void da_asg_free(Assign *item) { DB( g_print("da_asg_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); g_free(item->name); - g_free(item); + rc_free(item); } } @@ -52,7 +55,7 @@ Assign * da_asg_malloc(void) { DB( g_print("da_asg_malloc\n") ); - return g_malloc0(sizeof(Assign)); + return rc_alloc(sizeof(Assign)); } diff --git a/src/hb-category.c b/src/hb-category.c index 3d9b15a..3dad28b 100644 --- a/src/hb-category.c +++ b/src/hb-category.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-category.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ @@ -40,7 +43,7 @@ extern struct HomeBank *GLOBALS; Category * da_cat_clone(Category *src_item) { -Category *new_item = g_memdup(src_item, sizeof(Category)); +Category *new_item = rc_dup(src_item, sizeof(Category)); DB( g_print("da_cat_clone\n") ); if(new_item) @@ -56,12 +59,12 @@ void da_cat_free(Category *item) { DB( g_print("da_cat_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); g_free(item->name); - g_free(item); + rc_free(item); } } @@ -70,7 +73,7 @@ Category * da_cat_malloc(void) { DB( g_print("da_cat_malloc\n") ); - return g_malloc0(sizeof(Category)); + return rc_alloc(sizeof(Category)); } diff --git a/src/hb-payee.c b/src/hb-payee.c index 4a99bb3..6a51cc7 100644 --- a/src/hb-payee.c +++ b/src/hb-payee.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-payee.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ @@ -41,12 +44,12 @@ void da_pay_free(Payee *item) { DB( g_print("da_pay_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); g_free(item->name); - g_free(item); + rc_free(item); } } @@ -55,7 +58,7 @@ Payee * da_pay_malloc(void) { DB( g_print("da_pay_malloc\n") ); - return g_malloc0(sizeof(Payee)); + return rc_alloc(sizeof(Payee)); } diff --git a/src/hb-preferences.c b/src/hb-preferences.c index 6fead0d..e7695bc 100644 --- a/src/hb-preferences.c +++ b/src/hb-preferences.c @@ -282,6 +282,9 @@ void homebank_pref_free(void) g_free(PREFS->minor_cur.decimal_char); g_free(PREFS->minor_cur.grouping_char); + g_strfreev(PREFS->ext_path); + g_list_free_full(PREFS->ext_whitelist, g_free); + memset(PREFS, 0, sizeof(struct Preferences)); } @@ -397,6 +400,23 @@ gint i; PREFS->vehicle_unit_ismile = FALSE; PREFS->vehicle_unit_isgal = FALSE; + gchar** plugin_path = g_new0(gchar**, 4); + i = 0; + const gchar* env = g_getenv("HOMEBANK_PLUGINS"); + if (env) { + if (g_path_is_absolute(env)) { + plugin_path[i++] = g_strdup(env); + } else { + gchar* cur = g_get_current_dir(); + plugin_path[i++] = g_build_filename(cur, env, NULL); + g_free(cur); + } + } + plugin_path[i++] = g_build_filename(homebank_app_get_config_dir(), "plugins", NULL); + plugin_path[i++] = g_build_filename(homebank_app_get_pkglib_dir(), "plugins", NULL); + PREFS->ext_path = plugin_path; + PREFS->ext_whitelist = NULL; + _homebank_pref_createformat(); _homebank_pref_init_measurement_units(); @@ -429,9 +449,6 @@ static void homebank_pref_get_wingeometry( } } - - - static void homebank_pref_get_boolean( GKeyFile *key_file, const gchar *group_name, @@ -869,6 +886,27 @@ GError *error = NULL; //PREFS->chart_legend = g_key_file_get_boolean (keyfile, group, "Legend", NULL); + group = "Plugins"; + { + DB( g_print(" -> ** Plugins\n") ); + + gchar** strv = g_key_file_get_string_list(keyfile, group, "Path", NULL, NULL); + if (strv) { + g_strfreev(PREFS->ext_path); + PREFS->ext_path = strv; + } + + strv = g_key_file_get_string_list(keyfile, group, "Whitelist", NULL, NULL); + if (strv) { + gchar** it; + for (it = strv; it && *it; ++it) { + PREFS->ext_whitelist = g_list_append(PREFS->ext_whitelist, g_strdup(*it)); + } + g_strfreev(strv); + } + } + + /* #if MYDEBUG == 1 gsize length; @@ -1041,6 +1079,21 @@ gsize length; //group = "Chart"; //g_key_file_set_boolean (keyfile, group, "Legend", PREFS->chart_legend); + group = "Plugins"; + { + g_key_file_set_string_list(keyfile, group, "Path", (const gchar* const*)PREFS->ext_path, g_strv_length(PREFS->ext_path)); + + gsize len = g_list_length(PREFS->ext_whitelist); + gchar** strv = g_new0(gchar*, len + 1); + guint i; + + for (i = 0; i < len; ++i) { + strv[i] = g_list_nth_data(PREFS->ext_whitelist, i); + } + g_key_file_set_string_list(keyfile, group, "Whitelist", (const gchar* const*)strv, len); + g_free(strv); + } + //g_key_file_set_string (keyfile, group, "", PREFS->); //g_key_file_set_boolean (keyfile, group, "", PREFS->); //g_key_file_set_integer (keyfile, group, "", PREFS->); diff --git a/src/hb-preferences.h b/src/hb-preferences.h index 938a1ec..ae8d259 100644 --- a/src/hb-preferences.h +++ b/src/hb-preferences.h @@ -167,6 +167,10 @@ struct Preferences gchar *vehicle_unit_100; gchar *vehicle_unit_distbyvol; + // plugins + gchar** ext_path; + GList* ext_whitelist; + }; diff --git a/src/hb-tag.c b/src/hb-tag.c index 8554cfa..c8353a4 100644 --- a/src/hb-tag.c +++ b/src/hb-tag.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-tag.h" +#include "ext.h" +#include "refcount.h" + #define MYDEBUG 0 #if MYDEBUG @@ -37,12 +40,12 @@ extern struct HomeBank *GLOBALS; void da_tag_free(Tag *item) { DB( g_print("da_tag_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); g_free(item->name); - g_free(item); + rc_free(item); } } @@ -50,7 +53,7 @@ void da_tag_free(Tag *item) Tag *da_tag_malloc(void) { DB( g_print("da_tag_malloc\n") ); - return g_malloc0(sizeof(Tag)); + return rc_alloc(sizeof(Tag)); } diff --git a/src/hb-transaction.c b/src/hb-transaction.c index 3a5f6cf..1d66998 100644 --- a/src/hb-transaction.c +++ b/src/hb-transaction.c @@ -22,6 +22,9 @@ #include "hb-transaction.h" #include "hb-tag.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -202,10 +205,10 @@ da_transaction_clean(Transaction *item) void da_transaction_free(Transaction *item) { - if(item != NULL) + if(rc_unref(item)) { da_transaction_clean(item); - g_free(item); + rc_free(item); } } @@ -213,7 +216,7 @@ da_transaction_free(Transaction *item) Transaction * da_transaction_malloc(void) { - return g_malloc0(sizeof(Transaction)); + return rc_alloc(sizeof(Transaction)); } @@ -262,7 +265,7 @@ Transaction *da_transaction_init_from_template(Transaction *txn, Archive *arc) Transaction *da_transaction_clone(Transaction *src_item) { -Transaction *new_item = g_memdup(src_item, sizeof(Transaction)); +Transaction *new_item = rc_dup(src_item, sizeof(Transaction)); guint count; DB( g_print("da_transaction_clone\n") ); @@ -632,6 +635,9 @@ gchar swap; if(treeview != NULL) transaction_add_treeview(child, treeview, ope->kacc); + + GValue txn_value = G_VALUE_INIT; + ext_hook("transaction_inserted", EXT_TRANSACTION(&txn_value, child), NULL); } } @@ -827,6 +833,9 @@ Account *acc; { transaction_xfer_search_or_add_child(newope, treeview); } + + GValue txn_value = G_VALUE_INIT; + ext_hook("transaction_inserted", EXT_TRANSACTION(&txn_value, newope), NULL); } } diff --git a/src/hb-xml.c b/src/hb-xml.c index d859c35..9db1e30 100644 --- a/src/hb-xml.c +++ b/src/hb-xml.c @@ -23,6 +23,8 @@ #include "hb-transaction.h" #include "hb-xml.h" +#include "ext.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -427,6 +429,9 @@ gboolean rc; DB( g_print("\n[hb-xml] homebank_load_xml\n") ); + GValue filename_val = G_VALUE_INIT; + ext_hook("load_file", EXT_STRING(&filename_val, filename), NULL); + retval = XML_OK; if (!g_file_get_contents (filename, &buffer, &length, &error)) { @@ -1186,6 +1191,9 @@ char buf1[G_ASCII_DTOSTR_BUF_SIZE]; gchar *outstr; gint retval = XML_OK; + GValue filename_val = G_VALUE_INIT; + ext_hook("save_file", EXT_STRING(&filename_val, filename), NULL); + io = g_io_channel_new_file(filename, "w", NULL); if(io == NULL) { diff --git a/src/homebank.c b/src/homebank.c index c452bdd..106b8ec 100644 --- a/src/homebank.c +++ b/src/homebank.c @@ -18,6 +18,7 @@ */ #include "homebank.h" +#include "ext.h" #include "dsp_mainwindow.h" #include "hb-preferences.h" @@ -52,6 +53,7 @@ static gchar *pixmaps_dir = NULL; static gchar *locale_dir = NULL; static gchar *help_dir = NULL; static gchar *datas_dir = NULL; +static gchar *pkglib_dir = NULL; //#define MARKUP_STRING "%s" @@ -543,7 +545,8 @@ homebank_register_stock_icons() "prf-display", "prf-euro", "prf-report", - "prf-import" + "prf-import", + "prf-plugins" }; factory = gtk_icon_factory_new (); @@ -635,6 +638,12 @@ homebank_app_get_datas_dir (void) return datas_dir; } +const gchar * +homebank_app_get_pkglib_dir (void) +{ + return pkglib_dir; +} + /* build package paths at runtime */ static void @@ -651,6 +660,7 @@ build_package_paths (void) pixmaps_dir = g_build_filename (prefix, "share", PACKAGE, "icons", NULL); help_dir = g_build_filename (prefix, "share", PACKAGE, "help", NULL); datas_dir = g_build_filename (prefix, "share", PACKAGE, "datas", NULL); + pkglib_dir = g_build_filename (prefix, "lib", PACKAGE, NULL); #ifdef PORTABLE_APP DB( g_print("- app is portable under windows\n") ); config_dir = g_build_filename(prefix, "config", NULL); @@ -664,8 +674,9 @@ build_package_paths (void) pixmaps_dir = g_build_filename (DATA_DIR, PACKAGE, "icons", NULL); help_dir = g_build_filename (DATA_DIR, PACKAGE, "help", NULL); datas_dir = g_build_filename (DATA_DIR, PACKAGE, "datas", NULL); - config_dir = g_build_filename(g_get_user_config_dir(), HB_DATA_PATH, NULL); - + config_dir = g_build_filename (g_get_user_config_dir(), HB_DATA_PATH, NULL); + pkglib_dir = g_build_filename (PKGLIB_DIR, NULL); + //#870023 Ubuntu packages the help files in "/usr/share/doc/homebank-data/help/" for some strange reason if(! g_file_test(help_dir, (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) { @@ -680,6 +691,7 @@ build_package_paths (void) DB( g_print("- locale_dir : %s\n", locale_dir) ); DB( g_print("- help_dir : %s\n", help_dir) ); DB( g_print("- datas_dir : %s\n", datas_dir) ); + DB( g_print("- pkglib_dir : %s\n", pkglib_dir) ); } @@ -826,6 +838,7 @@ static void homebank_cleanup() g_free (pixmaps_dir); g_free (locale_dir); g_free (help_dir); + g_free (pkglib_dir); } @@ -957,7 +970,7 @@ homebank_init_i18n (void) int -main (int argc, char *argv[]) +main (int argc, char *argv[], char *env[]) { GOptionContext *option_context; GOptionGroup *option_group; @@ -1029,6 +1042,22 @@ gboolean openlast; /* change the locale if a language is specified */ language_init (PREFS->language); + DB( g_print(" -> loading plugins\n") ); + ext_init(&argc, &argv, &env); + + GList* it; + for (it = PREFS->ext_whitelist; it; it = g_list_next(it)) { + ext_load_plugin(it->data); + } + + gchar** plugins = ext_list_plugins(); + gchar** plugins_it; + for (plugins_it = plugins; *plugins_it; ++plugins_it) { + gboolean loaded = ext_is_plugin_loaded(*plugins_it); + g_print("found plugin: %s, loaded: %d\n", *plugins_it, loaded); + } + g_strfreev(plugins); + if( PREFS->showsplash == TRUE ) { splash = homebank_construct_splash(); @@ -1054,6 +1083,9 @@ gboolean openlast; mainwin = (GtkWidget *)create_hbfile_window (NULL); + GValue mainwin_val = G_VALUE_INIT; + ext_hook("create_main_window", EXT_OBJECT(&mainwin_val, mainwin), NULL); + if(mainwin) { @@ -1134,11 +1166,17 @@ gboolean openlast; /* update the mainwin display */ ui_mainwindow_update(mainwin, GINT_TO_POINTER(UF_TITLE+UF_SENSITIVE+UF_BALANCE+UF_VISUAL)); - DB( g_print(" -> gtk_main()\n" ) ); + ext_hook("enter_main_loop", NULL); + DB( g_print(" -> gtk_main()\n" ) ); gtk_main (); + + ext_hook("exit_main_loop", NULL); } + DB( g_print(" -> unloading plugins\n") ); + ext_term(); + } diff --git a/src/homebank.h b/src/homebank.h index 745203f..9acc5fc 100644 --- a/src/homebank.h +++ b/src/homebank.h @@ -220,6 +220,7 @@ const gchar *homebank_app_get_pixmaps_dir (void); const gchar *homebank_app_get_locale_dir (void); const gchar *homebank_app_get_help_dir (void); const gchar *homebank_app_get_datas_dir (void); +const gchar *homebank_app_get_pkglib_dir (void); guint32 homebank_app_date_get_julian(void); /* - - - - obsolete things - - - - */ diff --git a/src/refcount.h b/src/refcount.h new file mode 100644 index 0000000..f97b93a --- /dev/null +++ b/src/refcount.h @@ -0,0 +1,54 @@ + +#ifndef __REFCOUNT_H__ +#define __REFCOUNT_H__ + +#include + + +static inline gpointer rc_alloc(size_t size) +{ + gpointer chunk = g_malloc0(size + sizeof(long)); + (*(long*)chunk) = 1; + //g_print("ALLOC: %p (ref %ld)\n", (long*)chunk + 1, *(long*)chunk); + return (long*)chunk + 1; +} + +static inline gpointer rc_ref(gpointer p) +{ + //g_print(" REF: %p (ref %ld)\n", p, *((long*)p - 1)); + if (p) { + ++(*((long*)p - 1)); + } + return p; +} + +static inline gboolean rc_unref(gpointer p) +{ + //g_print("UNREF: %p (ref %ld)\n", p, *((long*)p - 1)); + if (p && --(*((long*)p - 1)) <= 0) { + return TRUE; + } + return FALSE; +} + +static inline void rc_free(gpointer p) +{ + //g_print(" FREE: %p (ref %ld)\n", p, *((long*)p - 1)); + g_free((long*)p - 1); +} + +static inline gpointer rc_dup(gpointer p, size_t size) +{ + if (p) { + gpointer chunk = (long*)p - 1; + gpointer new_chunk = g_memdup(chunk, size + sizeof(long)); + *(long*)new_chunk = 1; + //g_print(" DUP: %p (ref %ld) -> %p (ref %ld)\n", p, *((long*)p - 1), (long*)new_chunk + 1, *(long*)new_chunk); + return (long*)new_chunk + 1; + } + //g_print(" DUP: NULL\n"); + return NULL; +} + + +#endif diff --git a/src/typemap b/src/typemap new file mode 100644 index 0000000..fc4a616 --- /dev/null +++ b/src/typemap @@ -0,0 +1,60 @@ + +TYPEMAP + +Account* T_HB_OBJECT +Transaction* T_HB_OBJECT +GObject* T_GOBJECT + +gint T_IV +guint T_UV +gdouble T_NV +gboolean T_GBOOLEAN +gchar T_CHAR +gchar* T_GCHAR_PTR +const gchar* T_GCHAR_PTR + +GPtrArray* T_GPTRARRAY +GHashTable* T_GHASHTABLE + + +INPUT + +T_HB_OBJECT + EXT_P2C_OBJECT(\"HomeBank::${ my ($t) = $ntype =~ /(.+)Ptr$/; \$t }\", $arg, $var, $type); + +T_GOBJECT + $var = SvGobject($arg); + +T_GCHAR_PTR + $var = SvGchar_ptr($arg); + +T_GBOOLEAN + $var = SvGboolean($arg); + +T_GPTRARRAY + $var = SvGptrarray($arg); + +T_GHASHTABLE + $var = SvGhashtable($arg); + + +OUTPUT + +T_HB_OBJECT + EXT_C2P_OBJECT(\"HomeBank::${ my ($t) = $ntype =~ /(.+)Ptr$/; \$t }\", $arg, rc_ref($var)); + +T_GOBJECT + $arg = newSVgobject($var); + +T_GCHAR_PTR + $arg = newSVgchar_ptr($var); + +T_GBOOLEAN + $arg = newSVgboolean($var); + +T_GPTRARRAY + $arg = newSVgptrarray($var); + +T_GHASHTABLE + $arg = newSVghashtable($var); + diff --git a/src/ui-pref.c b/src/ui-pref.c index 558f1df..4cc1eeb 100644 --- a/src/ui-pref.c +++ b/src/ui-pref.c @@ -24,6 +24,7 @@ #include "dsp_mainwindow.h" #include "gtk-chart-colors.h" +#include "ext.h" /****************************************************************************/ /* Debug macros */ @@ -59,14 +60,11 @@ enum enum { - PREF_GENERAL, - PREF_INTERFACE, - PREF_COLUMNS, - PREF_DISPLAY, - PREF_IMPORT, - PREF_REPORT, - PREF_EURO, - PREF_MAX + EXT_COLUMN_ENABLED = 0, + EXT_COLUMN_LABEL, + EXT_COLUMN_TOOLTIP, + EXT_COLUMN_PLUGIN_NAME, + EXT_NUM_COLUMNS }; GdkPixbuf *pref_pixbuf[PREF_MAX]; @@ -80,6 +78,7 @@ static gchar *pref_pixname[PREF_MAX] = { "prf-import", "prf-report", "prf-euro", // to be renamed +"prf-plugins", //"prf_charts.svg" }; @@ -90,7 +89,8 @@ N_("Transactions"), N_("Display format"), N_("Import/Export"), N_("Report"), -N_("Euro minor") +N_("Euro minor"), +N_("Plugins") // }; @@ -205,6 +205,7 @@ GtkWidget *list_txn_colprefcreate(void); static void list_txn_colpref_get(GtkTreeView *treeview, gboolean *columns); +static void list_ext_colpref_get(GtkTreeView *treeview, GList **columns); @@ -1247,6 +1248,7 @@ const gchar *lang; //PREFS->chart_legend = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->CM_chartlegend)); + list_ext_colpref_get(GTK_TREE_VIEW(data->PI_plugin_columns), &(PREFS->ext_whitelist)); } /* @@ -2063,6 +2065,202 @@ gint row; return(container); } + +void plugin_execute_action(GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata); + +static void +toggle_plugin(GtkCellRendererToggle *cell, gchar* path_str, gpointer data) +{ + GtkTreeModel *model = (GtkTreeModel*)data; + GtkTreeIter iter; + GtkTreePath *path = gtk_tree_path_new_from_string(path_str); + + const gchar* plugin; + + gtk_tree_model_get_iter(model, &iter, path); + gtk_tree_model_get(model, &iter, EXT_COLUMN_PLUGIN_NAME, &plugin, -1); + + gboolean enabled = ext_is_plugin_loaded(plugin); + if (enabled) { + ext_unload_plugin(plugin); + enabled = FALSE; + } else { + enabled = (ext_load_plugin(plugin) == 0); + if (!enabled) { + ext_run_modal(_("Plugin Error"), _("The plugin failed to load properly."), "error"); + } + } + + /* set new value */ + gtk_list_store_set(GTK_LIST_STORE (model), &iter, EXT_COLUMN_ENABLED, enabled, -1); + + /* clean up */ + gtk_tree_path_free(path); +} + + +void plugin_execute_action(GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata) +{ + GtkTreeModel* model = gtk_tree_view_get_model(treeview); + GtkTreeIter iter; + + if (gtk_tree_model_get_iter(model, &iter, path)) { + gchar* plugin_filename; + gtk_tree_model_get(model, &iter, EXT_COLUMN_PLUGIN_NAME, &plugin_filename, -1); + ext_execute_action(plugin_filename); + g_free(plugin_filename); + } +} + +static GtkWidget *defpref_page_plugins (struct defpref_data *data) +{ + GtkWidget *container; + GtkListStore *store; + GtkTreeIter it; + GtkWidget* view; + + container = gtk_vbox_new(FALSE, 0); + + store = gtk_list_store_new(EXT_NUM_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + + gchar** plugins = ext_list_plugins(); + gchar** plugins_it; + for (plugins_it = plugins; *plugins_it; ++plugins_it) { + + gboolean enabled = ext_is_plugin_loaded(*plugins_it); + GHashTable* metadata = ext_read_plugin_metadata(*plugins_it); + if (!metadata) { + metadata = g_hash_table_new(g_str_hash, g_str_equal); + } + + gchar* tmp = NULL; + + // NAME + gchar* name = g_hash_table_lookup(metadata, "name"); + if (!name || *name == '\0') { + name = *plugins_it; + } + name = g_markup_escape_text(name, -1); + gchar* label = g_strdup_printf("%s", name); + gchar* tooltip = g_strdup_printf("%s", name); + g_free(name); + + // VERSION + gchar* version = g_hash_table_lookup(metadata, "version"); + if (version) { + version = g_markup_escape_text(version, -1); + tmp = label; + label = g_strdup_printf("%s %s", tmp, version); + g_free(tmp); + tmp = tooltip; + tooltip = g_strdup_printf("%s %s", tmp, version); + g_free(tmp); + g_free(version); + } + + // ABSTRACT + gchar* abstract = g_hash_table_lookup(metadata, "abstract"); + if (abstract) { + abstract = g_markup_escape_text(abstract, -1); + tmp = label; + label = g_strdup_printf("%s\n%s", tmp, abstract); + g_free(tmp); + g_free(abstract); + } + + // AUTHOR + gchar* author = g_hash_table_lookup(metadata, "author"); + if (author) { + author = g_markup_escape_text(author, -1); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s", tmp, author); + g_free(tmp); + g_free(author); + } + + // WEBSITE + gchar* website = g_hash_table_lookup(metadata, "website"); + if (website) { + website = g_markup_escape_text(website, -1); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s: %s", tmp, _("Website"), website); + g_free(tmp); + g_free(website); + } + + // FILEPATH + tmp = ext_find_plugin(*plugins_it); + gchar* full = g_markup_escape_text(tmp, -1); + g_free(tmp); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s: %s", tmp, _("File"), full); + g_free(tmp); + g_free(full); + + g_hash_table_unref(metadata); + + gtk_list_store_append(store, &it); + gtk_list_store_set(store, &it, + EXT_COLUMN_ENABLED, enabled, + EXT_COLUMN_LABEL, label, + EXT_COLUMN_TOOLTIP, tooltip, + EXT_COLUMN_PLUGIN_NAME, *plugins_it, + -1); + + g_free(label); + g_free(tooltip); + } + g_strfreev(plugins); + + view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); + g_object_unref(store); + + g_signal_connect(view, "row-activated", (GCallback)plugin_execute_action, NULL); + + gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), TRUE); + gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(view), EXT_COLUMN_TOOLTIP); + + + GtkTreeViewColumn *col; + GtkCellRenderer *renderer; + + + col = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(col, _("Enabled")); + gtk_tree_view_column_set_sort_column_id(col, EXT_COLUMN_ENABLED); + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); + + renderer = gtk_cell_renderer_toggle_new(); + gtk_tree_view_column_pack_start(col, renderer, TRUE); + gtk_tree_view_column_add_attribute(col, renderer, "active", 0); + g_signal_connect(renderer, "toggled", G_CALLBACK(toggle_plugin), store); + + col = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(col, _("Plugin")); + gtk_tree_view_column_set_sort_column_id(col, EXT_COLUMN_LABEL); + gtk_tree_view_column_set_expand(col, TRUE); + /*gtk_tree_view_column_set_sort_order(col, GTK_SORT_ASCENDING);*/ + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); + + renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start(col, renderer, TRUE); + gtk_tree_view_column_add_attribute(col, renderer, "markup", EXT_COLUMN_LABEL); + + data->PI_plugin_columns = view; + + GtkWidget* sw = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_ETCHED_IN); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(sw), view); + + gtk_box_pack_start(GTK_BOX(container), sw, TRUE, TRUE, 0); + + return(container); +} + + static void defpref_selection(GtkTreeSelection *treeselection, gpointer user_data) { struct defpref_data *data; @@ -2161,7 +2359,7 @@ gint result; // the window creation -GtkWidget *defpref_dialog_new (void) +GtkWidget *defpref_dialog_new (gint initial_selection) { struct defpref_data data; GtkWidget *window, *content, *mainvbox; @@ -2290,6 +2488,10 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; page = defpref_page_euro(&data); gtk_notebook_append_page (GTK_NOTEBOOK (notebook), page, NULL); + //plugins + page = defpref_page_plugins(&data); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), page, NULL); + //todo:should move this gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data.CM_euro_enable), PREFS->euro_active); @@ -2345,7 +2547,8 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; //select first row - GtkTreePath *path = gtk_tree_path_new_first (); + GtkTreePath *path = gtk_tree_path_new_from_indices(initial_selection, -1); + gtk_tree_selection_select_path (gtk_tree_view_get_selection(GTK_TREE_VIEW(data.LV_page)), path); @@ -2354,6 +2557,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; gtk_tree_path_free(path); gtk_widget_show_all (window); + gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), initial_selection); gint result; gchar *old_lang; @@ -2655,3 +2859,32 @@ gint i; return(view); } + +static void list_ext_colpref_get(GtkTreeView *treeview, GList **columns) +{ + GtkTreeModel *model; + GtkTreeIter iter; + + g_list_free_full(*columns, g_free); + *columns = NULL; + + model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview)); + + gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter); + while (valid) { + gboolean enabled = FALSE; + const gchar* name; + + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, + EXT_COLUMN_ENABLED, &enabled, + EXT_COLUMN_PLUGIN_NAME, &name, + -1); + + if (enabled) { + *columns = g_list_append(*columns, g_strdup(name)); + } + + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter); + } +} + diff --git a/src/ui-pref.h b/src/ui-pref.h index 06b33e6..47c0b01 100644 --- a/src/ui-pref.h +++ b/src/ui-pref.h @@ -115,12 +115,26 @@ struct defpref_data GtkWidget *CY_dtex_datefmt; GtkWidget *CY_dtex_ofxmemo; + GtkWidget *PI_plugin_columns; +}; + +enum +{ + PREF_GENERAL, + PREF_INTERFACE, + PREF_COLUMNS, + PREF_DISPLAY, + PREF_IMPORT, + PREF_REPORT, + PREF_EURO, + PREF_PLUGINS, + PREF_MAX }; void free_pref_icons(void); void load_pref_icons(void); -GtkWidget *defpref_dialog_new (void); +GtkWidget *defpref_dialog_new (gint initial_selection); #endif -- 2.44.0