Merge lp:~adorsaz/homebank/adv-budget-with-categories into lp:homebank

Proposed by Adrien Dorsaz on 2019-02-07
Status: Needs review
Proposed branch: lp:~adorsaz/homebank/adv-budget-with-categories
Merge into: lp:homebank
Diff against target: 2805 lines (+2714/-7)
5 files modified
src/Makefile.am (+2/-0)
src/Makefile.in (+11/-7)
src/dsp_mainwindow.c (+10/-0)
src/ui-adv-budget.c (+2630/-0)
src/ui-adv-budget.h (+61/-0)
To merge this branch: bzr merge lp:~adorsaz/homebank/adv-budget-with-categories
Reviewer Review Type Date Requested Status
Maxime DOYEN 2019-02-07 Pending
Review via email: mp+362864@code.launchpad.net

Description of the change

Hello,

As I've said, I've worked again last month on the advanced budget manager for Home Bank last month.

This new merge propose is rebased on Home Bank 5.2.1 and includes already the first merge propose.

This one add the following features:

1. Categories can be managed directly from the view (rename category from the view, add, remove, merge categories from toolbar)
2. The view sorts by name categories inside the same hierarchy level
3. The view allows to expand and collapse lines as other Home Bank dialogs with a toolbar
4. A search widget is available to look fast for a category inside the view
5. The code has been refactored to correctly uses GTK objects (filter, sort...): the main model is built only once on dialog open and never rebuilt.
6. The "Monthly" columns now directly contains a toggle button to enable "same amount" feature and the entry to set the monthly amount (instead of two columns of first implementation)
7. The "Category Name" column now displays a toggle button in "Income" and "Expense" display mode to force the display in Balance mode
8. The view has vertical and horizontal scrollbars enabled (before, the horizontal one was disabled, but it did too big window when amounts were large)

To implement category management, I've based my work on the ui-category.c file (I needed to rewrite the UI part, but I was able to use same logic for the non-UI part).

Regards,
Adrien

To post a comment you must log in.
Maxime DOYEN (mdoyen) wrote :

merged this code with my local trunk for review

Unmerged revisions

104. By Adrien Dorsaz on 2019-01-27

ui-adv-budget: implements merge UI

103. By Adrien Dorsaz on 2019-01-27

ui-adv-budget: clean some unused data.

102. By Adrien Dorsaz on 2019-01-26

ui-adv-budget: on row selection, enable only useful actions

101. By Adrien Dorsaz on 2019-01-26

ui-adv-budget: remove unused variable

100. By Adrien Dorsaz on 2019-01-25

ui-adv-budget: use COMBO prefix instead of CB, as we'll have also CheckButton which share CB prefix too.

99. By Adrien Dorsaz on 2019-01-25

ui-adv-budget: wip to add merging function

That first step shows a combo box list with mergeable categories.

98. By Adrien Dorsaz on 2019-01-25

ui-adv-budget: update model to contains full category name and always add a child header with a separator

These child headers and separators will be needed for merge function
which allow to select any category to merge (except technical ones)

97. By Adrien Dorsaz on 2019-01-24

ui-adv-budget: add explicit UI to run a search

96. By Adrien Dorsaz on 2019-01-24

ui-adv-budget: fix add category crash due to g_free out of the good scope

95. By Adrien Dorsaz on 2019-01-24

ui-adv-budget: sort with utf8 string function

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/Makefile.am'
2--- src/Makefile.am 2017-09-16 15:26:49 +0000
3+++ src/Makefile.am 2019-02-07 14:10:29 +0000
4@@ -90,6 +90,8 @@
5 rep_vehicle.h \
6 ui-account.c \
7 ui-account.h \
8+ ui-adv-budget.c \
9+ ui-adv-budget.h \
10 ui-archive.c \
11 ui-archive.h \
12 ui-assign.c \
13
14=== modified file 'src/Makefile.in'
15--- src/Makefile.in 2018-09-09 09:20:25 +0000
16+++ src/Makefile.in 2019-02-07 14:10:29 +0000
17@@ -115,13 +115,14 @@
18 list_upcoming.$(OBJEXT) rep_balance.$(OBJEXT) \
19 rep_budget.$(OBJEXT) rep_stats.$(OBJEXT) rep_time.$(OBJEXT) \
20 rep_vehicle.$(OBJEXT) ui-account.$(OBJEXT) \
21- ui-archive.$(OBJEXT) ui-assign.$(OBJEXT) \
22- ui-assist-import.$(OBJEXT) ui-assist-start.$(OBJEXT) \
23- ui-budget.$(OBJEXT) ui-category.$(OBJEXT) \
24- ui-currency.$(OBJEXT) ui-dialogs.$(OBJEXT) ui-filter.$(OBJEXT) \
25- ui-hbfile.$(OBJEXT) ui-payee.$(OBJEXT) ui-pref.$(OBJEXT) \
26- ui-split.$(OBJEXT) ui-transaction.$(OBJEXT) \
27- ui-txn-multi.$(OBJEXT) ui-widgets.$(OBJEXT)
28+ ui-adv-budget.$(OBJEXT) ui-archive.$(OBJEXT) \
29+ ui-assign.$(OBJEXT) ui-assist-import.$(OBJEXT) \
30+ ui-assist-start.$(OBJEXT) ui-budget.$(OBJEXT) \
31+ ui-category.$(OBJEXT) ui-currency.$(OBJEXT) \
32+ ui-dialogs.$(OBJEXT) ui-filter.$(OBJEXT) ui-hbfile.$(OBJEXT) \
33+ ui-payee.$(OBJEXT) ui-pref.$(OBJEXT) ui-split.$(OBJEXT) \
34+ ui-transaction.$(OBJEXT) ui-txn-multi.$(OBJEXT) \
35+ ui-widgets.$(OBJEXT)
36 homebank_OBJECTS = $(am_homebank_OBJECTS)
37 am__DEPENDENCIES_1 =
38 homebank_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
39@@ -391,6 +392,8 @@
40 rep_vehicle.h \
41 ui-account.c \
42 ui-account.h \
43+ ui-adv-budget.c \
44+ ui-adv-budget.h \
45 ui-archive.c \
46 ui-archive.h \
47 ui-assign.c \
48@@ -557,6 +560,7 @@
49 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rep_time.Po@am__quote@
50 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rep_vehicle.Po@am__quote@
51 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ui-account.Po@am__quote@
52+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ui-adv-budget.Po@am__quote@
53 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ui-archive.Po@am__quote@
54 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ui-assign.Po@am__quote@
55 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ui-assist-import.Po@am__quote@
56
57=== modified file 'src/dsp_mainwindow.c'
58--- src/dsp_mainwindow.c 2018-09-15 17:25:46 +0000
59+++ src/dsp_mainwindow.c 2019-02-07 14:10:29 +0000
60@@ -36,6 +36,7 @@
61 #include "ui-archive.h"
62 #include "ui-assign.h"
63 #include "ui-budget.h"
64+#include "ui-adv-budget.h"
65 #include "ui-pref.h"
66 #include "ui-hbfile.h"
67 #include "ui-transaction.h"
68@@ -95,6 +96,7 @@
69 static void ui_mainwindow_action_defcategory(void);
70 static void ui_mainwindow_action_defarchive(void);
71 static void ui_mainwindow_action_defbudget(void);
72+static void ui_mainwindow_action_defadvancedbudget(void);
73 static void ui_mainwindow_action_defassign(void);
74 static void ui_mainwindow_action_preferences(void);
75
76@@ -216,6 +218,7 @@
77 { "Category" , ICONNAME_HB_CATEGORY , N_("Categories...") , NULL, N_("Configure the categories"), G_CALLBACK (ui_mainwindow_action_defcategory) },
78 { "Archive" , ICONNAME_HB_ARCHIVE , N_("Scheduled/Template...") , NULL, N_("Configure the scheduled/template transactions"), G_CALLBACK (ui_mainwindow_action_defarchive) },
79 { "Budget" , ICONNAME_HB_BUDGET , N_("Budget...") , NULL, N_("Configure the budget"), G_CALLBACK (ui_mainwindow_action_defbudget) },
80+ { "AdvancedBudget", ICONNAME_HB_BUDGET , N_("Advanced Budget...") , NULL, N_("Configure the budget"), G_CALLBACK (ui_mainwindow_action_defadvancedbudget) },
81 { "Assign" , ICONNAME_HB_ASSIGN , N_("Assignments..."), NULL, N_("Configure the automatic assignments"), G_CALLBACK (ui_mainwindow_action_defassign) },
82
83 /* TxnMenu */
84@@ -309,6 +312,7 @@
85 " <menuitem action='Category'/>"
86 " <menuitem action='Archive'/>"
87 " <menuitem action='Budget'/>"
88+" <menuitem action='AdvancedBudget'/>"
89 " <menuitem action='Assign'/>"
90 " <menuitem action='Currency'/>"
91 " </menu>"
92@@ -683,6 +687,12 @@
93 ui_mainwindow_update(GLOBALS->mainwindow, GINT_TO_POINTER(UF_TITLE+UF_SENSITIVE));
94 }
95
96+static void ui_mainwindow_action_defadvancedbudget(void)
97+{
98+ ui_adv_bud_manage_dialog();
99+ ui_mainwindow_update(GLOBALS->mainwindow, GINT_TO_POINTER(UF_TITLE+UF_SENSITIVE));
100+}
101+
102
103 static void ui_mainwindow_action_defassign(void)
104 {
105
106=== added file 'src/ui-adv-budget.c'
107--- src/ui-adv-budget.c 1970-01-01 00:00:00 +0000
108+++ src/ui-adv-budget.c 2019-02-07 14:10:29 +0000
109@@ -0,0 +1,2630 @@
110+/* HomeBank -- Free, easy, personal accounting for everyone.
111+ * Copyright (C) 2018-2019 Adrien Dorsaz <adrien@adorsaz.ch>
112+ *
113+ * This file is part of HomeBank.
114+ *
115+ * HomeBank is free software; you can redistribute it and/or modify
116+ * it under the terms of the GNU General Public License as published by
117+ * the Free Software Foundation; either version 2 of the License, or
118+ * (at your option) any later version.
119+ *
120+ * HomeBank is distributed in the hope that it will be useful,
121+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
122+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
123+ * GNU General Public License for more details.
124+ *
125+ * You should have received a copy of the GNU General Public License
126+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
127+ */
128+
129+
130+#include "homebank.h"
131+#include "hb-misc.h"
132+#include "dsp_mainwindow.h"
133+#include "hb-category.h"
134+#include "ui-adv-budget.h"
135+
136+/****************************************************************************/
137+/* Implementation notes */
138+/****************************************************************************/
139+/*
140+ * This dialog allows user to manage its budget with a GtkTreeView.
141+ *
142+ * The view rows are separated in three main tree roots:
143+ * - Income: contains all Homebank categories of income type (see GF_INCOME)
144+ * - Expense: contains all Homebank categories of expense type
145+ * - Total: contains 3 sub-rows:
146+ * - Income: sum all amounts of the Income root
147+ * - Expense: sum all amounts of the Expense root
148+ * - Balance: difference between the two above sub-rows
149+ *
150+ * The view columns contain:
151+ * - Category: Homebank categories organised in hierarchy
152+ * See the main tree roots above.
153+ * - That column show also a check box (only in non-Balance mode):
154+ * Check it to enable the GF_FORCED flag of Homebank categories
155+ * "Is this category forced to be displayed, even if no budget has been set?"
156+ *
157+ * - Monthly: set the monthly amount when the Same flag is active
158+ * - That column contains a toggle check box to enable or not monthly values
159+ * Check it to disable the GF_CUSTOM flag of Homebank categories
160+ * "Does this category has same amount planned every month ?"
161+ *
162+ * - 12 columns for each month of the year containing their specific amount
163+ *
164+ * - Total: sum all amounts of the year for the category
165+ *
166+ * The dialog shows 3 radio buttons to choose between 3 edition modes:
167+ * - Balance: show Homebank categories with budget set or set with GF_FORCED
168+ * This mode hide the toggle in the Category column
169+ * - Income: show all available Homebank categories of income type
170+ * - Expense: show all available Homebank categories of expense type
171+ *
172+ */
173+
174+/****************************************************************************/
175+/* Debug macros */
176+/****************************************************************************/
177+#define MYDEBUG 0
178+
179+#if MYDEBUG
180+#define DB(x) (x);
181+#else
182+#define DB(x);
183+#endif
184+
185+/* Global data */
186+extern struct HomeBank *GLOBALS;
187+extern struct Preferences *PREFS;
188+
189+static gchar *ADVBUD_MONTHS[] = {
190+ N_("Jan"), N_("Feb"), N_("Mar"),
191+ N_("Apr"), N_("May"), N_("Jun"),
192+ N_("Jul"), N_("Aug"), N_("Sept"),
193+ N_("Oct"), N_("Nov"), N_("Dec"),
194+ NULL};
195+
196+/* The different view mode available */
197+static gchar *ADVBUD_VIEW_MODE[] = {
198+ N_("Balance"),
199+ N_("Income"),
200+ N_("Expense"),
201+ NULL
202+};
203+
204+/* These values has to correspond to ADVBUD_VIEW_MODE[] */
205+enum advbud_view_mode {
206+ ADVBUD_VIEW_BALANCE = 0,
207+ ADVBUD_VIEW_INCOME,
208+ ADVBUD_VIEW_EXPENSE
209+};
210+typedef enum advbud_view_mode advbud_view_mode_t;
211+
212+/* These values corresponds to the return of category_type_get from hb-category */
213+enum advbud_cat_type {
214+ ADVBUD_CAT_TYPE_EXPENSE = -1,
215+ ADVBUD_CAT_TYPE_NONE = 0, // Not real category type: used to retrieve tree roots
216+ ADVBUD_CAT_TYPE_INCOME = 1
217+};
218+typedef enum advbud_cat_type advbud_cat_type_t;
219+
220+/* enum for the Budget Tree Store model */
221+enum advbud_store {
222+ ADVBUD_CATEGORY_KEY = 0,
223+ ADVBUD_CATEGORY_NAME,
224+ ADVBUD_CATEGORY_FULLNAME,
225+ ADVBUD_CATEGORY_TYPE,
226+ ADVBUD_ISROOT, // To retrieve easier the 3 main tree roots
227+ ADVBUD_ISTOTAL, // To retrieve rows inside the Total root
228+ ADVBUD_ISCHILDHEADER, // The row corresponds to the head child which is shown before the separator
229+ ADVBUD_ISSEPARATOR, // Row to just display a separator in Tree View
230+ ADVBUD_ISDISPLAYFORCED,
231+ ADVBUD_ISSAMEAMOUNT,
232+ ADVBUD_SAMEAMOUNT,
233+ ADVBUD_JANUARY,
234+ ADVBUD_FEBRUARY,
235+ ADVBUD_MARCH,
236+ ADVBUD_APRIL,
237+ ADVBUD_MAY,
238+ ADVBUD_JUNE,
239+ ADVBUD_JULY,
240+ ADVBUD_AUGUST,
241+ ADVBUD_SEPTEMBER,
242+ ADVBUD_OCTOBER,
243+ ADVBUD_NOVEMBER,
244+ ADVBUD_DECEMBER,
245+ ADVBUD_NUMCOLS
246+};
247+typedef enum advbud_store advbud_store_t;
248+
249+// Retrieve a row iterator according to specific criterias
250+const struct advbud_search_criteria {
251+ // Search by non-zero category key
252+ guint32 row_category_key;
253+ // Search by other criterias
254+ advbud_cat_type_t row_category_type;
255+ gboolean row_isroot;
256+ gboolean row_istotal;
257+ // Found iterator, NULL if not found
258+ GtkTreeIter *iterator;
259+} advbud_search_criteria_default = {0, ADVBUD_CAT_TYPE_NONE, FALSE, FALSE, NULL} ;
260+typedef struct advbud_search_criteria advbud_search_criteria_t;
261+
262+/*
263+ * Local headers
264+ **/
265+
266+// GtkTreeStore model
267+static gboolean ui_adv_bud_model_search_iterator (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, advbud_search_criteria_t *search);
268+static void ui_adv_bud_model_add_category_with_lineage(GtkTreeStore *budget, GtkTreeIter *balanceIter, guint32 *key_category);
269+static void ui_adv_bud_model_collapse (GtkTreeView *view);
270+static void ui_adv_bud_model_insert_roots(GtkTreeStore* budget);
271+static void ui_adv_bud_model_update_monthlytotal(GtkTreeStore* budget);
272+static gboolean ui_adv_bud_model_row_filter (GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
273+static gboolean ui_adv_bud_model_row_filter_with_headers (GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data);
274+static gboolean ui_adv_bud_model_row_filter_parents (GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
275+static gint ui_adv_bud_model_row_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data);
276+static GtkTreeModel * ui_adv_bud_model_new ();
277+
278+// GtkTreeView widget
279+static void ui_adv_bud_view_display_amount (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data);
280+static void ui_adv_bud_view_display_issameamount (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data);
281+static void ui_adv_bud_view_display_isdisplayforced (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data);
282+static void ui_adv_bud_view_display_annualtotal (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data);
283+static void ui_adv_bud_view_toggle (gpointer user_data, advbud_view_mode_t view_mode);
284+static gboolean ui_adv_bud_view_search (GtkTreeModel *model, gint column, const gchar *key, GtkTreeIter *iter, gpointer data);
285+static gboolean ui_adv_view_separator (GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
286+static void ui_adv_bud_view_on_select(GtkTreeSelection *treeselection, gpointer user_data);
287+static GtkWidget *ui_adv_bud_view_new (gpointer user_data);
288+
289+// UI actions
290+static void ui_adv_bud_cell_update_category(GtkCellRendererText *renderer, gchar *filter_path, gchar *new_text, gpointer user_data);
291+static void ui_adv_bud_cell_update_amount(GtkCellRendererText *renderer, gchar *filter_path, gchar *new_text, gpointer user_data);
292+static void ui_adv_bud_cell_update_issameamount(GtkCellRendererText *renderer, gchar *filter_path, gpointer user_data);
293+static void ui_adv_bud_cell_update_isdisplayforced(GtkCellRendererText *renderer, gchar *filter_path, gpointer user_data);
294+static void ui_adv_bud_view_update_mode (GtkToggleButton *button, gpointer user_data);
295+static void ui_adv_bud_view_expand (GtkButton *button, gpointer user_data);
296+static void ui_adv_bud_view_collapse (GtkButton *button, gpointer user_data);
297+static void ui_adv_bud_category_add_full_filled (GtkWidget *source, gpointer user_data);
298+static void ui_adv_bud_category_add (GtkButton *button, gpointer user_data);
299+static void ui_adv_bud_category_delete (GtkButton *button, gpointer user_data);
300+static void ui_adv_bud_category_merge_full_filled (GtkWidget *source, gpointer user_data);
301+static void ui_adv_bud_category_merge (GtkButton *button, gpointer user_data);
302+static gboolean ui_adv_bud_on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
303+static void ui_adv_bud_dialog_close(adv_bud_data_t *data, gint response);
304+
305+/**
306+ * GtkTreeStore model
307+ **/
308+
309+// Look for category by deterministic characteristics
310+// Only categories with specific characteristics can be easily found
311+// like roots, total rows and categories with real key id
312+// You are responsible to g_free iterator
313+static gboolean ui_adv_bud_model_search_iterator (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, advbud_search_criteria_t *search)
314+{
315+guint32 category_key;
316+advbud_cat_type_t category_type;
317+gboolean is_found = FALSE, is_root, is_total, is_separator;
318+
319+ search->iterator = NULL;
320+
321+ gtk_tree_model_get (model, iter,
322+ ADVBUD_CATEGORY_KEY, &category_key,
323+ ADVBUD_CATEGORY_TYPE, &category_type,
324+ ADVBUD_ISROOT, &is_root,
325+ ADVBUD_ISTOTAL, &is_total,
326+ ADVBUD_ISSEPARATOR, &is_separator,
327+ -1);
328+
329+ if (search->row_category_key > 0 // Look for iter of real category row
330+ && category_key == search->row_category_key
331+ && !(is_total)
332+ && !(is_root)
333+ )
334+ {
335+ DB(g_print("\tFound row with key %d\n", category_key));
336+ is_found = TRUE;
337+ }
338+ else if (search->row_category_key == 0 // Look for iter of fake category row
339+ && is_root == search->row_isroot
340+ && is_total == search->row_istotal
341+ && category_type == search->row_category_type
342+ && !is_separator
343+ )
344+ {
345+ DB(g_print("\tFound row with is_root = %d, is_total %d, type = %d\n", is_root, is_total, category_type));
346+ is_found = TRUE;
347+ }
348+
349+ // If found, save result to struct
350+ if (is_found)
351+ {
352+ search->iterator = g_malloc0(sizeof(GtkTreeIter));
353+ *search->iterator = *iter;
354+ }
355+
356+ return is_found;
357+}
358+
359+/* Recursive function which add a new row in the budget model with all its ancestors */
360+static void ui_adv_bud_model_add_category_with_lineage(GtkTreeStore *budget, GtkTreeIter *balanceIter, guint32 *key_category)
361+{
362+GtkTreeIter child;
363+GtkTreeIter *parent;
364+Category *bdg_category;
365+gboolean cat_is_sameamount;
366+advbud_search_criteria_t parent_search = advbud_search_criteria_default;
367+
368+ bdg_category = da_cat_get(*key_category);
369+
370+ if (bdg_category == NULL)
371+ {
372+ return;
373+ }
374+
375+ cat_is_sameamount = (! (bdg_category->flags & GF_CUSTOM));
376+
377+ /* Check if parent category already exists */
378+ parent_search.row_category_key = bdg_category->parent;
379+
380+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
381+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
382+ &parent_search);
383+
384+ if (bdg_category->parent == 0)
385+ {
386+ // If we are one of the oldest parent, stop recursion
387+ gtk_tree_store_insert (
388+ budget,
389+ &child,
390+ balanceIter,
391+ -1);
392+ }
393+ else
394+ {
395+ if (parent_search.iterator)
396+ {
397+ DB(g_print("\tRecursion optimisation: parent key %d already exists\n", parent_search.row_category_key));
398+ // If parent already exists, stop recursion
399+ parent = parent_search.iterator;
400+ }
401+ else
402+ {
403+ // Parent has not been found, ask to create it first
404+ ui_adv_bud_model_add_category_with_lineage(budget, balanceIter, &(bdg_category->parent));
405+
406+ // Now, we are sure parent exists, look for it again
407+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
408+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
409+ &parent_search);
410+
411+ parent = parent_search.iterator;
412+ }
413+
414+ gtk_tree_store_insert (
415+ budget,
416+ &child,
417+ parent,
418+ -1);
419+ }
420+
421+ DB(g_print("insert new category %s (key: %d, type: %d)\n",
422+ bdg_category->name, bdg_category->key, category_type_get (bdg_category)));
423+
424+ gtk_tree_store_set(
425+ budget,
426+ &child,
427+ ADVBUD_CATEGORY_KEY, bdg_category->key,
428+ ADVBUD_CATEGORY_NAME, bdg_category->name,
429+ ADVBUD_CATEGORY_FULLNAME, bdg_category->fullname,
430+ ADVBUD_CATEGORY_TYPE, category_type_get (bdg_category),
431+ ADVBUD_ISDISPLAYFORCED, (bdg_category->flags & GF_FORCED),
432+ ADVBUD_ISROOT, FALSE,
433+ ADVBUD_ISSAMEAMOUNT, cat_is_sameamount,
434+ ADVBUD_ISTOTAL, FALSE,
435+ ADVBUD_SAMEAMOUNT, bdg_category->budget[0],
436+ ADVBUD_JANUARY, bdg_category->budget[1],
437+ ADVBUD_FEBRUARY, bdg_category->budget[2],
438+ ADVBUD_MARCH, bdg_category->budget[3],
439+ ADVBUD_APRIL, bdg_category->budget[4],
440+ ADVBUD_MAY, bdg_category->budget[5],
441+ ADVBUD_JUNE, bdg_category->budget[6],
442+ ADVBUD_JULY, bdg_category->budget[7],
443+ ADVBUD_AUGUST, bdg_category->budget[8],
444+ ADVBUD_SEPTEMBER, bdg_category->budget[9],
445+ ADVBUD_OCTOBER, bdg_category->budget[10],
446+ ADVBUD_NOVEMBER, bdg_category->budget[11],
447+ ADVBUD_DECEMBER, bdg_category->budget[12],
448+ -1);
449+
450+ // Always add child header and separator
451+ parent = gtk_tree_iter_copy(&child);
452+ gtk_tree_store_insert_with_values(
453+ budget,
454+ &child,
455+ parent,
456+ -1,
457+ ADVBUD_CATEGORY_KEY, bdg_category->key,
458+ ADVBUD_CATEGORY_NAME, bdg_category->name,
459+ ADVBUD_CATEGORY_FULLNAME, bdg_category->fullname,
460+ ADVBUD_CATEGORY_TYPE, category_type_get (bdg_category),
461+ ADVBUD_ISCHILDHEADER, TRUE,
462+ -1);
463+
464+ gtk_tree_store_insert_with_values(
465+ budget,
466+ &child,
467+ parent,
468+ -1,
469+ ADVBUD_CATEGORY_KEY, bdg_category->key,
470+ ADVBUD_CATEGORY_TYPE, category_type_get (bdg_category),
471+ ADVBUD_ISSEPARATOR, TRUE,
472+ -1);
473+
474+ gtk_tree_iter_free(parent);
475+
476+ g_free(parent_search.iterator);
477+
478+ return;
479+}
480+
481+// Collapse all categories except root
482+static void ui_adv_bud_model_collapse (GtkTreeView *view) {
483+GtkTreeModel *budget;
484+GtkTreePath *path;
485+advbud_search_criteria_t root_search = advbud_search_criteria_default;
486+
487+ budget = gtk_tree_view_get_model (view);
488+
489+ gtk_tree_view_collapse_all(view);
490+
491+ // Keep root categories expanded
492+
493+ // Retrieve income root
494+ root_search.row_isroot = TRUE;
495+ root_search.row_istotal = FALSE;
496+ root_search.row_category_type = ADVBUD_CAT_TYPE_INCOME;
497+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
498+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
499+ &root_search);
500+
501+ if (root_search.iterator != NULL) {
502+ path = gtk_tree_model_get_path(budget, root_search.iterator);
503+ gtk_tree_view_expand_row(view, path, FALSE);
504+ }
505+
506+ // Retrieve expense root
507+ root_search.row_isroot = TRUE;
508+ root_search.row_istotal = FALSE;
509+ root_search.row_category_type = ADVBUD_CAT_TYPE_EXPENSE;
510+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
511+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
512+ &root_search);
513+
514+ if (root_search.iterator != NULL) {
515+ path = gtk_tree_model_get_path(budget, root_search.iterator);
516+ gtk_tree_view_expand_row(view, path, FALSE);
517+ }
518+
519+ // Retrieve total root
520+ root_search.row_isroot = TRUE;
521+ root_search.row_istotal = FALSE;
522+ root_search.row_category_type = ADVBUD_CAT_TYPE_NONE;
523+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
524+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
525+ &root_search);
526+
527+ if (root_search.iterator != NULL) {
528+ path = gtk_tree_model_get_path(budget, root_search.iterator);
529+ gtk_tree_view_expand_row(view, path, FALSE);
530+ }
531+
532+ g_free(root_search.iterator);
533+
534+ return;
535+}
536+
537+// Create tree roots for the store
538+static void ui_adv_bud_model_insert_roots(GtkTreeStore* budget)
539+{
540+GtkTreeIter iter, root;
541+
542+ gtk_tree_store_insert_with_values (
543+ budget,
544+ &root,
545+ NULL,
546+ -1,
547+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
548+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
549+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_INCOME,
550+ ADVBUD_ISROOT, TRUE,
551+ ADVBUD_ISTOTAL, FALSE,
552+ -1);
553+
554+ // For add category dialog: copy of the root to be able to select it
555+ gtk_tree_store_insert_with_values (
556+ budget,
557+ &iter,
558+ &root,
559+ -1,
560+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
561+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
562+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_INCOME,
563+ ADVBUD_CATEGORY_KEY, 0,
564+ ADVBUD_ISROOT, FALSE,
565+ ADVBUD_ISTOTAL, FALSE,
566+ ADVBUD_ISCHILDHEADER, TRUE,
567+ -1);
568+
569+ // For add category dialog: add a separator to distinguish root with children
570+ gtk_tree_store_insert_with_values (
571+ budget,
572+ &iter,
573+ &root,
574+ -1,
575+ ADVBUD_ISSEPARATOR, TRUE,
576+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_INCOME,
577+ -1);
578+
579+ gtk_tree_store_insert_with_values (
580+ budget,
581+ &root,
582+ NULL,
583+ -1,
584+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
585+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
586+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_EXPENSE,
587+ ADVBUD_ISROOT, TRUE,
588+ ADVBUD_ISTOTAL, FALSE,
589+ -1);
590+
591+ // For add category dialog: copy of the root to be able to select it
592+ gtk_tree_store_insert_with_values (
593+ budget,
594+ &iter,
595+ &root,
596+ -1,
597+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
598+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
599+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_EXPENSE,
600+ ADVBUD_CATEGORY_KEY, 0,
601+ ADVBUD_ISROOT, FALSE,
602+ ADVBUD_ISTOTAL, FALSE,
603+ ADVBUD_ISCHILDHEADER, TRUE,
604+ -1);
605+
606+ // For add category dialog: add a separator to distinguish root with children
607+ gtk_tree_store_insert_with_values (
608+ budget,
609+ &iter,
610+ &root,
611+ -1,
612+ ADVBUD_ISSEPARATOR, TRUE,
613+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_EXPENSE,
614+ -1);
615+
616+ gtk_tree_store_insert_with_values (
617+ budget,
618+ &root,
619+ NULL,
620+ -1,
621+ ADVBUD_CATEGORY_NAME, _("Totals"),
622+ ADVBUD_CATEGORY_FULLNAME, _("Totals"),
623+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_NONE,
624+ ADVBUD_ISROOT, TRUE,
625+ ADVBUD_ISTOTAL, FALSE,
626+ -1);
627+
628+ return;
629+}
630+
631+// Update (or insert) total rows for a budget according to the view mode
632+// This function will is used to initiate model and to refresh it after change by user
633+static void ui_adv_bud_model_update_monthlytotal(GtkTreeStore* budget)
634+{
635+advbud_search_criteria_t root_search = advbud_search_criteria_default;
636+GtkTreeIter total_root, child;
637+double total_income[12] = {0}, total_expense[12] = {0};
638+gboolean cat_is_sameamount;
639+int n_category;
640+
641+ // Go through all categories to compute totals
642+ n_category = da_cat_get_max_key();
643+
644+ for(guint32 i=1; i<=n_category; ++i)
645+ {
646+ Category *bdg_category;
647+ gboolean cat_is_income;
648+
649+ bdg_category = da_cat_get(i);
650+
651+ if (bdg_category == NULL)
652+ {
653+ continue;
654+ }
655+
656+ cat_is_income = (category_type_get (bdg_category) == 1);
657+ cat_is_sameamount = (! (bdg_category->flags & GF_CUSTOM));
658+
659+ for (gint j=0; j<=11; ++j)
660+ {
661+ if (cat_is_income)
662+ {
663+ if (cat_is_sameamount)
664+ {
665+ total_income[j] += bdg_category->budget[0];
666+ }
667+ else
668+ {
669+ total_income[j] += bdg_category->budget[j+1];
670+ }
671+ }
672+ else
673+ {
674+ if (cat_is_sameamount)
675+ {
676+ total_expense[j] += bdg_category->budget[0];
677+ }
678+ else
679+ {
680+ total_expense[j] += bdg_category->budget[j+1];
681+ }
682+ }
683+ }
684+ }
685+
686+ // Retrieve total root and insert required total rows
687+ root_search.row_isroot = TRUE;
688+ root_search.row_istotal = FALSE;
689+ root_search.row_category_type = ADVBUD_CAT_TYPE_NONE;
690+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
691+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
692+ &root_search);
693+
694+ if (!root_search.iterator) {
695+ return;
696+ }
697+
698+ total_root = *root_search.iterator;
699+
700+ // Retrieve and set totals
701+ root_search.row_isroot = FALSE;
702+ root_search.row_istotal = TRUE;
703+
704+ // First, look for Incomes
705+ root_search.row_category_type = ADVBUD_CAT_TYPE_INCOME;
706+
707+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
708+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
709+ &root_search);
710+
711+ if (root_search.iterator) {
712+ child = *root_search.iterator;
713+ }
714+ else {
715+ gtk_tree_store_insert(budget, &child, &total_root, -1);
716+ }
717+
718+ gtk_tree_store_set (
719+ budget,
720+ &child,
721+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
722+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_INCOME]),
723+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_INCOME,
724+ ADVBUD_ISTOTAL, TRUE,
725+ ADVBUD_JANUARY, total_income[0],
726+ ADVBUD_FEBRUARY, total_income[1],
727+ ADVBUD_MARCH, total_income[2],
728+ ADVBUD_APRIL, total_income[3],
729+ ADVBUD_MAY, total_income[4],
730+ ADVBUD_JUNE, total_income[5],
731+ ADVBUD_JULY, total_income[6],
732+ ADVBUD_AUGUST, total_income[7],
733+ ADVBUD_SEPTEMBER, total_income[8],
734+ ADVBUD_OCTOBER, total_income[9],
735+ ADVBUD_NOVEMBER, total_income[10],
736+ ADVBUD_DECEMBER, total_income[11],
737+ -1);
738+
739+ // Then look for Expenses
740+ root_search.row_category_type = ADVBUD_CAT_TYPE_EXPENSE;
741+
742+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
743+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
744+ &root_search);
745+
746+ if (root_search.iterator) {
747+ child = *root_search.iterator;
748+ }
749+ else {
750+ gtk_tree_store_insert(budget, &child, &total_root, -1);
751+ }
752+
753+ gtk_tree_store_set (
754+ budget,
755+ &child,
756+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
757+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_EXPENSE]),
758+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_EXPENSE,
759+ ADVBUD_ISTOTAL, TRUE,
760+ ADVBUD_JANUARY, total_expense[0],
761+ ADVBUD_FEBRUARY, total_expense[1],
762+ ADVBUD_MARCH, total_expense[2],
763+ ADVBUD_APRIL, total_expense[3],
764+ ADVBUD_MAY, total_expense[4],
765+ ADVBUD_JUNE, total_expense[5],
766+ ADVBUD_JULY, total_expense[6],
767+ ADVBUD_AUGUST, total_expense[7],
768+ ADVBUD_SEPTEMBER, total_expense[8],
769+ ADVBUD_OCTOBER, total_expense[9],
770+ ADVBUD_NOVEMBER, total_expense[10],
771+ ADVBUD_DECEMBER, total_expense[11],
772+ -1);
773+
774+ // Finally, set Balance total row
775+ root_search.row_category_type = ADVBUD_CAT_TYPE_NONE;
776+
777+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
778+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
779+ &root_search);
780+
781+ if (root_search.iterator) {
782+ child = *root_search.iterator;
783+ }
784+ else {
785+ gtk_tree_store_insert(budget, &child, &total_root, -1);
786+ }
787+
788+ gtk_tree_store_set (
789+ budget,
790+ &child,
791+ ADVBUD_CATEGORY_NAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_BALANCE]),
792+ ADVBUD_CATEGORY_FULLNAME, _(ADVBUD_VIEW_MODE[ADVBUD_VIEW_BALANCE]),
793+ ADVBUD_CATEGORY_TYPE, ADVBUD_CAT_TYPE_NONE,
794+ ADVBUD_ISTOTAL, TRUE,
795+ ADVBUD_JANUARY, total_income[0] + total_expense[0],
796+ ADVBUD_FEBRUARY, total_income[1] + total_expense[1],
797+ ADVBUD_MARCH, total_income[2] + total_expense[2],
798+ ADVBUD_APRIL, total_income[3] + total_expense[3],
799+ ADVBUD_MAY, total_income[4] + total_expense[4],
800+ ADVBUD_JUNE, total_income[5] + total_expense[5],
801+ ADVBUD_JULY, total_income[6] + total_expense[6],
802+ ADVBUD_AUGUST, total_income[7] + total_expense[7],
803+ ADVBUD_SEPTEMBER, total_income[8] + total_expense[8],
804+ ADVBUD_OCTOBER, total_income[9] + total_expense[9],
805+ ADVBUD_NOVEMBER, total_income[10] + total_expense[10],
806+ ADVBUD_DECEMBER, total_income[11] + total_expense[11],
807+ -1);
808+
809+ g_free(root_search.iterator);
810+
811+ return;
812+}
813+
814+// Filter shown rows according to VIEW mode
815+static gboolean ui_adv_bud_model_row_filter (GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
816+{
817+gboolean is_visible, is_root, is_total, is_separator, is_childheader;
818+adv_bud_data_t* data;
819+advbud_view_mode_t view_mode;
820+guint32 category_key;
821+advbud_cat_type_t category_type;
822+Category *bdg_category;
823+
824+ is_visible = TRUE;
825+ data = user_data;
826+ view_mode = radio_get_active(GTK_CONTAINER(data->RA_mode));
827+
828+ gtk_tree_model_get(model, iter,
829+ ADVBUD_ISROOT, &is_root,
830+ ADVBUD_ISTOTAL, &is_total,
831+ ADVBUD_ISSEPARATOR, &is_separator,
832+ ADVBUD_ISCHILDHEADER, &is_childheader,
833+ ADVBUD_CATEGORY_KEY, &category_key,
834+ ADVBUD_CATEGORY_TYPE, &category_type,
835+ -1);
836+
837+ // On specific mode, hidde categories of opposite type
838+ if (!is_total
839+ && category_type == ADVBUD_CAT_TYPE_INCOME
840+ && view_mode == ADVBUD_VIEW_EXPENSE)
841+ {
842+ is_visible = FALSE;
843+ }
844+
845+ if (!is_total
846+ && category_type == ADVBUD_CAT_TYPE_EXPENSE
847+ && view_mode == ADVBUD_VIEW_INCOME)
848+ {
849+ is_visible = FALSE;
850+ }
851+
852+ // Hidde fake first child root used for add dialog
853+ if (is_childheader
854+ || is_separator)
855+ {
856+ is_visible = FALSE;
857+ }
858+
859+ // On balance mode, hidde not forced empty categories
860+ if (!is_total
861+ && !is_root
862+ && !is_childheader
863+ && !is_separator
864+ && view_mode == ADVBUD_VIEW_BALANCE)
865+ {
866+ bdg_category = da_cat_get(category_key);
867+
868+ if (bdg_category != NULL)
869+ {
870+ // Either the category has some budget, or its display is forced
871+ is_visible = (bdg_category->flags & (GF_BUDGET|GF_FORCED));
872+
873+ // Force display if one of its children should be displayed
874+ if (!is_visible)
875+ {
876+ GtkTreeIter child;
877+ Category *subcat;
878+ guint32 subcat_key;
879+ gint child_id=0;
880+
881+ while (gtk_tree_model_iter_nth_child(model,
882+ &child,
883+ iter,
884+ child_id))
885+ {
886+ gtk_tree_model_get(model, &child,
887+ ADVBUD_CATEGORY_KEY, &subcat_key,
888+ -1);
889+
890+ if (subcat_key != 0)
891+ {
892+ subcat = da_cat_get (subcat_key);
893+
894+ if (subcat != NULL)
895+ {
896+ is_visible = (subcat->flags & (GF_BUDGET|GF_FORCED));
897+ }
898+ }
899+
900+ // Stop loop on first visible children
901+ if (is_visible)
902+ {
903+ break;
904+ }
905+
906+ ++child_id;
907+ }
908+ }
909+ }
910+ }
911+
912+ return is_visible;
913+}
914+
915+
916+// Filter rows to show only parent categories
917+static gboolean ui_adv_bud_model_row_filter_parents (GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
918+{
919+gboolean is_visible, is_root, is_total, is_separator, is_childheader;
920+Category *bdg_category;
921+guint32 category_key;
922+advbud_cat_type_t category_type;
923+
924+ is_visible = TRUE;
925+
926+ gtk_tree_model_get(model, iter,
927+ ADVBUD_CATEGORY_KEY, &category_key,
928+ ADVBUD_CATEGORY_TYPE, &category_type,
929+ ADVBUD_ISROOT, &is_root,
930+ ADVBUD_ISTOTAL, &is_total,
931+ ADVBUD_ISCHILDHEADER, &is_childheader,
932+ ADVBUD_ISSEPARATOR, &is_separator,
933+ -1);
934+
935+ // Hidde Total root
936+ if (is_root
937+ && category_type == ADVBUD_CAT_TYPE_NONE )
938+ {
939+ is_visible = FALSE;
940+ }
941+
942+ // Hidde Total rows
943+ if (is_total) {
944+ is_visible = FALSE;
945+ }
946+
947+ if (category_key > 0 && (is_separator || is_childheader))
948+ {
949+ is_visible = FALSE;
950+ }
951+ else if (category_key > 0)
952+ {
953+ // Show categories without parents
954+ bdg_category = da_cat_get(category_key);
955+
956+ if (bdg_category != NULL)
957+ {
958+ if (bdg_category->parent > 0)
959+ {
960+ is_visible = FALSE;
961+ }
962+ }
963+ }
964+
965+ return is_visible;
966+}
967+
968+// Filter rows to show only mergeable categories
969+static gboolean ui_adv_bud_model_row_filter_with_headers (GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
970+{
971+gboolean is_visible, is_root, is_total, is_separator, is_childheader;
972+guint32 category_key;
973+advbud_cat_type_t category_type;
974+
975+ is_visible = TRUE;
976+
977+ gtk_tree_model_get(model, iter,
978+ ADVBUD_CATEGORY_KEY, &category_key,
979+ ADVBUD_CATEGORY_TYPE, &category_type,
980+ ADVBUD_ISROOT, &is_root,
981+ ADVBUD_ISTOTAL, &is_total,
982+ ADVBUD_ISCHILDHEADER, &is_childheader,
983+ ADVBUD_ISSEPARATOR, &is_separator,
984+ -1);
985+
986+ // Hidde Total root
987+ if (is_root
988+ && category_type == ADVBUD_CAT_TYPE_NONE )
989+ {
990+ is_visible = FALSE;
991+ }
992+
993+ // Hidde Total rows
994+ if (is_total) {
995+ is_visible = FALSE;
996+ }
997+
998+ if ((is_separator || is_childheader))
999+ {
1000+ GtkTreeIter parent;
1001+ gtk_tree_model_iter_parent(model, &parent, iter);
1002+
1003+ // Show child header and separator if parent has more than 2 children
1004+ is_visible = (gtk_tree_model_iter_n_children(model, &parent) > 2);
1005+ }
1006+
1007+ return is_visible;
1008+}
1009+
1010+static gint ui_adv_bud_model_row_sort (GtkTreeModel *model, GtkTreeIter *cat_a, GtkTreeIter *cat_b, gpointer user_data)
1011+{
1012+const gchar* cat_a_name;
1013+const gchar* cat_b_name;
1014+advbud_cat_type_t cat_a_type, cat_b_type;
1015+guint32 cat_a_key, cat_b_key;
1016+gboolean cat_a_is_childheader, cat_a_is_separator, cat_b_is_childheader, cat_b_is_separator;
1017+gint order = 0;
1018+
1019+ gtk_tree_model_get(model, cat_a,
1020+ ADVBUD_CATEGORY_NAME, &cat_a_name,
1021+ ADVBUD_CATEGORY_TYPE, &cat_a_type,
1022+ ADVBUD_CATEGORY_KEY, &cat_a_key,
1023+ ADVBUD_ISCHILDHEADER, &cat_a_is_childheader,
1024+ ADVBUD_ISSEPARATOR, &cat_a_is_separator,
1025+ -1);
1026+
1027+ gtk_tree_model_get(model, cat_b,
1028+ ADVBUD_CATEGORY_NAME, &cat_b_name,
1029+ ADVBUD_CATEGORY_TYPE, &cat_b_type,
1030+ ADVBUD_CATEGORY_KEY, &cat_b_key,
1031+ ADVBUD_ISCHILDHEADER, &cat_b_is_childheader,
1032+ ADVBUD_ISSEPARATOR, &cat_b_is_separator,
1033+ -1);
1034+
1035+ // Sort first by category type
1036+ if (cat_a_type != cat_b_type)
1037+ {
1038+ switch (cat_a_type)
1039+ {
1040+ case ADVBUD_CAT_TYPE_INCOME:
1041+ order = -1;
1042+ break;
1043+ case ADVBUD_CAT_TYPE_EXPENSE:
1044+ order = 0;
1045+ break;
1046+ case ADVBUD_CAT_TYPE_NONE:
1047+ order = 1;
1048+ break;
1049+ }
1050+ }
1051+ else
1052+ {
1053+ // On standard categories, just order by name
1054+ if (!cat_a_is_childheader && !cat_a_is_separator
1055+ && !cat_b_is_childheader && !cat_b_is_separator)
1056+ {
1057+ order = g_utf8_collate(g_utf8_casefold(cat_a_name, -1),
1058+ g_utf8_casefold(cat_b_name, -1)
1059+ );
1060+ }
1061+ // Otherwise, fake categories have to be first (header and separator)
1062+ else if (cat_a_is_childheader || cat_a_is_separator)
1063+ {
1064+ if (!cat_b_is_separator && !cat_b_is_childheader)
1065+ {
1066+ order = -1;
1067+ }
1068+ // When both are fake, header has to be first
1069+ else
1070+ {
1071+ order = (cat_a_is_childheader ? -1 : 1);
1072+ }
1073+ }
1074+ else
1075+ {
1076+ // Same idea for fake categories when cat_b is fake, but
1077+ // with reversed result, because sort function return
1078+ // result according to cat_a
1079+
1080+ if (!cat_a_is_separator && !cat_a_is_childheader)
1081+ {
1082+ order = 1;
1083+ }
1084+ else
1085+ {
1086+ order = (cat_b_is_childheader ? 1 : -1);
1087+ }
1088+ }
1089+ }
1090+
1091+ return order;
1092+}
1093+
1094+// the budget model creation
1095+static GtkTreeModel * ui_adv_bud_model_new ()
1096+{
1097+GtkTreeStore *budget;
1098+GtkTreeIter *iter_income, *iter_expense;
1099+guint32 n_category;
1100+advbud_search_criteria_t root_search = advbud_search_criteria_default;
1101+
1102+ // Create Tree Store
1103+ budget = gtk_tree_store_new ( ADVBUD_NUMCOLS,
1104+ G_TYPE_UINT, // ADVBUD_CATEGORY_KEY
1105+ G_TYPE_STRING, // ADVBUD_CATEGORY_NAME
1106+ G_TYPE_STRING, // ADVBUD_CATEGORY_FULLNAME
1107+ G_TYPE_INT, // ADVBUD_CATEGORY_TYPE
1108+ G_TYPE_BOOLEAN, // ADVBUD_ISROOT
1109+ G_TYPE_BOOLEAN, // ADVBUD_ISTOTAL
1110+ G_TYPE_BOOLEAN, // ADVBUD_ISCHILDHEADER
1111+ G_TYPE_BOOLEAN, // ADVBUD_ISSEPARATOR
1112+ G_TYPE_BOOLEAN, // ADVBUD_ISDISPLAYFORCED
1113+ G_TYPE_BOOLEAN, // ADVBUD_ISSAMEAMOUNT
1114+ G_TYPE_DOUBLE, // ADVBUD_SAMEAMOUNT
1115+ G_TYPE_DOUBLE, // ADVBUD_JANUARY
1116+ G_TYPE_DOUBLE, // ADVBUD_FEBRUARY
1117+ G_TYPE_DOUBLE, // ADVBUD_MARCH
1118+ G_TYPE_DOUBLE, // ADVBUD_APRIL
1119+ G_TYPE_DOUBLE, // ADVBUD_MAY
1120+ G_TYPE_DOUBLE, // ADVBUD_JUNE
1121+ G_TYPE_DOUBLE, // ADVBUD_JULY
1122+ G_TYPE_DOUBLE, // ADVBUD_AUGUST
1123+ G_TYPE_DOUBLE, // ADVBUD_SEPTEMBER
1124+ G_TYPE_DOUBLE, // ADVBUD_OCTOBER
1125+ G_TYPE_DOUBLE, // ADVBUD_NOVEMBER
1126+ G_TYPE_DOUBLE // ADVBUD_DECEMBER
1127+ );
1128+
1129+ // Populate the store
1130+
1131+ /* Create tree roots */
1132+ ui_adv_bud_model_insert_roots (budget);
1133+
1134+ // Retrieve required root
1135+ root_search.row_isroot = TRUE;
1136+ root_search.row_istotal = FALSE;
1137+
1138+ root_search.row_category_type = ADVBUD_CAT_TYPE_INCOME;
1139+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
1140+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
1141+ &root_search);
1142+ iter_income = root_search.iterator;
1143+
1144+ root_search.row_category_type = ADVBUD_CAT_TYPE_EXPENSE;
1145+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
1146+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
1147+ &root_search);
1148+ iter_expense = root_search.iterator;
1149+
1150+ /* Create rows for real categories */
1151+ n_category = da_cat_get_max_key();
1152+
1153+ for(guint32 i=1; i<=n_category; ++i)
1154+ {
1155+ Category *bdg_category;
1156+ gboolean cat_is_income;
1157+
1158+ bdg_category = da_cat_get(i);
1159+
1160+ if (bdg_category == NULL)
1161+ {
1162+ continue;
1163+ }
1164+
1165+ cat_is_income = (category_type_get (bdg_category) == 1);
1166+
1167+ DB(g_print(" category %d:'%s' isincome=%d, issub=%d hasbudget=%d parent=%d\n",
1168+ bdg_category->key, bdg_category->name,
1169+ cat_is_income, (bdg_category->flags & GF_SUB),
1170+ (bdg_category->flags & GF_BUDGET), bdg_category->parent));
1171+
1172+ // Compute totals and initiate category in right tree root
1173+ if (cat_is_income)
1174+ {
1175+ ui_adv_bud_model_add_category_with_lineage(budget, iter_income, &(bdg_category->key));
1176+ }
1177+ else if (!cat_is_income)
1178+ {
1179+ ui_adv_bud_model_add_category_with_lineage(budget, iter_expense, &(bdg_category->key));
1180+ }
1181+ }
1182+
1183+ /* Create rows for total root */
1184+ ui_adv_bud_model_update_monthlytotal(GTK_TREE_STORE(budget));
1185+
1186+ /* Sort categories on same node level */
1187+ gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE(budget),
1188+ ADVBUD_CATEGORY_NAME, ui_adv_bud_model_row_sort,
1189+ NULL, NULL);
1190+ gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (budget),
1191+ ADVBUD_CATEGORY_NAME, GTK_SORT_ASCENDING);
1192+
1193+ g_free(root_search.iterator);
1194+
1195+ return GTK_TREE_MODEL(budget);
1196+}
1197+
1198+/**
1199+ * GtkTreeView functions
1200+ **/
1201+
1202+// to enable or not edition on month columns
1203+static void ui_adv_bud_view_display_amount (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
1204+{
1205+GtkAdjustment *adjustment;
1206+gboolean is_sameamount, is_root, is_total, is_visible, is_sensitive, is_editable;
1207+advbud_cat_type_t row_category_type;
1208+gdouble amount = 0.0;
1209+gchar *text;
1210+gchar *fgcolor;
1211+const advbud_store_t column_id = GPOINTER_TO_INT(user_data);
1212+
1213+ gtk_tree_model_get(model, iter,
1214+ ADVBUD_CATEGORY_TYPE, &row_category_type,
1215+ ADVBUD_ISROOT, &is_root,
1216+ ADVBUD_ISSAMEAMOUNT, &is_sameamount,
1217+ ADVBUD_ISTOTAL, &is_total,
1218+ -1);
1219+
1220+ // Text to display
1221+ if (is_sameamount)
1222+ {
1223+ gtk_tree_model_get(model, iter, ADVBUD_SAMEAMOUNT, &amount, -1);
1224+ }
1225+ else if (column_id >= ADVBUD_JANUARY && column_id <= ADVBUD_DECEMBER)
1226+ {
1227+ gtk_tree_model_get(model, iter, column_id, &amount, -1);
1228+ }
1229+
1230+ text = g_strdup_printf("%.2f", amount);
1231+ fgcolor = get_minimum_color_amount(amount, 0.0);
1232+
1233+ // Default styling values
1234+ is_visible = TRUE;
1235+ is_editable = FALSE;
1236+ is_sensitive = TRUE;
1237+
1238+ if (is_root)
1239+ {
1240+ is_visible = FALSE;
1241+ is_editable = FALSE;
1242+ is_sensitive = FALSE;
1243+ }
1244+ else if (is_total)
1245+ {
1246+ is_visible = TRUE;
1247+ is_editable = FALSE;
1248+ is_sensitive = FALSE;
1249+
1250+ if (column_id == ADVBUD_SAMEAMOUNT)
1251+ {
1252+ is_visible = FALSE;
1253+ }
1254+ }
1255+ else if (is_sameamount)
1256+ {
1257+ is_visible = TRUE;
1258+ is_editable = FALSE;
1259+ is_sensitive = FALSE;
1260+
1261+ if (column_id == ADVBUD_SAMEAMOUNT)
1262+ {
1263+ is_sensitive = TRUE;
1264+ is_editable = TRUE;
1265+ }
1266+ }
1267+ else if (! is_sameamount)
1268+ {
1269+ is_visible = TRUE;
1270+ is_editable = TRUE;
1271+ is_sensitive = TRUE;
1272+
1273+ if (column_id == ADVBUD_SAMEAMOUNT)
1274+ {
1275+ is_sensitive = FALSE;
1276+ is_editable = FALSE;
1277+ }
1278+ }
1279+
1280+ adjustment = gtk_adjustment_new(
1281+ 0.0, // initial-value
1282+ -G_MAXDOUBLE, // minmal-value
1283+ G_MAXDOUBLE, // maximal-value
1284+ 0.5, // step increment
1285+ 10, // page increment
1286+ 0); // page size (0 because irrelevant for GtkSpinButton)
1287+
1288+ g_object_set(renderer,
1289+ "text", text,
1290+ "visible", is_visible,
1291+ "editable", is_editable,
1292+ "sensitive", is_sensitive,
1293+ "foreground", fgcolor,
1294+ "xalign", 1.0,
1295+ "adjustment", adjustment,
1296+ "digits", 2,
1297+ NULL);
1298+
1299+ g_free(text);
1300+}
1301+
1302+// to enable or not edition on month columns
1303+static void ui_adv_bud_view_display_issameamount (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
1304+{
1305+gboolean is_sameamount, is_root, is_total, is_visible, is_sensitive;
1306+
1307+ gtk_tree_model_get(model, iter,
1308+ ADVBUD_ISROOT, &is_root,
1309+ ADVBUD_ISSAMEAMOUNT, &is_sameamount,
1310+ ADVBUD_ISTOTAL, &is_total,
1311+ -1);
1312+
1313+ // Default values
1314+ is_visible = TRUE;
1315+ is_sensitive = TRUE;
1316+
1317+ if (is_root || is_total)
1318+ {
1319+ is_visible = FALSE;
1320+ is_sensitive = FALSE;
1321+ }
1322+
1323+ g_object_set(renderer,
1324+ "activatable", TRUE,
1325+ "active", is_sameamount,
1326+ "visible", is_visible,
1327+ "sensitive", is_sensitive,
1328+ NULL);
1329+}
1330+
1331+// Toggle force display
1332+static void ui_adv_bud_view_display_isdisplayforced (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
1333+{
1334+gboolean is_displayforced, is_root, is_total, is_visible, is_sensitive;
1335+
1336+ gtk_tree_model_get(model, iter,
1337+ ADVBUD_ISDISPLAYFORCED, &is_displayforced,
1338+ ADVBUD_ISROOT, &is_root,
1339+ ADVBUD_ISTOTAL, &is_total,
1340+ -1);
1341+
1342+ // Default values
1343+ is_visible = TRUE;
1344+ is_sensitive = TRUE;
1345+
1346+ if (is_root || is_total)
1347+ {
1348+ is_visible = FALSE;
1349+ is_sensitive = FALSE;
1350+ }
1351+
1352+ g_object_set(renderer,
1353+ "activatable", TRUE,
1354+ "active", is_displayforced,
1355+ "visible", is_visible,
1356+ "sensitive", is_sensitive,
1357+ NULL);
1358+}
1359+
1360+// Compute dynamically the yearly total
1361+static void ui_adv_bud_view_display_annualtotal (GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
1362+{
1363+gboolean is_sameamount = FALSE, is_total = FALSE, is_root = FALSE;
1364+gdouble amount = 0.0;
1365+gdouble total = 0.0;
1366+gchar *text;
1367+gchar *fgcolor;
1368+gboolean is_visible = TRUE;
1369+
1370+ gtk_tree_model_get(model, iter,
1371+ ADVBUD_ISROOT, &is_root,
1372+ ADVBUD_ISSAMEAMOUNT, &is_sameamount,
1373+ ADVBUD_SAMEAMOUNT, &amount,
1374+ ADVBUD_ISTOTAL, &is_total,
1375+ -1);
1376+
1377+ if (is_sameamount)
1378+ {
1379+ total = 12.0 * amount;
1380+ }
1381+ else
1382+ {
1383+ for (int i = ADVBUD_JANUARY ; i <= ADVBUD_DECEMBER ; ++i)
1384+ {
1385+ gtk_tree_model_get(model, iter, i, &amount, -1);
1386+ total += amount;
1387+ }
1388+ }
1389+
1390+ text = g_strdup_printf("%.2f", total);
1391+ fgcolor = get_minimum_color_amount(total, 0.0);
1392+
1393+ if (is_root)
1394+ {
1395+ is_visible = FALSE;
1396+ }
1397+
1398+ g_object_set(renderer,
1399+ "text", text,
1400+ "foreground", fgcolor,
1401+ "visible", is_visible,
1402+ "sensitive", FALSE,
1403+ "xalign", 1.0,
1404+ NULL);
1405+
1406+ g_free(text);
1407+}
1408+
1409+// When view mode is toggled:
1410+// - recreate the view to update columns rendering
1411+static void ui_adv_bud_view_toggle (gpointer user_data, advbud_view_mode_t view_mode)
1412+{
1413+adv_bud_data_t *data = user_data;
1414+GtkTreeModel *budget;
1415+GtkWidget *view;
1416+
1417+ view = data->TV_budget;
1418+
1419+ budget = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
1420+
1421+ if (view_mode == ADVBUD_VIEW_BALANCE)
1422+ {
1423+ gtk_tree_view_column_set_visible(data->TVC_category, TRUE);
1424+ gtk_tree_view_column_set_visible(data->TVC_category_with_force, FALSE);
1425+ }
1426+ else
1427+ {
1428+ gtk_tree_view_column_set_visible(data->TVC_category, FALSE);
1429+ gtk_tree_view_column_set_visible(data->TVC_category_with_force, TRUE);
1430+ }
1431+
1432+ gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(budget));
1433+
1434+ if (data->TV_isexpanded) {
1435+ gtk_tree_view_expand_all(GTK_TREE_VIEW(view));
1436+ }
1437+ else {
1438+ ui_adv_bud_model_collapse(GTK_TREE_VIEW(view));
1439+ }
1440+
1441+ DB(g_print("[ui_adv_bud] : button state changed to: %d\n", view_mode));
1442+
1443+ return;
1444+}
1445+
1446+static gboolean ui_adv_bud_view_search (GtkTreeModel *filter, gint column, const gchar *key, GtkTreeIter *filter_iter, gpointer data)
1447+{
1448+gboolean is_matching = FALSE, is_root, is_total;
1449+GtkTreeModel *budget;
1450+GtkTreeIter iter;
1451+gchar *category_name;
1452+
1453+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1454+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1455+ &iter,
1456+ filter_iter);
1457+
1458+ gtk_tree_model_get(budget, &iter,
1459+ ADVBUD_CATEGORY_NAME, &category_name,
1460+ ADVBUD_ISROOT, &is_root,
1461+ ADVBUD_ISTOTAL, &is_total,
1462+ -1);
1463+
1464+ if (!is_root && !is_total
1465+ && g_strstr_len(g_utf8_casefold(category_name, -1), -1,
1466+ g_utf8_casefold(key, -1)))
1467+ {
1468+ is_matching = TRUE;
1469+ }
1470+
1471+ // GtkTreeViewSearchEqualFunc has to return FALSE only if iter matches.
1472+ return !is_matching;
1473+}
1474+
1475+static gboolean ui_adv_view_separator (GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
1476+{
1477+gboolean is_separator;
1478+
1479+ gtk_tree_model_get(model, iter,
1480+ ADVBUD_ISSEPARATOR, &is_separator,
1481+ -1);
1482+
1483+ return is_separator;
1484+}
1485+
1486+// the budget view creation which run the model creation tool
1487+
1488+static GtkWidget *ui_adv_bud_view_new (gpointer user_data)
1489+{
1490+GtkTreeViewColumn *col;
1491+GtkCellRenderer *renderer, *cat_name_renderer;
1492+GtkWidget *view;
1493+adv_bud_data_t *data = user_data;
1494+
1495+ view = gtk_tree_view_new();
1496+
1497+ /* --- Category column --- */
1498+ col = gtk_tree_view_column_new();
1499+ data->TVC_category = col;
1500+
1501+ gtk_tree_view_column_set_title(col, _("Category"));
1502+ gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1503+
1504+ // Category Name
1505+ cat_name_renderer = gtk_cell_renderer_text_new();
1506+ gtk_tree_view_column_pack_start (col, cat_name_renderer, TRUE);
1507+ gtk_tree_view_column_add_attribute(col, cat_name_renderer, "text", ADVBUD_CATEGORY_NAME);
1508+
1509+ g_object_set(cat_name_renderer, "editable", TRUE, NULL);
1510+ g_signal_connect(cat_name_renderer, "edited", G_CALLBACK(ui_adv_bud_cell_update_category), (gpointer) data);
1511+
1512+ /* --- Category column with is forced display check --- */
1513+ col = gtk_tree_view_column_new();
1514+ data->TVC_category_with_force = col;
1515+
1516+ gtk_tree_view_column_set_title(col, _("Category (check to force display)"));
1517+ gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1518+
1519+ // Is display forced ?
1520+ renderer = gtk_cell_renderer_toggle_new();
1521+ gtk_tree_view_column_pack_start (col, renderer, TRUE);
1522+ gtk_tree_view_column_set_cell_data_func(col, renderer, ui_adv_bud_view_display_isdisplayforced, NULL, NULL);
1523+
1524+ g_signal_connect (renderer, "toggled", G_CALLBACK(ui_adv_bud_cell_update_isdisplayforced), (gpointer) data);
1525+
1526+ // Category Name
1527+ gtk_tree_view_column_pack_start (col, cat_name_renderer, TRUE);
1528+ gtk_tree_view_column_add_attribute(col, cat_name_renderer, "text", ADVBUD_CATEGORY_NAME);
1529+
1530+ /* --- Monthly column --- */
1531+ col = gtk_tree_view_column_new();
1532+ gtk_tree_view_column_set_title(col, _("Monthly"));
1533+ gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1534+
1535+ // Monthly toggler
1536+ renderer = gtk_cell_renderer_toggle_new();
1537+ gtk_tree_view_column_pack_start(col, renderer, TRUE);
1538+ gtk_tree_view_column_set_cell_data_func(col, renderer, ui_adv_bud_view_display_issameamount, NULL, NULL);
1539+
1540+ g_signal_connect (renderer, "toggled", G_CALLBACK(ui_adv_bud_cell_update_issameamount), (gpointer) data);
1541+
1542+ // Monthly amount
1543+ renderer = gtk_cell_renderer_spin_new();
1544+ gtk_tree_view_column_pack_start(col, renderer, TRUE);
1545+ gtk_tree_view_column_set_cell_data_func(col, renderer, ui_adv_bud_view_display_amount, GINT_TO_POINTER(ADVBUD_SAMEAMOUNT), NULL);
1546+
1547+ g_object_set_data(G_OBJECT(renderer), "ui_adv_bud_column_id", GINT_TO_POINTER(ADVBUD_SAMEAMOUNT));
1548+ g_signal_connect(renderer, "edited", G_CALLBACK(ui_adv_bud_cell_update_amount), (gpointer) data);
1549+
1550+ /* --- Each month amount --- */
1551+ for (int i = ADVBUD_JANUARY ; i <= ADVBUD_DECEMBER ; ++i)
1552+ {
1553+ int month = i - ADVBUD_JANUARY ;
1554+ col = gtk_tree_view_column_new();
1555+
1556+ gtk_tree_view_column_set_title(col, _(ADVBUD_MONTHS[month]));
1557+ gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1558+ renderer = gtk_cell_renderer_spin_new();
1559+
1560+ gtk_tree_view_column_pack_start(col, renderer, TRUE);
1561+ gtk_tree_view_column_set_cell_data_func(col, renderer, ui_adv_bud_view_display_amount, GINT_TO_POINTER(i), NULL);
1562+
1563+ g_object_set_data(G_OBJECT(renderer), "ui_adv_bud_column_id", GINT_TO_POINTER(i));
1564+ g_signal_connect(renderer, "edited", G_CALLBACK(ui_adv_bud_cell_update_amount), (gpointer) data);
1565+ }
1566+
1567+ /* --- Year Total -- */
1568+ col = gtk_tree_view_column_new();
1569+ gtk_tree_view_column_set_title(col, _("Total"));
1570+
1571+ gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);
1572+ renderer = gtk_cell_renderer_text_new();
1573+ gtk_tree_view_column_pack_start(col, renderer, TRUE);
1574+
1575+ gtk_tree_view_column_set_cell_data_func(col, renderer, ui_adv_bud_view_display_annualtotal, NULL, NULL);
1576+
1577+ gtk_tree_selection_set_mode(gtk_tree_view_get_selection(GTK_TREE_VIEW(view)), GTK_SELECTION_SINGLE);
1578+
1579+ gtk_tree_view_set_enable_search(GTK_TREE_VIEW(view), TRUE);
1580+ gtk_tree_view_set_search_column(GTK_TREE_VIEW(view), ADVBUD_CATEGORY_NAME);
1581+ gtk_tree_view_set_search_equal_func(GTK_TREE_VIEW(view), (GtkTreeViewSearchEqualFunc) ui_adv_bud_view_search, NULL, NULL);
1582+
1583+ g_object_set(view,
1584+ "enable-grid-lines", PREFS->grid_lines,
1585+ "enable-tree-lines", TRUE,
1586+ NULL);
1587+
1588+ return view;
1589+}
1590+
1591+/*
1592+ * UI actions
1593+ **/
1594+
1595+// Update homebank category on user change
1596+static void ui_adv_bud_cell_update_category(GtkCellRendererText *renderer, gchar *filter_path, gchar *new_text, gpointer user_data)
1597+{
1598+adv_bud_data_t *data = user_data;
1599+GtkWidget *view;
1600+GtkTreeIter filter_iter, iter;
1601+GtkTreeModel *filter, *budget;
1602+Category* category;
1603+guint32 category_key;
1604+gboolean is_root, is_total;
1605+
1606+ DB(g_print("\n[ui_adv_bud] category name updated with new name '%s'\n", new_text));
1607+
1608+ view = data->TV_budget;
1609+
1610+ // Read filter data
1611+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
1612+ gtk_tree_model_get_iter_from_string (filter, &filter_iter, filter_path);
1613+
1614+ // Convert data to budget model
1615+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1616+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1617+ &iter,
1618+ &filter_iter);
1619+
1620+ gtk_tree_model_get (budget, &iter,
1621+ ADVBUD_CATEGORY_KEY, &category_key,
1622+ ADVBUD_ISROOT, &is_root,
1623+ ADVBUD_ISTOTAL, &is_total,
1624+ -1);
1625+
1626+ category = da_cat_get (category_key);
1627+
1628+ if (! category || is_root || is_total)
1629+ {
1630+ return;
1631+ }
1632+
1633+ // Update category name
1634+ category_rename(category, new_text);
1635+
1636+ // Notify of changes
1637+ data->change++;
1638+
1639+ // Update budget model
1640+
1641+ // Current row
1642+ gtk_tree_store_set(
1643+ GTK_TREE_STORE(budget),
1644+ &iter,
1645+ ADVBUD_CATEGORY_NAME, category->name,
1646+ ADVBUD_CATEGORY_FULLNAME, category->fullname,
1647+ -1);
1648+
1649+ return;
1650+}
1651+
1652+// Update amount in budget model and homebank category on user change
1653+static void ui_adv_bud_cell_update_amount(GtkCellRendererText *renderer, gchar *filter_path, gchar *new_text, gpointer user_data)
1654+{
1655+const advbud_store_t column_id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(renderer), "ui_adv_bud_column_id"));
1656+adv_bud_data_t *data = user_data;
1657+GtkWidget *view;
1658+GtkTreeIter filter_iter, iter;
1659+GtkTreeModel *filter, *budget;
1660+Category* category;
1661+gdouble amount;
1662+guint32 category_key;
1663+
1664+ DB(g_print("\n[ui_adv_bud] amount updated:\n"));
1665+
1666+ view = data->TV_budget;
1667+
1668+ // Read filter data
1669+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
1670+ gtk_tree_model_get_iter_from_string (filter, &filter_iter, filter_path);
1671+
1672+ // Convert data to budget model
1673+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1674+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1675+ &iter,
1676+ &filter_iter);
1677+
1678+ gtk_tree_model_get (budget, &iter,
1679+ ADVBUD_CATEGORY_KEY, &category_key,
1680+ -1);
1681+
1682+ category = da_cat_get (category_key);
1683+
1684+ if (! category)
1685+ {
1686+ return;
1687+ }
1688+
1689+ amount = g_strtod(new_text, NULL);
1690+
1691+ DB(g_print("\tcolumn: %d (month: %d), category key: %d, amount %.2f\n", column_id, column_id - ADVBUD_JANUARY + 1, category_key, amount));
1692+
1693+ // Update Category
1694+ category->budget[column_id - ADVBUD_JANUARY + 1] = amount;
1695+
1696+ // Reset Budget Flag
1697+ category->flags &= ~(GF_BUDGET);
1698+ for(gint budget_id = 0; budget_id <=12; ++budget_id)
1699+ {
1700+ if( category->budget[budget_id] != 0.0)
1701+ {
1702+ category->flags |= GF_BUDGET;
1703+ break;
1704+ }
1705+ }
1706+
1707+ // Notify of changes
1708+ data->change++;
1709+
1710+ // Update budget model
1711+
1712+ // Current row
1713+ gtk_tree_store_set(
1714+ GTK_TREE_STORE(budget),
1715+ &iter,
1716+ column_id, amount,
1717+ -1);
1718+
1719+ // Refresh total rows
1720+ ui_adv_bud_model_update_monthlytotal (GTK_TREE_STORE(budget));
1721+
1722+ return;
1723+}
1724+
1725+// Update the row to (dis/enable) same amount for this category
1726+static void ui_adv_bud_cell_update_issameamount(GtkCellRendererText *renderer, gchar *filter_path, gpointer user_data)
1727+{
1728+adv_bud_data_t *data = user_data;
1729+GtkWidget *view;
1730+GtkTreeIter filter_iter, iter;
1731+GtkTreeModel *filter, *budget;
1732+Category* category;
1733+gboolean issame;
1734+guint32 category_key;
1735+
1736+ DB(g_print("\n[ui_adv_bud] Is same amount updated:\n"));
1737+
1738+ view = data->TV_budget;
1739+
1740+ // Read filter data
1741+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
1742+ gtk_tree_model_get_iter_from_string (filter, &filter_iter, filter_path);
1743+
1744+ // Convert data to budget model
1745+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1746+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1747+ &iter,
1748+ &filter_iter);
1749+
1750+ gtk_tree_model_get (budget, &iter,
1751+ ADVBUD_CATEGORY_KEY, &category_key,
1752+ ADVBUD_ISSAMEAMOUNT, &issame,
1753+ -1);
1754+
1755+ category = da_cat_get (category_key);
1756+
1757+ if (! category)
1758+ {
1759+ return;
1760+ }
1761+
1762+ // Value has been toggled !
1763+ issame = !(issame);
1764+
1765+ DB(g_print("\tcategory key: %d, issame: %d (before: %d)\n", category_key, issame, !(issame)));
1766+
1767+ // Update Category
1768+
1769+ // Reset Forced Flag
1770+ category->flags &= ~(GF_CUSTOM);
1771+
1772+ if (issame == FALSE)
1773+ {
1774+ category->flags |= (GF_CUSTOM);
1775+ }
1776+
1777+ // Notify of changes
1778+ data->change++;
1779+
1780+ // Update budget model
1781+
1782+ // Current row
1783+ gtk_tree_store_set(
1784+ GTK_TREE_STORE(budget),
1785+ &iter,
1786+ ADVBUD_ISSAMEAMOUNT, issame,
1787+ -1);
1788+
1789+ // Refresh total rows
1790+ ui_adv_bud_model_update_monthlytotal (GTK_TREE_STORE(budget));
1791+
1792+ return;
1793+}
1794+
1795+// Update the row to (dis/enable) forced display for this category
1796+static void ui_adv_bud_cell_update_isdisplayforced(GtkCellRendererText *renderer, gchar *filter_path, gpointer user_data)
1797+{
1798+adv_bud_data_t *data = user_data;
1799+GtkWidget *view;
1800+GtkTreeIter filter_iter, iter;
1801+GtkTreeModel *filter, *budget;
1802+Category* category;
1803+gboolean isdisplayforced;
1804+guint32 category_key;
1805+
1806+ DB(g_print("\n[ui_adv_bud] Is display forced updated:\n"));
1807+
1808+ view = data->TV_budget;
1809+
1810+ // Read filter data
1811+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
1812+ gtk_tree_model_get_iter_from_string (filter, &filter_iter, filter_path);
1813+
1814+ // Convert data to budget model
1815+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1816+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1817+ &iter,
1818+ &filter_iter);
1819+
1820+ gtk_tree_model_get (budget, &iter,
1821+ ADVBUD_CATEGORY_KEY, &category_key,
1822+ ADVBUD_ISDISPLAYFORCED, &isdisplayforced,
1823+ -1);
1824+
1825+ category = da_cat_get (category_key);
1826+
1827+ if (! category)
1828+ {
1829+ return;
1830+ }
1831+
1832+ // Value has been toggled !
1833+ isdisplayforced = !(isdisplayforced);
1834+
1835+ DB(g_print("\tcategory key: %d, isdisplayforced: %d (before: %d)\n", category_key, isdisplayforced, !(isdisplayforced)));
1836+
1837+ // Update Category
1838+
1839+ // Reset Forced Flag
1840+ category->flags &= ~(GF_FORCED);
1841+
1842+ if (isdisplayforced == TRUE)
1843+ {
1844+ category->flags |= (GF_FORCED);
1845+ }
1846+
1847+ // Notify of changes
1848+ data->change++;
1849+
1850+ // Update budget model
1851+
1852+ // Current row
1853+ gtk_tree_store_set(
1854+ GTK_TREE_STORE(budget),
1855+ &iter,
1856+ ADVBUD_ISDISPLAYFORCED, isdisplayforced,
1857+ -1);
1858+
1859+ // Refresh total rows
1860+ ui_adv_bud_model_update_monthlytotal (GTK_TREE_STORE(budget));
1861+
1862+ return;
1863+}
1864+
1865+// Update budget view and model according to the new view mode selected
1866+static void ui_adv_bud_view_update_mode (GtkToggleButton *button, gpointer user_data)
1867+{
1868+adv_bud_data_t *data = user_data;
1869+advbud_view_mode_t view_mode = ADVBUD_VIEW_BALANCE;
1870+
1871+ // Only run once the view update, so only run on the activated button signal
1872+ if(!gtk_toggle_button_get_active(button))
1873+ {
1874+ return;
1875+ }
1876+
1877+ // Mode is directly set by radio button, because the ADVBUD_VIEW_MODE and enum
1878+ // for view mode are constructed to correspond (manually)
1879+ view_mode = radio_get_active(GTK_CONTAINER(data->RA_mode));
1880+
1881+ DB(g_print("\n[ui_adv_bud] view mode toggled to: %d\n", view_mode));
1882+
1883+ ui_adv_bud_view_toggle((gpointer) data, view_mode);
1884+
1885+ return;
1886+}
1887+
1888+// Expand all categories inside the current view
1889+static void ui_adv_bud_view_expand (GtkButton *button, gpointer user_data)
1890+{
1891+adv_bud_data_t *data = user_data;
1892+GtkWidget *view;
1893+
1894+ view = data->TV_budget;
1895+
1896+ data->TV_isexpanded = TRUE;
1897+
1898+ gtk_tree_view_expand_all(GTK_TREE_VIEW(view));
1899+
1900+ return;
1901+}
1902+
1903+// Collapse all categories inside the current view
1904+static void ui_adv_bud_view_collapse (GtkButton *button, gpointer user_data)
1905+{
1906+adv_bud_data_t *data = user_data;
1907+GtkWidget *view;
1908+
1909+ view = data->TV_budget;
1910+
1911+ data->TV_isexpanded = FALSE;
1912+
1913+ ui_adv_bud_model_collapse (GTK_TREE_VIEW(view));
1914+
1915+ return;
1916+}
1917+
1918+// Check if add category dialog is full filled
1919+static void ui_adv_bud_category_add_full_filled (GtkWidget *source, gpointer user_data)
1920+{
1921+adv_bud_data_t *data = user_data;
1922+const gchar* new_raw_name;
1923+gchar* new_name;
1924+gboolean is_name_filled = FALSE, is_parent_choosen = FALSE;
1925+
1926+ // Check a name for the new category is given:
1927+ new_raw_name = gtk_entry_get_text(GTK_ENTRY(data->EN_add_name));
1928+
1929+ if (new_raw_name && *new_raw_name)
1930+ {
1931+ new_name = g_strdup(new_raw_name);
1932+ g_strstrip(new_name);
1933+
1934+ if (strlen(new_name) > 0)
1935+ {
1936+ is_name_filled = TRUE;
1937+ }
1938+
1939+ g_free(new_name);
1940+ }
1941+
1942+ // Check an entry has been selected in parent combobox
1943+ is_parent_choosen = (gtk_combo_box_get_active(GTK_COMBO_BOX(data->COMBO_add_parent)) > -1);
1944+
1945+ // Dis/Enable apply dialog button
1946+ gtk_widget_set_sensitive(data->BT_apply, is_name_filled && is_parent_choosen);
1947+
1948+ return;
1949+}
1950+
1951+// Add a category according to the current selection
1952+static void ui_adv_bud_category_add (GtkButton *button, gpointer user_data)
1953+{
1954+adv_bud_data_t *data = user_data;
1955+GtkWidget *view, *apply;
1956+advbud_view_mode_t view_mode;
1957+GtkTreeModel *filter, *budget, *categories;
1958+GtkTreeSelection *selection;
1959+GtkTreeIter filter_iter, iter, categories_iter;
1960+GtkWidget *dialog, *content_area, *grid, *combobox, *textentry, *widget;
1961+GtkCellRenderer *renderer;
1962+gint gridrow, response, item_key;
1963+gboolean exists_default_select = FALSE;
1964+
1965+ view = data->TV_budget;
1966+ view_mode = radio_get_active(GTK_CONTAINER(data->RA_mode));
1967+
1968+ // Read filter to retrieve the currently selected row
1969+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
1970+ selection = data->TV_selection;
1971+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1972+
1973+ // Selectable categories from original model
1974+ categories = gtk_tree_model_filter_new(budget, NULL);
1975+ gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(categories),
1976+ ui_adv_bud_model_row_filter_parents, data, NULL);
1977+
1978+ // Retrieve default selection from budget dialog
1979+ if (gtk_tree_selection_get_selected(selection, &filter, &filter_iter))
1980+ {
1981+ exists_default_select = TRUE;
1982+
1983+ // Convert data to budget model
1984+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
1985+ &iter,
1986+ &filter_iter);
1987+
1988+ // If currently selected row is a leaf, take its parent
1989+ gtk_tree_model_get (budget, &iter,
1990+ ADVBUD_CATEGORY_KEY, &item_key,
1991+ -1);
1992+
1993+ if (item_key != 0)
1994+ {
1995+ Category *category;
1996+ category = da_cat_get(item_key);
1997+
1998+ if (category != NULL && category->parent != 0)
1999+ {
2000+ advbud_search_criteria_t parent_search = advbud_search_criteria_default;
2001+ parent_search.row_category_key = category->parent;
2002+
2003+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
2004+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
2005+ &parent_search);
2006+
2007+ if (!parent_search.iterator) {
2008+ DB(g_print(" -> error: not found good parent iterator !\n"));
2009+ return;
2010+ }
2011+
2012+ iter = *parent_search.iterator;
2013+
2014+ g_free(parent_search.iterator);
2015+ }
2016+ }
2017+
2018+
2019+ gtk_tree_model_filter_convert_child_iter_to_iter(GTK_TREE_MODEL_FILTER(categories),
2020+ &categories_iter,
2021+ &iter);
2022+ }
2023+
2024+ DB( g_print("[ui_adv_bud] open sub-dialog to add a category\n") );
2025+
2026+ dialog = gtk_dialog_new_with_buttons (_("Add a category"),
2027+ GTK_WINDOW(data->dialog),
2028+ GTK_DIALOG_MODAL,
2029+ _("_Cancel"),
2030+ GTK_RESPONSE_CANCEL,
2031+ NULL);
2032+
2033+ // Apply button will be enabled only when parent category and name are choosen
2034+ apply = gtk_dialog_add_button(GTK_DIALOG(dialog),
2035+ _("_Apply"),
2036+ GTK_RESPONSE_APPLY);
2037+ data->BT_apply = apply;
2038+ gtk_widget_set_sensitive(apply, FALSE);
2039+
2040+ //window contents
2041+ content_area = gtk_dialog_get_content_area(GTK_DIALOG (dialog));
2042+
2043+ // design content
2044+ grid = gtk_grid_new ();
2045+ gtk_grid_set_row_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2046+ gtk_grid_set_column_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2047+ g_object_set(grid, "margin", SPACING_MEDIUM, NULL);
2048+ gtk_container_add(GTK_CONTAINER(content_area), grid);
2049+
2050+ // First row display parent selector
2051+ gridrow = 0;
2052+
2053+ widget = gtk_label_new(_("Parent category"));
2054+ gtk_grid_attach (GTK_GRID (grid), widget, 0, gridrow, 1, 1);
2055+
2056+ combobox = gtk_combo_box_new_with_model(categories);
2057+ data->COMBO_add_parent = combobox;
2058+ gtk_grid_attach (GTK_GRID (grid), combobox, 1, gridrow, 1, 1);
2059+
2060+ gtk_combo_box_set_row_separator_func(
2061+ GTK_COMBO_BOX(combobox),
2062+ ui_adv_view_separator,
2063+ data,
2064+ NULL
2065+ );
2066+
2067+ gtk_combo_box_set_id_column(GTK_COMBO_BOX(combobox), ADVBUD_CATEGORY_KEY);
2068+
2069+ renderer = gtk_cell_renderer_text_new();
2070+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT(combobox), renderer, TRUE);
2071+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combobox), renderer, "text", ADVBUD_CATEGORY_FULLNAME);
2072+
2073+ // Next row displays the new category entry
2074+ gridrow++;
2075+
2076+ widget = gtk_label_new(_("Category name"));
2077+ gtk_grid_attach (GTK_GRID (grid), widget, 0, gridrow, 1, 1);
2078+
2079+ textentry = gtk_entry_new();
2080+ data->EN_add_name = textentry;
2081+ gtk_grid_attach (GTK_GRID (grid), textentry, 1, gridrow, 1, 1);
2082+
2083+ // Signals to enable Apply button
2084+ g_signal_connect (data->COMBO_add_parent, "changed", G_CALLBACK(ui_adv_bud_category_add_full_filled), (gpointer)data);
2085+ g_signal_connect (data->EN_add_name, "changed", G_CALLBACK(ui_adv_bud_category_add_full_filled), (gpointer)data);
2086+
2087+ if (exists_default_select)
2088+ {
2089+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combobox), &categories_iter);
2090+ }
2091+
2092+ gtk_widget_show_all (dialog);
2093+
2094+ response = gtk_dialog_run (GTK_DIALOG (dialog));
2095+
2096+ // When the response is APPLY, the form was full filled
2097+ if (response == GTK_RESPONSE_APPLY) {
2098+ Category *new_item;
2099+ const gchar *new_name;
2100+ gchar *parent_name;
2101+ guint32 parent_key;
2102+ advbud_cat_type_t parent_type;
2103+ advbud_search_criteria_t root_search = advbud_search_criteria_default;
2104+ GtkTreeIter *root_iter;
2105+
2106+ DB( g_print("[ui_adv_bud] applying creation of a new category\n") );
2107+
2108+ // Retrieve info from dialog
2109+ gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combobox), &categories_iter);
2110+
2111+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(categories),
2112+ &iter,
2113+ &categories_iter);
2114+
2115+ gtk_tree_model_get (budget, &iter,
2116+ ADVBUD_CATEGORY_NAME, &parent_name,
2117+ ADVBUD_CATEGORY_KEY, &parent_key,
2118+ ADVBUD_CATEGORY_TYPE, &parent_type,
2119+ -1);
2120+
2121+ DB( g_print(" -> from parent cat: %s (key: %d, type: %d)\n",
2122+ parent_name, parent_key ,parent_type) );
2123+
2124+ // Retrieve required root
2125+ root_search.row_isroot = TRUE;
2126+ root_search.row_istotal = FALSE;
2127+ root_search.row_category_type = parent_type;
2128+ gtk_tree_model_foreach(GTK_TREE_MODEL(budget),
2129+ (GtkTreeModelForeachFunc) ui_adv_bud_model_search_iterator,
2130+ &root_search);
2131+
2132+ if (!root_search.iterator) {
2133+ DB(g_print(" -> error: not found good tree root !\n"));
2134+ return;
2135+ }
2136+
2137+ root_iter = root_search.iterator;
2138+
2139+ // Build new category from name and parent iterator
2140+ new_name = gtk_entry_get_text(GTK_ENTRY(textentry));
2141+
2142+ data->change++;
2143+ new_item = da_cat_malloc();
2144+ new_item->name = g_strdup(new_name);
2145+ g_strstrip(new_item->name);
2146+
2147+ new_item->parent = parent_key;
2148+
2149+ if (parent_key)
2150+ {
2151+ new_item->flags |= GF_SUB;
2152+ }
2153+
2154+ if (parent_type == ADVBUD_CAT_TYPE_INCOME)
2155+ {
2156+ new_item->flags |= GF_INCOME;
2157+ }
2158+
2159+ // On balance mode, enable forced display too to render it to user
2160+ if (view_mode == ADVBUD_VIEW_BALANCE)
2161+ {
2162+ new_item->flags |= GF_FORCED;
2163+ }
2164+
2165+ if(da_cat_append(new_item))
2166+ {
2167+ GtkTreePath *path;
2168+
2169+ DB( g_print(" => add cat: %p (%d), type=%d\n", new_item->name, new_item->key, category_type_get(new_item)) );
2170+
2171+ // Finally add it to model
2172+ ui_adv_bud_model_add_category_with_lineage (GTK_TREE_STORE(budget), root_iter, &(new_item->key));
2173+
2174+ // Expand view up to the newly added item, so expand its parent which is already known as iter
2175+ if(gtk_tree_model_filter_convert_child_iter_to_iter(GTK_TREE_MODEL_FILTER(filter),
2176+ &filter_iter,
2177+ &iter)
2178+ )
2179+ {
2180+ path = gtk_tree_model_get_path(filter, &filter_iter);
2181+ gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path, TRUE);
2182+ }
2183+ }
2184+ }
2185+
2186+ gtk_widget_destroy(dialog);
2187+
2188+ return;
2189+}
2190+
2191+// Delete a category according to the current selection
2192+static void ui_adv_bud_category_delete (GtkButton *button, gpointer user_data)
2193+{
2194+adv_bud_data_t *data = user_data;
2195+GtkWidget *view;
2196+GtkTreeModel *filter, *budget;
2197+GtkTreeSelection *selection;
2198+GtkTreeIter filter_iter, iter;
2199+gint response;
2200+
2201+ DB( g_print("[ui_adv_bud] open sub-dialog to delete a category\n") );
2202+
2203+ view = data->TV_budget;
2204+
2205+ // Read filter to retrieve the currently selected row
2206+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
2207+ selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
2208+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
2209+
2210+ // Retrieve selected row from filter if possible
2211+ if (gtk_tree_selection_get_selected(selection, &filter, &filter_iter))
2212+ {
2213+ Category* category;
2214+ guint32 item_key;
2215+
2216+ // Convert data to budget model
2217+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
2218+ &iter,
2219+ &filter_iter);
2220+
2221+ // If currently selected row is a leaf, take its parent
2222+ gtk_tree_model_get (budget, &iter,
2223+ ADVBUD_CATEGORY_KEY, &item_key,
2224+ -1);
2225+
2226+ // Can't delete fake categories
2227+ if (item_key != 0)
2228+ {
2229+ gchar *title = NULL;
2230+ gchar *secondtext = NULL;
2231+
2232+ category = da_cat_get(item_key);
2233+
2234+ title = g_strdup_printf (
2235+ _("Are you sure you want to permanently delete '%s'?"), category->name);
2236+
2237+ if( category->usage_count > 0 )
2238+ {
2239+ secondtext = _("This category is used.\n"
2240+ "Any transaction using that category will be set to (no category)");
2241+ }
2242+
2243+ response = ui_dialog_msg_confirm_alert(
2244+ GTK_WINDOW(data->dialog),
2245+ title,
2246+ secondtext,
2247+ _("_Delete")
2248+ );
2249+
2250+ g_free(title);
2251+
2252+ if( response == GTK_RESPONSE_OK )
2253+ {
2254+ gtk_tree_store_remove(GTK_TREE_STORE(budget),
2255+ &iter);
2256+
2257+ category_move(category->key, 0);
2258+ da_cat_remove(category->key);
2259+ data->change++;
2260+ }
2261+ }
2262+ }
2263+
2264+
2265+ return;
2266+}
2267+
2268+// Check if add category dialog is full filled
2269+static void ui_adv_bud_category_merge_full_filled (GtkWidget *source, gpointer user_data)
2270+{
2271+adv_bud_data_t *data = user_data;
2272+gboolean is_target_choosen = FALSE;
2273+
2274+ is_target_choosen = (gtk_combo_box_get_active(GTK_COMBO_BOX(data->COMBO_merge_target)) > -1);
2275+
2276+ // Dis/Enable apply dialog button
2277+ gtk_widget_set_sensitive(data->BT_apply, is_target_choosen);
2278+
2279+ return;
2280+}
2281+
2282+static void ui_adv_bud_category_merge (GtkButton *button, gpointer user_data)
2283+{
2284+adv_bud_data_t *data = user_data;
2285+GtkWidget *view, *apply;
2286+GtkTreeModel *filter, *budget, *categories;
2287+GtkTreeSelection *selection;
2288+GtkTreeIter filter_iter, iter_source, iter, categories_iter;
2289+GtkWidget *dialog, *content_area, *grid, *combobox, *widget, *checkbutton;
2290+GtkCellRenderer *renderer;
2291+gint gridrow, response, item_key;
2292+Category *merge_source;
2293+gchar *label_source, *label_delete;
2294+
2295+ view = data->TV_budget;
2296+
2297+ // Read filter to retrieve the currently selected row
2298+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
2299+ selection = data->TV_selection;
2300+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
2301+
2302+ // Selectable categories from original model
2303+ categories = gtk_tree_model_filter_new(budget, NULL);
2304+ gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(categories),
2305+ ui_adv_bud_model_row_filter_with_headers, data, NULL);
2306+
2307+ // Retrieve source of merge
2308+ if (gtk_tree_selection_get_selected(selection, &filter, &filter_iter))
2309+ {
2310+ // Convert data to budget model
2311+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
2312+ &iter_source,
2313+ &filter_iter);
2314+
2315+ // If currently selected row is a leaf, take its parent
2316+ gtk_tree_model_get (budget, &iter_source,
2317+ ADVBUD_CATEGORY_KEY, &item_key,
2318+ -1);
2319+
2320+ // No control as the merge button is only enabled when selected row is valid to merge
2321+ merge_source = da_cat_get(item_key);
2322+
2323+ if(!merge_source)
2324+ {
2325+ return;
2326+ }
2327+
2328+ DB( g_print("[ui_adv_bud] open sub-dialog to merge category: %s\n", merge_source->name) );
2329+
2330+ dialog = gtk_dialog_new_with_buttons (_("Merge categories"),
2331+ GTK_WINDOW(data->dialog),
2332+ GTK_DIALOG_MODAL,
2333+ _("_Cancel"),
2334+ GTK_RESPONSE_CANCEL,
2335+ NULL);
2336+
2337+ // Apply button will be enabled only when a target merge category is choosen
2338+ apply = gtk_dialog_add_button(GTK_DIALOG(dialog),
2339+ _("_Apply"),
2340+ GTK_RESPONSE_APPLY);
2341+ data->BT_apply = apply;
2342+ gtk_widget_set_sensitive(apply, FALSE);
2343+
2344+ //window contents
2345+ content_area = gtk_dialog_get_content_area(GTK_DIALOG (dialog));
2346+
2347+ // design content
2348+ grid = gtk_grid_new ();
2349+ gtk_grid_set_row_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2350+ gtk_grid_set_column_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2351+ g_object_set(grid, "margin", SPACING_MEDIUM, NULL);
2352+ gtk_container_add(GTK_CONTAINER(content_area), grid);
2353+
2354+ // First row display parent selector
2355+ gridrow = 0;
2356+
2357+ label_source = g_strdup_printf(_("Transactions assigned to category %s, will be moved to the category selected below."), merge_source->name);
2358+ widget = gtk_label_new (label_source);
2359+ gtk_grid_attach (GTK_GRID (grid), widget, 0, gridrow, 4, 1);
2360+
2361+ // Line to select merge target
2362+ gridrow++;
2363+
2364+ widget = gtk_label_new(_("Target category"));
2365+ gtk_grid_attach (GTK_GRID (grid), widget, 1, gridrow, 1, 1);
2366+
2367+ combobox = gtk_combo_box_new_with_model(categories);
2368+ data->COMBO_merge_target = combobox;
2369+ gtk_grid_attach (GTK_GRID (grid), combobox, 2, gridrow, 1, 1);
2370+
2371+ gtk_combo_box_set_row_separator_func(
2372+ GTK_COMBO_BOX(combobox),
2373+ ui_adv_view_separator,
2374+ data,
2375+ NULL
2376+ );
2377+
2378+ gtk_combo_box_set_id_column(GTK_COMBO_BOX(combobox), ADVBUD_CATEGORY_KEY);
2379+
2380+ renderer = gtk_cell_renderer_text_new();
2381+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT(combobox), renderer, TRUE);
2382+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combobox), renderer, "text", ADVBUD_CATEGORY_FULLNAME);
2383+
2384+ // Next row displays the automatic delete option
2385+ gridrow++;
2386+
2387+ label_delete = g_strdup_printf (
2388+ _("_Delete the category '%s'"), merge_source->name);
2389+ checkbutton = gtk_check_button_new_with_mnemonic(label_delete);
2390+ gtk_grid_attach (GTK_GRID (grid), checkbutton, 0, gridrow, 4, 1);
2391+
2392+ // Signals to enable Apply button
2393+ g_signal_connect (data->COMBO_merge_target, "changed", G_CALLBACK(ui_adv_bud_category_merge_full_filled), (gpointer)data);
2394+
2395+ gtk_widget_show_all (dialog);
2396+
2397+ response = gtk_dialog_run (GTK_DIALOG (dialog));
2398+
2399+ // When the response is APPLY, the form was full filled
2400+ if (response == GTK_RESPONSE_APPLY) {
2401+ Category *merge_target, *parent_source;
2402+ gint target_key;
2403+
2404+ DB( g_print("[ui_adv_bud] applying merge\n") );
2405+
2406+ // Retrieve info from dialog
2407+ gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combobox), &categories_iter);
2408+
2409+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(categories),
2410+ &iter,
2411+ &categories_iter);
2412+
2413+ gtk_tree_model_get (budget, &iter,
2414+ ADVBUD_CATEGORY_KEY, &target_key,
2415+ -1);
2416+
2417+ merge_target = da_cat_get(target_key);
2418+
2419+ DB( g_print(" -> to target category: %s (key: %d)\n",
2420+ merge_target->name, target_key) );
2421+
2422+ // Merge categories (according to ui-category.c)
2423+ category_move(merge_source->key, merge_target->key);
2424+
2425+ merge_target->usage_count += merge_source->usage_count;
2426+ merge_source->usage_count = 0;
2427+
2428+ // Keep the income type with us
2429+ parent_source = da_cat_get(merge_source->parent);
2430+ if(parent_source != NULL && (parent_source->flags & GF_INCOME))
2431+ merge_target->flags |= GF_INCOME;
2432+
2433+ // Clean source merge
2434+ if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(checkbutton)))
2435+ {
2436+ da_cat_remove(merge_source->key);
2437+
2438+ gtk_tree_store_remove(GTK_TREE_STORE(budget),
2439+ &iter_source);
2440+ }
2441+
2442+ data->change++;
2443+ }
2444+
2445+ gtk_widget_destroy(dialog);
2446+
2447+ g_free(label_source);
2448+ g_free(label_delete);
2449+ }
2450+
2451+ return;
2452+}
2453+
2454+static gboolean ui_adv_bud_on_key_press(GtkWidget *source, GdkEventKey *event, gpointer user_data)
2455+{
2456+adv_bud_data_t *data = user_data;
2457+
2458+ // On Control-f enable search entry
2459+ if (event->state & GDK_CONTROL_MASK
2460+ && event->keyval == GDK_KEY_f)
2461+ {
2462+ gtk_widget_grab_focus(data->EN_search);
2463+ }
2464+
2465+ return GDK_EVENT_PROPAGATE;
2466+}
2467+
2468+static void ui_adv_bud_view_on_select(GtkTreeSelection *treeselection, gpointer user_data)
2469+{
2470+adv_bud_data_t *data = user_data;
2471+GtkWidget *view;
2472+GtkTreeModel *filter, *budget;
2473+GtkTreeIter filter_iter, iter;
2474+GtkTreeSelection *selection;
2475+gboolean is_root, is_total;
2476+advbud_cat_type_t category_type;
2477+
2478+ view = data->TV_budget;
2479+ selection = data->TV_selection;
2480+
2481+ // Reset buttons
2482+ gtk_widget_set_sensitive(data->BT_category_add, FALSE);
2483+ gtk_widget_set_sensitive(data->BT_category_delete, FALSE);
2484+ gtk_widget_set_sensitive(data->BT_category_merge, FALSE);
2485+
2486+ // Read filter to retrieve the currently selected row in real model
2487+ filter = gtk_tree_view_get_model (GTK_TREE_VIEW(view));
2488+ budget = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
2489+
2490+ // Activate buttons if selected row is editable
2491+ if (gtk_tree_selection_get_selected(selection, &filter, &filter_iter))
2492+ {
2493+ // Convert data to budget model
2494+ gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter),
2495+ &iter,
2496+ &filter_iter);
2497+
2498+ // Check the iter is an editable one
2499+ gtk_tree_model_get (budget, &iter,
2500+ ADVBUD_CATEGORY_TYPE, &category_type,
2501+ ADVBUD_ISROOT, &is_root,
2502+ ADVBUD_ISTOTAL, &is_total,
2503+ -1);
2504+
2505+ // If category is neither a root, neither a total row, every operations can be applied
2506+ if (!is_root && !is_total)
2507+ {
2508+ gtk_widget_set_sensitive(data->BT_category_add, TRUE);
2509+ gtk_widget_set_sensitive(data->BT_category_delete, TRUE);
2510+ gtk_widget_set_sensitive(data->BT_category_merge, TRUE);
2511+ }
2512+ // If category is a root (except the Total one), we can use it to add a child row
2513+ else if (category_type != ADVBUD_CAT_TYPE_NONE
2514+ && is_root
2515+ && !is_total)
2516+ {
2517+ gtk_widget_set_sensitive(data->BT_category_add, TRUE);
2518+ }
2519+ }
2520+
2521+ return;
2522+}
2523+
2524+static void ui_adv_bud_dialog_close(adv_bud_data_t *data, gint response)
2525+{
2526+ DB( g_print("[ui_adv_bud] dialog close\n") );
2527+
2528+ GLOBALS->changes_count += data->change;
2529+
2530+ return;
2531+}
2532+
2533+// Open / create the main dialog, the budget view and the budget model
2534+GtkWidget *ui_adv_bud_manage_dialog(void)
2535+{
2536+adv_bud_data_t *data;
2537+GtkWidget *dialog, *content_area, *grid;
2538+GtkWidget *radiomode;
2539+GtkWidget *widget;
2540+GtkWidget *vbox, *hbox;
2541+GtkWidget *search_entry;
2542+GtkWidget *scrolledwindow, *treeview;
2543+GtkWidget *toolbar;
2544+GtkToolItem *toolitem;
2545+GtkTreeModel *model, *filter;
2546+gint response;
2547+gint gridrow, w, h;
2548+
2549+ data = g_malloc0(sizeof(adv_bud_data_t));
2550+ data->change = 0;
2551+ if(!data) return NULL;
2552+
2553+ DB( g_print("\n[ui_adv_bud] open dialog\n") );
2554+
2555+ // create window
2556+ dialog = gtk_dialog_new_with_buttons (_("Advanced Budget Management"),
2557+ GTK_WINDOW(GLOBALS->mainwindow),
2558+ GTK_DIALOG_MODAL,
2559+ _("_Close"),
2560+ GTK_RESPONSE_ACCEPT,
2561+ NULL);
2562+
2563+ data->dialog = dialog;
2564+
2565+ gtk_window_set_icon_name(GTK_WINDOW (dialog), ICONNAME_HB_BUDGET);
2566+
2567+ //window contents
2568+ content_area = gtk_dialog_get_content_area(GTK_DIALOG (dialog)); // return a vbox
2569+
2570+ // store data inside dialog property to retrieve them easily in callbacks
2571+ g_object_set_data(G_OBJECT(dialog), "inst_data", (gpointer)&data);
2572+ DB( g_print(" - new dialog=%p, inst_data=%p\n", dialog, data) );
2573+
2574+ // set a nice default dialog size
2575+ gtk_window_get_size(GTK_WINDOW(GLOBALS->mainwindow), &w, &h);
2576+ gtk_window_resize (GTK_WINDOW(data->dialog), w * 0.9, h * 0.9);
2577+
2578+ // design content
2579+ grid = gtk_grid_new ();
2580+ gtk_grid_set_row_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2581+ gtk_grid_set_column_spacing (GTK_GRID (grid), SPACING_MEDIUM);
2582+ g_object_set(grid, "margin", SPACING_MEDIUM, NULL);
2583+ gtk_container_add(GTK_CONTAINER(content_area), grid);
2584+
2585+ // First row displays radio button to change mode (edition / view) and menu
2586+ gridrow = 0;
2587+
2588+ // edition mode radio buttons
2589+ //
2590+ radiomode = make_radio(ADVBUD_VIEW_MODE, TRUE, GTK_ORIENTATION_HORIZONTAL);
2591+ data->RA_mode = radiomode;
2592+ gtk_widget_set_halign (radiomode, GTK_ALIGN_CENTER);
2593+ gtk_grid_attach (GTK_GRID (grid), radiomode, 0, gridrow, 1, 1);
2594+
2595+ // Next row displays the budget tree with its toolbar
2596+ gridrow++;
2597+
2598+ // We use a Vertical Box to link tree with searchbar and toolbar
2599+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
2600+ gtk_widget_set_margin_right(vbox, SPACING_SMALL);
2601+ gtk_grid_attach (GTK_GRID (grid), vbox, 0, gridrow, 1, 1);
2602+
2603+ // Scrolled Window will permit to display budgets with a lot of active categories
2604+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
2605+ gtk_widget_set_hexpand (scrolledwindow, TRUE);
2606+ gtk_widget_set_vexpand (scrolledwindow, TRUE);
2607+ gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolledwindow), GTK_SHADOW_ETCHED_IN);
2608+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2609+ gtk_box_pack_start (GTK_BOX (vbox), scrolledwindow, TRUE, TRUE, 0);
2610+
2611+ treeview = ui_adv_bud_view_new ((gpointer) data);
2612+ data->TV_budget = treeview;
2613+ gtk_container_add(GTK_CONTAINER(scrolledwindow), treeview);
2614+
2615+ data->TV_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
2616+
2617+ // Toolbar to add, remove categories, expand and collapse categorie
2618+ toolbar = gtk_toolbar_new();
2619+ gtk_toolbar_set_icon_size (GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
2620+ gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
2621+ gtk_box_pack_start (GTK_BOX (vbox), toolbar, FALSE, FALSE, 0);
2622+
2623+ gtk_style_context_add_class (gtk_widget_get_style_context (toolbar), GTK_STYLE_CLASS_INLINE_TOOLBAR);
2624+
2625+ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
2626+
2627+ toolitem = gtk_tool_item_new();
2628+ gtk_container_add (GTK_CONTAINER(toolitem), hbox);
2629+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(toolitem), -1);
2630+
2631+ // Add / Remove / Merge
2632+ widget = make_image_button(ICONNAME_LIST_ADD, _("Add category"));
2633+ data->BT_category_add = widget;
2634+ gtk_widget_set_sensitive(widget, FALSE);
2635+ gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
2636+
2637+ widget = make_image_button(ICONNAME_LIST_REMOVE, _("Remove category"));
2638+ data->BT_category_delete = widget;
2639+ gtk_widget_set_sensitive(widget, FALSE);
2640+ gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
2641+
2642+ widget = gtk_button_new_with_label (_("Merge"));
2643+ data->BT_category_merge = widget;
2644+ gtk_widget_set_sensitive(widget, FALSE);
2645+ gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
2646+
2647+ // Separator
2648+ toolitem = gtk_separator_tool_item_new ();
2649+ gtk_tool_item_set_expand (toolitem, TRUE);
2650+ gtk_separator_tool_item_set_draw(GTK_SEPARATOR_TOOL_ITEM(toolitem), FALSE);
2651+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(toolitem), -1);
2652+
2653+ // Search
2654+ toolitem = gtk_tool_item_new();
2655+ gtk_tool_item_set_expand (toolitem, TRUE);
2656+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(toolitem), -1);
2657+
2658+ search_entry = gtk_search_entry_new();
2659+ data->EN_search = search_entry;
2660+ gtk_container_add (GTK_CONTAINER(toolitem), search_entry);
2661+
2662+ gtk_tree_view_set_search_entry(GTK_TREE_VIEW(treeview), GTK_ENTRY(search_entry));
2663+
2664+ // Separator
2665+ toolitem = gtk_separator_tool_item_new ();
2666+ gtk_tool_item_set_expand (toolitem, TRUE);
2667+ gtk_separator_tool_item_set_draw(GTK_SEPARATOR_TOOL_ITEM(toolitem), FALSE);
2668+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(toolitem), -1);
2669+
2670+ // Expand / Collapse
2671+ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
2672+
2673+ toolitem = gtk_tool_item_new();
2674+ gtk_container_add (GTK_CONTAINER(toolitem), hbox);
2675+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(toolitem), -1);
2676+
2677+ widget = make_image_button(ICONNAME_HB_BUTTON_EXPAND, _("Expand all"));
2678+ data->BT_expand = widget;
2679+ gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
2680+
2681+ widget = make_image_button(ICONNAME_HB_BUTTON_COLLAPSE, _("Collapse all"));
2682+ data->BT_collapse = widget;
2683+ gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
2684+
2685+ /* signal connect */
2686+
2687+ // connect every radio button to the toggled signal to correctly update the view
2688+ for (int i=0; ADVBUD_VIEW_MODE[i] != NULL; i++)
2689+ {
2690+ widget = radio_get_nth_widget (GTK_CONTAINER(radiomode), i);
2691+
2692+ if (widget)
2693+ {
2694+ g_signal_connect (widget, "toggled", G_CALLBACK (ui_adv_bud_view_update_mode), (gpointer)data);
2695+ }
2696+ }
2697+
2698+ // Connect to key press to handle some events like Control-f
2699+ g_signal_connect (dialog, "key-press-event", G_CALLBACK (ui_adv_bud_on_key_press), (gpointer)data);
2700+
2701+ // Tree View
2702+ g_signal_connect (data->TV_selection, "changed", G_CALLBACK(ui_adv_bud_view_on_select), (gpointer)data);
2703+
2704+ // toolbar buttons
2705+ g_signal_connect (data->BT_category_add, "clicked", G_CALLBACK(ui_adv_bud_category_add), (gpointer)data);
2706+ g_signal_connect (data->BT_category_delete, "clicked", G_CALLBACK (ui_adv_bud_category_delete), (gpointer)data);
2707+ g_signal_connect (data->BT_category_merge, "clicked", G_CALLBACK (ui_adv_bud_category_merge), (gpointer)data);
2708+ g_signal_connect (data->BT_expand, "clicked", G_CALLBACK (ui_adv_bud_view_expand), (gpointer)data);
2709+ g_signal_connect (data->BT_collapse, "clicked", G_CALLBACK (ui_adv_bud_view_collapse), (gpointer)data);
2710+
2711+ // dialog
2712+ g_signal_connect (dialog, "destroy", G_CALLBACK (gtk_widget_destroyed), &dialog);
2713+
2714+ // tree model to map HomeBank categories to the tree view
2715+ model = ui_adv_bud_model_new();
2716+
2717+ filter = gtk_tree_model_filter_new(model, NULL);
2718+ gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter), ui_adv_bud_model_row_filter, data, NULL);
2719+ gtk_tree_view_set_model(GTK_TREE_VIEW(treeview), filter);
2720+
2721+ g_object_unref(model); // Remove model with filter
2722+ g_object_unref(filter); // Remove filter with view
2723+
2724+ // By default, show the balance mode with all categories expanded
2725+ data->TV_isexpanded = TRUE;
2726+ ui_adv_bud_view_toggle((gpointer) data, ADVBUD_VIEW_BALANCE);
2727+
2728+ gtk_widget_show_all (dialog);
2729+
2730+ response = gtk_dialog_run (GTK_DIALOG (dialog));
2731+
2732+ ui_adv_bud_dialog_close(data, response);
2733+ gtk_widget_destroy (dialog);
2734+
2735+ g_free(data);
2736+
2737+ return NULL;
2738+}
2739+
2740
2741=== added file 'src/ui-adv-budget.h'
2742--- src/ui-adv-budget.h 1970-01-01 00:00:00 +0000
2743+++ src/ui-adv-budget.h 2019-02-07 14:10:29 +0000
2744@@ -0,0 +1,61 @@
2745+/* HomeBank -- Free, easy, personal accounting for everyone.
2746+ * Copyright (C) 2018-2019 Adrien Dorsaz <adrien@adorsaz.ch>
2747+ *
2748+ * This file is part of HomeBank.
2749+ *
2750+ * HomeBank is free software; you can redistribute it and/or modify
2751+ * it under the terms of the GNU General Public License as published by
2752+ * the Free Software Foundation; either version 2 of the License, or
2753+ * (at your option) any later version.
2754+ *
2755+ * HomeBank is distributed in the hope that it will be useful,
2756+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2757+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2758+ * GNU General Public License for more details.
2759+ *
2760+ * You should have received a copy of the GNU General Public License
2761+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2762+ */
2763+
2764+#ifndef __HOMEBANK_ADVBUDGET_H__
2765+#define __HOMEBANK_ADVBUDGET_H__
2766+
2767+struct adv_bud_data
2768+{
2769+ GtkWidget *dialog;
2770+
2771+ // Number of changes to notify globally
2772+ gint change;
2773+
2774+ // Tree view with budget
2775+ GtkWidget *TV_budget;
2776+ GtkTreeViewColumn *TVC_category;
2777+ GtkTreeViewColumn *TVC_category_with_force;
2778+ GtkTreeSelection *TV_selection;
2779+
2780+ // Radio buttons of view mode
2781+ GtkWidget *RA_mode;
2782+
2783+ // Tool bar
2784+ GtkWidget *BT_category_add, *BT_category_delete, *BT_category_merge, *BT_expand, *BT_collapse;
2785+
2786+ // Should the tree be collapsed
2787+ gboolean TV_isexpanded;
2788+
2789+ // Add Dialog
2790+ GtkWidget *COMBO_add_parent, *EN_add_name, *BT_apply;
2791+
2792+ // Merge Dialog
2793+ GtkWidget *COMBO_merge_target, *CHECK_merge_delete;
2794+
2795+ // Search
2796+ GtkWidget *EN_search;
2797+
2798+ GtkUIManager *ui;
2799+};
2800+typedef struct adv_bud_data adv_bud_data_t;
2801+
2802+GtkWidget *ui_adv_bud_manage_dialog(void);
2803+
2804+
2805+#endif

Subscribers

People subscribed via source and target branches

to all changes: