1 /*******************************************************************\
2  * assistant-csv-price-import.cpp -- An assistant for importing     *
3  *                                     Prices from a file.          *
4  *                                                                  *
5  * Copyright (C) 2017 Robert Fewell                                 *
6  *                                                                  *
7  * This program is free software; you can redistribute it and/or    *
8  * modify it under the terms of the GNU General Public License as   *
9  * published by the Free Software Foundation; either version 2 of   *
10  * the License, or (at your option) any later version.              *
11  *                                                                  *
12  * This program is distributed in the hope that it will be useful,  *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
15  * GNU General Public License for more details.                     *
16  *                                                                  *
17  * You should have received a copy of the GNU General Public License*
18  * along with this program; if not, contact:                        *
19  *                                                                  *
20  * Free Software Foundation           Voice:  +1-617-542-5942       *
21  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
22  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
23 \********************************************************************/
24 /** @file assistant-csv-price-import.cpp
25     @brief CSV Import Assistant
26     @author Copyright (c) 2016 Geert Janssens
27     @author Copyright (c) 2017 Robert Fewell
28 */
29 
30 #include <guid.hpp>
31 
32 extern "C"
33 {
34 #include "config.h"
35 
36 #include <gtk/gtk.h>
37 #include <glib/gi18n.h>
38 #include <stdlib.h>
39 
40 #include "gnc-ui.h"
41 #include "gnc-uri-utils.h"
42 #include "gnc-ui-util.h"
43 #include "dialog-utils.h"
44 
45 #include "gnc-component-manager.h"
46 
47 #include "gnc-state.h"
48 
49 #include "assistant-csv-price-import.h"
50 
51 #include "gnc-csv-gnumeric-popup.h"
52 #include "go-charmap-sel.h"
53 }
54 
55 #include <algorithm>
56 #include <exception>
57 #include <iostream>
58 #include <memory>
59 #include <string>
60 #include <tuple>
61 
62 #include "gnc-imp-settings-csv-price.hpp"
63 #include "gnc-import-price.hpp"
64 #include "gnc-tokenizer-fw.hpp"
65 #include "gnc-tokenizer-csv.hpp"
66 
67 #define MIN_COL_WIDTH 70
68 #define GNC_PREFS_GROUP "dialogs.import.csv"
69 #define ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS "assistant-csv-price-import"
70 
71 /* This static indicates the debugging module that this .o belongs to.  */
72 static QofLogModule log_module = GNC_MOD_ASSISTANT;
73 
74 /* Note on memory management
75  *
76  * The same notes as for assistant-csv-trans-import.cpp apply to
77  * this assistant as well. Please read the note at the top of that
78  * file to understand important details about the use of several
79  * memory management models in one file.
80  */
81 
82 class  CsvImpPriceAssist
83 {
84 public:
85     CsvImpPriceAssist ();
86     ~CsvImpPriceAssist ();
87 
88     /* Delete copy and move constructor/assignments
89      * We don't want gui elements to be moved around or copied at all */
90     CsvImpPriceAssist(const CsvImpPriceAssist&) = delete;            // copy constructor
91     CsvImpPriceAssist& operator=(const CsvImpPriceAssist&) = delete; // copy assignment
92     CsvImpPriceAssist(CsvImpPriceAssist&&) = delete;                 // move constructor
93     CsvImpPriceAssist& operator=(CsvImpPriceAssist&&) = delete;      // move assignment
94 
95     void assist_prepare_cb (GtkWidget *page);
96     void assist_file_page_prepare ();
97     void assist_preview_page_prepare ();
98     void assist_confirm_page_prepare ();
99     void assist_summary_page_prepare ();
100     void assist_finish ();
101     void assist_compmgr_close ();
102 
103     void file_activated_cb ();
104     void file_selection_changed_cb ();
105 
106     void preview_settings_delete ();
107     void preview_settings_save ();
108     void preview_settings_name (GtkEntry* entry);
109     void preview_settings_load ();
110     void preview_update_skipped_rows ();
111     void preview_over_write (bool over);
112     void preview_update_separators (GtkWidget* widget);
113     void preview_update_file_format ();
114     void preview_update_encoding (const char* encoding);
115     void preview_update_date_format ();
116     void preview_update_currency_format ();
117     void preview_update_currency ();
118     void preview_update_commodity ();
119     void preview_reparse_col_type (GncPricePropType type);
120     void preview_update_col_type (GtkComboBox* cbox);
121     void preview_update_fw_columns (GtkTreeView* treeview, GdkEventButton* event);
122 
123     void preview_populate_settings_combo();
124     void preview_handle_save_del_sensitivity (GtkComboBox* combo);
125     void preview_split_column (int col, int offset);
126     void preview_refresh_table ();
127     void preview_refresh ();
128     void preview_validate_settings ();
129 
130     friend gboolean
131     fixed_context_menu_handler_price (GnumericPopupMenuElement const *element,
132             gpointer userdata);
133 private:
134     /* helper functions to manage the context menu for fixed with columns */
135     uint32_t get_new_col_rel_pos (GtkTreeViewColumn *tcol, int dx);
136     void fixed_context_menu (GdkEventButton *event, int col, int dx);
137     /* helper function to calculate row colors for the preview table (to visualize status) */
138     void preview_row_fill_state_cells (GtkListStore *store, GtkTreeIter *iter,
139             std::string& err_msg, bool skip);
140     /* helper function to create preview header cell combo boxes listing available column types */
141     GtkWidget* preview_cbox_factory (GtkTreeModel* model, uint32_t colnum);
142     /* helper function to set rendering parameters for preview data columns */
143     void preview_style_column (uint32_t col_num, GtkTreeModel* model);
144     /* helper function to check for a valid filename as opposed to a directory */
145     bool check_for_valid_filename ();
146 
147     GtkAssistant    *csv_imp_asst;
148 
149     GtkWidget       *file_page;                     /**< Assistant file page widget */
150     GtkWidget       *file_chooser;                  /**< The widget for the file chooser */
151     std::string      m_file_name;                   /**< The import file name */
152 
153     GtkWidget       *preview_page;                  /**< Assistant preview page widget */
154     GtkComboBox     *settings_combo;                /**< The Settings Combo */
155     GtkWidget       *save_button;                   /**< The Save Settings button */
156     GtkWidget       *del_button;                    /**< The Delete Settings button */
157 
158     GtkWidget       *combo_hbox;                    /**< The Settings Combo hbox */
159     GtkSpinButton   *start_row_spin;                /**< The widget for the start row spinner */
160     GtkSpinButton   *end_row_spin;                  /**< The widget for the end row spinner */
161     GtkWidget       *skip_alt_rows_button;          /**< The widget for Skip alternate rows from start row */
162     GtkWidget       *skip_errors_button;            /**< The widget for Skip error rows*/
163     GtkWidget       *csv_button;                    /**< The widget for the CSV button */
164     GtkWidget       *fixed_button;                  /**< The widget for the Fixed Width button */
165     GtkWidget       *over_write_cbutton;            /**< The widget for Price Overwrite */
166     GtkWidget       *commodity_selector;            /**< The widget for commodity combo box */
167     GtkWidget       *currency_selector;             /**< The widget for currency combo box */
168     GOCharmapSel    *encselector;                   /**< The widget for selecting the encoding */
169     GtkWidget       *separator_table;               /**< Container for the separator checkboxes */
170     GtkCheckButton  *sep_button[SEP_NUM_OF_TYPES];  /**< Checkbuttons for common separators */
171     GtkWidget       *fw_instructions_hbox;          /**< Container for fixed-width instructions */
172     GtkCheckButton  *custom_cbutton;                /**< The checkbutton for a custom separator */
173     GtkEntry        *custom_entry;                  /**< The entry for custom separators */
174     GtkComboBoxText *date_format_combo;             /**< The Combo Text widget for selecting the date format */
175     GtkComboBoxText *currency_format_combo;         /**< The Combo Text widget for selecting the currency format */
176     GtkTreeView     *treeview;                      /**< The treeview containing the data */
177     GtkLabel        *instructions_label;            /**< The instructions label */
178     GtkImage        *instructions_image;            /**< The instructions image */
179     bool             encoding_selected_called;      /**< Before encoding_selected is first called, this is false.
180                                                        * error lines, instead of all the file data. */
181     int              fixed_context_col;             /**< The number of the column the user has clicked */
182     int              fixed_context_offset;          /**< The offset (in characters) in the column
183                                                        * the user has clicked */
184 
185     GtkWidget       *confirm_page;                  /**< Assistant confirm page widget */
186 
187     GtkWidget       *summary_page;                  /**< Assistant summary page widget */
188     GtkWidget       *summary_label;                 /**< The summary text */
189 
190     std::unique_ptr<GncPriceImport> price_imp;      /**< The actual data we are previewing */
191 };
192 
193 
194 /*******************************************************
195  * Assistant call back functions
196  *******************************************************/
197 
198 extern "C"
199 {
200 void csv_price_imp_assist_prepare_cb (GtkAssistant  *assistant, GtkWidget *page, CsvImpPriceAssist* info);
201 void csv_price_imp_assist_close_cb (GtkAssistant *gtkassistant, CsvImpPriceAssist* info);
202 void csv_price_imp_assist_finish_cb (GtkAssistant *gtkassistant, CsvImpPriceAssist* info);
203 void csv_price_imp_file_activated_changed_cb (GtkFileChooser *chooser, CsvImpPriceAssist *info);
204 void csv_price_imp_file_selection_changed_cb (GtkFileChooser *chooser, CsvImpPriceAssist *info);
205 void csv_price_imp_preview_del_settings_cb (GtkWidget *button, CsvImpPriceAssist *info);
206 void csv_price_imp_preview_save_settings_cb (GtkWidget *button, CsvImpPriceAssist *info);
207 void csv_price_imp_preview_settings_sel_changed_cb (GtkComboBox *combo, CsvImpPriceAssist *info);
208 void csv_price_imp_preview_settings_text_inserted_cb (GtkEditable *entry, gchar *new_text,
209         gint new_text_length, gint *position, CsvImpPriceAssist *info);
210 void csv_price_imp_preview_settings_text_changed_cb (GtkEntry *entry, CsvImpPriceAssist *info);
211 void csv_price_imp_preview_srow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info);
212 void csv_price_imp_preview_erow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info);
213 void csv_price_imp_preview_skiprows_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info);
214 void csv_price_imp_preview_skiperrors_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info);
215 void csv_price_imp_preview_overwrite_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info);
216 void csv_price_imp_preview_sep_button_cb (GtkWidget* widget, CsvImpPriceAssist* info);
217 void csv_price_imp_preview_sep_fixed_sel_cb (GtkToggleButton* csv_button, CsvImpPriceAssist* info);
218 void csv_price_imp_preview_acct_sel_cb (GtkWidget* widget, CsvImpPriceAssist* info);
219 void csv_price_imp_preview_enc_sel_cb (GOCharmapSel* selector, const char* encoding,
220                               CsvImpPriceAssist* info);
221 }
222 
223 void
csv_price_imp_assist_prepare_cb(GtkAssistant * assistant,GtkWidget * page,CsvImpPriceAssist * info)224 csv_price_imp_assist_prepare_cb (GtkAssistant *assistant, GtkWidget *page,
225         CsvImpPriceAssist* info)
226 {
227     info->assist_prepare_cb(page);
228 }
229 
230 void
csv_price_imp_assist_close_cb(GtkAssistant * assistant,CsvImpPriceAssist * info)231 csv_price_imp_assist_close_cb (GtkAssistant *assistant, CsvImpPriceAssist* info)
232 {
233     gnc_close_gui_component_by_data (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, info);
234 }
235 
236 void
csv_price_imp_assist_finish_cb(GtkAssistant * assistant,CsvImpPriceAssist * info)237 csv_price_imp_assist_finish_cb (GtkAssistant *assistant, CsvImpPriceAssist* info)
238 {
239     info->assist_finish ();
240 }
241 
csv_price_imp_file_activated_changed_cb(GtkFileChooser * chooser,CsvImpPriceAssist * info)242 void csv_price_imp_file_activated_changed_cb (GtkFileChooser *chooser, CsvImpPriceAssist *info)
243 {
244     info->file_activated_cb();
245 }
246 
csv_price_imp_file_selection_changed_cb(GtkFileChooser * chooser,CsvImpPriceAssist * info)247 void csv_price_imp_file_selection_changed_cb (GtkFileChooser *chooser, CsvImpPriceAssist *info)
248 {
249     info->file_selection_changed_cb();
250 }
251 
csv_price_imp_preview_del_settings_cb(GtkWidget * button,CsvImpPriceAssist * info)252 void csv_price_imp_preview_del_settings_cb (GtkWidget *button, CsvImpPriceAssist *info)
253 {
254     info->preview_settings_delete();
255 }
256 
csv_price_imp_preview_save_settings_cb(GtkWidget * button,CsvImpPriceAssist * info)257 void csv_price_imp_preview_save_settings_cb (GtkWidget *button, CsvImpPriceAssist *info)
258 {
259     info->preview_settings_save();
260 }
261 
csv_price_imp_preview_settings_sel_changed_cb(GtkComboBox * combo,CsvImpPriceAssist * info)262 void csv_price_imp_preview_settings_sel_changed_cb (GtkComboBox *combo, CsvImpPriceAssist *info)
263 {
264     info->preview_settings_load();
265 }
266 
267 void
csv_price_imp_preview_settings_text_inserted_cb(GtkEditable * entry,gchar * new_text,gint new_text_length,gint * position,CsvImpPriceAssist * info)268 csv_price_imp_preview_settings_text_inserted_cb (GtkEditable *entry, gchar *new_text,
269         gint new_text_length, gint *position, CsvImpPriceAssist *info)
270 {
271     if (!new_text)
272         return;
273 
274     /* Prevent entering [], which are invalid characters in key files */
275     auto base_txt = std::string (new_text);
276     auto mod_txt = base_txt;
277     std::replace (mod_txt.begin(), mod_txt.end(), '[', '(');
278     std::replace (mod_txt.begin(), mod_txt.end(), ']', ')');
279     if (base_txt == mod_txt)
280         return;
281     g_signal_handlers_block_by_func (entry, (gpointer) csv_price_imp_preview_settings_text_inserted_cb, info);
282     gtk_editable_insert_text (entry, mod_txt.c_str(), mod_txt.size() , position);
283     g_signal_handlers_unblock_by_func (entry, (gpointer) csv_price_imp_preview_settings_text_inserted_cb, info);
284 
285     g_signal_stop_emission_by_name (entry, "insert_text");
286 }
287 
288 void
csv_price_imp_preview_settings_text_changed_cb(GtkEntry * entry,CsvImpPriceAssist * info)289 csv_price_imp_preview_settings_text_changed_cb (GtkEntry *entry, CsvImpPriceAssist *info)
290 {
291     info->preview_settings_name(entry);
292 }
293 
csv_price_imp_preview_srow_cb(GtkSpinButton * spin,CsvImpPriceAssist * info)294 void csv_price_imp_preview_srow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info)
295 {
296     info->preview_update_skipped_rows();
297 }
298 
csv_price_imp_preview_erow_cb(GtkSpinButton * spin,CsvImpPriceAssist * info)299 void csv_price_imp_preview_erow_cb (GtkSpinButton *spin, CsvImpPriceAssist *info)
300 {
301     info->preview_update_skipped_rows();
302 }
303 
csv_price_imp_preview_skiprows_cb(GtkToggleButton * checkbox,CsvImpPriceAssist * info)304 void csv_price_imp_preview_skiprows_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info)
305 {
306     info->preview_update_skipped_rows();
307 }
308 
csv_price_imp_preview_skiperrors_cb(GtkToggleButton * checkbox,CsvImpPriceAssist * info)309 void csv_price_imp_preview_skiperrors_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info)
310 {
311     info->preview_update_skipped_rows();
312 }
313 
csv_price_imp_preview_overwrite_cb(GtkToggleButton * checkbox,CsvImpPriceAssist * info)314 void csv_price_imp_preview_overwrite_cb (GtkToggleButton *checkbox, CsvImpPriceAssist *info)
315 {
316     info->preview_over_write (gtk_toggle_button_get_active (checkbox));
317 }
318 
csv_price_imp_preview_sep_button_cb(GtkWidget * widget,CsvImpPriceAssist * info)319 void csv_price_imp_preview_sep_button_cb (GtkWidget* widget, CsvImpPriceAssist* info)
320 {
321     info->preview_update_separators(widget);
322 }
323 
csv_price_imp_preview_sep_fixed_sel_cb(GtkToggleButton * csv_button,CsvImpPriceAssist * info)324 void csv_price_imp_preview_sep_fixed_sel_cb (GtkToggleButton* csv_button, CsvImpPriceAssist* info)
325 {
326     info->preview_update_file_format();
327 }
328 
csv_price_imp_preview_enc_sel_cb(GOCharmapSel * selector,const char * encoding,CsvImpPriceAssist * info)329 void csv_price_imp_preview_enc_sel_cb (GOCharmapSel* selector, const char* encoding,
330                               CsvImpPriceAssist* info)
331 {
332     info->preview_update_encoding(encoding);
333 }
334 
csv_price_imp_preview_date_fmt_sel_cb(GtkComboBox * format_selector,CsvImpPriceAssist * info)335 static void csv_price_imp_preview_date_fmt_sel_cb (GtkComboBox* format_selector, CsvImpPriceAssist* info)
336 {
337     info->preview_update_date_format();
338 }
339 
csv_price_imp_preview_currency_fmt_sel_cb(GtkComboBox * format_selector,CsvImpPriceAssist * info)340 static void csv_price_imp_preview_currency_fmt_sel_cb (GtkComboBox* format_selector, CsvImpPriceAssist* info)
341 {
342     info->preview_update_currency_format();
343 }
344 
345 enum GncCommColumn {DISPLAYED_COMM, SORT_COMM, COMM_PTR, SEP};
346 
csv_price_imp_preview_currency_sel_cb(GtkComboBox * currency_selector,CsvImpPriceAssist * info)347 static void csv_price_imp_preview_currency_sel_cb (GtkComboBox* currency_selector, CsvImpPriceAssist* info)
348 {
349     info->preview_update_currency();
350 }
351 
separator_row_func(GtkTreeModel * smodel,GtkTreeIter * siter,gpointer data)352 static gboolean separator_row_func (GtkTreeModel *smodel, GtkTreeIter *siter, gpointer data)
353 {
354     gboolean      sep_row;
355     GtkTreeModel *store;
356     GtkTreeIter   iter;
357 
358     store = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT(smodel));
359 
360     gtk_tree_model_sort_convert_iter_to_child_iter (GTK_TREE_MODEL_SORT(smodel),
361                                                     &iter, siter);
362 
363     gtk_tree_model_get (GTK_TREE_MODEL(store), &iter, SEP, &sep_row, -1);
364 
365     return sep_row;
366 }
367 
csv_price_imp_preview_commodity_sel_cb(GtkComboBox * commodity_selector,CsvImpPriceAssist * info)368 static void csv_price_imp_preview_commodity_sel_cb (GtkComboBox* commodity_selector, CsvImpPriceAssist* info)
369 {
370     info->preview_update_commodity();
371 }
372 
csv_price_imp_preview_col_type_changed_cb(GtkComboBox * cbox,CsvImpPriceAssist * info)373 static void csv_price_imp_preview_col_type_changed_cb (GtkComboBox* cbox, CsvImpPriceAssist* info)
374 {
375     info->preview_update_col_type (cbox);
376 }
377 
378 static gboolean
csv_price_imp_preview_treeview_clicked_cb(GtkTreeView * treeview,GdkEventButton * event,CsvImpPriceAssist * info)379 csv_price_imp_preview_treeview_clicked_cb (GtkTreeView* treeview, GdkEventButton* event,
380                                         CsvImpPriceAssist* info)
381 {
382     info->preview_update_fw_columns(treeview, event);
383     return false;
384 }
385 
386 static
get_commodity_from_combo(GtkComboBox * combo)387 gnc_commodity *get_commodity_from_combo (GtkComboBox *combo)
388 {
389     GtkTreeModel *model, *sort_model;
390     GtkTreeIter  iter, siter;
391     gchar *string;
392     gnc_commodity *comm;
393 
394     if (!gtk_combo_box_get_active_iter (combo, &siter))
395         return nullptr;
396 
397     sort_model = gtk_combo_box_get_model (combo);
398     model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT(sort_model));
399 
400     gtk_tree_model_sort_convert_iter_to_child_iter (GTK_TREE_MODEL_SORT(sort_model),
401                                                     &iter, &siter);
402 
403     gtk_tree_model_get (GTK_TREE_MODEL(model), &iter,
404                         DISPLAYED_COMM, &string, COMM_PTR, &comm, -1);
405 
406     PINFO("Commodity string is %s", string);
407 
408     g_free (string);
409     return comm;
410 }
411 
412 static void
set_commodity_for_combo(GtkComboBox * combo,gnc_commodity * comm)413 set_commodity_for_combo (GtkComboBox *combo, gnc_commodity *comm)
414 {
415     GtkTreeModel *model, *sort_model;
416     GtkTreeIter  iter, siter;
417     gnc_commodity *model_comm;
418     gboolean valid;
419 
420     sort_model = gtk_combo_box_get_model (combo);
421     model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT(sort_model));
422     valid = gtk_tree_model_get_iter_first (model, &iter);
423 
424     while (valid)
425     {
426         gtk_tree_model_get (model, &iter, COMM_PTR, &model_comm, -1);
427         if (model_comm == comm)
428         {
429             if (gtk_tree_model_sort_convert_child_iter_to_iter (GTK_TREE_MODEL_SORT(sort_model), &siter, &iter))
430             {
431                 gtk_combo_box_set_active_iter (combo, &siter);
432                 return;
433             }
434         }
435         /* Make iter point to the next row in the list store */
436         valid = gtk_tree_model_iter_next (model, &iter);
437     }
438     // Not found, set it to first iter
439     gtk_tree_model_get_iter_first (model, &iter);
440     if (gtk_tree_model_sort_convert_child_iter_to_iter (GTK_TREE_MODEL_SORT(sort_model), &siter, &iter))
441         gtk_combo_box_set_active_iter (combo, &siter);
442 }
443 
444 static
get_model(bool all_commodity)445 GtkTreeModel *get_model (bool all_commodity)
446 {
447     GtkTreeModel *store, *model;
448     const gnc_commodity_table *commodity_table = gnc_get_current_commodities ();
449     gnc_commodity *tmp_commodity = nullptr;
450     char  *tmp_namespace = nullptr;
451     GList *commodity_list = nullptr;
452     GList *namespace_list = gnc_commodity_table_get_namespaces (commodity_table);
453     GtkTreeIter iter;
454 
455     store = GTK_TREE_MODEL(gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING,
456                                                   G_TYPE_POINTER, G_TYPE_BOOLEAN));
457     model = gtk_tree_model_sort_new_with_model (store);
458     // set sort order
459     gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(model), SORT_COMM, GTK_SORT_ASCENDING);
460 
461     gtk_list_store_append (GTK_LIST_STORE(store), &iter);
462     gtk_list_store_set (GTK_LIST_STORE(store), &iter,
463                             DISPLAYED_COMM, " ", SORT_COMM, " ", COMM_PTR, nullptr, SEP, false, -1);
464 
465     namespace_list = g_list_first (namespace_list);
466     while (namespace_list != nullptr)
467     {
468         tmp_namespace = (char*)namespace_list->data;
469         DEBUG("Looking at namespace %s", tmp_namespace);
470 
471         /* Hide the template entry */
472         if (g_utf8_collate (tmp_namespace, "template" ) != 0)
473         {
474             if ((g_utf8_collate (tmp_namespace, GNC_COMMODITY_NS_CURRENCY ) == 0) || (all_commodity == true))
475             {
476                 commodity_list = gnc_commodity_table_get_commodities (commodity_table, tmp_namespace);
477                 commodity_list  = g_list_first (commodity_list);
478 
479                 // if this is the CURRENCY, add a row to be identified as a separator row
480                 if ((g_utf8_collate (tmp_namespace, GNC_COMMODITY_NS_CURRENCY) == 0) && (all_commodity == true))
481                 {
482                     gtk_list_store_append (GTK_LIST_STORE(store), &iter);
483                     gtk_list_store_set (GTK_LIST_STORE(store), &iter, DISPLAYED_COMM, " ",
484                                            SORT_COMM, "CURRENCY-", COMM_PTR, nullptr, SEP, true, -1);
485                 }
486 
487                 while (commodity_list != nullptr)
488                 {
489                     const gchar *name_str;
490                     gchar *sort_str;
491                     tmp_commodity = (gnc_commodity*)commodity_list->data;
492                     DEBUG("Looking at commodity %s", gnc_commodity_get_fullname (tmp_commodity));
493 
494                     name_str = gnc_commodity_get_printname (tmp_commodity);
495 
496                     if (g_utf8_collate (tmp_namespace, GNC_COMMODITY_NS_CURRENCY) == 0)
497                         sort_str = g_strconcat ("CURRENCY-", name_str, nullptr);
498                     else
499                         sort_str = g_strconcat ("ALL-OTHER-", name_str, nullptr);
500 
501                     DEBUG("Name string is '%s', Sort string is '%s'", name_str, sort_str);
502 
503                     gtk_list_store_append (GTK_LIST_STORE(store), &iter);
504                     gtk_list_store_set (GTK_LIST_STORE(store), &iter, DISPLAYED_COMM, name_str,
505                                            SORT_COMM, sort_str, COMM_PTR, tmp_commodity, SEP, false, -1);
506 
507                     g_free (sort_str);
508                     commodity_list = g_list_next (commodity_list);
509                 }
510             }
511         }
512         namespace_list = g_list_next (namespace_list);
513     }
514     g_list_free (commodity_list);
515     g_list_free (namespace_list);
516     g_object_unref (store);
517 
518     return model;
519 }
520 
521 
522 /*******************************************************
523  * Assistant Constructor
524  *******************************************************/
CsvImpPriceAssist()525 CsvImpPriceAssist::CsvImpPriceAssist ()
526 {
527     auto builder = gtk_builder_new();
528     gnc_builder_add_from_file  (builder , "assistant-csv-price-import.glade", "start_row_adj");
529     gnc_builder_add_from_file  (builder , "assistant-csv-price-import.glade", "end_row_adj");
530     gnc_builder_add_from_file  (builder , "assistant-csv-price-import.glade", "liststore1");
531     gnc_builder_add_from_file  (builder , "assistant-csv-price-import.glade", "liststore2");
532     gnc_builder_add_from_file  (builder , "assistant-csv-price-import.glade", "CSV Price Assistant");
533     csv_imp_asst = GTK_ASSISTANT(gtk_builder_get_object (builder, "CSV Price Assistant"));
534 
535     // Set the name for this assistant so it can be easily manipulated with css
536     gtk_widget_set_name (GTK_WIDGET(csv_imp_asst), "gnc-id-assistant-csv-price-import");
537     gnc_widget_style_context_add_class (GTK_WIDGET(csv_imp_asst), "gnc-class-imports");
538 
539     /* Enable buttons on all page. */
540     gtk_assistant_set_page_complete (csv_imp_asst,
541                                      GTK_WIDGET(gtk_builder_get_object (builder, "start_page")),
542                                      true);
543     gtk_assistant_set_page_complete (csv_imp_asst,
544                                      GTK_WIDGET(gtk_builder_get_object (builder, "file_page")),
545                                      false);
546     gtk_assistant_set_page_complete (csv_imp_asst,
547                                      GTK_WIDGET(gtk_builder_get_object (builder, "preview_page")),
548                                      false);
549     gtk_assistant_set_page_complete (csv_imp_asst,
550                                      GTK_WIDGET(gtk_builder_get_object (builder, "confirm_page")),
551                                      true);
552     gtk_assistant_set_page_complete (csv_imp_asst,
553                                      GTK_WIDGET(gtk_builder_get_object (builder, "summary_page")),
554                                      true);
555 
556     /* File chooser Page */
557     file_page = GTK_WIDGET(gtk_builder_get_object (builder, "file_page"));
558     file_chooser = gtk_file_chooser_widget_new (GTK_FILE_CHOOSER_ACTION_OPEN);
559 
560     g_signal_connect (G_OBJECT(file_chooser), "selection-changed",
561                       G_CALLBACK(csv_price_imp_file_selection_changed_cb), this);
562     g_signal_connect (G_OBJECT(file_chooser), "file-activated",
563                       G_CALLBACK(csv_price_imp_file_activated_changed_cb), this);
564 
565     auto box = GTK_WIDGET(gtk_builder_get_object (builder, "file_page"));
566     gtk_box_pack_start (GTK_BOX(box), file_chooser, TRUE, TRUE, 6);
567     gtk_widget_show (file_chooser);
568 
569     /* Preview Settings Page */
570     {
571         preview_page = GTK_WIDGET(gtk_builder_get_object (builder, "preview_page"));
572 
573         // Add Settings combo
574         auto settings_store = gtk_list_store_new (2, G_TYPE_POINTER, G_TYPE_STRING);
575         settings_combo = GTK_COMBO_BOX(gtk_combo_box_new_with_model_and_entry (GTK_TREE_MODEL(settings_store)));
576         g_object_unref (settings_store);
577 
578         gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX(settings_combo), SET_NAME);
579         gtk_combo_box_set_active (GTK_COMBO_BOX(settings_combo), 0);
580 
581         combo_hbox = GTK_WIDGET(gtk_builder_get_object (builder, "combo_hbox"));
582         gtk_box_pack_start (GTK_BOX(combo_hbox), GTK_WIDGET(settings_combo), true, true, 6);
583         gtk_widget_show (GTK_WIDGET(settings_combo));
584 
585         g_signal_connect (G_OBJECT(settings_combo), "changed",
586                          G_CALLBACK(csv_price_imp_preview_settings_sel_changed_cb), this);
587 
588         // Additionally connect to the changed signal of the embedded GtkEntry
589         auto emb_entry = gtk_bin_get_child (GTK_BIN (settings_combo));
590         g_signal_connect (G_OBJECT(emb_entry), "changed",
591                          G_CALLBACK(csv_price_imp_preview_settings_text_changed_cb), this);
592         g_signal_connect (G_OBJECT(emb_entry), "insert-text",
593                          G_CALLBACK(csv_price_imp_preview_settings_text_inserted_cb), this);
594 
595         // Add Save Settings button
596         save_button = GTK_WIDGET(gtk_builder_get_object (builder, "save_settings"));
597 
598         // Add Delete Settings button
599         del_button = GTK_WIDGET(gtk_builder_get_object (builder, "delete_settings"));
600 
601         /* The table containing the separator configuration widgets */
602         start_row_spin = GTK_SPIN_BUTTON(gtk_builder_get_object (builder, "start_row"));
603         end_row_spin = GTK_SPIN_BUTTON(gtk_builder_get_object (builder, "end_row"));
604         skip_alt_rows_button = GTK_WIDGET(gtk_builder_get_object (builder, "skip_rows"));
605         skip_errors_button = GTK_WIDGET(gtk_builder_get_object (builder, "skip_errors_button"));
606         over_write_cbutton = GTK_WIDGET(gtk_builder_get_object (builder, "over_write_button"));
607         separator_table = GTK_WIDGET(gtk_builder_get_object (builder, "separator_table"));
608         fw_instructions_hbox = GTK_WIDGET(gtk_builder_get_object (builder, "fw_instructions_hbox"));
609 
610         /* Load the separator buttons from the glade builder file into the
611          * sep_buttons array. */
612         const char* sep_button_names[] = {
613                 "space_cbutton",
614                 "tab_cbutton",
615                 "comma_cbutton",
616                 "colon_cbutton",
617                 "semicolon_cbutton",
618                 "hyphen_cbutton"
619             };
620         for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
621             sep_button[i]
622                 = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, sep_button_names[i]));
623 
624         /* Load and connect the custom separator checkbutton in the same way
625          * as the other separator buttons. */
626         custom_cbutton
627             = (GtkCheckButton*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_cbutton"));
628 
629         /* Load the entry for the custom separator entry. Connect it to the
630          * sep_button_clicked event handler as well. */
631         custom_entry = (GtkEntry*)GTK_WIDGET(gtk_builder_get_object (builder, "custom_entry"));
632 
633         /* Create the encoding selector widget and add it to the assistant */
634         encselector = GO_CHARMAP_SEL(go_charmap_sel_new(GO_CHARMAP_SEL_TO_UTF8));
635         /* Connect the selector to the encoding_selected event handler. */
636         g_signal_connect (G_OBJECT(encselector), "charmap_changed",
637                          G_CALLBACK(csv_price_imp_preview_enc_sel_cb), this);
638 
639         auto encoding_container = GTK_CONTAINER(gtk_builder_get_object (builder, "encoding_container"));
640         gtk_container_add (encoding_container, GTK_WIDGET(encselector));
641         gtk_widget_set_hexpand (GTK_WIDGET(encselector), true);
642         gtk_widget_show_all (GTK_WIDGET(encoding_container));
643 
644         /* Add commodity selection widget */
645         commodity_selector = GTK_WIDGET(gtk_builder_get_object (builder, "commodity_cbox"));
646         gtk_combo_box_set_model (GTK_COMBO_BOX(commodity_selector), get_model (true));
647         gtk_combo_box_set_row_separator_func (GTK_COMBO_BOX(commodity_selector),
648                                               separator_row_func, nullptr, nullptr);
649         g_signal_connect (G_OBJECT(commodity_selector), "changed",
650                           G_CALLBACK(csv_price_imp_preview_commodity_sel_cb), this);
651 
652         /* Add currency selection widget */
653         currency_selector = GTK_WIDGET(gtk_builder_get_object (builder, "currency_cbox"));
654         gtk_combo_box_set_model (GTK_COMBO_BOX(currency_selector), get_model (false));
655         g_signal_connect(G_OBJECT(currency_selector), "changed",
656                          G_CALLBACK(csv_price_imp_preview_currency_sel_cb), this);
657 
658         /* The instructions label and image */
659         instructions_label = GTK_LABEL(gtk_builder_get_object (builder, "instructions_label"));
660         instructions_image = GTK_IMAGE(gtk_builder_get_object (builder, "instructions_image"));
661 
662         /* Add in the date format combo box and hook it up to an event handler. */
663         date_format_combo = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new());
664         for (auto& date_fmt : GncDate::c_formats)
665             gtk_combo_box_text_append_text (date_format_combo, _(date_fmt.m_fmt.c_str()));
666         gtk_combo_box_set_active (GTK_COMBO_BOX(date_format_combo), 0);
667         g_signal_connect (G_OBJECT(date_format_combo), "changed",
668                          G_CALLBACK(csv_price_imp_preview_date_fmt_sel_cb), this);
669 
670         /* Add it to the assistant. */
671         auto date_format_container = GTK_CONTAINER(gtk_builder_get_object (builder, "date_format_container"));
672         gtk_container_add (date_format_container, GTK_WIDGET(date_format_combo));
673         gtk_widget_set_hexpand (GTK_WIDGET(date_format_combo), true);
674         gtk_widget_show_all (GTK_WIDGET(date_format_container));
675 
676         /* Add in the currency format combo box and hook it up to an event handler. */
677         currency_format_combo = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new());
678         for (int i = 0; i < num_currency_formats_price; i++)
679         {
680             gtk_combo_box_text_append_text (currency_format_combo, _(currency_format_user_price[i]));
681         }
682         /* Default will the locale */
683         gtk_combo_box_set_active (GTK_COMBO_BOX(currency_format_combo), 0);
684         g_signal_connect (G_OBJECT(currency_format_combo), "changed",
685                          G_CALLBACK(csv_price_imp_preview_currency_fmt_sel_cb), this);
686 
687         /* Add it to the assistant. */
688         auto currency_format_container = GTK_CONTAINER(gtk_builder_get_object (builder, "currency_format_container"));
689         gtk_container_add (currency_format_container, GTK_WIDGET(currency_format_combo));
690         gtk_widget_set_hexpand (GTK_WIDGET(currency_format_combo), true);
691         gtk_widget_show_all (GTK_WIDGET(currency_format_container));
692 
693         /* Connect the CSV/Fixed-Width radio button event handler. */
694         csv_button = GTK_WIDGET(gtk_builder_get_object (builder, "csv_button"));
695         fixed_button = GTK_WIDGET(gtk_builder_get_object (builder, "fixed_button"));
696 
697         /* Load the data treeview and connect it to its resizing event handler. */
698         treeview = (GtkTreeView*)GTK_WIDGET(gtk_builder_get_object (builder, "treeview"));
699         gtk_tree_view_set_headers_clickable (treeview, true);
700 
701         /* This is true only after encoding_selected is called, so we must
702          * set it initially to false. */
703         encoding_selected_called = false;
704     }
705 
706     /* Confirm Page */
707     confirm_page = GTK_WIDGET(gtk_builder_get_object (builder, "confirm_page"));
708 
709     /* Summary Page */
710     summary_page  = GTK_WIDGET(gtk_builder_get_object (builder, "summary_page"));
711     summary_label = GTK_WIDGET(gtk_builder_get_object (builder, "summary_label"));
712 
713     gnc_restore_window_size (GNC_PREFS_GROUP,
714                              GTK_WINDOW(csv_imp_asst), gnc_ui_get_main_window(nullptr));
715 
716     gtk_builder_connect_signals (builder, this);
717     g_object_unref (G_OBJECT(builder));
718 
719     gtk_widget_show_all (GTK_WIDGET(csv_imp_asst));
720     gnc_window_adjust_for_screen (GTK_WINDOW(csv_imp_asst));
721 }
722 
723 /*******************************************************
724  * Assistant Destructor
725  *******************************************************/
~CsvImpPriceAssist()726 CsvImpPriceAssist::~CsvImpPriceAssist ()
727 {
728     gtk_widget_destroy (GTK_WIDGET(csv_imp_asst));
729 }
730 
731 /**************************************************
732  * Code related to the file chooser page
733  **************************************************/
734 
735 /* check_for_valid_filename for a valid file to activate the "Next" button
736  */
737 bool
check_for_valid_filename()738 CsvImpPriceAssist::check_for_valid_filename ()
739 {
740     auto file_name = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER(file_chooser));
741     if (!file_name || g_file_test (file_name, G_FILE_TEST_IS_DIR))
742         return false;
743 
744     auto filepath = gnc_uri_get_path (file_name);
745     auto starting_dir = g_path_get_dirname (filepath);
746 
747     m_file_name = file_name;
748     gnc_set_default_directory (GNC_PREFS_GROUP, starting_dir);
749 
750     DEBUG("file_name selected is %s", m_file_name.c_str());
751     DEBUG("starting directory is %s", starting_dir);
752 
753     g_free (filepath);
754     g_free (file_name);
755     g_free (starting_dir);
756 
757     return true;
758 }
759 
760 /* csv_price_imp_file_activated_cb
761  *
762  * call back for file chooser widget
763  */
764 void
file_activated_cb()765 CsvImpPriceAssist::file_activated_cb ()
766 {
767     gtk_assistant_set_page_complete (csv_imp_asst, file_page, false);
768 
769     /* Test for a valid filename and not a directory */
770     if (check_for_valid_filename ())
771     {
772         gtk_assistant_set_page_complete (csv_imp_asst, file_page, true);
773         gtk_assistant_next_page (csv_imp_asst);
774     }
775 }
776 
777 /* csv_price_imp_file_selection_changed_cb
778  *
779  * call back for file chooser widget
780  */
781 void
file_selection_changed_cb()782 CsvImpPriceAssist::file_selection_changed_cb ()
783 {
784     /* Enable the "Next" button based on a valid filename */
785     gtk_assistant_set_page_complete (csv_imp_asst, file_page,
786         check_for_valid_filename ());
787 }
788 
789 
790 /**************************************************
791  * Code related to the preview page
792  **************************************************/
793 
794 /* Set the available presets in the settings combo box
795  */
preview_populate_settings_combo()796 void CsvImpPriceAssist::preview_populate_settings_combo()
797 {
798     // Clear the list store
799     auto model = gtk_combo_box_get_model (settings_combo);
800     gtk_list_store_clear (GTK_LIST_STORE(model));
801 
802     // Append the default entry
803     auto presets = get_import_presets_price ();
804     for (auto preset : presets)
805     {
806         GtkTreeIter iter;
807         gtk_list_store_append (GTK_LIST_STORE(model), &iter);
808         /* FIXME we store the raw pointer to the preset, while it's
809          * managed by a shared pointer. This is dangerous because
810          * when the shared pointer goes out of scope, our pointer will dangle.
811          * For now this is safe, because the shared pointers in this case are
812          * long-lived, but this may need refactoring.
813          */
814         gtk_list_store_set (GTK_LIST_STORE(model), &iter, SET_GROUP, preset.get(), SET_NAME, _(preset->m_name.c_str()), -1);
815     }
816 }
817 
818 /* Enable or disable the save and delete settings buttons
819  * depending on what is selected and entered as settings name
820  */
preview_handle_save_del_sensitivity(GtkComboBox * combo)821 void CsvImpPriceAssist::preview_handle_save_del_sensitivity (GtkComboBox* combo)
822 {
823     GtkTreeIter iter;
824     auto can_delete = false;
825     auto can_save = false;
826     auto entry = gtk_bin_get_child (GTK_BIN(combo));
827     auto entry_text = gtk_entry_get_text (GTK_ENTRY(entry));
828     /* Handle sensitivity of the delete and save button */
829     if (gtk_combo_box_get_active_iter (combo, &iter))
830     {
831         CsvPriceImpSettings *preset;
832         GtkTreeModel *model = gtk_combo_box_get_model (combo);
833         gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1);
834 
835         if (preset && !preset_is_reserved_name (preset->m_name))
836         {
837             /* Current preset is not read_only, so buttons can be enabled */
838             can_delete = true;
839             can_save = true;
840         }
841     }
842     else if (entry_text && (strlen (entry_text) > 0) &&
843             !preset_is_reserved_name (std::string(entry_text)))
844         can_save = true;
845 
846     gtk_widget_set_sensitive (save_button, can_save);
847     gtk_widget_set_sensitive (del_button, can_delete);
848 }
849 
850 void
preview_settings_name(GtkEntry * entry)851 CsvImpPriceAssist::preview_settings_name (GtkEntry* entry)
852 {
853     auto text = gtk_entry_get_text (entry);
854     if (text)
855         price_imp->settings_name(text);
856 
857     auto box = gtk_widget_get_parent (GTK_WIDGET(entry));
858     auto combo = gtk_widget_get_parent (GTK_WIDGET(box));
859 
860     preview_handle_save_del_sensitivity (GTK_COMBO_BOX(combo));
861 }
862 
863 /* Use selected preset to configure the import. Triggered when
864  * a preset is selected in the settings combo.
865  */
866 void
preview_settings_load()867 CsvImpPriceAssist::preview_settings_load ()
868 {
869     // Get the Active Selection
870     GtkTreeIter iter;
871     if (!gtk_combo_box_get_active_iter (settings_combo, &iter))
872         return;
873 
874     CsvPriceImpSettings *preset = nullptr;
875     auto model = gtk_combo_box_get_model (settings_combo);
876     gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1);
877 
878     if (!preset)
879         return;
880 
881     price_imp->settings (*preset);
882     if (preset->m_load_error)
883         gnc_error_dialog (GTK_WINDOW(csv_imp_asst),
884             "%s", _("There were problems reading some saved settings, continuing to load.\n"
885                     "Please review and save again."));
886 
887     preview_refresh ();
888     preview_handle_save_del_sensitivity (settings_combo);
889 }
890 
891 /* Callback to delete a settings entry
892  */
893 void
preview_settings_delete()894 CsvImpPriceAssist::preview_settings_delete ()
895 {
896     // Get the Active Selection
897     GtkTreeIter iter;
898     if (!gtk_combo_box_get_active_iter (settings_combo, &iter))
899         return;
900 
901     CsvPriceImpSettings *preset = nullptr;
902     auto model = gtk_combo_box_get_model (settings_combo);
903     gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1);
904 
905     auto response = gnc_ok_cancel_dialog (GTK_WINDOW(csv_imp_asst),
906                                 GTK_RESPONSE_CANCEL,
907                                 "%s", _("Delete the Import Settings."));
908     if (response == GTK_RESPONSE_OK)
909     {
910         preset->remove();
911         preview_populate_settings_combo();
912         gtk_combo_box_set_active (settings_combo, 0); // Default
913         preview_refresh (); // Reset the widgets
914     }
915 }
916 
917 /* Callback to save the current settings to the gnucash state file.
918  */
919 void
preview_settings_save()920 CsvImpPriceAssist::preview_settings_save ()
921 {
922     auto new_name = price_imp->settings_name();
923 
924     /* Check if the entry text matches an already existing preset */
925     GtkTreeIter iter;
926     if (!gtk_combo_box_get_active_iter (settings_combo, &iter))
927     {
928 
929         auto model = gtk_combo_box_get_model (settings_combo);
930         bool valid = gtk_tree_model_get_iter_first (model, &iter);
931         while (valid)
932         {
933             // Walk through the list, reading each row
934             CsvPriceImpSettings *preset;
935             gtk_tree_model_get (model, &iter, SET_GROUP, &preset, -1);
936 
937             if (preset && (preset->m_name == std::string(new_name)))
938             {
939                 auto response = gnc_ok_cancel_dialog (GTK_WINDOW(csv_imp_asst),
940                         GTK_RESPONSE_OK,
941                         "%s", _("Setting name already exists, overwrite?"));
942                 if (response != GTK_RESPONSE_OK)
943                     return;
944 
945                 break;
946             }
947             valid = gtk_tree_model_iter_next (model, &iter);
948         }
949     }
950 
951     /* All checks passed, let's save this preset */
952     if (!price_imp->save_settings())
953     {
954         gnc_info_dialog (GTK_WINDOW(csv_imp_asst),
955             "%s", _("The settings have been saved."));
956 
957         // Update the settings store
958         preview_populate_settings_combo();
959         auto model = gtk_combo_box_get_model (settings_combo);
960 
961         // Get the first entry in model
962         GtkTreeIter   iter;
963         bool valid = gtk_tree_model_get_iter_first (model, &iter);
964         while (valid)
965         {
966             // Walk through the list, reading each row
967             gchar *name = nullptr;
968             gtk_tree_model_get (model, &iter, SET_NAME, &name, -1);
969 
970             if (g_strcmp0 (name, new_name.c_str()) == 0) // Set Active, the one Saved.
971                 gtk_combo_box_set_active_iter (settings_combo, &iter);
972 
973             g_free (name);
974 
975             valid = gtk_tree_model_iter_next (model, &iter);
976         }
977     }
978     else
979         gnc_error_dialog (GTK_WINDOW(csv_imp_asst),
980             "%s", _("There was a problem saving the settings, please try again."));
981 }
982 
983 /* Callback triggered when user adjusts skip start lines
984  */
preview_update_skipped_rows()985 void CsvImpPriceAssist::preview_update_skipped_rows ()
986 {
987     /* Update skip rows in the parser */
988     price_imp->update_skipped_lines (gtk_spin_button_get_value_as_int (start_row_spin),
989         gtk_spin_button_get_value_as_int (end_row_spin),
990         gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(skip_alt_rows_button)),
991         gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(skip_errors_button)));
992 
993     /* And adjust maximum number of lines that can be skipped at each end accordingly */
994     auto adj = gtk_spin_button_get_adjustment (end_row_spin);
995     gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size()
996             - price_imp->skip_start_lines() -1);
997 
998     adj = gtk_spin_button_get_adjustment (start_row_spin);
999     gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size()
1000             - price_imp->skip_end_lines() - 1);
1001 
1002     preview_refresh_table ();
1003 }
1004 
1005 /* Callback triggered when user clicks on Overwrite option
1006  */
preview_over_write(bool over)1007 void CsvImpPriceAssist::preview_over_write (bool over)
1008 {
1009     price_imp->over_write (over);
1010 }
1011 
1012 /** Event handler for separator changes. This function is called
1013  * whenever one of the widgets for configuring the separators (the
1014  * separator checkbuttons or the custom separator entry) is
1015  * changed.
1016  * @param widget The widget that was changed
1017  * @param info The data that is being configured
1018  */
preview_update_separators(GtkWidget * widget)1019 void CsvImpPriceAssist::preview_update_separators (GtkWidget* widget)
1020 {
1021     /* Only manipulate separator characters if the currently open file is
1022      * csv separated. */
1023     if (price_imp->file_format() != GncImpFileFormat::CSV)
1024         return;
1025 
1026     /* Add the corresponding characters to checked_separators for each
1027      * button that is checked. */
1028     auto checked_separators = std::string();
1029     const auto stock_sep_chars = std::string (" \t,:;-");
1030     for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
1031     {
1032         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(sep_button[i])))
1033             checked_separators += stock_sep_chars[i];
1034     }
1035 
1036     /* Add the custom separator if the user checked its button. */
1037     if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(custom_cbutton)))
1038     {
1039         auto custom_sep = gtk_entry_get_text (custom_entry);
1040         if (custom_sep[0] != '\0') /* Don't add a blank separator (bad things will happen!). */
1041             checked_separators += custom_sep;
1042     }
1043 
1044     /* Set the parse options using the checked_separators list. */
1045     price_imp->separators (checked_separators);
1046 
1047     /* Parse the data using the new options. We don't want to reguess
1048      * the column types because we want to leave the user's
1049      * configurations intact. */
1050     try
1051     {
1052         price_imp->tokenize (false);
1053         preview_refresh_table ();
1054     }
1055     catch (std::range_error &e)
1056     {
1057         /* Warn the user there was a problem and try to undo what caused
1058          * the error. (This will cause a reparsing and ideally a usable
1059          * configuration.) */
1060         gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "Error in parsing");
1061         /* If we're here because the user changed the file format, we should just wait for the user
1062          * to update the configuration */
1063         if (!widget)
1064             return;
1065         /* If the user changed the custom separator, erase that custom separator. */
1066         if (widget == GTK_WIDGET(custom_entry))
1067             gtk_entry_set_text (GTK_ENTRY(widget), "");
1068         /* If the user checked a checkbutton, toggle that checkbutton back. */
1069         else
1070             gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(widget),
1071                                          !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget)));
1072         return;
1073     }
1074 }
1075 
1076 /** Event handler for clicking one of the format type radio
1077  * buttons. This occurs if the format (Fixed-Width or CSV) is changed.
1078  * @param csv_button The "Separated" radio button
1079  * @param info The display of the data being imported
1080  */
preview_update_file_format()1081 void CsvImpPriceAssist::preview_update_file_format ()
1082 {
1083     /* Set the parsing type correctly. */
1084     try
1085     {
1086         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(csv_button)))
1087         {
1088             price_imp->file_format (GncImpFileFormat::CSV);
1089             g_signal_handlers_disconnect_by_func(G_OBJECT(treeview),
1090                     (gpointer)csv_price_imp_preview_treeview_clicked_cb, (gpointer)this);
1091             gtk_widget_set_visible (separator_table, true);
1092             gtk_widget_set_visible (fw_instructions_hbox, false);
1093         }
1094         else
1095         {
1096             price_imp->file_format (GncImpFileFormat::FIXED_WIDTH);
1097             /* Enable context menu for adding/removing columns. */
1098             g_signal_connect (G_OBJECT(treeview), "button-press-event",
1099                     G_CALLBACK(csv_price_imp_preview_treeview_clicked_cb), (gpointer)this);
1100             gtk_widget_set_visible (separator_table, false);
1101             gtk_widget_set_visible (fw_instructions_hbox, true);
1102 
1103         }
1104         price_imp->tokenize (false);
1105         preview_refresh_table ();
1106     }
1107     catch (std::range_error &e)
1108     {
1109         /* Parsing failed ... */
1110         gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", e.what());
1111         return;
1112     }
1113     catch (...)
1114     {
1115         // FIXME Handle file loading errors (possibly thrown by file_format above)
1116         PWARN("Got an error during file loading");
1117     }
1118 }
1119 
1120 /** Event handler for a new encoding. This is called when the user
1121  * selects a new encoding; the data is reparsed and shown to the
1122  * user.
1123  * @param selector The widget the user uses to select a new encoding
1124  * @param encoding The encoding that the user selected
1125  */
1126 void
preview_update_encoding(const char * encoding)1127 CsvImpPriceAssist::preview_update_encoding (const char* encoding)
1128 {
1129     /* This gets called twice every time a new encoding is selected. The
1130      * second call actually passes the correct data; thus, we only do
1131      * something the second time this is called. */
1132 
1133     /* If this is the second time the function is called ... */
1134     if (encoding_selected_called)
1135     {
1136         std::string previous_encoding = price_imp->m_tokenizer->encoding();
1137         /* Try converting the new encoding and reparsing. */
1138         try
1139         {
1140             price_imp->encoding (encoding);
1141             preview_refresh_table ();
1142         }
1143         catch (...)
1144         {
1145             /* If it fails, change back to the old encoding. */
1146             gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", _("Invalid encoding selected"));
1147             go_charmap_sel_set_encoding (encselector, previous_encoding.c_str());
1148         }
1149     }
1150     encoding_selected_called = !encoding_selected_called;
1151 }
1152 
1153 void
preview_update_date_format()1154 CsvImpPriceAssist::preview_update_date_format ()
1155 {
1156     price_imp->date_format (gtk_combo_box_get_active (GTK_COMBO_BOX(date_format_combo)));
1157     preview_refresh_table ();
1158 }
1159 
1160 void
preview_update_currency_format()1161 CsvImpPriceAssist::preview_update_currency_format ()
1162 {
1163     price_imp->currency_format (gtk_combo_box_get_active (GTK_COMBO_BOX(currency_format_combo)));
1164     preview_refresh_table ();
1165 }
1166 
1167 void
preview_update_currency()1168 CsvImpPriceAssist::preview_update_currency ()
1169 {
1170     gnc_commodity *comm = get_commodity_from_combo (GTK_COMBO_BOX(currency_selector));
1171     price_imp->to_currency (comm);
1172     preview_refresh_table ();
1173 }
1174 
1175 void
preview_update_commodity()1176 CsvImpPriceAssist::preview_update_commodity ()
1177 {
1178     gnc_commodity *comm = get_commodity_from_combo (GTK_COMBO_BOX(commodity_selector));
1179     price_imp->from_commodity (comm);
1180     preview_refresh_table ();
1181 }
1182 
1183 static gboolean
csv_imp_preview_queue_rebuild_table(CsvImpPriceAssist * assist)1184 csv_imp_preview_queue_rebuild_table (CsvImpPriceAssist *assist)
1185 {
1186     assist->preview_refresh_table ();
1187     return false;
1188 }
1189 
1190 /* Internally used enum to access the columns in the comboboxes
1191  * the user can click to set a type for each column of the data
1192  */
1193 enum PreviewHeaderComboCols { COL_TYPE_NAME, COL_TYPE_ID };
1194 /* Internally used enum to access the first two (fixed) columns
1195  * in the model used to display the prased data.
1196  */
1197 enum PreviewDataTableCols {
1198     PREV_COL_FCOLOR,
1199     PREV_COL_BCOLOR,
1200     PREV_COL_STRIKE,
1201     PREV_COL_ERROR,
1202     PREV_COL_ERR_ICON,
1203     PREV_N_FIXED_COLS };
1204 
1205 
1206 void
preview_reparse_col_type(GncPricePropType type)1207 CsvImpPriceAssist::preview_reparse_col_type (GncPricePropType type)
1208 {
1209     auto column_types = price_imp->column_types_price();
1210 
1211     // look for column type and force a reparse
1212     auto col_type = std::find (column_types.begin(),
1213                 column_types.end(), type);
1214     if (col_type != column_types.end())
1215     {
1216         price_imp->set_column_type_price (col_type -column_types.begin(),
1217                         type, true);
1218     }
1219 }
1220 
1221 /** Event handler for the user selecting a new column type. When the
1222  * user selects a new column type, that column's text must be changed
1223  * to the selection, and any other columns containing that selection
1224  * must be changed to "None" because we don't allow duplicates.
1225  * @param renderer The renderer of the column the user changed
1226  * @param path There is only 1 row in info->ctreeview, so this is always 0.
1227  * @param new_text The text the user selected
1228  * @param info The display of the data being imported
1229  */
preview_update_col_type(GtkComboBox * cbox)1230 void CsvImpPriceAssist::preview_update_col_type (GtkComboBox* cbox)
1231 {
1232     /* Get the new text */
1233     GtkTreeIter iter;
1234     auto model = gtk_combo_box_get_model (cbox);
1235     gtk_combo_box_get_active_iter (cbox, &iter);
1236     auto new_col_type = GncPricePropType::NONE;
1237     gtk_tree_model_get (model, &iter, COL_TYPE_ID, &new_col_type, -1);
1238 
1239     auto col_num = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT(cbox), "col-num"));
1240 
1241     auto column_types = price_imp->column_types_price();
1242     auto old_col_type = column_types.at(col_num);
1243 
1244     price_imp->set_column_type_price (col_num, new_col_type);
1245 
1246     // if old_col_type is TO_CURRENCY, force a reparse of commodity
1247     if (old_col_type == GncPricePropType::TO_CURRENCY)
1248     {
1249         // look for a from_commodity column to reparse
1250         preview_reparse_col_type (GncPricePropType::FROM_SYMBOL);
1251         preview_reparse_col_type (GncPricePropType::FROM_NAMESPACE);
1252     }
1253 
1254     // if old_col_type is FROM_SYMBOL, or FROM_NAMESPACE force a reparse of currency
1255     if ((old_col_type == GncPricePropType::FROM_SYMBOL) ||
1256         (old_col_type == GncPricePropType::FROM_NAMESPACE))
1257     {
1258         // look for a to_currency column to reparse
1259         preview_reparse_col_type (GncPricePropType::TO_CURRENCY);
1260     }
1261 
1262     /* Delay rebuilding our data table to avoid critical warnings due to
1263      * pending events still acting on them after this event is processed.
1264      */
1265     g_idle_add ((GSourceFunc)csv_imp_preview_queue_rebuild_table, this);
1266 }
1267 
1268 /*======================================================================*/
1269 /*================== Beginning of Gnumeric Code ========================*/
1270 
1271 /* The following is code copied from Gnumeric 1.7.8 licensed under the
1272  * GNU General Public License version 2 and/or version 3. It is from the file
1273  * gnumeric/gnucash/dialogs/dialog-stf-fixed-page.c, and it has been
1274  * modified slightly to work within GnuCash. */
1275 
1276 /*
1277  * Copyright 2001 Almer S. Tigelaar <almer@gnome.org>
1278  * Copyright 2003 Morten Welinder <terra@gnome.org>
1279  *
1280  * This program is free software; you can redistribute it and/or modify
1281  * it under the terms of the GNU General Public License as published by
1282  * the Free Software Foundation; either version 2 of the License, or
1283  * (at your option) any later version.
1284  *
1285  * This program is distributed in the hope that it will be useful,
1286  * but WITHOUT ANY WARRANTY; without even the implied warranty of
1287  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1288  * GNU General Public License for more details.
1289  *
1290  * You should have received a copy of the GNU General Public License
1291  * along with this program; if not, write to the Free Software
1292  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
1293  */
1294 
1295 enum
1296 {
1297     CONTEXT_STF_IMPORT_MERGE_LEFT = 1,
1298     CONTEXT_STF_IMPORT_MERGE_RIGHT = 2,
1299     CONTEXT_STF_IMPORT_SPLIT = 3,
1300     CONTEXT_STF_IMPORT_WIDEN = 4,
1301     CONTEXT_STF_IMPORT_NARROW = 5
1302 };
1303 
1304 static GnumericPopupMenuElement const popup_elements[] =
1305 {
1306     {
1307         N_("Merge with column on _left"), "list-remove",
1308         0, 1 << CONTEXT_STF_IMPORT_MERGE_LEFT, CONTEXT_STF_IMPORT_MERGE_LEFT
1309     },
1310     {
1311         N_("Merge with column on _right"), "list-remove",
1312         0, 1 << CONTEXT_STF_IMPORT_MERGE_RIGHT, CONTEXT_STF_IMPORT_MERGE_RIGHT
1313     },
1314     { "", nullptr, 0, 0, 0 },
1315     {
1316         N_("_Split this column"), nullptr,
1317         0, 1 << CONTEXT_STF_IMPORT_SPLIT, CONTEXT_STF_IMPORT_SPLIT
1318     },
1319     { "", nullptr, 0, 0, 0 },
1320     {
1321         N_("_Widen this column"), "go-next",
1322         0, 1 << CONTEXT_STF_IMPORT_WIDEN, CONTEXT_STF_IMPORT_WIDEN
1323     },
1324     {
1325         N_("_Narrow this column"), "go-previous",
1326         0, 1 << CONTEXT_STF_IMPORT_NARROW, CONTEXT_STF_IMPORT_NARROW
1327     },
1328     { nullptr, nullptr, 0, 0, 0 },
1329 };
1330 
get_new_col_rel_pos(GtkTreeViewColumn * tcol,int dx)1331 uint32_t CsvImpPriceAssist::get_new_col_rel_pos (GtkTreeViewColumn *tcol, int dx)
1332 {
1333     auto renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT(tcol));
1334     auto cell = GTK_CELL_RENDERER(renderers->data);
1335     g_list_free (renderers);
1336     PangoFontDescription *font_desc;
1337     g_object_get (G_OBJECT(cell), "font_desc", &font_desc, nullptr);
1338 
1339     PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET(treeview), "x");
1340     pango_layout_set_font_description (layout, font_desc);
1341     int width;
1342     pango_layout_get_pixel_size (layout, &width, nullptr);
1343     if (width < 1) width = 1;
1344     uint32_t charindex = (dx + width / 2) / width;
1345     g_object_unref (layout);
1346     pango_font_description_free (font_desc);
1347 
1348     return charindex;
1349 }
1350 
1351 gboolean
fixed_context_menu_handler_price(GnumericPopupMenuElement const * element,gpointer userdata)1352 fixed_context_menu_handler_price (GnumericPopupMenuElement const *element,
1353         gpointer userdata)
1354 {
1355     auto info = (CsvImpPriceAssist*)userdata;
1356     auto fwtok = dynamic_cast<GncFwTokenizer*>(info->price_imp->m_tokenizer.get());
1357 
1358     switch (element->index)
1359     {
1360     case CONTEXT_STF_IMPORT_MERGE_LEFT:
1361         fwtok->col_delete (info->fixed_context_col - 1);
1362         break;
1363     case CONTEXT_STF_IMPORT_MERGE_RIGHT:
1364         fwtok->col_delete (info->fixed_context_col);
1365         break;
1366     case CONTEXT_STF_IMPORT_SPLIT:
1367         fwtok->col_split (info->fixed_context_col, info->fixed_context_offset);
1368         break;
1369     case CONTEXT_STF_IMPORT_WIDEN:
1370         fwtok->col_widen (info->fixed_context_col);
1371         break;
1372     case CONTEXT_STF_IMPORT_NARROW:
1373         fwtok->col_narrow (info->fixed_context_col);
1374         break;
1375     default:
1376         ; /* Nothing */
1377     }
1378 
1379     try
1380     {
1381         info->price_imp->tokenize (false);
1382     }
1383     catch(std::range_error& e)
1384     {
1385         gnc_error_dialog (GTK_WINDOW (info->csv_imp_asst), "%s", e.what());
1386         return false;
1387     }
1388     info->preview_refresh_table ();
1389     return true;
1390 }
1391 
1392 void
fixed_context_menu(GdkEventButton * event,int col,int offset)1393 CsvImpPriceAssist::fixed_context_menu (GdkEventButton *event,
1394                     int col, int offset)
1395 {
1396     auto fwtok = dynamic_cast<GncFwTokenizer*>(price_imp->m_tokenizer.get());
1397     fixed_context_col = col;
1398     fixed_context_offset = offset;
1399 
1400     int sensitivity_filter = 0;
1401     if (!fwtok->col_can_delete (col - 1))
1402         sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_LEFT);
1403     if (!fwtok->col_can_delete (col))
1404         sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_MERGE_RIGHT);
1405     if (!fwtok->col_can_split (col, offset))
1406         sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_SPLIT);
1407     if (!fwtok->col_can_widen (col))
1408         sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_WIDEN);
1409     if (!fwtok->col_can_narrow (col))
1410         sensitivity_filter |= (1 << CONTEXT_STF_IMPORT_NARROW);
1411 
1412     gnumeric_create_popup_menu (popup_elements, &fixed_context_menu_handler_price,
1413                                 this, 0,
1414                                 sensitivity_filter, event);
1415 }
1416 
1417 /*===================== End of Gnumeric Code ===========================*/
1418 /*======================================================================*/
1419 void
preview_split_column(int col,int offset)1420 CsvImpPriceAssist::preview_split_column (int col, int offset)
1421 {
1422     auto fwtok = dynamic_cast<GncFwTokenizer*>(price_imp->m_tokenizer.get());
1423     fwtok->col_split (col, offset);
1424     try
1425     {
1426         price_imp->tokenize (false);
1427     }
1428     catch (std::range_error& e)
1429     {
1430         gnc_error_dialog (GTK_WINDOW (csv_imp_asst), "%s", e.what());
1431         return;
1432     }
1433     preview_refresh_table();
1434 }
1435 
1436 /** Event handler for clicking on column headers. This function is
1437  * called whenever the user clicks on column headers in
1438  * preview->treeview to modify columns when in fixed-width mode.
1439  * @param button The button at the top of a column of the treeview
1440  * @param event The event that happened (where the user clicked)
1441  * @param info The data being configured
1442  * @returns true if further processing of this even should stop, false
1443  *               if other event handlers can have a go at this as well
1444  */
1445 void
preview_update_fw_columns(GtkTreeView * treeview,GdkEventButton * event)1446 CsvImpPriceAssist::preview_update_fw_columns (GtkTreeView* treeview, GdkEventButton* event)
1447 {
1448     /* Nothing to do if this was not triggered on our treeview body */
1449     if (event->window != gtk_tree_view_get_bin_window (treeview))
1450         return;
1451 
1452     /* Find the column that was clicked. */
1453     GtkTreeViewColumn *tcol = nullptr;
1454     int cell_x = 0;
1455     auto success = gtk_tree_view_get_path_at_pos (treeview,
1456             (int)event->x, (int)event->y,
1457             nullptr, &tcol, &cell_x, nullptr);
1458     if (!success)
1459         return;
1460 
1461     /* Stop if no column found in this treeview (-1) or
1462      * if column is the error messages column (0) */
1463     auto tcol_list = gtk_tree_view_get_columns(treeview);
1464     auto tcol_num = g_list_index (tcol_list, tcol);
1465     g_list_free (tcol_list);
1466     if (tcol_num <= 0)
1467         return;
1468 
1469     /* Data columns in the treeview are offset by one
1470      * because the first column is the error column
1471      */
1472     auto dcol = tcol_num - 1;
1473     auto offset = get_new_col_rel_pos (tcol, cell_x);
1474     if (event->type == GDK_2BUTTON_PRESS && event->button == 1)
1475         /* Double clicks can split columns. */
1476         preview_split_column (dcol, offset);
1477     else if (event->type == GDK_BUTTON_PRESS && event->button == 3)
1478         /* Right clicking brings up a context menu. */
1479         fixed_context_menu (event, dcol, offset);
1480 }
1481 
1482 /* Convert state info (errors/skipped) in visual feedback to decorate the preview table */
1483 void
preview_row_fill_state_cells(GtkListStore * store,GtkTreeIter * iter,std::string & err_msg,bool skip)1484 CsvImpPriceAssist::preview_row_fill_state_cells (GtkListStore *store, GtkTreeIter *iter,
1485         std::string& err_msg, bool skip)
1486 {
1487     /* Extract error status for all non-skipped lines */
1488     const char *c_err_msg = nullptr;
1489     const char *icon_name = nullptr;
1490     const char *fcolor = nullptr;
1491     const char *bcolor = nullptr;
1492     if (!skip && !err_msg.empty())
1493     {
1494         fcolor = "black";
1495         bcolor = "pink";
1496         c_err_msg = err_msg.c_str();
1497         icon_name = "dialog-error";
1498     }
1499     gtk_list_store_set (store, iter,
1500             PREV_COL_FCOLOR, fcolor,
1501             PREV_COL_BCOLOR, bcolor,
1502             PREV_COL_STRIKE, skip,
1503             PREV_COL_ERROR, c_err_msg,
1504             PREV_COL_ERR_ICON, icon_name, -1);
1505 }
1506 
1507 /* Helper function that creates a combo_box using a model
1508  * with valid column types and selects the given column type
1509  */
1510 GtkWidget*
preview_cbox_factory(GtkTreeModel * model,uint32_t colnum)1511 CsvImpPriceAssist::preview_cbox_factory (GtkTreeModel* model, uint32_t colnum)
1512 {
1513     GtkTreeIter iter;
1514     auto cbox = gtk_combo_box_new_with_model(model);
1515 
1516     /* Set up a renderer for this combobox. */
1517     auto renderer = gtk_cell_renderer_text_new();
1518     gtk_cell_layout_pack_start (GTK_CELL_LAYOUT(cbox),
1519             renderer, true);
1520     gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT(cbox),
1521             renderer, "text", COL_TYPE_NAME);
1522 
1523     auto valid = gtk_tree_model_get_iter_first (model, &iter);
1524     while (valid)
1525     {
1526         gint stored_col_type;
1527         gtk_tree_model_get (model, &iter,
1528                 COL_TYPE_ID, &stored_col_type, -1);
1529         if (stored_col_type == static_cast<int>( price_imp->column_types_price()[colnum]))
1530             break;
1531         valid = gtk_tree_model_iter_next(model, &iter);
1532     }
1533     if (valid)
1534         gtk_combo_box_set_active_iter (GTK_COMBO_BOX(cbox), &iter);
1535 
1536     g_object_set_data (G_OBJECT(cbox), "col-num", GUINT_TO_POINTER(colnum));
1537     g_signal_connect (G_OBJECT(cbox), "changed",
1538                      G_CALLBACK(csv_price_imp_preview_col_type_changed_cb), (gpointer)this);
1539 
1540     gtk_widget_show (cbox);
1541     return cbox;
1542 }
1543 
1544 void
preview_style_column(uint32_t col_num,GtkTreeModel * model)1545 CsvImpPriceAssist::preview_style_column (uint32_t col_num, GtkTreeModel* model)
1546 {
1547     auto col = gtk_tree_view_get_column (treeview, col_num);
1548     auto renderer = static_cast<GtkCellRenderer*>(gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(col))->data);
1549 
1550     /* First column -the error status column- is rendered differently */
1551     if (col_num == 0)
1552     {
1553         gtk_tree_view_column_set_attributes (col, renderer,
1554                 "icon-name", PREV_COL_ERR_ICON,
1555                 "cell-background", PREV_COL_BCOLOR, nullptr);
1556         g_object_set (G_OBJECT(renderer), "stock-size", GTK_ICON_SIZE_MENU, nullptr);
1557         g_object_set (G_OBJECT(col), "sizing", GTK_TREE_VIEW_COLUMN_FIXED,
1558                 "fixed-width", 20, nullptr);
1559         gtk_tree_view_column_set_resizable (col, false);
1560     }
1561     else
1562     {
1563         gtk_tree_view_column_set_attributes (col, renderer,
1564                 "foreground", PREV_COL_FCOLOR,
1565                 "background", PREV_COL_BCOLOR,
1566                 "strikethrough", PREV_COL_STRIKE,
1567                 "text", col_num + PREV_N_FIXED_COLS -1, nullptr);
1568 
1569         /* We want a monospace font fixed-width data is properly displayed. */
1570         g_object_set (G_OBJECT(renderer), "family", "monospace", nullptr);
1571 
1572         /* Add a combobox to select column types as column header. Each uses the same
1573          * common model for the dropdown list. The selected value is taken
1574          * from the column_types vector. */
1575         auto cbox = preview_cbox_factory (GTK_TREE_MODEL(model), col_num - 1);
1576         gtk_tree_view_column_set_widget (col, cbox);
1577 
1578         /* Enable resizing of the columns. */
1579         gtk_tree_view_column_set_resizable (col, true);
1580         gtk_tree_view_column_set_clickable (col, true);
1581     }
1582 }
1583 
1584 /* Helper to create a shared store for the header comboboxes in the preview treeview.
1585  * It holds the possible column types */
1586 static GtkTreeModel*
make_column_header_model_price(void)1587 make_column_header_model_price (void)
1588 {
1589     auto combostore = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT);
1590     for (auto col_type : gnc_price_col_type_strs)
1591     {
1592         GtkTreeIter iter;
1593         gtk_list_store_append (combostore, &iter);
1594         gtk_list_store_set (combostore, &iter,
1595                 COL_TYPE_NAME, _(col_type.second),
1596                 COL_TYPE_ID, static_cast<int>(col_type.first), -1);
1597     }
1598     return GTK_TREE_MODEL(combostore);
1599 }
1600 
1601 /* Updates the preview treeview to show the data as parsed based on the user's
1602  * import parameters.
1603  */
preview_refresh_table()1604 void CsvImpPriceAssist::preview_refresh_table ()
1605 {
1606     preview_validate_settings ();
1607 
1608     /* Create a new liststore to hold status and data from the file being imported.
1609        The first columns hold status information (row-color, row-errors, row-error-icon,...
1610        All following columns represent the tokenized data as strings. */
1611     auto ncols = PREV_N_FIXED_COLS + price_imp->column_types_price().size();
1612     auto model_col_types = g_new (GType, ncols);
1613     model_col_types[PREV_COL_FCOLOR] = G_TYPE_STRING;
1614     model_col_types[PREV_COL_BCOLOR] = G_TYPE_STRING;
1615     model_col_types[PREV_COL_ERROR] = G_TYPE_STRING;
1616     model_col_types[PREV_COL_ERR_ICON] = G_TYPE_STRING;
1617     model_col_types[PREV_COL_STRIKE] = G_TYPE_BOOLEAN;
1618     for (guint i = PREV_N_FIXED_COLS; i <  ncols; i++)
1619         model_col_types[i] = G_TYPE_STRING;
1620     auto store = gtk_list_store_newv (ncols, model_col_types);
1621     g_free (model_col_types);
1622 
1623     /* Fill the data liststore with data from importer object. */
1624     for (auto parse_line : price_imp->m_parsed_lines)
1625     {
1626         /* Fill the state cells */
1627         GtkTreeIter iter;
1628         gtk_list_store_append (store, &iter);
1629         preview_row_fill_state_cells (store, &iter,
1630                 std::get<PL_ERROR>(parse_line), std::get<PL_SKIP>(parse_line));
1631 
1632         /* Fill the data cells. */
1633         for (auto cell_str_it = std::get<PL_INPUT>(parse_line).cbegin(); cell_str_it != std::get<PL_INPUT>(parse_line).cend(); cell_str_it++)
1634         {
1635             uint32_t pos = PREV_N_FIXED_COLS + cell_str_it - std::get<PL_INPUT>(parse_line).cbegin();
1636             gtk_list_store_set (store, &iter, pos, cell_str_it->c_str(), -1);
1637         }
1638     }
1639     gtk_tree_view_set_model (treeview, GTK_TREE_MODEL(store));
1640     gtk_tree_view_set_tooltip_column (treeview, PREV_COL_ERROR);
1641 
1642     /* Adjust treeview to go with the just created model. This consists of adding
1643      * or removing columns and resetting any parameters related to how
1644      * the columns and data should be rendered.
1645      */
1646 
1647     /* Start with counting the current number of columns (ntcols)
1648      * we have in the treeview */
1649     auto columns = gtk_tree_view_get_columns (treeview);
1650     auto ntcols = g_list_length(columns);
1651     g_list_free (columns);
1652 
1653     /* Drop redundant columns if the model has less data columns than the new model
1654      * ntcols = n° of columns in treeview (1 error column + x data columns)
1655      * ncols = n° of columns in model (fixed state columns + x data columns)
1656      */
1657     while (ntcols > ncols - PREV_N_FIXED_COLS + 1)
1658     {
1659         auto col = gtk_tree_view_get_column (treeview, ntcols - 1);
1660         gtk_tree_view_column_clear (col);
1661         ntcols = gtk_tree_view_remove_column(treeview, col);
1662     }
1663 
1664     /* Insert columns if the model has more data columns than the treeview. */
1665     while (ntcols < ncols - PREV_N_FIXED_COLS + 1)
1666     {
1667         /* Default cell renderer is text, except for the first (error) column */
1668         auto renderer = gtk_cell_renderer_text_new();
1669         if (ntcols == 0)
1670             renderer = gtk_cell_renderer_pixbuf_new(); // Error column uses an icon
1671         auto col = gtk_tree_view_column_new ();
1672         gtk_tree_view_column_pack_start (col, renderer, false);
1673         ntcols = gtk_tree_view_append_column (treeview, col);
1674     }
1675 
1676     /* Reset column attributes as they are undefined after recreating the model */
1677     auto combostore = make_column_header_model_price ();
1678     for (uint32_t i = 0; i < ntcols; i++)
1679         preview_style_column (i, combostore);
1680 
1681     auto column_types = price_imp->column_types_price();
1682 
1683     // look for a namespace column, clear the commodity combo
1684     auto col_type_name = std::find (column_types.begin(),
1685                 column_types.end(), GncPricePropType::FROM_NAMESPACE);
1686     if (col_type_name != column_types.end())
1687     {
1688         g_signal_handlers_block_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this);
1689         set_commodity_for_combo (GTK_COMBO_BOX(commodity_selector), nullptr);
1690         g_signal_handlers_unblock_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this);
1691     }
1692 
1693     // look for a symbol column, clear the commodity combo
1694     auto col_type_sym = std::find (column_types.begin(),
1695                 column_types.end(), GncPricePropType::FROM_SYMBOL);
1696     if (col_type_sym != column_types.end())
1697     {
1698         g_signal_handlers_block_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this);
1699         set_commodity_for_combo (GTK_COMBO_BOX(commodity_selector), nullptr);
1700         g_signal_handlers_unblock_by_func (commodity_selector, (gpointer) csv_price_imp_preview_commodity_sel_cb, this);
1701     }
1702 
1703     // look for a currency column, clear the currency combo
1704     auto col_type_curr = std::find (column_types.begin(),
1705                 column_types.end(), GncPricePropType::TO_CURRENCY);
1706     if (col_type_curr != column_types.end())
1707     {
1708         g_signal_handlers_block_by_func (currency_selector, (gpointer) csv_price_imp_preview_currency_sel_cb, this);
1709         set_commodity_for_combo (GTK_COMBO_BOX(currency_selector), nullptr);
1710         g_signal_handlers_unblock_by_func (currency_selector, (gpointer) csv_price_imp_preview_currency_sel_cb, this);
1711     }
1712 
1713     /* Release our reference for the stores to allow proper memory management. */
1714     g_object_unref (store);
1715     g_object_unref (combostore);
1716 
1717     /* Make the things actually appear. */
1718     gtk_widget_show_all (GTK_WIDGET(treeview));
1719 }
1720 
1721 /* Update the preview page based on the current state of the importer.
1722  * Should be called when settings are changed.
1723  */
1724 void
preview_refresh()1725 CsvImpPriceAssist::preview_refresh ()
1726 {
1727     // Set start row
1728     auto adj = gtk_spin_button_get_adjustment (start_row_spin);
1729     gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size());
1730     gtk_spin_button_set_value (start_row_spin,
1731             price_imp->skip_start_lines());
1732 
1733     // Set end row
1734     adj = gtk_spin_button_get_adjustment (end_row_spin);
1735     gtk_adjustment_set_upper (adj, price_imp->m_parsed_lines.size());
1736     gtk_spin_button_set_value (end_row_spin,
1737             price_imp->skip_end_lines());
1738 
1739     // Set Alternate rows
1740     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(skip_alt_rows_button),
1741             price_imp->skip_alt_lines());
1742 
1743     // Set over-write indicator
1744     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(over_write_cbutton),
1745             price_imp->over_write());
1746 
1747     // Set Import Format
1748     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(csv_button),
1749             (price_imp->file_format() == GncImpFileFormat::CSV));
1750     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(fixed_button),
1751             (price_imp->file_format() != GncImpFileFormat::CSV));
1752 
1753     // This section deals with the combo's and character encoding
1754     gtk_combo_box_set_active (GTK_COMBO_BOX(date_format_combo),
1755             price_imp->date_format());
1756     gtk_combo_box_set_active (GTK_COMBO_BOX(currency_format_combo),
1757             price_imp->currency_format());
1758     go_charmap_sel_set_encoding (encselector, price_imp->encoding().c_str());
1759 
1760     // Set the commodity and currency combos
1761     set_commodity_for_combo(GTK_COMBO_BOX(commodity_selector),
1762             price_imp->from_commodity());
1763 
1764     set_commodity_for_combo(GTK_COMBO_BOX(currency_selector),
1765             price_imp->to_currency());
1766 
1767     // Handle separator checkboxes and custom field, only relevant if the file format is csv
1768     if (price_imp->file_format() == GncImpFileFormat::CSV)
1769     {
1770         auto separators = price_imp->separators();
1771         const auto stock_sep_chars = std::string (" \t,:;-");
1772         for (int i = 0; i < SEP_NUM_OF_TYPES; i++)
1773             gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(sep_button[i]),
1774                 separators.find (stock_sep_chars[i]) != std::string::npos);
1775 
1776         // If there are any other separators in the separators string,
1777         // add them as custom separators
1778         auto pos = separators.find_first_of (stock_sep_chars);
1779         while (!separators.empty() && pos != std::string::npos)
1780         {
1781             separators.erase(pos);
1782             pos = separators.find_first_of (stock_sep_chars);
1783         }
1784         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(custom_cbutton),
1785                 !separators.empty());
1786         gtk_entry_set_text (GTK_ENTRY(custom_entry), separators.c_str());
1787     }
1788     // Repopulate the parsed data table
1789     g_idle_add ((GSourceFunc)csv_imp_preview_queue_rebuild_table, this);
1790 }
1791 
1792 /* Check if all selected data can be parsed sufficiently to continue
1793  */
preview_validate_settings()1794 void CsvImpPriceAssist::preview_validate_settings ()
1795 {
1796     /* Allow the user to proceed only if there are no inconsistencies in the settings */
1797     auto error_msg = price_imp->verify();
1798     gtk_assistant_set_page_complete (csv_imp_asst, preview_page, error_msg.empty());
1799     gtk_label_set_markup(GTK_LABEL(instructions_label), error_msg.c_str());
1800     gtk_widget_set_visible (GTK_WIDGET(instructions_image), !error_msg.empty());
1801 }
1802 
1803 /*******************************************************
1804  * Assistant page prepare functions
1805  *******************************************************/
1806 
1807 void
assist_file_page_prepare()1808 CsvImpPriceAssist::assist_file_page_prepare ()
1809 {
1810     /* Disable the "Next" Assistant Button */
1811     gtk_assistant_set_page_complete (csv_imp_asst, file_page, false);
1812     gtk_assistant_set_page_complete (csv_imp_asst, preview_page, false);
1813 
1814     /* Set the default directory */
1815     auto starting_dir = gnc_get_default_directory (GNC_PREFS_GROUP);
1816     if (starting_dir)
1817     {
1818         gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(file_chooser), starting_dir);
1819         g_free (starting_dir);
1820     }
1821 }
1822 
1823 void
assist_preview_page_prepare()1824 CsvImpPriceAssist::assist_preview_page_prepare ()
1825 {
1826     auto go_back = false;
1827 
1828     /* Load the file into parse_data, reset it if altrady loaded. */
1829     if (price_imp)
1830         price_imp.reset();
1831 
1832     /* Load the file into parse_data. */
1833     price_imp = std::unique_ptr<GncPriceImport>(new GncPriceImport);
1834     /* Assume data is CSV. User can later override to Fixed Width if needed */
1835     try
1836     {
1837         price_imp->file_format (GncImpFileFormat::CSV);
1838         price_imp->load_file (m_file_name);
1839         price_imp->tokenize (true);
1840     }
1841     catch (std::ifstream::failure& e)
1842     {
1843         /* File loading failed ... */
1844         gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "%s", e.what());
1845         go_back = true;
1846     }
1847     catch (std::range_error &e)
1848     {
1849         /* Parsing failed ... */
1850         gnc_error_dialog (GTK_WINDOW(csv_imp_asst), "%s", _(e.what()));
1851         go_back = true;
1852     }
1853 
1854     if (go_back)
1855         gtk_assistant_previous_page (csv_imp_asst);
1856     else
1857     {
1858         /* Get settings store and populate */
1859         preview_populate_settings_combo();
1860         gtk_combo_box_set_active (settings_combo, 0);
1861 
1862         // set over_write to false as default
1863         price_imp->over_write (false);
1864 
1865         /* Disable the "Next" Assistant Button */
1866         gtk_assistant_set_page_complete (csv_imp_asst, preview_page, false);
1867 
1868         /* Load the data into the treeview. */
1869         g_idle_add ((GSourceFunc)csv_imp_preview_queue_rebuild_table, this);
1870     }
1871 }
1872 
1873 void
assist_confirm_page_prepare()1874 CsvImpPriceAssist::assist_confirm_page_prepare ()
1875 {
1876     /* Confirm Page */
1877 }
1878 
1879 void
assist_summary_page_prepare()1880 CsvImpPriceAssist::assist_summary_page_prepare ()
1881 {
1882     auto text = std::string("<span size=\"medium\"><b>");
1883     /* Translators: This is a ngettext(3) message, %d is the number of prices added */
1884     auto added_str = g_strdup_printf (ngettext ("%d added price",
1885                                                 "%d added prices",
1886                                                 price_imp->m_prices_added),
1887                                       price_imp->m_prices_added);
1888     /* Translators: This is a ngettext(3) message, %d is the number of duplicate prices */
1889     auto dupl_str = g_strdup_printf (ngettext ("%d duplicate price",
1890                                                "%d duplicate prices",
1891                                                price_imp->m_prices_duplicated),
1892                                      price_imp->m_prices_duplicated);
1893     /* Translators: This is a ngettext(3) message, %d is the number of replaced prices */
1894     auto repl_str = g_strdup_printf (ngettext ("%d replaced price",
1895                                                "%d replaced prices",
1896                                                price_imp->m_prices_replaced),
1897                                      price_imp->m_prices_replaced);
1898     auto msg = g_strdup_printf (
1899         _("The prices were imported from file '%s'.\n\n"
1900           "Import summary:\n"
1901           "- %s\n"
1902           "- %s\n"
1903           "- %s"),
1904           m_file_name.c_str(), added_str, dupl_str,repl_str);
1905     text += msg;
1906     text += "</b></span>";
1907 
1908     g_free (added_str);
1909     g_free (dupl_str);
1910     g_free (repl_str);
1911 
1912     gtk_label_set_markup (GTK_LABEL(summary_label), text.c_str());
1913 }
1914 
1915 void
assist_prepare_cb(GtkWidget * page)1916 CsvImpPriceAssist::assist_prepare_cb (GtkWidget *page)
1917 {
1918     if (page == file_page)
1919         assist_file_page_prepare ();
1920     else if (page == preview_page)
1921         assist_preview_page_prepare ();
1922     else if (page == confirm_page)
1923         assist_confirm_page_prepare ();
1924     else if (page == summary_page)
1925         assist_summary_page_prepare ();
1926 }
1927 
1928 void
assist_finish()1929 CsvImpPriceAssist::assist_finish ()
1930 {
1931     /* Start the import */
1932     /* Create prices from the parsed data */
1933     try
1934     {
1935         price_imp->create_prices ();
1936     }
1937     catch (const std::invalid_argument& err)
1938     {
1939         /* Oops! This shouldn't happen when using the import assistant !
1940          * Inform the user and go back to the preview page.
1941          */
1942         gnc_error_dialog (GTK_WINDOW(csv_imp_asst),
1943             _("An unexpected error has occurred while creating prices. Please report this as a bug.\n\n"
1944               "Error message:\n%s"), err.what());
1945         gtk_assistant_set_current_page (csv_imp_asst, 2);
1946     }
1947 }
1948 
1949 void
assist_compmgr_close()1950 CsvImpPriceAssist::assist_compmgr_close ()
1951 {
1952     gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(csv_imp_asst));
1953 }
1954 
1955 static void
csv_price_imp_close_handler(gpointer user_data)1956 csv_price_imp_close_handler (gpointer user_data)
1957 {
1958     auto info = (CsvImpPriceAssist*)user_data;
1959     gnc_unregister_gui_component_by_data (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS, info);
1960     info->assist_compmgr_close();
1961     delete info;
1962 }
1963 
1964 /********************************************************************\
1965  * gnc_file_csv_price_import                                        *
1966  * opens up a assistant to import prices.                           *
1967  *                                                                  *
1968  * Args:   none                                                     *
1969  * Return: nothing                                                  *
1970 \********************************************************************/
1971 void
gnc_file_csv_price_import(void)1972 gnc_file_csv_price_import(void)
1973 {
1974     auto info = new CsvImpPriceAssist;
1975     gnc_register_gui_component (ASSISTANT_CSV_IMPORT_PRICE_CM_CLASS,
1976                                 nullptr, csv_price_imp_close_handler,
1977                                 info);
1978 }
1979