]> Dogcows Code - chaz/homebank/commitdiff
add plugin engine (supports C and Perl plugins)
authorCharles McGarvey <chazmcgarvey@brokenzipper.com>
Thu, 28 Aug 2014 04:00:43 +0000 (22:00 -0600)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Wed, 28 Dec 2016 22:54:19 +0000 (15:54 -0700)
36 files changed:
.gitignore
Makefile.am
bootstrap
configure.ac
data/icons/Makefile.am
data/icons/hicolor_status_22x22_prf-plugins.png [new file with mode: 0644]
data/icons/hicolor_status_48x48_prf-plugins.png [new file with mode: 0644]
plugins/Makefile.am [new file with mode: 0644]
plugins/hello.pl [new file with mode: 0644]
plugins/native.c [new file with mode: 0644]
plugins/transfer-matcher.pl [new file with mode: 0644]
src/HomeBank.pm [new file with mode: 0644]
src/Makefile.am
src/dsp_mainwindow.c
src/ext-native.c [new file with mode: 0644]
src/ext-perl.xs [new file with mode: 0644]
src/ext-value.c [new file with mode: 0644]
src/ext-value.h [new file with mode: 0644]
src/ext.c [new file with mode: 0644]
src/ext.h [new file with mode: 0644]
src/hb-account.c
src/hb-archive.c
src/hb-assign.c
src/hb-category.c
src/hb-payee.c
src/hb-preferences.c
src/hb-preferences.h
src/hb-tag.c
src/hb-transaction.c
src/hb-xml.c
src/homebank.c
src/homebank.h
src/refcount.h [new file with mode: 0644]
src/typemap [new file with mode: 0644]
src/ui-pref.c
src/ui-pref.h

index 894d66d807e18d4e3955052393ebfccec5e242fd..67b5baf92c6c9f0b934ebf18483c13a7ab935e32 100644 (file)
@@ -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
index e91e4b7732fe2ebbe6e06a65e00be044d19fc276..ac9cc1c743eb55e2148d5656cdc424a565821b8e 100644 (file)
@@ -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
index 83328e3fbdc22af2dbad4a156e978dc26813dc8a..616d23a04447a9e67b8ea9af2038355c1557ecd4 100755 (executable)
--- a/bootstrap
+++ b/bootstrap
@@ -1,6 +1,7 @@
 #!/bin/sh
 
-aclocal \
+libtoolize \
+&& aclocal \
 && autoheader \
 && automake --gnu --add-missing \
 && autoconf
index 601558ba20ff849e63dcee4379274f42126a4cdb..dfaa66d48e303bd8ca6dcc9021bce1ed7a4a4a37 100644 (file)
@@ -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
index 266287e22eba118a206a18463240e8f5b51a9aa7..e7880ab3be9c301c2554ec4096885b2de6dd07a1 100644 (file)
@@ -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 (file)
index 0000000..64cdca6
Binary files /dev/null and b/data/icons/hicolor_status_22x22_prf-plugins.png differ
diff --git a/data/icons/hicolor_status_48x48_prf-plugins.png b/data/icons/hicolor_status_48x48_prf-plugins.png
new file mode 100644 (file)
index 0000000..3950019
Binary files /dev/null and b/data/icons/hicolor_status_48x48_prf-plugins.png differ
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
new file mode 100644 (file)
index 0000000..62a7111
--- /dev/null
@@ -0,0 +1,12 @@
+
+plugindir              = $(pkglibdir)/plugins
+
+native_la_LDFLAGS      = -module -avoid-version -shared -export-dynamic
+
+native_la_SOURCES      = native.c
+
+native_la_CPPFLAGS     = $(DEPS_CFLAGS) -I$(top_srcdir)/src
+native_la_LIBADD       = $(DEPS_LIBS)
+
+plugin_LTLIBRARIES     = native.la
+
diff --git a/plugins/hello.pl b/plugins/hello.pl
new file mode 100644 (file)
index 0000000..c6abdf9
--- /dev/null
@@ -0,0 +1,281 @@
+
+# NAME:     Hello World
+# VERSION:  0.01
+# ABSTRACT: This is the "hello world" of HomeBank plugins.
+# AUTHOR:   Charles McGarvey <chazmcgarvey@brokenzipper.com>
+# 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 (file)
index 0000000..c0a5461
--- /dev/null
@@ -0,0 +1,57 @@
+
+#include <gmodule.h>
+
+#include "ext.h"
+
+
+const gchar* metadata[] = {
+"NAME:     Some Native Plugin",
+"VERSION:  0.0105",
+"ABSTRACT: Native plugins are also possible.",
+"AUTHOR:   Charles McGarvey <chazmcgarvey@brokenzipper.com>",
+"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 (file)
index 0000000..d173990
--- /dev/null
@@ -0,0 +1,28 @@
+
+# NAME:     Transfer Matcher
+# VERSION:  0.01
+# ABSTRACT: Automatically find and pair together internal transfers.
+# AUTHOR:   Charles McGarvey <chazmcgarvey@brokenzipper.com>
+# 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 (file)
index 0000000..e976610
--- /dev/null
@@ -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<homebank>
+
+=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<HomeBank> 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<homebank> 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<load_plugin>.
+
+=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 <jdoe@acme.tld>
+    # 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<all> 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 <chazmcgarvey@brokenzipper.com>
+
+=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 <http://www.gnu.org/licenses/>.
+
+=cut
+
+1;
index 4115add2ba511449e529e3d53f04a9161f664ffc..71c57f193b249e9e6bf3cf952f3539645e6ddad2 100644 (file)
@@ -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
+
index 4ada4acc86fd5855c0bfc2c795c3417a6f392a7c..98c5ea1406b6a56da082d978602163f410e75f48 100644 (file)
@@ -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..."), "<control>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 =
 "      <menuitem action='RBudget'/>"
 "      <menuitem action='RVehiculeCost'/>"
 "    </menu>"
+"    <menu action='PluginMenu'>"
+"      <separator/>"
+"      <menuitem action='PluginPreferences'/>"
+"      <separator/>"
+"    </menu>"
 "    <menu action='HelpMenu'>"
 "      <menuitem action='Contents'/>"
 "        <separator/>"
@@ -310,6 +324,7 @@ static const gchar *ui_info =
 "    <toolitem action='RBalance'/>"
 "    <toolitem action='RBudget'/>"
 "    <toolitem action='RVehiculeCost'/>"
+"      <separator/>"
 "  </toolbar>"
 
 "</ui>";
@@ -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 (file)
index 0000000..fce6315
--- /dev/null
@@ -0,0 +1,192 @@
+
+#include <glib/gstdio.h>
+#include <gmodule.h>
+
+#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 (file)
index 0000000..8002047
--- /dev/null
@@ -0,0 +1,1042 @@
+
+#include <EXTERN.h>
+#include <perl.h>
+#include <XSUB.h>
+
+#include <string.h>
+
+#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 (file)
index 0000000..47c4829
--- /dev/null
@@ -0,0 +1,82 @@
+
+#include <stdarg.h>
+
+#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 (file)
index 0000000..a5002bb
--- /dev/null
@@ -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 (file)
index 0000000..b366491
--- /dev/null
+++ b/src/ext.c
@@ -0,0 +1,327 @@
+
+#include <stdarg.h>
+#include <string.h>
+#include <gmodule.h>
+#include <gtk/gtk.h>
+
+#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 (file)
index 0000000..edc3a44
--- /dev/null
+++ b/src/ext.h
@@ -0,0 +1,46 @@
+
+#ifndef __EXT_H__
+#define __EXT_H__
+
+#include <glib.h>
+
+#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
+
index da1f30a853285a2d3dbf1183c933559f2b55390e..c3366e770167a414e02984c55bd8c19409f56847 100644 (file)
@@ -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;
                }
        }
index 0ede27030cce95f71fb29d1ef1f7c0f67da9a627..e3faf5d40101af4c8958df252789db54a2a3eb56 100644 (file)
@@ -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);
        }
 }
 
index ea50aa67bb65e1998956fa7915d1182edf037399..8303436ea80d7a7e0ef0276e2a8a0d51ecb50eb0 100644 (file)
@@ -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));
 }
 
 
index 3d9b15a18903f9cf13fafd470412e239911c7d4f..3dad28b5ea092b960ae44b44e3398dae8ffc7080 100644 (file)
@@ -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));
 }
 
 
index 4a99bb33f2d0136a412e2de00cebc13f3501ca58..6a51cc7c2563267772ba6853633341a5e55efda6 100644 (file)
@@ -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));
 }
 
 
index 6fead0dbdc6e2b2b12ce51cd8cff998d160fa602..e7695bc5f83877af411c5ce15dc1bf7d42a67199 100644 (file)
@@ -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->);
index 938a1ec59e22494c7b0b13ce8573647a992c55df..ae8d259a36184001e0f69ae0da3d8ea7d6bb3f5d 100644 (file)
@@ -167,6 +167,10 @@ struct Preferences
        gchar      *vehicle_unit_100;
        gchar      *vehicle_unit_distbyvol;
 
+       // plugins
+       gchar** ext_path;
+       GList* ext_whitelist;
+
 };
 
 
index 8554cfa3ad747d884431f24675b585c9bd0af383..c8353a489d0916c616c4929c81db437ba2d97d1c 100644 (file)
@@ -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));
 }
 
 
index 3a5f6cfa7676fa649827765995f74e6c0d829a1e..1d6699871a85d174156d7e893b3d8ee2fe1be5de 100644 (file)
@@ -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);
        }
 }
 
index d859c3510c8353ea9e7c6b90967382a2d61d04d0..9db1e3014744c1f69aaddddcae19eebeee4c6665 100644 (file)
@@ -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)
        {
index c452bdd263d1b2a0c031c9600674e0eaeb38e0a8..106b8ecb90e5e7d97dcb0a521aad1aa48c6f8229 100644 (file)
@@ -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 "<span size='small'>%s</span>"
@@ -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();
+
        }
 
 
index 745203f06f69e8e4fdac546072eaa5af87d55446..9acc5fc8f9c5ef25a2379c73c6fab003a18a55f4 100644 (file)
@@ -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 (file)
index 0000000..f97b93a
--- /dev/null
@@ -0,0 +1,54 @@
+
+#ifndef __REFCOUNT_H__
+#define __REFCOUNT_H__
+
+#include <glib.h>
+
+
+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 (file)
index 0000000..fc4a616
--- /dev/null
@@ -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);
+
index 558f1df2c92ca2d754754980d50876fcbb0f24ac..4cc1eebf66c78871d9fa8711977476b9312b8d27 100644 (file)
@@ -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("<b>%s</b>", name);
+               gchar* tooltip = g_strdup_printf("<span size='x-large' weight='bold'>%s</span>", 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<b>%s:</b> %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<b>%s:</b> %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);
+       }
+}
+
index 06b33e64945e061d41107ed76b7793d0a08ce97b..47c0b011f588182a312695beb09738f3a0e3411b 100644 (file)
@@ -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
This page took 0.115764 seconds and 4 git commands to generate.