From: Charles McGarvey Date: Sun, 26 May 2019 20:39:23 +0000 (-0600) Subject: Merge branch 'master' into ext-perl X-Git-Url: https://git.dogcows.com/gitweb?a=commitdiff_plain;h=ee5038e6ccae412295b44be762d3445419042d59;hp=e479b37a8ea13230b81b43ecba00f89586a8f91a;p=chaz%2Fhomebank Merge branch 'master' into ext-perl --- diff --git a/.gitignore b/.gitignore index 66770f3..5d51d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,9 @@ /po/POTFILES /po/stamp-it /src/.deps +/src/ext-perl.c /src/homebank +/src/perlxsi.c /stamp-h1 Makefile Makefile.in diff --git a/Makefile.am b/Makefile.am index e5c6cfe..45d1f49 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,9 @@ # HomeBank Makefile.am +ACLOCAL_AMFLAGS = -I m4 + #SUBDIRS = src -SUBDIRS = src data images mime pixmaps themes po doc +SUBDIRS = src data images mime pixmaps themes po doc plugins datasdir = $(datadir)/homebank/datas/ @@ -26,7 +28,6 @@ DISTCLEANFILES = ... intltool-extract \ intltool-update \ po/.intltool-merge-cache - # we clean every folder under /usr/share/homebank as well on uninstall uninstall-hook: -rm -rf $(datadir)/homebank/datas @@ -34,3 +35,10 @@ uninstall-hook: -rm -rf $(datadir)/homebank/icons -rm -rf $(datadir)/homebank/images #-rmdir $(datadir)/homebank + +run: all + PERL5LIB=src src/homebank + +debug: all + PERL5LIB=src gdb src/homebank + diff --git a/README b/README index 4c74015..99caa95 100644 --- a/README +++ b/README @@ -11,3 +11,10 @@ HomeBank home page is at: Please be sure to visit this site for information, documentation, tutorials, news, etc. + + +2. This Version +=============== + +This fork of HomeBank supports plugins written in C and Perl. + diff --git a/bootstrap b/bootstrap index 83328e3..616d23a 100755 --- a/bootstrap +++ b/bootstrap @@ -1,6 +1,7 @@ #!/bin/sh -aclocal \ +libtoolize \ +&& aclocal \ && autoheader \ && automake --gnu --add-missing \ && autoconf diff --git a/configure.ac b/configure.ac index a28cc9d..baf00bf 100644 --- a/configure.ac +++ b/configure.ac @@ -9,6 +9,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. @@ -22,7 +26,7 @@ AC_PROG_INSTALL AC_PROG_INTLTOOL # Checks for libraries. -PKG_CHECK_MODULES(DEPS, gtk+-3.0 >= 3.16 glib-2.0 >= 2.39) +PKG_CHECK_MODULES(DEPS, gtk+-3.0 >= 3.16 glib-2.0 >= 2.39 gmodule-2.0 >= 2.39) AC_SUBST(DEPS_CFLAGS) AC_SUBST(DEPS_LIBS) AC_CHECK_LIB(m, pow) @@ -66,7 +70,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]) @@ -78,6 +82,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]) @@ -114,6 +153,7 @@ themes/hicolor/Makefile po/Makefile.in doc/Makefile doc/images/Makefile +plugins/Makefile ]) AC_OUTPUT @@ -126,6 +166,7 @@ echo $PACKAGE $VERSION echo echo Compiler................ : $CC echo Build with OFX support.. : $build_ofx +echo Build with perl support. : $build_perl if test "x$noofx" = "xtrue" ; then echo ........................ : **error** libofx header is missing, ofx feature will be disabled. Check your libofx installation fi diff --git a/plugins/Makefile.am b/plugins/Makefile.am new file mode 100644 index 0000000..62a7111 --- /dev/null +++ b/plugins/Makefile.am @@ -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 index 0000000..25520b5 --- /dev/null +++ b/plugins/hello.pl @@ -0,0 +1,234 @@ + +# NAME: Hello World +# VERSION: 0.01 +# ABSTRACT: This is the "hello world" of HomeBank plugins. +# AUTHOR: Charles McGarvey +# 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; +} + +sub on_create_main_window { + my $self = shift; + my $window = shift; + + if (!$window) { + require Gtk3; + $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); +} + +my $test_win; + +sub on_test { + my $self = shift; + require Gtk3; + + my $window = Gtk3::Window->new('toplevel'); + use Devel::Peek; + Dump($window); + print Dumper($window); + $window->set_title("Hello World"); + #$window->signal_connect(delete_event => sub { Gtk3->main_quit }); + $window->signal_connect(delete_event => sub { undef $test_win }); + + my $button = Gtk3::Button->new('Click Me!'); + Dump($button); + print Dumper($button); + $button->signal_connect(clicked => sub { + print "Hello Gtk3-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 Gtk3; + # + 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->memo, ", ", $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 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; +} + +sub EXECUTE { + print "the perl plugin is being configured.....\n"; + HomeBank->info("Hello Prefs", "YEEEEEARGGH!!!!!"); +} + +#__PACKAGE__->meta->make_immutable; diff --git a/plugins/native.c b/plugins/native.c new file mode 100644 index 0000000..c0a5461 --- /dev/null +++ b/plugins/native.c @@ -0,0 +1,57 @@ + +#include + +#include "ext.h" + + +const gchar* metadata[] = { +"NAME: Some Native Plugin", +"VERSION: 0.0105", +"ABSTRACT: Native plugins are also possible.", +"AUTHOR: Charles McGarvey ", +"WEBSITE: http://acme.tld/", +}; + + +G_MODULE_EXPORT void load(void); +G_MODULE_EXPORT void unload(void); +G_MODULE_EXPORT void execute(void); + +G_MODULE_EXPORT void on_create_main_window(GList* args); +G_MODULE_EXPORT void on_enter_main_loop(GList* args); + + +G_MODULE_EXPORT void load() +{ + g_print("loading native plugin....... %p\n", load); +} + +G_MODULE_EXPORT void unload() +{ + g_print("destroy native plugin....... %p\n", unload); +} + +G_MODULE_EXPORT void execute() +{ + g_print("Configuring that native plugin!!!\n"); +} + +static GtkWidget* win = NULL; + +G_MODULE_EXPORT void on_create_main_window(GList* args) +{ + GList* it = g_list_first(args); + win = g_value_get_object(it->data); + /*gtk_window_set_title(GTK_WINDOW(GLOBALS->mainwindow), "This is the native hello-world plugin!");*/ +} + +G_MODULE_EXPORT void on_enter_main_loop(GList* args) +{ + g_print("setting main window title.....\n"); + if (win) { + gtk_window_set_title(GTK_WINDOW(win), "This is the native hello-world plugin!"); + } else { + g_printerr("the main window is not set :(\n"); + } +} + diff --git a/plugins/transfer-matcher.pl b/plugins/transfer-matcher.pl new file mode 100644 index 0000000..d173990 --- /dev/null +++ b/plugins/transfer-matcher.pl @@ -0,0 +1,28 @@ + +# NAME: Transfer Matcher +# VERSION: 0.01 +# ABSTRACT: Automatically find and pair together internal transfers. +# AUTHOR: Charles McGarvey +# WEBSITE: http://www.homebank.free.fr/ + +eval { HomeBank->version } or die "Cannot run outside of HomeBank"; + +use warnings FATAL => 'all'; +use strict; + +my $days = 3; + +sub on_transaction_inserted { + my ($self, $txn) = @_; + + my @match = grep { + $txn->account_num != $_->account_num && + $txn->amount == -$_->amount && + abs($txn->date - $_->date) <= $days + } HomeBank::File->transactions; + + return unless @match; + + $txn->pair_with(@match); +} + diff --git a/src/HomeBank.pm b/src/HomeBank.pm new file mode 100644 index 0000000..e976610 --- /dev/null +++ b/src/HomeBank.pm @@ -0,0 +1,335 @@ +package HomeBank; + +use warnings FATAL => 'all'; +use strict; + +use Symbol qw/delete_package/; + +=head1 NAME + +HomeBank - Perl plugin bindings for C + +=head1 SYNOPSIS + + # NAME: Example Plugin + + sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + + $self->on( + terminate => sub { + print "Terminating...\n"; + }, + ); + + $self; + } + + sub on_unhandled { + my ($self, $hook_id) = @_; + print "An unhandled hook named '$hook_id' was called.\n"; + } + +=head1 DESCRIPTION + +The C class provides the infrastructure for loading plugins and handling the registration and calling of +hooks. + +=head1 VARIABLES + +=head2 %plugins + +Contains all of the information about each loaded perl plugin. Plugins probably shouldn't mess around with this. + +=cut + +our %plugins; + +=head1 METHODS + +=head2 load_plugin $filepath + +Load a plugin with the given name. Dies if a plugin with the given name cannot be found or if the plugin couldn't +successfully be eval'd. L calls this to load enabled plugins; plugins themselves probably shouldn't ever use +this. + +=cut + +sub load_plugin { + my $filepath = shift; + + my $package = _valid_package_name($filepath); + $plugins{$package} ||= {}; + + my $mtime = -M $filepath; + if (defined $plugins{$package}->{mtime} && $plugins{$package}->{mtime} <= $mtime) { + warn "Already loaded $filepath"; + } else { + delete_package $package if exists $plugins{$package}->{mtime}; + + open my $fh, $filepath or die "Open '$filepath' failed ($!)"; + binmode $fh, 'utf8'; + local $/ = undef; + my $code = <$fh>; + close $fh; + + my $eval = qq/# line 1 "$filepath"\npackage $package; use base 'HomeBank::Plugin'; $code/; + { + my (%plugins, $mtime, $package); + eval "$eval; 1" or die $@; + } + + $plugins{$package}->{mtime} = $mtime; + } + if (!exists $plugins{$package}->{instance}) { + $plugins{$package}->{instance} = $package->new or die "Plugin instantiation failed"; + } +} + +=head2 unload_plugin $filepath + +The opposite of L. + +=cut + +sub unload_plugin { + my $filepath = shift; + my $package = _valid_package_name($filepath); + + return unless exists $plugins{$package}; + + if ($package->can('delete_package_on_unload') && $package->delete_package_on_unload) { + delete $plugins{$package}; + delete_package $package; + } else { + delete $plugins{$package}->{instance}; + delete $plugins{$package}->{hooks}; + } +} + +=head2 execute_action $filepath + +Allow the plugin specified by C<$filepath> to perform an action. This is called when the plugin is "activated" by the +user. Most plugins should run a modal dialog to allow the user to see and edit plugin preferences. + +=cut + +sub execute_action { + my $filepath = shift; + my $package = _valid_package_name($filepath); + + return unless exists $plugins{$package}; + + my $instance = $plugins{$package}->{instance}; + $instance->EXECUTE if $instance && $instance->can('EXECUTE'); +} + +=head2 read_metadata $filepath + +Get the metadata for a plugin without evaluating it. Plugin metadata should be in the first 100 lines of the plugin file +and should look something like this: + + # NAME: Foobar + # VERSION: 0.01 + # ABSTRACT: This plugin does something. + # AUTHOR: John Doe + # WEBSITE: http://acme.tld/ + +=cut + +sub read_metadata { + my $filepath = shift; + + my $package = _valid_package_name($filepath); + $plugins{$package} ||= {}; + + return $plugins{$package}->{metadata} if exists $plugins{$package}->{metadata}; + + my @keywords = qw/name version abstract author website/; + my $keywords = join('|', @keywords); + + my $metadata = {}; + open my $fh, $filepath or die "Open '$filepath' failed ($!)"; + my $count = 0; + for my $line (<$fh>) { + last if 100 < ++$count; + my ($key, $val) = $line =~ /^#[ \t]*($keywords)[ \t]*[=:](.*)/i; + if ($key && $val) { + $val =~ s/^\s*//; + $val =~ s/\s*$//; + $metadata->{lc $key} = $val; + } + } + close $fh; + + $plugins{$package}->{metadata} = $metadata; +} + +=head2 call_hook $hook_id, ... + +Invoke each perl plugins' hook handlers for the given hook. Additional arguments are passed through to each handler. +Plugins shouldn't use this. + +=cut + +sub call_hook { + my $hook = shift; + + $hook =~ s/[.-]/_/g; + + for my $package (keys %plugins) { + my $hooks = ($plugins{$package} ||= {})->{hooks} ||= {}; + my $count = 0; + for my $cb (@{$hooks->{$hook} ||= []}) { + eval { $cb->(@_); 1 } or warn $@; + $count++; + } + if ($count == 0) { + for my $cb (@{$hooks->{unhandled} ||= []}) { + eval { $cb->($hook, @_); 1 } or warn $@; + } + } + } +} + +=head2 register_method_hooks $plugin + +Register hooks defined as methods that begin with `on_'. + +=cut + +sub register_method_hooks { + my $plugin = shift; + my $package = ref $plugin; + + no strict 'refs'; + my %subs = map { $_ =~ /^on_(.+)/ ? ($1 => $_) : () } keys %{"${package}::"}; + use strict 'refs'; + + register_hooks($plugin, %subs); +} + +=head2 register_hooks $plugin, %hooks + +Register hooks for a plugin. + +=cut + +sub register_hooks { + my ($plugin, %hooks) = @_; + my $package = ref $plugin; + + my $hooks = ($plugins{$package} ||= {})->{hooks} ||= {}; + for my $hook (keys %hooks) { + if (!ref($hooks{$hook}) && defined &{"${package}::$hooks{$hook}"}) { + push @{$hooks->{$hook} ||= []}, sub { unshift @_, $plugin; goto &{"${package}::$hooks{$hook}"} }; + } elsif (ref($hooks{$hook}) eq 'CODE') { + push @{$hooks->{$hook} ||= []}, $hooks{$hook}; + } else { + warn "Hook callback is unusable"; + } + } +} + +=head2 unregister_hooks $package, [@hooks] + +Unregister hooks for a package. If no hooks are specified, B hooks will be unregistered. + +=cut + +sub unregister_hooks { + my ($package, @hooks) = @_; + + if (@hooks) { + for my $hook (@hooks) { + (($plugins{$package} ||= {})->{hooks} ||= {})->{$hook} = []; + } + } else { + ($plugins{$package} ||= {})->{hooks} = {}; + } +} + +=head2 _valid_package_name $string + +Turn a string into a valid name of a package. + +=cut + +sub _valid_package_name { + my $str = shift; + $str =~ s|.*?([^/\\]+)\.pl$|$1|; + $str =~ s|([^A-Za-z0-9\/_])|sprintf("_%2x",unpack("C",$1))|eg; + $str =~ s|/(\d)|sprintf("/_%2x",unpack("C",$1))|eg; + $str =~ s|[/_]|::|g; + "HomeBank::Plugin::$str"; +} + + +package HomeBank::Boolean; + +use overload + '0+' => sub { ${$_[0]} }, + '++' => sub { $_[0] = ${$_[0]} + 1 }, + '--' => sub { $_[0] = ${$_[0]} - 1 }, + fallback => 1; + +package Types::Serialiser::Boolean; +@HomeBank::Boolean::ISA = Types::Serialiser::Boolean::; + + +package HomeBank::Plugin; + +sub new { + my ($class, $self) = (shift, shift || {}); + bless $self, $class; + HomeBank::register_method_hooks($self); + $self; +} + +sub on { + goto &HomeBank::register_hooks; +} + +sub off { + goto &HomeBank::unregister_hooks; +} + + +package HomeBank::Transaction; + +sub datetime { + require DateTime; + require DateTime::Format::Strptime; + my $dt = DateTime->new(shift->date); + $dt->set_formatter(DateTime::Format::Strptime->new(pattern => '%Y-%m-%d')); + $dt; +} + + +=head1 AUTHOR + +Charles McGarvey + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2013 Charles McGarvey. + +This file is part of HomeBank. + +HomeBank is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +HomeBank is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +=cut + +1; diff --git a/src/Makefile.am b/src/Makefile.am index 9e4fd20..7acfc87 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,7 +1,8 @@ common_defines = \ -DSHARE_DIR=\""$(pkgdatadir)"\" \ - -DDATA_DIR=\""$(datadir)"\" + -DDATA_DIR=\""$(datadir)"\" \ + -DPKGLIB_DIR=\""$(pkglibdir)"\" bin_PROGRAMS = homebank @@ -130,14 +131,37 @@ homebank_SOURCES = \ ui-txn-multi.h \ ui-widgets-data.c \ ui-widgets.c \ - ui-widgets.h + ui-widgets.h \ + refcount.h \ + ext.c \ + ext.h \ + ext-value.c \ + ext-value.h \ + ext-native.c \ + ext-perl.xs + +EXTRA_homebank_DEPENDENCIES = $(PERL_OBJS) homebank_LDADD = $(DEPS_LIBS) \ - $(LIBSOUP_LIBS) + $(LIBSOUP_LIBS) \ + $(PERL_OBJS) AM_CPPFLAGS = \ $(DEPS_CFLAGS) \ $(LIBSOUP_CFLAGS) \ $(common_defines) +$(PERL_OBJS): CPPFLAGS += $(PERL_CPPFLAGS) + +ext-perl.c: ext-perl.xs typemap + $(XSUBPP) -typemap $(PERL_PRIVLIBEXP)/ExtUtils/typemap -typemap typemap $< >$@ + +perlxsi.c: Makefile + $(PERL) -MExtUtils::Embed -e xsinit -- -std HomeBank + +CLEANFILES = ext-perl.c perlxsi.c + +pluginsupportdir = $(pkglibdir) +pluginsupport_DATA = HomeBank.pm + diff --git a/src/dsp-mainwindow.c b/src/dsp-mainwindow.c index 495dad8..927ebaa 100644 --- a/src/dsp-mainwindow.c +++ b/src/dsp-mainwindow.c @@ -22,6 +22,8 @@ #include "dsp-mainwindow.h" +#include "ext.h" + #include "list-account.h" #include "hub-account.h" @@ -124,6 +126,8 @@ static void ui_mainwindow_action_export(void); static void ui_mainwindow_action_anonymize(void); static void ui_mainwindow_action_file_statistics(void); +static void ui_mainwindow_action_pluginprefs(void); + static void ui_mainwindow_action_help(void); void ui_mainwindow_action_help_welcome(void); static void ui_mainwindow_action_help_online(void); @@ -150,6 +154,7 @@ void ui_mainwindow_recent_add (struct hbfile_data *data, const gchar *path); void ui_mainwindow_recent_add (struct hbfile_data *data, const gchar *path); +static void ui_mainwindow_showprefs(gint page); static GtkActionEntry entries[] = { @@ -164,6 +169,7 @@ static GtkActionEntry entries[] = { { "TxnMenu" , NULL, N_("_Transactions"), NULL, NULL, NULL }, { "ReportMenu" , NULL, N_("_Reports"), NULL, NULL, NULL }, { "ToolsMenu" , NULL, N_("_Tools"), NULL, NULL, NULL }, + { "PluginMenu" , NULL, N_("_Plugins"), NULL, NULL, NULL }, { "HelpMenu" , NULL, N_("_Help"), NULL, NULL, NULL }, // { "Import" , NULL, N_("Import") }, @@ -188,7 +194,7 @@ static GtkActionEntry entries[] = { //{ "ImportQIF" , ICONNAME_HB_FILE_IMPORT , N_("QIF file...") , NULL, N_("Open the import assistant"), G_CALLBACK (ui_mainwindow_action_import) }, //{ "ImportOFX" , ICONNAME_HB_FILE_IMPORT , N_("OFX/QFX file...") , NULL, N_("Open the import assistant"), G_CALLBACK (ui_mainwindow_action_import) }, //{ "ImportCSV" , ICONNAME_HB_FILE_IMPORT , N_("CSV file...") , NULL, N_("Open the import assistant"), G_CALLBACK (ui_mainwindow_action_import) }, - + { "ExportQIF" , ICONNAME_HB_FILE_EXPORT , N_("Export as QIF...") , NULL, N_("Export all account in a QIF file"), G_CALLBACK (ui_mainwindow_action_export) }, /* EditMenu */ @@ -223,7 +229,10 @@ static GtkActionEntry entries[] = { { "Welcome" , NULL , N_("Show welcome dialog...") , NULL, NULL, G_CALLBACK (ui_mainwindow_action_help_welcome) }, { "FileStats" , NULL , N_("File statistics...") , NULL, NULL, G_CALLBACK (ui_mainwindow_action_file_statistics) }, { "Anonymize" , NULL , N_("Anonymize...") , NULL, NULL, G_CALLBACK (ui_mainwindow_action_anonymize) }, - + + /* Plugins */ + { "PluginPreferences", "prf-plugins", N_("_Plugins..."), "U", N_("Configure plugin preferences"), G_CALLBACK(ui_mainwindow_action_pluginprefs) }, + /* HelpMenu */ { "Contents" , ICONNAME_HELP , N_("_Contents") , "F1", N_("Documentation about HomeBank"), G_CALLBACK (ui_mainwindow_action_help) }, { "Online" , "lpi-help" , N_("Get Help Online...") , NULL, N_("Connect to the LaunchPad website for online help"), G_CALLBACK (ui_mainwindow_action_help_online) }, @@ -321,6 +330,11 @@ static const gchar *ui_info = " " " " " " +" " +" " +" " +" " +" " " " " " " " @@ -355,6 +369,7 @@ static const gchar *ui_info = " " " " " " +" " " " ""; @@ -383,7 +398,7 @@ gint result; title = g_strdup_printf ( _("Revert unsaved changes to file '%s'?"), basename); - secondtext = + secondtext = _("- Changes made to the file will be permanently lost\n" "- File will be reloaded from the last save (.xhb~)"); @@ -400,7 +415,7 @@ gint result; if( result == GTK_RESPONSE_OK ) { DB( g_print(" - should revert\n") ); - + hbfile_change_filepath(hb_filename_new_with_extension(GLOBALS->xhb_filepath, "xhb~")); ui_mainwindow_open_internal(widget, NULL); hbfile_change_filepath(hb_filename_new_with_extension(GLOBALS->xhb_filepath, "xhb")); @@ -415,7 +430,7 @@ activate_url (GtkAboutDialog *about, gpointer data) { DB( g_print("activate url %s\n", link) ); - + homebank_util_url_show (link); } @@ -434,7 +449,8 @@ gchar *version; 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 }; @@ -454,18 +470,18 @@ gchar *version; gtk_get_major_version (), gtk_get_minor_version (), gtk_get_micro_version ()); - + dialog = gtk_about_dialog_new(); gtk_window_set_transient_for (GTK_WINDOW(dialog), GTK_WINDOW(GLOBALS->mainwindow)); gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); - + gtk_about_dialog_set_program_name (GTK_ABOUT_DIALOG(dialog), g_get_application_name ()); gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), version); gtk_about_dialog_set_copyright(GTK_ABOUT_DIALOG(dialog), copyright); gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dialog), _("Free, easy, personal accounting for everyone")); gtk_about_dialog_set_license_type (GTK_ABOUT_DIALOG(dialog), GTK_LICENSE_GPL_2_0); - + //gtk_about_dialog_set_wrap_license(GTK_ABOUT_DIALOG(dialog), ); gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dialog), "http://homebank.free.fr"); gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dialog), "Visit the HomeBank website"); @@ -481,7 +497,7 @@ gchar *version; gtk_about_dialog_set_logo(GTK_ABOUT_DIALOG(dialog), pixbuf); g_object_unref (pixbuf); } - + gtk_about_dialog_set_authors(GTK_ABOUT_DIALOG(dialog), authors); gtk_about_dialog_set_artists(GTK_ABOUT_DIALOG(dialog), artists); //gtk_about_dialog_set_documenters(GTK_ABOUT_DIALOG(dialog), ); @@ -494,7 +510,7 @@ gchar *version; gtk_widget_destroy (dialog); g_free(version); - + } @@ -570,6 +586,12 @@ static void ui_mainwindow_action_file_statistics(void) } +static void ui_mainwindow_action_pluginprefs(void) +{ + ui_mainwindow_showprefs(PREF_PLUGINS); +} + + static void ui_mainwindow_action_properties(void) { create_defhbfile_dialog(); @@ -584,7 +606,7 @@ gchar *secondtext; title = _("Are you sure you want to anonymize the file?"); - secondtext = + secondtext = _("Proceeding will anonymize any text, \n" "like 'account x', 'payee y', 'memo z', ..."); @@ -597,7 +619,7 @@ gchar *secondtext; //#1707201 //if( result == GTK_RESPONSE_CANCEL ) - // return; + // return; if( result == GTK_RESPONSE_OK ) { hbfile_anonymize(); @@ -692,10 +714,15 @@ static void ui_mainwindow_action_deftag(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"); @@ -743,7 +770,7 @@ struct hbfile_data *data = g_object_get_data(G_OBJECT(GLOBALS->mainwindow), "ins // top spending gtk_chart_show_minor(GTK_CHART(data->RE_pie), GLOBALS->minor); - + ui_hub_spending_update(data->window, data); } @@ -809,7 +836,7 @@ static void ui_mainwindow_action_statistic(void) static void ui_mainwindow_action_trendtime(void) { struct hbfile_data *data = g_object_get_data(G_OBJECT(GLOBALS->mainwindow), "inst_data"); - + ui_reptime_window_new(data->acc != NULL ? data->acc->key : 0); } @@ -937,7 +964,7 @@ GtkWidget *mainvbox, *widget, *label; NULL); content_area = gtk_dialog_get_content_area(GTK_DIALOG (dialog)); - + mainvbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start (GTK_BOX (content_area), mainvbox, FALSE, FALSE, 0); gtk_container_set_border_width (GTK_CONTAINER(mainvbox), SPACING_MEDIUM); @@ -963,11 +990,11 @@ GtkWidget *mainvbox, *widget, *label; widget = gtk_button_new_with_mnemonic(_("Read HomeBank _Manual")); gtk_box_pack_start (GTK_BOX (mainvbox), widget, FALSE, FALSE, 0); g_signal_connect (widget, "clicked", G_CALLBACK (ui_mainwindow_action_help_welcome1), dialog); - + widget = gtk_button_new_with_mnemonic(_("Configure _preferences")); gtk_box_pack_start (GTK_BOX (mainvbox), widget, FALSE, FALSE, 0); g_signal_connect (widget, "clicked", G_CALLBACK (ui_mainwindow_action_help_welcome2), dialog); - + widget = gtk_button_new_with_mnemonic(_("Create a _new file")); gtk_box_pack_start (GTK_BOX (mainvbox), widget, FALSE, FALSE, 0); g_signal_connect (widget, "clicked", G_CALLBACK (ui_mainwindow_action_help_welcome3), dialog); @@ -1102,10 +1129,10 @@ gboolean file_clear = GPOINTER_TO_INT(user_data); gtk_tree_store_clear(GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(data->LV_acc)))); gtk_list_store_clear(GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(data->LV_upc)))); gtk_list_store_clear(GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(data->LV_top)))); - + data->showall = FALSE; ui_hub_account_setup(data); - + hbfile_cleanup(file_clear); hbfile_setup(file_clear); @@ -1150,7 +1177,7 @@ gint account, count; if( PREFS->heritdate == FALSE ) //fix: 318733 ope->date = GLOBALS->today; - + da_transaction_set_default_template(ope); } @@ -1170,7 +1197,7 @@ gint account, count; DB( g_print(" - added 1 transaction to %d\n", ope->kacc) ); ui_hub_account_populate(GLOBALS->mainwindow, NULL); - + count++; //todo: still usefull ? store last date date = ope->date; @@ -1205,21 +1232,21 @@ gboolean result; basename = g_path_get_basename(filepath); secondtext = g_strdup_printf ( - _("Your are about to open the backup file '%s'.\n\nAre you sure you want to do this ?"), basename); + _("Your are about to open the backup file '%s'.\n\nAre you sure you want to do this ?"), basename); result = ui_dialog_msg_confirm_alert( GTK_WINDOW(GLOBALS->mainwindow), _("Open the backup file ?"), secondtext, _("_Open backup") - ); + ); g_free(secondtext); g_free(basename); if( result == GTK_RESPONSE_OK ) retval = TRUE; - + return retval; } @@ -1258,7 +1285,7 @@ gchar *filename = NULL; g_free(filename); return; } - } + } hbfile_change_filepath(filename); ui_mainwindow_open_internal(widget, NULL); @@ -1293,7 +1320,7 @@ gint r; GLOBALS->xhb_timemodified = hbfile_file_get_time_modified(GLOBALS->xhb_filepath); hbfile_file_hasrevert(GLOBALS->xhb_filepath); - + if(PREFS->appendscheduled) scheduled_post_all_pending(); @@ -1338,7 +1365,7 @@ gint r; ui_hub_scheduled_populate(GLOBALS->mainwindow, NULL); ui_hub_spending_populate(GLOBALS->mainwindow, NULL); ui_hub_transaction_populate(data); - + ui_mainwindow_update(GLOBALS->mainwindow, GINT_TO_POINTER(UF_TITLE+UF_SENSITIVE+UF_VISUAL)); } @@ -1400,7 +1427,7 @@ gint r = XML_UNSET; _("If you save it, all the external changes could be lost. Save it anyway?"), _("S_ave Anyway") ); - + if( result != GTK_RESPONSE_OK ) return; } @@ -1464,7 +1491,7 @@ gint flags; #else data->wintitle = g_strdup_printf("%s%s - %s - " PROGNAME, changed, basename, GLOBALS->owner); #endif - + gtk_window_set_title (GTK_WINDOW (gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW)), data->wintitle); g_free(basename); @@ -1590,7 +1617,7 @@ gint flags; gtk_widget_hide(GTK_WIDGET(data->GR_top)); - + DB( g_print(" - show upcoming=%d\n", PREFS->wal_upcoming) ); if(PREFS->wal_upcoming) gtk_widget_show(GTK_WIDGET(data->GR_upc)); @@ -1681,6 +1708,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] delete-event\n") ); //store position and size @@ -1699,7 +1729,7 @@ gboolean retval = FALSE; if(PREFS->pnl_list_tab) g_free(PREFS->pnl_list_tab); PREFS->pnl_list_tab = g_strdup(gtk_stack_get_visible_child_name(GTK_STACK(data->stack))); - + //todo if(ui_dialog_msg_savechanges(widget, NULL) == FALSE) { @@ -1714,7 +1744,7 @@ gboolean retval = FALSE; g_free(data->wintitle); da_flt_free(data->filter); g_free(user_data); - + gtk_main_quit(); } @@ -1828,7 +1858,7 @@ gchar **uris, **str; gchar *newseldata; gint n_uris, filetype, slen; GError *error = NULL; - + if (info != TARGET_URI_LIST) return; @@ -1841,7 +1871,7 @@ GError *error = NULL; newseldata[slen] = 0; //DB( g_print(" - seldata ='%s'\n", gtk_selection_data_get_data(selection_data) ) ); //DB( g_print(" - newseldata ='%s'\n", newseldata ) ); - + uris = g_uri_list_extract_uris (newseldata); n_uris = g_strv_length(uris); DB( g_print(" - dragged %d files (len=%d)\n", n_uris, slen ) ); @@ -1850,7 +1880,7 @@ GError *error = NULL; //single file: check for xhb if(n_uris == 1) - { + { filetype = hb_filename_type_get_by_extension(*uris); DB( g_print(" - filetype is homebank (%d)\n", filetype) ); @@ -1883,7 +1913,7 @@ GError *error = NULL; //collect known filetype to import DB( g_print(" - collect %d files\n", n_uris) ); - + gchar **paths = g_new (gchar *, n_uris + 1); slen = 0; for (str = uris; *str; str++) @@ -1898,7 +1928,7 @@ GError *error = NULL; DB( g_print(" - append %d '%s'\n", slen, path ) ); paths[slen++] = path; } - } + } } paths[slen] = NULL; @@ -1906,8 +1936,8 @@ GError *error = NULL; { ui_import_assistant_new( paths ); } - - + + end_drop: g_strfreev (uris); } @@ -2109,7 +2139,7 @@ GtkWidget *bar, *label; gtk_label_set_markup (GTK_LABEL(label), "Unstable Development Version"); gtk_box_pack_start (GTK_BOX (gtk_info_bar_get_content_area (GTK_INFO_BAR (bar))), label, FALSE, FALSE, 0); #endif - + /* Add the main area */ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); //gtk_container_set_border_width (GTK_CONTAINER(vbox), SPACING_MEDIUM); @@ -2143,7 +2173,7 @@ GtkWidget *bar, *label; gtk_stack_sidebar_set_stack (GTK_STACK_SIDEBAR (sidebar), GTK_STACK (stack)); data->stack = stack; gtk_box_pack_start (GTK_BOX (box), stack, TRUE, TRUE, 0); - + page = ui_hub_scheduled_create(data); gtk_stack_add_titled (GTK_STACK (stack), page, "sched", _("Scheduled")); //gtk_paned_pack2 (GTK_PANED(vpaned), widget, TRUE, FALSE); @@ -2153,7 +2183,7 @@ GtkWidget *bar, *label; page = ui_hub_transaction_create(data, HUB_TXN_TYPE_REMIND); gtk_stack_add_titled (GTK_STACK (stack), page, "remin", _("Remind")); - + //setup, init and show window wg = &PREFS->wal_wg; @@ -2178,7 +2208,7 @@ GtkWidget *bar, *label; if( PREFS->pnl_list_tab != NULL ) gtk_stack_set_visible_child_name (GTK_STACK(data->stack), PREFS->pnl_list_tab); - + //todo: move this elsewhere DB( g_print(" - setup stuff\n") ); diff --git a/src/ext-native.c b/src/ext-native.c new file mode 100644 index 0000000..fce6315 --- /dev/null +++ b/src/ext-native.c @@ -0,0 +1,192 @@ + +#include +#include + +#include "ext.h" + + +static gint ext_native_init(int* argc, char** argv[], char** env[]); +static void ext_native_term(void); +static gboolean ext_native_check_file(const gchar* plugin_filename); +static GHashTable* ext_native_read_plugin_metadata(const gchar* plugin_filepath); +static gint ext_native_load_plugin(const gchar* plugin_filepath); +static void ext_native_unload_plugin(const gchar* plugin_filepath); +static void ext_native_execute_action(const gchar* plugin_filepath); +static void ext_native_call_hook(const gchar* hook_id, GList* args); + +static gchar* _read_data_for_keyword(const gchar* keyword, const gchar* bytes, gsize len); + + +static GHashTable* _loaded_plugins = NULL; + + +static gint ext_native_init(int* argc, char** argv[], char** env[]) +{ + if (!_loaded_plugins) { + _loaded_plugins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_module_close); + } + return 0; +} + +static void ext_native_term(void) +{ + if (_loaded_plugins) { + ext_native_call_hook("unload", NULL); + g_hash_table_unref(_loaded_plugins); + _loaded_plugins = NULL; + } +} + +static gboolean ext_native_check_file(const gchar* plugin_filename) +{ + if (g_str_has_suffix(plugin_filename, "."G_MODULE_SUFFIX)) { + return TRUE; + } + if (g_str_has_suffix(plugin_filename, ".la")) { + // allow a .la file only if no actual native plugin is found + gboolean check = FALSE; + gchar* copy = g_strdup(plugin_filename); + gchar* ext = g_strrstr(copy, ".la"); + if (ext) { + *ext = '\0'; + gchar* native_filename = g_strconcat(copy, "."G_MODULE_SUFFIX, NULL); + gchar* native_filepath = ext_find_plugin(native_filename); + check = !native_filepath; + g_free(native_filepath); + g_free(native_filename); + } + g_free(copy); + return check; + } + return FALSE; +} + +static GHashTable* ext_native_read_plugin_metadata(const gchar* plugin_filepath) +{ + GMappedFile* file = g_mapped_file_new(plugin_filepath, FALSE, NULL); + if (!file) { + g_printerr("mapping plugin file at %s failed\n", plugin_filepath); + return NULL; + } + + gchar* bytes = g_mapped_file_get_contents(file); + gsize len = g_mapped_file_get_length(file); + if (len == 0 || !bytes) { + g_mapped_file_unref(file); + g_printerr("no data in plugin file at %s failed\n", plugin_filepath); + return NULL; + } + + GHashTable* table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + + const gchar* keywords[] = { "name", "version", "abstract", "author", "website", NULL }; + const gchar** it; + for (it = keywords; *it; ++it) { + gchar* value = _read_data_for_keyword(*it, bytes, len); + g_hash_table_insert(table, g_strdup(*it), value); + } + + g_mapped_file_unref(file); + + return table; +} + +static gint ext_native_load_plugin(const gchar* plugin_filepath) +{ + if (g_hash_table_contains(_loaded_plugins, plugin_filepath)) { + return 0; + } + + GModule* module = g_module_open(plugin_filepath, G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL); + if (!module) { + g_printerr("Could not load native plugin: %s\n", g_module_error()); + return -1; + } + + g_hash_table_insert(_loaded_plugins, g_strdup(plugin_filepath), module); + + void (*symbol)(); + if (g_module_symbol(module, "load", (gpointer)&symbol)) { + symbol(); + } + + return 0; +} + +static void ext_native_unload_plugin(const gchar* plugin_filepath) +{ + GModule* module = g_hash_table_lookup(_loaded_plugins, plugin_filepath); + if (module) { + void (*symbol)(); + if (g_module_symbol(module, "unload", (gpointer)&symbol)) { + symbol(); + } + } + + g_hash_table_remove(_loaded_plugins, plugin_filepath); +} + +static void ext_native_execute_action(const gchar* plugin_filepath) +{ + GModule* module = g_hash_table_lookup(_loaded_plugins, plugin_filepath); + if (module) { + void (*symbol)(); + if (g_module_symbol(module, "execute", (gpointer)&symbol)) { + symbol(); + } + } +} + +static void ext_native_call_hook(const gchar* hook_id, GList* args) +{ + gchar* symbol_name = g_strconcat("on_", hook_id, NULL); + void (*symbol)(GList*); + + GHashTableIter it; + g_hash_table_iter_init(&it, _loaded_plugins); + GModule* module; + + while (g_hash_table_iter_next(&it, NULL, (gpointer*)&module)) { + if (g_module_symbol(module, symbol_name, (gpointer)&symbol)) { + symbol(args); + } + } + + g_free(symbol_name); +} + + +static gchar* _read_data_for_keyword(const gchar* keyword, const gchar* bytes, gsize len) +{ + gchar* value = NULL; + + gchar* pattern = g_strdup_printf("[\\x00\\t\\n ]%s\\s*[=:]\\s*([^\\x00]+)", keyword); + GRegex* r = g_regex_new(pattern, G_REGEX_CASELESS, 0, NULL); + g_free(pattern); + + GMatchInfo* match = NULL; + if (g_regex_match_full(r, bytes, len, 0, 0, &match, NULL)) { + value = g_match_info_fetch(match, 1); + } + + g_match_info_free(match); + g_regex_unref(r); + + return value; +} + + +static void _register(void) __attribute__((constructor)); +static void _register() +{ + ext_register("native", + ext_native_init, + ext_native_term, + ext_native_check_file, + ext_native_read_plugin_metadata, + ext_native_load_plugin, + ext_native_unload_plugin, + ext_native_execute_action, + ext_native_call_hook); +} + diff --git a/src/ext-perl.xs b/src/ext-perl.xs new file mode 100644 index 0000000..c8e1f25 --- /dev/null +++ b/src/ext-perl.xs @@ -0,0 +1,1043 @@ + +#include +#include +#include + +#include + +#undef _ +#include "homebank.h" +#include "ext.h" +#include "refcount.h" + +extern struct HomeBank *GLOBALS; +#include "dsp-mainwindow.h" +#include "dsp-account.h" +#include "ui-transaction.h" + + +static gint ext_perl_init(int* argc, char** argv[], char** env[]); +static void ext_perl_term(void); +static gboolean ext_perl_check_file(const gchar* plugin_filepath); +static GHashTable* ext_perl_read_plugin_metadata(const gchar* plugin_filepath); +static gint ext_perl_load_plugin(const gchar* plugin_filepath); +static void ext_perl_unload_plugin(const gchar* plugin_filepath); +static void ext_perl_execute_action(const gchar* plugin_filepath); +static void ext_perl_call_hook(const gchar* hook_id, GList* args); + +static SV* val_to_sv(GValue* val); +static GValue* sv_to_val(SV* sv); + +static gboolean gperl_value_from_sv(GValue* value, SV* sv); +static SV* gperl_sv_from_value(const GValue* value, gboolean copy_boxed); + + +static inline GValue* EXT_SV(GValue* v, SV* sv, GType type) +{ + g_value_init(v, type); + gperl_value_from_sv(v, sv); + return v; +} + + +#define EXT_P2C_OBJECT(PKG, ARG, VAR, TYP) \ +if (sv_derived_from(ARG, PKG)) { \ + IV iv = SvIV((SV*)SvRV(ARG)); \ + VAR = INT2PTR(TYP, iv); \ +} else { \ + croak(#VAR" is not of type "PKG); \ +} + +#define EXT_C2P_OBJECT(PKG, ARG, VAR) \ +sv_setref_pv(ARG, PKG, (void*)VAR) + + +static inline GPtrArray* SvGptrarray(const SV* sv) +{ + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVAV) { + AV* av = (AV*)sv; + int i; + int top = av_len(av); + GPtrArray* array = g_ptr_array_new(); + for (i = 0; i <= top; ++i) { + SV** item = av_fetch(av, i, 0); + if (!item) continue; + g_ptr_array_add(array, sv_to_val(*item)); + } + return array; + // TODO- leaking + } else { + croak("var is not an array"); + } +} + +static inline SV* newSVgptrarray(const GPtrArray* a) +{ + if (a) { + AV* av = newAV(); + int i; + for (i = 0; i < a->len; ++i) { + GValue* item = g_ptr_array_index(a, i); + av_push(av, val_to_sv(item)); + } + return newRV((SV*)av); + } + return &PL_sv_undef; +} + + +static inline GHashTable* SvGhashtable(const SV* sv) +{ + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVHV) { + HV* hv = (HV*)sv; + hv_iterinit(hv); + gchar* key; + I32 len; + SV* item; + GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal); + while ((item = hv_iternextsv(hv, &key, &len))) { + g_hash_table_insert(hash, key, sv_to_val(item)); + } + return hash; + // TODO- leaking + } else { + croak("var is not a hash"); + } +} + +static inline SV* newSVghashtable(GHashTable* h) +{ + if (h) { + HV* hv = newHV(); + GHashTableIter it; + g_hash_table_iter_init(&it, h); + gchar* key = NULL; + GValue* item = NULL; + while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&item)) { + hv_store(hv, key, -g_utf8_strlen(key, -1), val_to_sv(item), 0); + } + return newRV((SV*)hv); + } + return &PL_sv_undef; +} + + +static inline gboolean SvGboolean(SV* sv) +{ + if (!sv) { + return FALSE; + } + if (SvROK(sv)) { + return !!SvIV(SvRV(sv)); + } else { + return SvTRUE(sv); + } +} + +static inline SV* newSVgboolean(gboolean b) +{ + return sv_setref_iv(newSV(0), "HomeBank::Boolean", !!b); +} + + +static inline gchar* SvGchar_ptr(SV* sv) +{ + return SvPVutf8_nolen(sv); +} + +static inline SV* newSVgchar_ptr(const gchar* str) +{ + if (!str) return &PL_sv_undef; + + SV* sv = newSVpv(str, 0); + SvUTF8_on(sv); + return sv; +} + + +static inline GObject* SvGobject(const SV* sv) +{ + GObject* (*func)(const SV*) = ext_symbol_lookup("gperl_get_object"); + if (func) { + return func(sv); + } + return NULL; +} + +static inline SV* newSVgobject(const GObject* o) +{ + SV* (*func)(const GObject*, gboolean) = ext_symbol_lookup("gperl_new_object"); + if (func) { + return func(o, FALSE); + } + return &PL_sv_undef; +} + + +static PerlInterpreter* context = NULL; + + +static gint ext_perl_init(int* argc, char** argv[], char** env[]) +{ + int ret = 0; + + PERL_SYS_INIT3(argc, argv, env); + context = perl_alloc(); + perl_construct(context); + + PL_exit_flags |= PERL_EXIT_DESTRUCT_END; + PL_origalen = 1; + PL_perl_destruct_level = 1; + + gchar* bootstrap = g_strdup_printf("-e" + "use lib '%s';" + "use HomeBank;" + "HomeBank->bootstrap;", + homebank_app_get_pkglib_dir()); + char *args[] = { "", bootstrap }; + + EXTERN_C void xs_init(pTHX); + if (perl_parse(context, xs_init, 2, args, NULL) || perl_run(context)) { + ext_perl_term(); + ret = -1; + } + + g_free(bootstrap); + return ret; +} + +static void ext_perl_term(void) +{ + if (context) { + perl_destruct(context); + perl_free(context); + context = NULL; + } + PERL_SYS_TERM(); +} + +static gboolean ext_perl_check_file(const gchar* plugin_filepath) +{ + if (g_str_has_suffix(plugin_filepath, ".pl")) { + return TRUE; + } + return FALSE; +} + +static GHashTable* ext_perl_read_plugin_metadata(const gchar* plugin_filepath) +{ + GHashTable* table = NULL; + + if (!context) return NULL; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + + int ret = call_pv("HomeBank::read_metadata", G_SCALAR | G_EVAL); + + SPAGAIN; + + if (ret == 1) { + table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + SV* sv = POPs; + if (SvROK(sv)) { + sv = MUTABLE_SV(SvRV(sv)); + } + if (SvTYPE(sv) == SVt_PVHV) { + HV* hv = (HV*)sv; + hv_iterinit(hv); + gchar* key; + I32 len; + SV* item; + while ((item = hv_iternextsv(hv, &key, &len))) { + if (SvPOK(item)) { + gchar* val = SvPVutf8_nolen(item); + g_hash_table_insert(table, g_strdup(key), g_strdup(val)); + } + } + } + } + + PUTBACK; + FREETMPS; + LEAVE; + + return table; +} + +static gint ext_perl_load_plugin(const gchar* plugin_filepath) +{ + if (!context) return -1; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::load_plugin", G_DISCARD | G_EVAL); + SPAGAIN; + + gint ret = 0; + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + ret = -1; + } + + PUTBACK; + FREETMPS; + LEAVE; + + return ret; +} + +static void ext_perl_unload_plugin(const gchar* plugin_filepath) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::unload_plugin", G_DISCARD | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + } + + PUTBACK; + FREETMPS; + LEAVE; +} + +static void ext_perl_execute_action(const gchar* plugin_filepath) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(plugin_filepath)); + PUTBACK; + call_pv("HomeBank::execute_action", G_DISCARD | G_EVAL); + SPAGAIN; + + if (SvTRUE(ERRSV)) { + g_printerr("%s", SvPV_nolen(ERRSV)); + } + + PUTBACK; + FREETMPS; + LEAVE; +} + +static void ext_perl_call_hook(const gchar* hook_id, GList* args) +{ + if (!context) return; + PERL_SET_CONTEXT(context); + + dSP; + ENTER; + SAVETMPS; + PUSHMARK(SP); + mXPUSHs(newSVgchar_ptr(hook_id)); + + GList *list = g_list_first(args); + while (list) { + GValue* val = list->data; + XPUSHs(sv_2mortal(val_to_sv(val))); + list = g_list_next(list); + } + + PUTBACK; + call_pv("HomeBank::call_hook", G_ARRAY); + SPAGAIN; + POPi; + PUTBACK; + FREETMPS; + LEAVE; +} + + +static SV* val_to_sv(GValue* val) +{ + if (!val || !G_IS_VALUE(val) || G_VALUE_TYPE(val) == G_TYPE_NONE) { + return &PL_sv_undef; + } + if (G_VALUE_TYPE(val) == G_TYPE_BOOLEAN) { + return newSVgboolean(g_value_get_boolean(val)); + } + if (G_VALUE_TYPE(val) == G_TYPE_PTR_ARRAY) { + return newSVgptrarray((GPtrArray*)g_value_get_boxed(val)); + } + if (G_VALUE_TYPE(val) == G_TYPE_HASH_TABLE) { + return newSVghashtable((GHashTable*)g_value_get_boxed(val)); + } +#define obj(CTYPE, _2, PART, GTYPE, _5) \ + if (G_VALUE_TYPE(val) == GTYPE) { \ + SV* sv = newSV(0); \ + CTYPE* ptr = (CTYPE*)g_value_get_##PART(val); \ + EXT_C2P_OBJECT("HomeBank::"#CTYPE, sv, rc_ref(ptr)); \ + return sv; \ + } +#include "ext-value.h" +#undef obj + return gperl_sv_from_value(val, FALSE); +} + +static GValue* sv_to_val(SV* sv) +{ + GValue* val = g_new0(GValue, 1); + + if (SvUOK(sv)) return EXT_SV(val, sv, G_TYPE_UINT); + if (SvIOK(sv)) return EXT_SV(val, sv, G_TYPE_INT); + if (SvNOK(sv)) return EXT_SV(val, sv, G_TYPE_DOUBLE); + if (SvPOK(sv)) return EXT_SV(val, sv, G_TYPE_STRING); + if (sv_isobject(sv)) { + if (sv_derived_from(sv, "HomeBank::Boolean")) { + return EXT_BOOLEAN(val, SvGboolean(sv)); + } +#define obj(CTYPE, NAME, _3, _4, _5) \ + if (sv_derived_from(sv, "HomeBank::"#CTYPE)) { \ + CTYPE* ptr; \ + EXT_P2C_OBJECT("HomeBank::"#CTYPE, sv, ptr, CTYPE*); \ + return EXT_##NAME(val, ptr); \ + } +#include "ext-value.h" +#undef obj + return EXT_SV(val, sv, G_TYPE_OBJECT); + } + if (SvROK(sv)) { + sv = SvRV(sv); + switch (SvTYPE(sv)) { + case SVt_IV: + return EXT_BOOLEAN(val, SvGboolean(sv)); + case SVt_PVAV: + return EXT_ARRAY(val, SvGptrarray(sv)); + case SVt_PVHV: + return EXT_HASH_TABLE(val, SvGhashtable(sv)); + default: + break; + } + } + switch (SvTYPE(sv)) { + case SVt_PVAV: + return EXT_ARRAY(val, SvGptrarray(sv)); + case SVt_PVHV: + return EXT_HASH_TABLE(val, SvGhashtable(sv)); + default: + break; + } + + g_free(val); + return NULL; +} + + +static gboolean gperl_value_from_sv(GValue* value, SV* sv) +{ + gboolean (*func)(GValue*, SV*) = ext_symbol_lookup("gperl_value_from_sv"); + if (func) return func(value, sv); + + GType type = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(value)); + if (!SvOK(sv)) return TRUE; + switch (type) { + case G_TYPE_CHAR: + { + gchar *tmp = SvGchar_ptr(sv); + g_value_set_schar(value, (gint8)(tmp ? tmp[0] : 0)); + break; + } + case G_TYPE_UCHAR: + { + char *tmp = SvPV_nolen(sv); + g_value_set_uchar(value, (guchar)(tmp ? tmp[0] : 0)); + break; + } + case G_TYPE_BOOLEAN: + g_value_set_boolean(value, SvTRUE(sv)); + break; + case G_TYPE_INT: + g_value_set_int(value, SvIV(sv)); + break; + case G_TYPE_UINT: + g_value_set_uint(value, SvIV(sv)); + break; + case G_TYPE_LONG: + g_value_set_long(value, SvIV(sv)); + break; + case G_TYPE_ULONG: + g_value_set_ulong(value, SvIV(sv)); + break; + case G_TYPE_FLOAT: + g_value_set_float(value, (gfloat)SvNV(sv)); + break; + case G_TYPE_DOUBLE: + g_value_set_double(value, SvNV(sv)); + break; + case G_TYPE_STRING: + g_value_set_string(value, SvGchar_ptr(sv)); + break; + } + return TRUE; +} + +static SV* gperl_sv_from_value(const GValue* value, gboolean copy_boxed) +{ + SV* (*func)(const GValue*, gboolean) = ext_symbol_lookup("gperl_sv_from_value"); + if (func) return func(value, copy_boxed); + + GType type = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(value)); + switch (type) { + case G_TYPE_CHAR: + return newSViv(g_value_get_schar(value)); + case G_TYPE_UCHAR: + return newSVuv(g_value_get_uchar(value)); + case G_TYPE_BOOLEAN: + return newSViv(g_value_get_boolean(value)); + case G_TYPE_INT: + return newSViv(g_value_get_int(value)); + case G_TYPE_UINT: + return newSVuv(g_value_get_uint(value)); + case G_TYPE_LONG: + return newSViv(g_value_get_long(value)); + case G_TYPE_ULONG: + return newSVuv(g_value_get_ulong(value)); + case G_TYPE_FLOAT: + return newSVnv(g_value_get_float(value)); + case G_TYPE_DOUBLE: + return newSVnv(g_value_get_double(value)); + case G_TYPE_STRING: + return newSVgchar_ptr(g_value_get_string(value)); + } + return &PL_sv_undef; +} + + +static void _register(void) __attribute__((constructor)); +static void _register() +{ + ext_register("perl", + ext_perl_init, + ext_perl_term, + ext_perl_check_file, + ext_perl_read_plugin_metadata, + ext_perl_load_plugin, + ext_perl_unload_plugin, + ext_perl_execute_action, + ext_perl_call_hook); +} + + +MODULE = HomeBank PACKAGE = HomeBank + +PROTOTYPES: ENABLE + +const gchar* +version(void) + CODE: + RETVAL = VERSION; + OUTPUT: + RETVAL + +const gchar* +config_dir(void) + CODE: + RETVAL = homebank_app_get_config_dir(); + OUTPUT: + RETVAL + +gboolean +has(const gchar* CLASS, ...) + PREINIT: + int i; + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = TRUE; + for (i = 1; i < items; ++i) { + gchar* feature = SvGchar_ptr(ST(i)); + if (!feature || !ext_has(feature)) { + RETVAL = FALSE; + break; + } + } + OUTPUT: + RETVAL + +GObject* +main_window(void) + CODE: + RETVAL = G_OBJECT(GLOBALS->mainwindow); + OUTPUT: + RETVAL + +GObject* +main_ui_manager(void) + PREINIT: + struct hbfile_data *data; + CODE: + RETVAL = NULL; + if (GLOBALS->mainwindow) { + data = g_object_get_data(G_OBJECT(gtk_widget_get_ancestor(GLOBALS->mainwindow, GTK_TYPE_WINDOW)), "inst_data"); + if (data) { + RETVAL = G_OBJECT(data->manager); + } + } + OUTPUT: + RETVAL + +void +info(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "info"); + +void +warn(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "warn"); + +void +error(const gchar* CLASS, const gchar* title, const gchar* text) + CODE: + PERL_UNUSED_ARG(CLASS); + ext_run_modal(title, text, "error"); + +void +hook(const gchar* CLASS, const gchar* hook_name, ...) + PREINIT: + int i; + GList* list = NULL; + CODE: + PERL_UNUSED_ARG(CLASS); + for (i = 2; i < items; ++i) { + SV* sv = ST(i); + GValue *val = sv_to_val(sv); + list = g_list_append(list, val); + } + CLEANUP: + ext_vhook(hook_name, list); + g_list_free(list); + // TODO free all the things + +GObject* +open_prefs(const gchar* CLASS) + CODE: + PERL_UNUSED_ARG(CLASS); + RETVAL = G_OBJECT(defpref_dialog_new(PREF_GENERAL)); + OUTPUT: + RETVAL + + +MODULE = HomeBank PACKAGE = HomeBank::File + +const gchar* +owner(const gchar* CLASS, ...) + CODE: + PERL_UNUSED_ARG(CLASS); + if (1 < items) { + hbfile_change_owner(g_strdup(SvGchar_ptr(ST(1)))); + } + RETVAL = GLOBALS->owner; + OUTPUT: + RETVAL + +void +transactions(const gchar* CLASS) + PPCODE: + PERL_UNUSED_ARG(CLASS); + + GList* acc_list = g_hash_table_get_values(GLOBALS->h_acc); + GList* acc_link = g_list_first(acc_list); + for (; acc_link; acc_link = g_list_next(acc_link)) { + Account *acc = acc_link->data; + + GList* txn_link = g_queue_peek_head_link(acc->txn_queue); + for (; txn_link; txn_link = g_list_next(txn_link)) { + Transaction* txn = txn_link->data; + + GValue val = G_VALUE_INIT; + SV* sv = val_to_sv(EXT_TRANSACTION(&val, txn)); + mXPUSHs(sv); + } + } + + g_list_free(acc_list); + +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 + +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_queue_peek_head_link(SELF->txn_queue); + for (; list; list = g_list_next(list)) { + Transaction* txn = list->data; + 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)); + 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* +memo(Transaction* SELF, ...) + CODE: + if (1 < items) { + if (SELF->memo) g_free(SELF->memo); + SELF->memo = g_strdup(SvGchar_ptr(ST(1))); + } + RETVAL = SELF->memo ? SELF->memo : ""; + 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, FALSE, 0)); + 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); + } + ui_dialog_transaction_xfer_select_child(NULL, SELF, list, &other); + } + 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->memo, SELF->date, SELF->kacc, SELF->kxferacc, SELF->flags, SELF->paymode, SELF->kpay, SELF->kcat); + diff --git a/src/ext-value.c b/src/ext-value.c new file mode 100644 index 0000000..47c4829 --- /dev/null +++ b/src/ext-value.c @@ -0,0 +1,82 @@ + +#include + +#include "ext-value.h" + + +const GValue* ext_value_undef() +{ + static GValue v = G_VALUE_INIT; + return &v; +} + +const GValue* ext_value_true() +{ + static GValue v = G_VALUE_INIT; + if (!G_VALUE_HOLDS_BOOLEAN(&v)) EXT_BOOLEAN(&v, TRUE); + return &v; +} + +const GValue* ext_value_false() +{ + static GValue v = G_VALUE_INIT; + if (!G_VALUE_HOLDS_BOOLEAN(&v)) EXT_BOOLEAN(&v, FALSE); + return &v; +} + + +GValue* EXT_LIST(GValue* v, ...) +{ + GPtrArray* a = g_ptr_array_new(); + + va_list ap; + va_start(ap, v); + + for (;;) { + GValue* item = (GValue*)va_arg(ap, GValue*); + if (!item) break; + g_ptr_array_add(a, item); + } + + va_end(ap); + + return EXT_ARRAY(v, a); +} + +GValue* EXT_HASH(GValue* v, ...) +{ + GHashTable* h = g_hash_table_new(g_str_hash, g_str_equal); + + va_list ap; + va_start(ap, v); + + for (;;) { + gchar* key = (gchar*)va_arg(ap, gchar*); + if (!key) break; + GValue* val = (GValue*)va_arg(ap, GValue*); + g_hash_table_insert(h, key, val); + } + + va_end(ap); + + return EXT_HASH_TABLE(v, h); +} + +GValue* EXT_JULIAN(GValue* v, guint32 d) +{ + GDate* date = g_date_new_julian(d); + return EXT_DATE(v, date); +} + + +#define obj(CTYPE, _2, _3, _4, PREFIX) \ +GType PREFIX##get_type() \ +{ \ + static GType type = 0; \ + if (type == 0) \ + type = g_pointer_type_register_static(#CTYPE); \ + return type; \ +} +#include "ext-value.h" +#undef obj + diff --git a/src/ext-value.h b/src/ext-value.h new file mode 100644 index 0000000..a5002bb --- /dev/null +++ b/src/ext-value.h @@ -0,0 +1,64 @@ + +#ifndef __EXT_VALUE_H__ +#define __EXT_VALUE_H__ + +#include "homebank.h" + + +#define DA_TYPE_ACC (da_acc_get_type()) +#define DA_TYPE_TRANSACTION (da_transaction_get_type()) + +#define obj(_1, _2, _3, _4, PREFIX) GType PREFIX##get_type(void); +#include "ext-value.h" +#undef obj + + +#define val(CTYPE, NAME, PART, GTYPE) \ +static inline GValue* EXT_##NAME(GValue* v, CTYPE c) { \ + g_value_init(v, GTYPE); \ + g_value_set_##PART(v, c); \ + return v; \ +} +#define obj(CTYPE, NAME, PART, GTYPE, _5) val(CTYPE*, NAME, PART, GTYPE) +#include "ext-value.h" +#undef val +#undef obj + + +const GValue* ext_value_undef(void); +const GValue* ext_value_true(void); +const GValue* ext_value_false(void); + +#define EXT_UNDEF (ext_value_undef()) +#define EXT_TRUE (ext_value_true()) +#define EXT_FALSE (ext_value_false()) + +GValue* EXT_LIST(GValue* v, ...); +GValue* EXT_HASH(GValue* v, ...); +GValue* EXT_JULIAN(GValue* v, guint32 d); + + +#else + +#ifdef val +// C type, name, fundamental, GType +val(gboolean, BOOLEAN, boolean, G_TYPE_BOOLEAN) +val(gint, INT, int, G_TYPE_INT) +val(guint, UINT, uint, G_TYPE_UINT) +val(gdouble, DOUBLE, double, G_TYPE_DOUBLE) +val(gchar, CHAR, schar, G_TYPE_CHAR) +val(gchar*, STRING, string, G_TYPE_STRING) +val(GPtrArray*, ARRAY, boxed, G_TYPE_PTR_ARRAY) +val(GHashTable*, HASH_TABLE, boxed, G_TYPE_HASH_TABLE) +val(GDate*, DATE, boxed, G_TYPE_DATE) +val(void*, OBJECT, object, G_TYPE_OBJECT) +#endif + +#ifdef obj +// C type, name, fundamental, GType, prefix +obj(Account, ACCOUNT, pointer, DA_TYPE_ACC, da_acc_) +obj(Transaction, TRANSACTION, pointer, DA_TYPE_TRANSACTION, da_transaction_) +#endif + +#endif + diff --git a/src/ext.c b/src/ext.c new file mode 100644 index 0000000..d652c0e --- /dev/null +++ b/src/ext.c @@ -0,0 +1,327 @@ + +#include +#include +#include +#include + +#include "ext.h" + +extern struct Preferences *PREFS; + + +const int _hook_recursion_soft_limit = 50; +const int _hook_recursion_hard_limit = 99; + + +struct PluginEngine +{ + const gchar* type; + PluginEngineInitializer init; + PluginEngineTerminator term; + PluginEngineFileChecker check_file; + PluginMetadataReader read_metadata; + PluginLoader load_plugin; + PluginUnloader unload_plugin; + PluginExecutor execute; + PluginHookCaller call_hook; +}; + + +static GList* _engine_list = NULL; +static GHashTable* _loaded_plugins = NULL; + + +void ext_init(int* argc, char** argv[], char** env[]) +{ + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->init(argc, argv, env); + list = g_list_next(list); + } + if (!_loaded_plugins) { + _loaded_plugins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + } +} + +void ext_term(void) +{ + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->term(); + list = g_list_next(list); + } + g_list_free(_engine_list); + _engine_list = NULL; + + if (_loaded_plugins) { + g_hash_table_unref(_loaded_plugins); + _loaded_plugins = NULL; + } +} + +void ext_register(const gchar* type, + PluginEngineInitializer init, + PluginEngineTerminator term, + PluginEngineFileChecker check_file, + PluginMetadataReader read_metadata, + PluginLoader load_plugin, + PluginUnloader unload_plugin, + PluginExecutor execute, + PluginHookCaller call_hook) +{ + struct PluginEngine* engine = g_malloc0(sizeof(struct PluginEngine)); + engine->type = type; + engine->init = init; + engine->term = term; + engine->check_file = check_file; + engine->read_metadata = read_metadata; + engine->load_plugin = load_plugin; + engine->unload_plugin = unload_plugin; + engine->execute = execute; + engine->call_hook = call_hook; + _engine_list = g_list_append(_engine_list, engine); +} + + +static struct PluginEngine* _get_engine_for_plugin(const gchar* plugin_filename) +{ + if (!plugin_filename) { + return NULL; + } + + GList *list = g_list_first(_engine_list); + while (list) { + struct PluginEngine* engine = list->data; + if (engine->check_file(plugin_filename)) { + return engine; + } + list = g_list_next(list); + } + return NULL; +} + +static void _read_directory(const gchar* directory, GHashTable* hash) +{ + GDir* dir = g_dir_open(directory, 0, NULL); + if (!dir) return; + + const gchar* filename; + while ((filename = g_dir_read_name(dir))) { + gchar* full = g_build_filename(directory, filename, NULL); + if (g_file_test(full, G_FILE_TEST_IS_REGULAR) && _get_engine_for_plugin(filename)) { + g_hash_table_insert(hash, g_strdup(filename), NULL); + } + g_free(full); + } + g_dir_close(dir); +} + +gchar** ext_list_plugins() +{ + GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal); + + gchar** it; + for (it = PREFS->ext_path; it && *it; ++it) { + _read_directory(*it, hash); + } + + GList* list = g_list_sort(g_hash_table_get_keys(hash), (GCompareFunc)g_utf8_collate); + g_hash_table_unref(hash); + + guint len = g_list_length(list); + gchar** strv = g_new0(gchar*, len + 1); + int i; + for (i = 0; i < len; ++i) { + strv[i] = g_list_nth_data(list, i); + } + g_list_free(list); + + return strv; +} + +gchar* ext_find_plugin(const gchar* plugin_filename) +{ + if (!plugin_filename) return NULL; + + gchar** it; + for (it = PREFS->ext_path; *it; ++it) { + if (!g_path_is_absolute(*it)) continue; + + gchar* full = g_build_filename(*it, plugin_filename, NULL); + if (g_file_test(full, G_FILE_TEST_IS_REGULAR)) { + return full; + } + g_free(full); + } + + return NULL; +} + +GHashTable* ext_read_plugin_metadata(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return NULL; + + GHashTable* ret = NULL; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->read_metadata) { + ret = engine->read_metadata(full); + } + + g_free(full); + return ret; +} + +gint ext_load_plugin(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return -1; + + gint ret = -1; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->load_plugin && engine->load_plugin(full) == 0) { + g_hash_table_insert(_loaded_plugins, g_strdup(plugin_filename), NULL); + ret = 0; + } + + g_free(full); + return ret; +} + +void ext_unload_plugin(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->unload_plugin) { + engine->unload_plugin(full); + } + + g_free(full); + g_hash_table_remove(_loaded_plugins, plugin_filename); +} + +gboolean ext_is_plugin_loaded(const gchar* plugin_filename) +{ + return g_hash_table_contains(_loaded_plugins, plugin_filename); +} + +void ext_execute_action(const gchar* plugin_filename) +{ + gchar* full = ext_find_plugin(plugin_filename); + if (!full) return; + + struct PluginEngine* engine = _get_engine_for_plugin(plugin_filename); + if (engine && engine->execute) { + engine->execute(full); + } + + g_free(full); +} + +void ext_hook(const gchar* hook_id, ...) +{ + GList *list = NULL; + + va_list ap; + va_start(ap, hook_id); + for (;;) { + GValue* val = (GValue*)va_arg(ap, GValue*); + if (!val) break; + list = g_list_append(list, val); + } + va_end(ap); + + ext_vhook(hook_id, list); + g_list_free(list); +} + +void ext_vhook(const gchar* hook_id, GList* args) +{ + static int recursion_level = 0; + + if (_hook_recursion_hard_limit <= recursion_level) { + return; + } else if (_hook_recursion_soft_limit <= recursion_level) { + int level = recursion_level; + recursion_level = -1; + GValue val_level = G_VALUE_INIT; + ext_hook("deep_hook_recursion", EXT_INT(&val_level, level), NULL); + recursion_level = level; + } + + ++recursion_level; + + g_print("ext_hook: %s (level %d)\n", hook_id, recursion_level); + GList *list = g_list_first(_engine_list); + while (list) + { + struct PluginEngine* engine = list->data; + engine->call_hook(hook_id, args); + list = g_list_next(list); + } + + --recursion_level; +} + +gboolean ext_has(const gchar* feature) +{ +#ifdef OFX_ENABLE + if (0 == g_utf8_collate(feature, "libofx")) { + return TRUE; + } +#endif +#ifdef PERL_ENABLE + if (0 == g_utf8_collate(feature, "perl")) { + return TRUE; + } +#endif + return FALSE; +} + + +void* ext_symbol_lookup(const gchar* symbol) +{ + static GModule* module = NULL; + if (!module) module = g_module_open(NULL, 0); + + void* ptr; + if (module && g_module_symbol(module, symbol, &ptr)) { + return ptr; + } + + return NULL; +} + + +void ext_run_modal(const gchar* title, const gchar* text, const gchar* type) +{ + GtkMessageType t = GTK_MESSAGE_INFO; + if (0 == g_utf8_collate(type, "error")) { + t = GTK_MESSAGE_ERROR; + } + if (0 == g_utf8_collate(type, "warn")) { + t = GTK_MESSAGE_WARNING; + } + if (0 == g_utf8_collate(type, "question")) { + t = GTK_MESSAGE_QUESTION; + } + + GtkWidget* dialog = gtk_message_dialog_new(NULL, + GTK_DIALOG_DESTROY_WITH_PARENT, t, + GTK_BUTTONS_CLOSE, "%s", text); + if (title) { + gtk_window_set_title(GTK_WINDOW(dialog), title); + } + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + diff --git a/src/ext.h b/src/ext.h new file mode 100644 index 0000000..edc3a44 --- /dev/null +++ b/src/ext.h @@ -0,0 +1,46 @@ + +#ifndef __EXT_H__ +#define __EXT_H__ + +#include + +#include "ext-value.h" + + +typedef gint (*PluginEngineInitializer)(int* argc, char** argv[], char** env[]); +typedef void (*PluginEngineTerminator)(); +typedef gboolean (*PluginEngineFileChecker)(const gchar* plugin_filepath); +typedef GHashTable* (*PluginMetadataReader)(const gchar* plugin_filepath); +typedef gint (*PluginLoader)(const gchar* plugin_filepath); +typedef void (*PluginUnloader)(const gchar* plugin_filepath); +typedef void (*PluginExecutor)(const gchar* plugin_filepath); +typedef void (*PluginHookCaller)(const gchar* hook_id, GList* args); + +void ext_init(int* argc, char** argv[], char** env[]); +void ext_term(void); +void ext_register(const gchar* type, + PluginEngineInitializer, + PluginEngineTerminator, + PluginEngineFileChecker, + PluginMetadataReader, + PluginLoader, + PluginUnloader, + PluginExecutor, + PluginHookCaller); + + +gchar** ext_list_plugins(void); +gchar* ext_find_plugin(const gchar* plugin_filename); +GHashTable* ext_read_plugin_metadata(const gchar* plugin_filename); +gint ext_load_plugin(const gchar* plugin_filename); +void ext_unload_plugin(const gchar* plugin_filename); +gboolean ext_is_plugin_loaded(const gchar* plugin_filename); +void ext_execute_action(const gchar* plugin_filename); +void ext_hook(const gchar* hook_id, ...); +void ext_vhook(const gchar* hook_id, GList* args); +gboolean ext_has(const gchar* feature); +void* ext_symbol_lookup(const gchar* symbol); +void ext_run_modal(const gchar* title, const gchar* text, const gchar* type); + +#endif + diff --git a/src/hb-account.c b/src/hb-account.c index b6888c9..2ecbb6f 100644 --- a/src/hb-account.c +++ b/src/hb-account.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-account.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -39,7 +42,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) ); @@ -47,10 +50,10 @@ da_acc_free(Account *item) g_free(item->number); g_free(item->bankname); g_free(item->notes); - + g_queue_free (item->txn_queue); - - g_free(item); + + rc_free(item); } } @@ -61,7 +64,7 @@ da_acc_malloc(void) Account *item; DB( g_print("da_acc_malloc\n") ); - item = g_malloc0(sizeof(Account)); + item = rc_alloc(sizeof(Account)); item->kcur = GLOBALS->kcur; item->txn_queue = g_queue_new (); return item; @@ -158,6 +161,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; } @@ -183,6 +189,10 @@ Account *existitem; item->key = da_acc_get_max_key() + 1; item->pos = da_acc_length() + 1; da_acc_insert(item); + + GValue item_val = G_VALUE_INIT; + ext_hook("account_inserted", EXT_ACCOUNT(&item_val, item), NULL); + return TRUE; } @@ -228,7 +238,7 @@ gchar *stripname; g_free(stripname); } - return retval; + return retval; } @@ -345,14 +355,14 @@ gboolean retval; while (lnk_acc != NULL) { Account *acc = lnk_acc->data; - + if(acc->key != key) { lnk_txn = g_queue_peek_head_link(acc->txn_queue); while (lnk_txn != NULL) { Transaction *entry = lnk_txn->data; - + if( key == entry->kxferacc) { retval = TRUE; @@ -427,12 +437,12 @@ gchar *stripname = account_get_stripname(newname); g_free(stripname); } - + return FALSE; } -/* +/* * change the account currency * change every txn to currency * ensure dst xfer transaction account will be set to same currency @@ -473,7 +483,7 @@ guint32 maxkey, i; acc->kcur = kcur; DB( g_print(" - '%s'\n", acc->name) ); - + for(i=1;idata; - + /* set initial amount */ acc->bal_bank = acc->initial; acc->bal_today = acc->initial; acc->bal_future = acc->initial; - + /* add every txn */ lnk_txn = g_queue_peek_head_link(acc->txn_queue); while (lnk_txn != NULL) { Transaction *txn = lnk_txn->data; - + if(!(txn->status == TXN_STATUS_REMIND)) { account_balances_add_internal(acc, txn); } lnk_txn = g_list_next(lnk_txn); } - + lnk_acc = g_list_next(lnk_acc); } g_list_free(lst_acc); diff --git a/src/hb-archive.c b/src/hb-archive.c index 890cd7f..db3b101 100644 --- a/src/hb-archive.c +++ b/src/hb-archive.c @@ -21,6 +21,9 @@ #include "hb-archive.h" #include "hb-split.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ /****************************************************************************/ @@ -40,7 +43,7 @@ Archive *da_archive_malloc(void) { Archive *item; - item = g_malloc0(sizeof(Archive)); + item = rc_alloc(sizeof(Archive)); item->key = 1; return item; } @@ -48,7 +51,7 @@ Archive *item; 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) { @@ -71,13 +74,13 @@ Archive *new_item = g_memdup(src_item, sizeof(Archive)); void da_archive_free(Archive *item) { - if(item != NULL) + if(rc_unref(item)) { if(item->memo != NULL) g_free(item->memo); if(item->splits != NULL) da_split_destroy(item->splits); - g_free(item); + rc_free(item); } } @@ -145,10 +148,10 @@ guint32 max_key = 0; { Archive *item = tmplist->data; - max_key = MAX(item->key, max_key); + max_key = MAX(item->key, max_key); tmplist = g_list_next(tmplist); } - + return max_key; } @@ -191,7 +194,7 @@ guint nbsplit; GLOBALS->changes_count++; } - //#1340142 check split category + //#1340142 check split category if( item->splits != NULL ) { nbsplit = da_splits_consistency(item->splits); @@ -203,7 +206,7 @@ guint nbsplit; GLOBALS->changes_count++; } } - + // check payee exists pay = da_pay_get(item->kpay); if(pay == NULL) @@ -253,7 +256,7 @@ Archive *da_archive_init_from_transaction(Archive *arc, Transaction *txn) arc->splits = da_splits_clone(txn->splits); if( da_splits_length (arc->splits) > 0 ) arc->flags |= OF_SPLIT; //Flag that Splits are active - + return arc; } @@ -289,7 +292,7 @@ guint32 nextpostdate = nextdate; /* get the final post date and free */ nextpostdate = g_date_get_julian(tmpdate); - + return nextpostdate; } @@ -317,7 +320,7 @@ gint shift; finalpostdate = postdate; - + tmpdate = g_date_new_julian(finalpostdate); /* manage weekend exception */ if( arc->weekend > 0 ) @@ -344,11 +347,11 @@ gint shift; } } } - + /* get the final post date and free */ finalpostdate = g_date_get_julian(tmpdate); g_date_free(tmpdate); - + return finalpostdate; } @@ -392,10 +395,10 @@ guint32 nblate = 0; if(arc->flags & OF_LIMIT) nblate = MIN(nblate, arc->limit); - + nblate = MIN(nblate, 11); */ - + // pre 5.1 way post_date = g_date_new(); @@ -451,7 +454,7 @@ gushort lastday; } arc->daygap = CLAMP(lastday - g_date_get_day(post_date), 0, 3); - + DB( g_print(" daygap is %d\n", arc->daygap) ); } else @@ -488,10 +491,10 @@ GDate *today, *maxdate; DB( g_print("\n[scheduled] date_get_post_max\n") ); //add until xx of the next month (excluded) - if(GLOBALS->auto_smode == 0) + if(GLOBALS->auto_smode == 0) { DB( g_print(" - max is %d of next month\n", GLOBALS->auto_weekday) ); - + today = g_date_new_julian(GLOBALS->today); //we compute user xx weekday of next month @@ -499,9 +502,9 @@ GDate *today, *maxdate; g_date_set_day(maxdate, GLOBALS->auto_weekday); if(g_date_get_day (today) >= GLOBALS->auto_weekday) g_date_add_months(maxdate, 1); - + nbdays = g_date_days_between(today, maxdate); - + g_date_free(maxdate); g_date_free(today); } @@ -532,7 +535,7 @@ Transaction *txn; maxpostdate = scheduled_date_get_post_max(); txn = da_transaction_malloc(); - + list = g_list_first(GLOBALS->arc_list); while (list != NULL) { @@ -552,7 +555,7 @@ Transaction *txn; while(mydate < maxpostdate) { DB( hb_print_date(mydate, arc->memo) ); - + da_transaction_init_from_template(txn, arc); txn->date = scheduled_get_postdate(arc, mydate); /* todo: ? fill in cheque number */ @@ -578,7 +581,7 @@ nextarchive: } da_transaction_free (txn); - + return count; } diff --git a/src/hb-assign.c b/src/hb-assign.c index 3d3f2a9..21a88fd 100644 --- a/src/hb-assign.c +++ b/src/hb-assign.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-assign.h" +#include "ext.h" +#include "refcount.h" + #define MYDEBUG 0 #if MYDEBUG @@ -38,12 +41,12 @@ void da_asg_free(Assign *item) { DB( g_print("da_asg_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->text) ); g_free(item->text); - g_free(item); + rc_free(item); } } @@ -52,7 +55,7 @@ Assign * da_asg_malloc(void) { DB( g_print("da_asg_malloc\n") ); - return g_malloc0(sizeof(Assign)); + return rc_alloc(sizeof(Assign)); } diff --git a/src/hb-category.c b/src/hb-category.c index 3dc34e4..e26b2b8 100644 --- a/src/hb-category.c +++ b/src/hb-category.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-category.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ @@ -40,7 +43,7 @@ extern struct HomeBank *GLOBALS; Category * da_cat_clone(Category *src_item) { -Category *new_item = g_memdup(src_item, sizeof(Category)); +Category *new_item = rc_dup(src_item, sizeof(Category)); DB( g_print("da_cat_clone\n") ); if(new_item) @@ -57,13 +60,13 @@ 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->fullname); - g_free(item); + rc_free(item); } } @@ -72,7 +75,7 @@ Category * da_cat_malloc(void) { DB( g_print("da_cat_malloc\n") ); - return g_malloc0(sizeof(Category)); + return rc_alloc(sizeof(Category)); } @@ -180,7 +183,7 @@ Category *parent; if( parent != NULL ) item->fullname = g_strconcat(parent->name, ":", item->name, NULL); } - + DB( g_print("- updated %d:'%s' fullname='%s'\n", item->key, item->name, item->fullname) ); } @@ -191,23 +194,23 @@ da_cat_rename(Category *item, gchar *newname) { DB( g_print("- renaming %s' => '%s'\n", item->name, newname) ); - + g_free(item->name); item->name = g_strdup(newname); da_cat_build_fullname(item); - + if( item->parent == 0 ) { GHashTableIter iter; gpointer value; DB( g_print("- updating subcat fullname\n") ); - + g_hash_table_iter_init (&iter, GLOBALS->h_cat); while (g_hash_table_iter_next (&iter, NULL, &value)) { Category *subcat = value; - + if( subcat->parent == item->key ) da_cat_build_fullname(subcat); } @@ -232,13 +235,13 @@ guint32 *new_key; DB( g_print("\nda_cat_insert\n") ); DB( g_print("- '%s'\n", item->name) ); - + new_key = g_new0(guint32, 1); *new_key = item->key; g_hash_table_insert(GLOBALS->h_cat, new_key, item); da_cat_build_fullname(item); - + return TRUE; } @@ -261,7 +264,7 @@ Category *existitem; if( !cat->fullname ) da_cat_build_fullname(cat); - + existitem = da_cat_get_by_fullname( cat->fullname ); if( existitem == NULL ) { @@ -321,7 +324,7 @@ gboolean valid = TRUE; if( outlen != NULL ) *outlen = len; - + if(len >= 1) { g_strstrip(partstr[0]); @@ -353,7 +356,7 @@ gchar **partstr; Category *parent = NULL; Category *retval = NULL; guint len; - + DB( g_print("\nda_cat_get_by_fullname\n") ); if( rawfullname ) @@ -365,7 +368,7 @@ guint len; parent = da_cat_get_by_name_find_internal(0, partstr[0]); retval = parent; } - + if( len == 2 && parent != NULL ) { retval = da_cat_get_by_name_find_internal(parent->key, partstr[1]); @@ -374,7 +377,7 @@ guint len; g_strfreev(partstr); } } - + return retval; } @@ -414,7 +417,7 @@ guint len; } retval = parent; } - + /* if we have a subcategory - xxx:xxx */ if( len == 2 && parent != NULL ) { @@ -433,7 +436,7 @@ guint len; } retval = newcat; } - + g_strfreev(partstr); } } @@ -508,7 +511,7 @@ gboolean isIncome; g_warning("category consistency: fixed null name"); GLOBALS->changes_count++; } - + } @@ -565,11 +568,11 @@ guint32 retval = 0; } -void +void category_delete_unused(void) { GList *lcat, *list; - + lcat = list = g_hash_table_get_values(GLOBALS->h_cat); while (list != NULL) { @@ -584,7 +587,7 @@ GList *lcat, *list; } -static void +static void category_fill_usage_count(guint32 kcat) { Category *cat = da_cat_get (kcat); @@ -642,12 +645,12 @@ guint i, nbsplit; for(i=0;isplits, i); - + category_fill_usage_count(split->kcat); } } else - category_fill_usage_count(txn->kcat); + category_fill_usage_count(txn->kcat); lnk_txn = g_list_next(lnk_txn); } @@ -678,7 +681,7 @@ guint i, nbsplit; for(i=0;isplits, i); - + category_fill_usage_count(split->kcat); } } @@ -720,7 +723,7 @@ guint i, nbsplit; while (lnk_txn != NULL) { Transaction *txn = lnk_txn->data; - + if(txn->kcat == key1) { txn->kcat = key2; @@ -742,7 +745,7 @@ guint i, nbsplit; lnk_txn = g_list_next(lnk_txn); } - + lnk_acc = g_list_next(lnk_acc); } g_list_free(lst_acc); @@ -1064,7 +1067,7 @@ category_get_type_char(Category *item) } -static gint +static gint category_change_type_eval(Category *item, gboolean isIncome) { if( (item->flags & (GF_INCOME)) && !isIncome ) @@ -1073,14 +1076,14 @@ category_change_type_eval(Category *item, gboolean isIncome) } -gint +gint category_change_type(Category *item, gboolean isIncome) { gint changes = 0; GList *lcat, *list; changes += category_change_type_eval(item, isIncome); - + item->flags &= ~(GF_INCOME); //delete flag if(isIncome == TRUE) item->flags |= GF_INCOME; diff --git a/src/hb-payee.c b/src/hb-payee.c index fa49406..fc1829b 100644 --- a/src/hb-payee.c +++ b/src/hb-payee.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-payee.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macros */ @@ -45,12 +48,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); } } @@ -59,7 +62,7 @@ Payee * da_pay_malloc(void) { DB( g_print("da_pay_malloc\n") ); - return g_malloc0(sizeof(Payee)); + return rc_alloc(sizeof(Payee)); } diff --git a/src/hb-preferences.c b/src/hb-preferences.c index d324049..c676a64 100644 --- a/src/hb-preferences.c +++ b/src/hb-preferences.c @@ -295,6 +295,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)); } @@ -448,6 +451,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_init_measurement_units(); } @@ -982,6 +1002,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; @@ -1178,6 +1219,21 @@ GError *error = NULL; //group = "Chart"; //g_key_file_set_boolean (keyfile, group, "Legend", PREFS->chart_legend); + group = "Plugins"; + { + g_key_file_set_string_list(keyfile, group, "Path", (const gchar* const*)PREFS->ext_path, g_strv_length(PREFS->ext_path)); + + gsize len = g_list_length(PREFS->ext_whitelist); + gchar** strv = g_new0(gchar*, len + 1); + guint i; + + for (i = 0; i < len; ++i) { + strv[i] = g_list_nth_data(PREFS->ext_whitelist, i); + } + g_key_file_set_string_list(keyfile, group, "Whitelist", (const gchar* const*)strv, len); + g_free(strv); + } + //g_key_file_set_string (keyfile, group, "", PREFS->); //g_key_file_set_boolean (keyfile, group, "", PREFS->); //g_key_file_set_integer (keyfile, group, "", PREFS->); diff --git a/src/hb-preferences.h b/src/hb-preferences.h index 2dddc2f..baf469a 100644 --- a/src/hb-preferences.h +++ b/src/hb-preferences.h @@ -176,6 +176,10 @@ struct Preferences gchar *vehicle_unit_100; gchar *vehicle_unit_distbyvol; + // plugins + gchar** ext_path; + GList* ext_whitelist; + }; diff --git a/src/hb-tag.c b/src/hb-tag.c index 781a439..ebe1fa6 100644 --- a/src/hb-tag.c +++ b/src/hb-tag.c @@ -20,6 +20,9 @@ #include "homebank.h" #include "hb-tag.h" +#include "ext.h" +#include "refcount.h" + #define MYDEBUG 0 #if MYDEBUG @@ -37,12 +40,12 @@ extern struct HomeBank *GLOBALS; void da_tag_free(Tag *item) { DB( g_print("da_tag_free\n") ); - if(item != NULL) + if(rc_unref(item)) { DB( g_print(" => %d, %s\n", item->key, item->name) ); g_free(item->name); - g_free(item); + rc_free(item); } } @@ -50,7 +53,7 @@ void da_tag_free(Tag *item) Tag *da_tag_malloc(void) { DB( g_print("da_tag_malloc\n") ); - return g_malloc0(sizeof(Tag)); + return rc_alloc(sizeof(Tag)); } diff --git a/src/hb-transaction.c b/src/hb-transaction.c index f7acc90..e6fe031 100644 --- a/src/hb-transaction.c +++ b/src/hb-transaction.c @@ -23,6 +23,9 @@ #include "hb-tag.h" #include "hb-split.h" +#include "ext.h" +#include "refcount.h" + /****************************************************************************/ /* Debug macro */ /****************************************************************************/ @@ -82,10 +85,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); } } @@ -93,7 +96,7 @@ da_transaction_free(Transaction *item) Transaction * da_transaction_malloc(void) { - return g_malloc0(sizeof(Transaction)); + return rc_alloc(sizeof(Transaction)); } @@ -152,7 +155,7 @@ 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)); DB( g_print("da_transaction_clone\n") ); @@ -531,6 +534,9 @@ gchar swap; account_balances_add (child); + GValue txn_value = G_VALUE_INIT; + ext_hook("transaction_inserted", EXT_TRANSACTION(&txn_value, child), NULL); + } } @@ -994,6 +1000,9 @@ Account *acc; { transaction_xfer_search_or_add_child(parent, newope, newope->kxferacc); } + + GValue txn_value = G_VALUE_INIT; + ext_hook("transaction_inserted", EXT_TRANSACTION(&txn_value, newope), NULL); } return newope; diff --git a/src/hb-xml.c b/src/hb-xml.c index ae3ea9b..cc99d48 100644 --- a/src/hb-xml.c +++ b/src/hb-xml.c @@ -23,6 +23,8 @@ #include "hb-transaction.h" #include "hb-xml.h" +#include "ext.h" + #include "ui-dialogs.h" /****************************************************************************/ @@ -913,6 +915,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)) { @@ -1664,6 +1669,9 @@ gchar *outstr; gint retval = XML_OK; GError *error = NULL; + GValue filename_val = G_VALUE_INIT; + ext_hook("save_file", EXT_STRING(&filename_val, filename), NULL); + io = g_io_channel_new_file(filename, "w", &error); if(error) { diff --git a/src/homebank.c b/src/homebank.c index a80d305..a1b9146 100644 --- a/src/homebank.c +++ b/src/homebank.c @@ -19,6 +19,7 @@ #include "homebank.h" +#include "ext.h" #include "dsp-mainwindow.h" #include "hb-preferences.h" @@ -55,6 +56,7 @@ static gchar *pixmaps_dir = NULL; static gchar *locale_dir = NULL; static gchar *help_dir = NULL; static gchar *datas_dir = NULL; +static gchar *pkglib_dir = NULL; //#define MARKUP_STRING "%s" @@ -518,6 +520,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 @@ -534,6 +542,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); @@ -547,6 +556,7 @@ 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); + pkglib_dir = g_build_filename (PKGLIB_DIR, NULL); config_dir = g_build_filename(g_get_user_config_dir(), HB_DATA_PATH, NULL); //#870023 Ubuntu packages the help files in "/usr/share/doc/homebank-data/help/" for some strange reason @@ -563,6 +573,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) ); } @@ -704,6 +715,7 @@ static void homebank_cleanup() g_free (pixmaps_dir); g_free (locale_dir); g_free (help_dir); + g_free (pkglib_dir); } @@ -834,7 +846,7 @@ homebank_init_i18n (void) int -main (int argc, char *argv[]) +main (int argc, char *argv[], char *env[]) { GOptionContext *option_context; GOptionGroup *option_group; @@ -906,6 +918,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(); @@ -924,6 +952,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) { @@ -1028,13 +1059,20 @@ nobak: /* update the mainwin display */ ui_mainwindow_update(mainwin, GINT_TO_POINTER(UF_TITLE+UF_SENSITIVE+UF_BALANCE+UF_VISUAL)); + ext_hook("enter_main_loop", NULL); + DB( g_print(" - gtk_main()\n" ) ); gtk_main (); + ext_hook("exit_main_loop", NULL); + DB( g_print(" - call destroy mainwin\n" ) ); gtk_widget_destroy(mainwin); } + DB( g_print(" - unloading plugins\n") ); + ext_term(); + } diff --git a/src/homebank.h b/src/homebank.h index fc0c5e2..b3e1875 100644 --- a/src/homebank.h +++ b/src/homebank.h @@ -331,6 +331,7 @@ const gchar *homebank_app_get_pixmaps_dir (void); const gchar *homebank_app_get_locale_dir (void); const gchar *homebank_app_get_help_dir (void); const gchar *homebank_app_get_datas_dir (void); +const gchar *homebank_app_get_pkglib_dir (void); guint32 homebank_app_date_get_julian(void); /* - - - - obsolete things - - - - */ diff --git a/src/refcount.h b/src/refcount.h new file mode 100644 index 0000000..f97b93a --- /dev/null +++ b/src/refcount.h @@ -0,0 +1,54 @@ + +#ifndef __REFCOUNT_H__ +#define __REFCOUNT_H__ + +#include + + +static inline gpointer rc_alloc(size_t size) +{ + gpointer chunk = g_malloc0(size + sizeof(long)); + (*(long*)chunk) = 1; + //g_print("ALLOC: %p (ref %ld)\n", (long*)chunk + 1, *(long*)chunk); + return (long*)chunk + 1; +} + +static inline gpointer rc_ref(gpointer p) +{ + //g_print(" REF: %p (ref %ld)\n", p, *((long*)p - 1)); + if (p) { + ++(*((long*)p - 1)); + } + return p; +} + +static inline gboolean rc_unref(gpointer p) +{ + //g_print("UNREF: %p (ref %ld)\n", p, *((long*)p - 1)); + if (p && --(*((long*)p - 1)) <= 0) { + return TRUE; + } + return FALSE; +} + +static inline void rc_free(gpointer p) +{ + //g_print(" FREE: %p (ref %ld)\n", p, *((long*)p - 1)); + g_free((long*)p - 1); +} + +static inline gpointer rc_dup(gpointer p, size_t size) +{ + if (p) { + gpointer chunk = (long*)p - 1; + gpointer new_chunk = g_memdup(chunk, size + sizeof(long)); + *(long*)new_chunk = 1; + //g_print(" DUP: %p (ref %ld) -> %p (ref %ld)\n", p, *((long*)p - 1), (long*)new_chunk + 1, *(long*)new_chunk); + return (long*)new_chunk + 1; + } + //g_print(" DUP: NULL\n"); + return NULL; +} + + +#endif diff --git a/src/typemap b/src/typemap new file mode 100644 index 0000000..fc4a616 --- /dev/null +++ b/src/typemap @@ -0,0 +1,60 @@ + +TYPEMAP + +Account* T_HB_OBJECT +Transaction* T_HB_OBJECT +GObject* T_GOBJECT + +gint T_IV +guint T_UV +gdouble T_NV +gboolean T_GBOOLEAN +gchar T_CHAR +gchar* T_GCHAR_PTR +const gchar* T_GCHAR_PTR + +GPtrArray* T_GPTRARRAY +GHashTable* T_GHASHTABLE + + +INPUT + +T_HB_OBJECT + EXT_P2C_OBJECT(\"HomeBank::${ my ($t) = $ntype =~ /(.+)Ptr$/; \$t }\", $arg, $var, $type); + +T_GOBJECT + $var = SvGobject($arg); + +T_GCHAR_PTR + $var = SvGchar_ptr($arg); + +T_GBOOLEAN + $var = SvGboolean($arg); + +T_GPTRARRAY + $var = SvGptrarray($arg); + +T_GHASHTABLE + $var = SvGhashtable($arg); + + +OUTPUT + +T_HB_OBJECT + EXT_C2P_OBJECT(\"HomeBank::${ my ($t) = $ntype =~ /(.+)Ptr$/; \$t }\", $arg, rc_ref($var)); + +T_GOBJECT + $arg = newSVgobject($var); + +T_GCHAR_PTR + $arg = newSVgchar_ptr($var); + +T_GBOOLEAN + $arg = newSVgboolean($var); + +T_GPTRARRAY + $arg = newSVgptrarray($var); + +T_GHASHTABLE + $arg = newSVghashtable($var); + diff --git a/src/ui-pref.c b/src/ui-pref.c index 7b927e2..322358e 100644 --- a/src/ui-pref.c +++ b/src/ui-pref.c @@ -24,6 +24,8 @@ #include "dsp-mainwindow.h" #include "gtk-chart-colors.h" +#include "ext.h" + #include "ui-currency.h" @@ -53,16 +55,11 @@ enum { enum { - PREF_GENERAL, - PREF_INTERFACE, - PREF_LOCALE, //old DISPLAY - PREF_TXN, //old COLUMNS - PREF_IMPORT, - PREF_REPORT, - PREF_BACKUP, - PREF_FOLDERS, - PREF_EURO, - PREF_MAX + EXT_COLUMN_ENABLED = 0, + EXT_COLUMN_LABEL, + EXT_COLUMN_TOOLTIP, + EXT_COLUMN_PLUGIN_NAME, + EXT_NUM_COLUMNS }; @@ -71,12 +68,13 @@ static gchar *pref_iconname[PREF_MAX] = { "prf-interface", "prf-locale", "prf-columns", -//"prf-display", +//"prf-display", "prf-import", "prf-report", "prf-backup", "prf-folder", "prf-euro", // to be renamed +"prf-plugins", //"prf_charts.svg" }; @@ -89,7 +87,8 @@ N_("Import/Export"), N_("Report"), N_("Backup"), N_("Folders"), -N_("Euro minor") +N_("Euro minor"), +N_("Plugins") // }; @@ -202,14 +201,17 @@ static EuroParams euro_params[] = }; +static void list_ext_colpref_get(GtkTreeView *treeview, GList **columns); + + /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =*/ static LangName languagenames[] = { -// af ar ast be bg ca cs cy da de el en_AU en_CA en_GB es et eu fa fi fr ga gl he hr hu id is it +// af ar ast be bg ca cs cy da de el en_AU en_CA en_GB es et eu fa fi fr ga gl he hr hu id is it //ja ka ko lt lv ms nb nds nl oc pl pt_BR pt pt_PT ro ru si sk sl sr sv tr uk vi zh_CN zh_TW - + { "aa", "Afar" }, { "ab", "Abkhazian" }, { "ae", "Avestan" }, @@ -417,7 +419,7 @@ gchar *name1, *name2; //keep system laguage on top if(code1 == NULL) name1 = NULL; if(code2 == NULL) name2 = NULL; - + retval = hb_string_utf8_compare(name1, name2); g_free(name2); @@ -466,7 +468,7 @@ const gchar *lang; g_warning(" locale name not found '%s'", locale); lang = locale; } - + } return lang; @@ -481,7 +483,7 @@ GtkTreeIter iter; model = gtk_combo_box_get_model(GTK_COMBO_BOX(combobox)); gtk_list_store_append (GTK_LIST_STORE(model), &iter); - gtk_list_store_set (GTK_LIST_STORE(model), &iter, + gtk_list_store_set (GTK_LIST_STORE(model), &iter, 0, NULL, 1, _("System Language"), -1); @@ -504,13 +506,13 @@ const gchar *dirname; { const gchar *lang; gchar *label; - + gtk_list_store_append (GTK_LIST_STORE(model), &iter); lang = ui_language_combobox_get_name(dirname); label = g_strdup_printf ("%s [%s]", lang, dirname); - gtk_list_store_set (GTK_LIST_STORE(model), &iter, + gtk_list_store_set (GTK_LIST_STORE(model), &iter, 0, dirname, 1, label, -1); @@ -547,7 +549,7 @@ GtkCellRenderer *renderer; gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combobox), renderer, "text", 1, NULL); gtk_combo_box_set_id_column( GTK_COMBO_BOX(combobox), 0); - + g_object_unref(store); if(label) @@ -556,7 +558,7 @@ GtkCellRenderer *renderer; ui_language_combobox_populate(combobox); gtk_combo_box_set_active(GTK_COMBO_BOX(combobox), 0); - + return combobox; } @@ -724,7 +726,7 @@ gchar buf[128]; cur.frac_digits = gtk_spin_button_get_value(GTK_SPIN_BUTTON(data->NB_euro_fracdigits)); da_cur_initformat (&cur); - + DB( g_print("fmt: %s\n", cur.format) ); g_ascii_formatd(formatd_buf, sizeof (formatd_buf), cur.format, HB_NUMBER_SAMPLE); @@ -764,11 +766,11 @@ struct defpref_data *data; EuroParams *euro; gchar *buf; gint active; - + DB( g_print("\n[ui-pref] eurosetcurrency\n") ); data = g_object_get_data(G_OBJECT(gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW)), "inst_data"); - + active = ui_euro_combobox_id_to_active(country); euro = &euro_params[active]; buf = g_strdup_printf("%s - %s", euro->iso, euro->name); @@ -901,7 +903,7 @@ struct defpref_data *data; data = g_object_get_data(G_OBJECT(gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW)), "inst_data"); gtk_widget_queue_draw (data->DA_colors); - + } @@ -1057,7 +1059,7 @@ const gchar *lang; { PREFS->language = g_strdup(lang); } - + PREFS->toolbar_style = gtk_combo_box_get_active(GTK_COMBO_BOX(data->CY_toolbar)); //PREFS->image_size = gtk_spin_button_get_value(GTK_SPIN_BUTTON(data->NB_image_size)); @@ -1075,7 +1077,7 @@ const gchar *lang; PREFS->grid_lines = gtk_combo_box_get_active(GTK_COMBO_BOX(data->CY_gridlines)); //list_txn_colpref_get(GTK_TREE_VIEW(data->LV_opecolumns), PREFS->lst_ope_columns); - // transaction + // transaction PREFS->date_range_txn = hbtk_combo_box_get_active_id(GTK_COMBO_BOX_TEXT(data->CY_daterange_txn)); PREFS->date_future_nbdays = gtk_spin_button_get_value(GTK_SPIN_BUTTON(data->ST_datefuture_nbdays)); PREFS->hidereconciled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->CM_hide_reconciled)); @@ -1099,7 +1101,7 @@ const gchar *lang; PREFS->dtex_qifswap = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->CM_dtex_qifswap)); ui_gtk_entry_replace_text(data->ST_path_import, &PREFS->path_import); ui_gtk_entry_replace_text(data->ST_path_export, &PREFS->path_export); - PREFS->dtex_csvsep = gtk_combo_box_get_active(GTK_COMBO_BOX(data->CY_dtex_csvsep)); + PREFS->dtex_csvsep = gtk_combo_box_get_active(GTK_COMBO_BOX(data->CY_dtex_csvsep)); // report PREFS->date_range_rep = hbtk_combo_box_get_active_id(GTK_COMBO_BOX_TEXT(data->CY_daterange_rep)); @@ -1122,6 +1124,8 @@ const gchar *lang; ui_gtk_entry_replace_text(data->ST_euro_groupingchar, &PREFS->minor_cur.grouping_char); PREFS->minor_cur.frac_digits = gtk_spin_button_get_value(GTK_SPIN_BUTTON(data->NB_euro_fracdigits)); //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)); } @@ -1140,10 +1144,10 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("General options")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); - + row = 1; label = make_label_widget(_("Date order:")); //----------------------------------------- l, r, t, b @@ -1157,17 +1161,17 @@ gint crow, row; widget = gtk_check_button_new_with_mnemonic (_("Sentence _case memo/payee")); data->CM_dtex_ucfirst = widget; gtk_grid_attach (GTK_GRID (group_grid), widget, 1, row, 2, 1); - + // group :: OFX/QFX options group_grid = gtk_grid_new (); gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("OFX/QFX options")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); - + row = 1; label = make_label_widget(_("OFX _Name:")); //----------------------------------------- l, r, t, b @@ -1191,10 +1195,10 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("QIF options")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); - + row = 1; widget = gtk_check_button_new_with_mnemonic (_("_Import memos")); data->CM_dtex_qifmemo = widget; @@ -1208,7 +1212,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("CSV options")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1243,7 +1247,7 @@ gint i, x, y; index = gtk_combo_box_get_active(GTK_COMBO_BOX(data->CY_color_scheme)); colorscheme_init(&scheme, index); - + gtk_widget_get_size_request (widget, &w, &h); x = y = 0; for(i=0;iCM_stat_byamount = widget; @@ -1344,10 +1348,10 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Budget options")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); - + row = 1; widget = gtk_check_button_new_with_mnemonic (_("Show _details")); data->CM_budg_showdetail = widget; @@ -1372,7 +1376,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("General")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1387,7 +1391,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Currency")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 4, 1); @@ -1407,7 +1411,7 @@ gint crow, row; //gtk_grid_attach (GTK_GRID (group_grid), data->CY_option[FILTER_DATE], 1, 2, row, row+1); gtk_grid_attach (GTK_GRID (group_grid), widget, 3, row, 1, 1); - + // group :: Exchange rate group_grid = gtk_grid_new (); data->GRP_rate = group_grid; @@ -1432,7 +1436,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Format")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1449,7 +1453,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_container_add (GTK_CONTAINER (expander), group_grid); - + row = 0; label = make_label_widget(_("_Symbol:")); gtk_grid_attach (GTK_GRID (group_grid), label, 1, row, 1, 1); @@ -1502,7 +1506,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("User interface")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1542,7 +1546,7 @@ gint crow, row; widget = gtk_image_new_from_icon_name (ICONNAME_INFO, GTK_ICON_SIZE_BUTTON); gtk_grid_attach (GTK_GRID (group_grid), widget, 3, row, 1, 1); - + gtk_widget_set_tooltip_text(widget, _("%a locale's abbreviated weekday name.\n" "%A locale's full weekday name. \n" @@ -1598,7 +1602,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Measurement units")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1631,7 +1635,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Transaction window")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1648,7 +1652,7 @@ gint crow, row; //----------------------------------------- l, r, t, b gtk_grid_attach (GTK_GRID (group_grid), label, 1, row, 1, 1); widget = make_numeric(NULL, 0, 366); - + data->ST_datefuture_nbdays = widget; gtk_grid_attach (GTK_GRID (group_grid), widget, 2, row, 1, 1); @@ -1671,7 +1675,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Multiple add")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1685,7 +1689,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Memo autocomplete")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1707,7 +1711,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Column list")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1744,7 +1748,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("General")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1776,7 +1780,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Amount colors")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1803,11 +1807,11 @@ gint crow, row; hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, SPACING_SMALL); data->GR_colors = hbox; gtk_grid_attach (GTK_GRID (group_grid), hbox, 2, row, 1, 1); - + widget = gtk_color_button_new (); data->CP_exp_color = widget; gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); - + label = make_label_widget(_("_Income:")); gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); @@ -1841,7 +1845,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Backup")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1889,7 +1893,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("HomeBank files")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -1917,10 +1921,10 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Exchange files")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); - + row = 1; label = make_label_widget(_("_Import:")); //----------------------------------------- l, r, t, b @@ -1980,7 +1984,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Program start")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -2010,7 +2014,7 @@ gint crow, row; gtk_grid_set_row_spacing (GTK_GRID (group_grid), SPACING_SMALL); gtk_grid_set_column_spacing (GTK_GRID (group_grid), SPACING_MEDIUM); gtk_grid_attach (GTK_GRID (content_grid), group_grid, 0, crow++, 1, 1); - + label = make_label_group(_("Main window reports")); gtk_grid_attach (GTK_GRID (group_grid), label, 0, 0, 3, 1); @@ -2025,6 +2029,201 @@ gint crow, row; } +void plugin_execute_action(GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata); + +static void +toggle_plugin(GtkCellRendererToggle *cell, gchar* path_str, gpointer data) +{ + GtkTreeModel *model = (GtkTreeModel*)data; + GtkTreeIter iter; + GtkTreePath *path = gtk_tree_path_new_from_string(path_str); + + const gchar* plugin; + + gtk_tree_model_get_iter(model, &iter, path); + gtk_tree_model_get(model, &iter, EXT_COLUMN_PLUGIN_NAME, &plugin, -1); + + gboolean enabled = ext_is_plugin_loaded(plugin); + if (enabled) { + ext_unload_plugin(plugin); + enabled = FALSE; + } else { + enabled = (ext_load_plugin(plugin) == 0); + if (!enabled) { + ext_run_modal(_("Plugin Error"), _("The plugin failed to load properly."), "error"); + } + } + + /* set new value */ + gtk_list_store_set(GTK_LIST_STORE (model), &iter, EXT_COLUMN_ENABLED, enabled, -1); + + /* clean up */ + gtk_tree_path_free(path); +} + + +void plugin_execute_action(GtkTreeView* treeview, GtkTreePath* path, GtkTreeViewColumn* col, gpointer userdata) +{ + GtkTreeModel* model = gtk_tree_view_get_model(treeview); + GtkTreeIter iter; + + if (gtk_tree_model_get_iter(model, &iter, path)) { + gchar* plugin_filename; + gtk_tree_model_get(model, &iter, EXT_COLUMN_PLUGIN_NAME, &plugin_filename, -1); + ext_execute_action(plugin_filename); + g_free(plugin_filename); + } +} + +static GtkWidget *defpref_page_plugins (struct defpref_data *data) +{ + GtkWidget *container; + GtkListStore *store; + GtkTreeIter it; + GtkWidget* view; + + container = gtk_vbox_new(FALSE, 0); + + store = gtk_list_store_new(EXT_NUM_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + + gchar** plugins = ext_list_plugins(); + gchar** plugins_it; + for (plugins_it = plugins; *plugins_it; ++plugins_it) { + + gboolean enabled = ext_is_plugin_loaded(*plugins_it); + GHashTable* metadata = ext_read_plugin_metadata(*plugins_it); + if (!metadata) { + metadata = g_hash_table_new(g_str_hash, g_str_equal); + } + + gchar* tmp = NULL; + + // NAME + gchar* name = g_hash_table_lookup(metadata, "name"); + if (!name || *name == '\0') { + name = *plugins_it; + } + name = g_markup_escape_text(name, -1); + gchar* label = g_strdup_printf("%s", name); + gchar* tooltip = g_strdup_printf("%s", name); + g_free(name); + + // VERSION + gchar* version = g_hash_table_lookup(metadata, "version"); + if (version) { + version = g_markup_escape_text(version, -1); + tmp = label; + label = g_strdup_printf("%s %s", tmp, version); + g_free(tmp); + tmp = tooltip; + tooltip = g_strdup_printf("%s %s", tmp, version); + g_free(tmp); + g_free(version); + } + + // ABSTRACT + gchar* abstract = g_hash_table_lookup(metadata, "abstract"); + if (abstract) { + abstract = g_markup_escape_text(abstract, -1); + tmp = label; + label = g_strdup_printf("%s\n%s", tmp, abstract); + g_free(tmp); + g_free(abstract); + } + + // AUTHOR + gchar* author = g_hash_table_lookup(metadata, "author"); + if (author) { + author = g_markup_escape_text(author, -1); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s", tmp, author); + g_free(tmp); + g_free(author); + } + + // WEBSITE + gchar* website = g_hash_table_lookup(metadata, "website"); + if (website) { + website = g_markup_escape_text(website, -1); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s: %s", tmp, _("Website"), website); + g_free(tmp); + g_free(website); + } + + // FILEPATH + tmp = ext_find_plugin(*plugins_it); + gchar* full = g_markup_escape_text(tmp, -1); + g_free(tmp); + tmp = tooltip; + tooltip = g_strdup_printf("%s\n%s: %s", tmp, _("File"), full); + g_free(tmp); + g_free(full); + + g_hash_table_unref(metadata); + + gtk_list_store_append(store, &it); + gtk_list_store_set(store, &it, + EXT_COLUMN_ENABLED, enabled, + EXT_COLUMN_LABEL, label, + EXT_COLUMN_TOOLTIP, tooltip, + EXT_COLUMN_PLUGIN_NAME, *plugins_it, + -1); + + g_free(label); + g_free(tooltip); + } + g_strfreev(plugins); + + view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); + g_object_unref(store); + + g_signal_connect(view, "row-activated", (GCallback)plugin_execute_action, NULL); + + gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), TRUE); + gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(view), EXT_COLUMN_TOOLTIP); + + + GtkTreeViewColumn *col; + GtkCellRenderer *renderer; + + + col = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(col, _("Enabled")); + gtk_tree_view_column_set_sort_column_id(col, EXT_COLUMN_ENABLED); + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); + + renderer = gtk_cell_renderer_toggle_new(); + gtk_tree_view_column_pack_start(col, renderer, TRUE); + gtk_tree_view_column_add_attribute(col, renderer, "active", 0); + g_signal_connect(renderer, "toggled", G_CALLBACK(toggle_plugin), store); + + col = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(col, _("Plugin")); + gtk_tree_view_column_set_sort_column_id(col, EXT_COLUMN_LABEL); + gtk_tree_view_column_set_expand(col, TRUE); + /*gtk_tree_view_column_set_sort_order(col, GTK_SORT_ASCENDING);*/ + gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); + + renderer = gtk_cell_renderer_text_new(); + g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_pack_start(col, renderer, TRUE); + gtk_tree_view_column_add_attribute(col, renderer, "markup", EXT_COLUMN_LABEL); + + data->PI_plugin_columns = view; + + GtkWidget* sw = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_ETCHED_IN); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(sw), view); + + gtk_box_pack_start(GTK_BOX(container), sw, TRUE, TRUE, 0); + + return(container); +} + + static void defpref_selection(GtkTreeSelection *treeselection, gpointer user_data) { struct defpref_data *data; @@ -2119,12 +2318,12 @@ gint result; homebank_pref_setdefault(); defpref_set(data); } - + } // the window creation -GtkWidget *defpref_dialog_new (void) +GtkWidget *defpref_dialog_new (gint initial_selection) { struct defpref_data data; GtkWidget *window, *content, *mainvbox; @@ -2142,7 +2341,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; NULL); data.window = window; - + //store our window private data g_object_set_data(G_OBJECT(window), "inst_data", (gpointer)&data); @@ -2160,7 +2359,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; //left part vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, SPACING_SMALL); gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, FALSE, 0); - + //list sw = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_ETCHED_IN); @@ -2185,11 +2384,11 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; #else GtkCssProvider *provider; provider = gtk_css_provider_new (); - gtk_css_provider_load_from_data (provider, + gtk_css_provider_load_from_data (provider, "#hbebox { color: @theme_selected_fg_color; background-color: @theme_selected_bg_color; }" , -1, NULL); gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER(provider), G_MAXUINT); - + // gtk_style_context_set_state(context, GTK_STATE_FLAG_SELECTED); #endif @@ -2276,6 +2475,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); @@ -2285,7 +2488,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; g_signal_connect (data.CM_bak_is_automatic, "toggled", G_CALLBACK (defpref_backuptoggle), NULL); - + //path selector g_signal_connect (data.BT_path_hbfile, "pressed", G_CALLBACK (defpref_pathselect), GINT_TO_POINTER(PRF_PATH_WALLET)); g_signal_connect (data.BT_path_import, "pressed", G_CALLBACK (defpref_pathselect), GINT_TO_POINTER(PRF_PATH_IMPORT)); @@ -2295,7 +2498,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; g_signal_connect (data.CY_colors, "changed", G_CALLBACK (defpref_colorpreset), NULL); - + g_signal_connect (gtk_tree_view_get_selection(GTK_TREE_VIEW(data.LV_page)), "changed", G_CALLBACK (defpref_selection), notebook); g_signal_connect (data.CM_euro_enable, "toggled", G_CALLBACK (defpref_eurotoggle), NULL); @@ -2332,7 +2535,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); @@ -2341,6 +2545,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; @@ -2358,14 +2563,14 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; ui_mainwindow_update(GLOBALS->mainwindow, GINT_TO_POINTER(UF_BALANCE+UF_VISUAL)); DB( g_print("old='%s' new='%s'\n", old_lang, PREFS->language) ); - + if(g_ascii_strncasecmp(old_lang == NULL ? "" : old_lang, PREFS->language == NULL ? "" : PREFS->language, -1) != 0) { ui_dialog_msg_infoerror(GTK_WINDOW(window), GTK_MESSAGE_INFO, _("Info"), _("You will have to restart HomeBank\nfor the language change to take effect.") ); - + } g_free(old_lang); @@ -2375,7 +2580,7 @@ GtkWidget *hbox, *vbox, *sw, *widget, *notebook, *page, *ebox, *image, *label; defpref_reset (window, NULL); break; } - + // cleanup and destroy //defhbfile_cleanup(&data, result); @@ -2456,7 +2661,7 @@ GtkTreeIter iter; GtkTreePath *path = gtk_tree_path_new_from_string (path_str); gboolean fixed; - // get toggled iter + // get toggled iter gtk_tree_model_get_iter (model, &iter, path); gtk_tree_model_get (model, &iter, COLUMN_VISIBLE, &fixed, -1); @@ -2480,7 +2685,7 @@ gboolean visible; gint i, id; DB( g_print("[lst_txn-colpref] store column order \n") ); - + model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview)); valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter); i = 0; @@ -2529,16 +2734,16 @@ gint i; gboolean visible; DB( g_print("eval %d, %s\n", i, list_txn_column_label[i]) ); - + if(i <= LST_DSPOPE_DATE) // status, date always displayed continue; //[i-1] here because lst_ope_columns[] do not store MODEL_TXN_POINTER - id = ABS(PREFS->lst_ope_columns[i-1]); + id = ABS(PREFS->lst_ope_columns[i-1]); if(id == 0) id = i; //if we pass here, new column or weird into pref file visible = (PREFS->lst_ope_columns[i-1] > 0) ? TRUE : FALSE; - + DB( g_print(" - pos=%2d, id=%2d - %d '%s'\n", i, id, visible, list_txn_column_label[id]) ); gtk_list_store_append (store, &iter); @@ -2547,7 +2752,7 @@ gint i; COLUMN_NAME, _(list_txn_column_label[id]), COLUMN_ID , id, -1); - + } //treeview @@ -2566,7 +2771,7 @@ gint i; g_signal_connect (renderer, "toggled", G_CALLBACK (list_txn_colpref_toggled_cell_data_function), store); - + renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes (_("Column"), renderer, @@ -2581,3 +2786,32 @@ gint i; } */ + +static void list_ext_colpref_get(GtkTreeView *treeview, GList **columns) +{ + GtkTreeModel *model; + GtkTreeIter iter; + + g_list_free_full(*columns, g_free); + *columns = NULL; + + model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview)); + + gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter); + while (valid) { + gboolean enabled = FALSE; + const gchar* name; + + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, + EXT_COLUMN_ENABLED, &enabled, + EXT_COLUMN_PLUGIN_NAME, &name, + -1); + + if (enabled) { + *columns = g_list_append(*columns, g_strdup(name)); + } + + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter); + } +} + diff --git a/src/ui-pref.h b/src/ui-pref.h index 2ea1703..524c6ad 100644 --- a/src/ui-pref.h +++ b/src/ui-pref.h @@ -67,7 +67,7 @@ struct defpref_data GtkWidget *BT_go_up; GtkWidget *BT_go_down; - + GtkWidget *CM_runwizard; GtkWidget *ST_path_import, *BT_path_import; @@ -88,24 +88,24 @@ struct defpref_data GtkWidget *CY_daterange_txn; GtkWidget *ST_datefuture_nbdays; GtkWidget *CY_daterange_rep; - + /* currencies */ GtkWidget *LB_default; - GtkWidget *BT_default; - + GtkWidget *BT_default; + GtkWidget *CM_euro_enable; GtkWidget *GRP_currency; GtkWidget *GRP_rate; GtkWidget *GRP_format; - + GtkWidget *CY_euro_preset; GtkWidget *ST_euro_country; GtkWidget *NB_euro_value; GtkWidget *ST_euro_symbol; GtkWidget *CM_euro_isprefix; - GtkWidget *ST_euro_decimalchar; - GtkWidget *ST_euro_groupingchar; + GtkWidget *ST_euro_decimalchar; + GtkWidget *ST_euro_groupingchar; GtkWidget *NB_euro_fracdigits; GtkWidget *LB_numbereuro; @@ -134,10 +134,26 @@ struct defpref_data gint country; + GtkWidget *PI_plugin_columns; }; +enum +{ + PREF_GENERAL, + PREF_INTERFACE, + PREF_LOCALE, //old DISPLAY + PREF_TXN, //old COLUMNS + PREF_IMPORT, + PREF_REPORT, + PREF_BACKUP, + PREF_FOLDERS, + PREF_EURO, + PREF_PLUGINS, + PREF_MAX +}; -typedef struct + +typedef struct { gchar *locale; gchar *name; @@ -169,6 +185,6 @@ enum { void free_pref_icons(void); void load_pref_icons(void); -GtkWidget *defpref_dialog_new (void); +GtkWidget *defpref_dialog_new (gint initial_selection); #endif diff --git a/themes/hicolor/Makefile.am b/themes/hicolor/Makefile.am index 48f5746..fd5bcc9 100644 --- a/themes/hicolor/Makefile.am +++ b/themes/hicolor/Makefile.am @@ -85,6 +85,7 @@ private_icons = \ hicolor_status_48x48_prf-interface.png \ hicolor_status_48x48_prf-locale.png \ hicolor_status_48x48_prf-report.png \ + hicolor_status_48x48_prf-plugins.png \ hicolor_actions_scalable_edit-split-symbolic.svg \ hicolor_actions_scalable_toggle-sign-symbolic.svg \ hicolor_actions_scalable_list-collapse-all-symbolic.svg \ diff --git a/themes/hicolor/hicolor_status_48x48_prf-plugins.png b/themes/hicolor/hicolor_status_48x48_prf-plugins.png new file mode 100644 index 0000000..3950019 Binary files /dev/null and b/themes/hicolor/hicolor_status_48x48_prf-plugins.png differ