diff --git a/gnucash/gnome-utils/CMakeLists.txt b/gnucash/gnome-utils/CMakeLists.txt index fb7e53c6b5..60a7994410 100644 --- a/gnucash/gnome-utils/CMakeLists.txt +++ b/gnucash/gnome-utils/CMakeLists.txt @@ -156,6 +156,7 @@ set (gnome_utils_HEADERS gnc-gui-query.h gnc-icons.h gnc-keyring.h + gnc-list-model-container.hpp gnc-main-window.h gnc-menu-extensions.h gnc-plugin-file-history.h diff --git a/gnucash/gnome-utils/gnc-list-model-container.hpp b/gnucash/gnome-utils/gnc-list-model-container.hpp new file mode 100644 index 0000000000..ff4d4011b8 --- /dev/null +++ b/gnucash/gnome-utils/gnc-list-model-container.hpp @@ -0,0 +1,187 @@ +/********************************************************************\ + * gnc-tree-container.hpp + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + +#ifndef GNC_LIST_MODEL_CONTAINER_HPP +#define GNC_LIST_MODEL_CONTAINER_HPP + +#include +#include +#include +#include + +class GncListModelData +{ +public: + GncListModelData (GtkTreeModel* model, const GtkTreeIter& iter) : m_model{model}, m_iter{iter} {}; + + template + T get_column (int column) + { + gpointer rv; + gtk_tree_model_get(m_model, &m_iter, column, &rv, -1); + return static_cast(rv); + } + + GtkTreeIter& get_iter () { return m_iter; }; + + int get_column_int (int column) + { + int rv; + gtk_tree_model_get(m_model, &m_iter, column, &rv, -1); + return rv; + } + + std::string get_column_string (int column) + { + auto str = get_column(column); + std::string rv{str}; + g_free (str); + return rv; + } + + void set_columns (int unused, ...) + { + va_list var_args; + va_start (var_args, unused); + gtk_list_store_set_valist (GTK_LIST_STORE(m_model), &get_iter(), var_args); + va_end (var_args); + } + + template + void set_column (int column, T data) { gtk_list_store_set(GTK_LIST_STORE(m_model), &get_iter(), column, data, -1); } + + // overloads the template when data is a std::string + void set_column (int column, const std::string& str) { set_column (column, str.c_str()); } + + bool operator==(const GncListModelData& other) const + { + return (m_model == other.m_model) && + (m_iter.stamp == other.m_iter.stamp) && + (m_iter.user_data == other.m_iter.user_data) && + (m_iter.user_data2 == other.m_iter.user_data2) && + (m_iter.user_data3 == other.m_iter.user_data3); + } + +private: + GtkTreeModel* m_model; + GtkTreeIter m_iter; +}; + +// Custom container class +template +class GncListModelContainer +{ +public: + + // Custom iterator class + class GncListModelIter + { + public: + /* Set iterator traits queried by STL algorithms. These are + required for std::find_if etc to iterate through the + container. */ + using iterator_category = std::forward_iterator_tag; + using value_type = ModelType; + using difference_type = std::ptrdiff_t; + using pointer = ModelType*; + using reference = ModelType&; + + GncListModelIter(GtkTreeModel* model, std::optional iter) : m_model(model), m_iter(iter) {} + + GncListModelIter(GtkTreeModel* model) : m_model (model) + { + GtkTreeIter iter; + m_iter = gtk_tree_model_get_iter_first(m_model, &iter) ? std::make_optional(iter) : std::nullopt; + } + + GncListModelIter& operator++() + { + if (!m_iter.has_value()) + throw "no value, cannot increment"; + if (!gtk_tree_model_iter_next(m_model, &m_iter.value())) + m_iter = std::nullopt; + return *this; + } + + ModelType operator*() const + { + if (!m_iter.has_value()) + throw "no value, cannot dereference"; + return ModelType (m_model, *m_iter); + } + + std::unique_ptr operator->() + { + if (!m_iter.has_value()) + throw "no value, cannot dereference"; + return std::make_unique (m_model, *m_iter); + } + + bool has_value() const { return m_iter.has_value(); }; + + bool operator==(const GncListModelIter& other) const + { + if (m_model != other.m_model) + return false; + if (!m_iter.has_value() && !other.m_iter.has_value()) + return true; + if (!m_iter.has_value() || !other.m_iter.has_value()) + return false; + return (ModelType (m_model, *m_iter) == ModelType (m_model, *other.m_iter)); + } + + bool operator!=(const GncListModelIter& other) const { return !(*this == other); } + + private: + GtkTreeModel* m_model; + std::optional m_iter; + }; + + GncListModelContainer(GtkTreeModel* model) : m_model(model) + { + g_return_if_fail (GTK_IS_TREE_MODEL (m_model)); + g_object_ref (m_model); + } + + ~GncListModelContainer () { g_object_unref (m_model); } + + GncListModelIter begin() const { return GncListModelIter(m_model); }; + + GncListModelIter end() const { return GncListModelIter(m_model, std::nullopt); }; + + GncListModelIter append() + { + GtkTreeIter iter; + gtk_list_store_append (GTK_LIST_STORE(m_model), &iter); + return GncListModelIter(m_model, iter); + }; + + size_t size() const { return std::distance (begin(), end()); } + + bool empty() const { return begin() == end(); }; + + void clear() { gtk_list_store_clear (GTK_LIST_STORE (m_model)); }; + +private: + GtkTreeModel* m_model; +}; + +#endif diff --git a/gnucash/gnome-utils/test/CMakeLists.txt b/gnucash/gnome-utils/test/CMakeLists.txt index ea25c04468..474c6bc7d8 100644 --- a/gnucash/gnome-utils/test/CMakeLists.txt +++ b/gnucash/gnome-utils/test/CMakeLists.txt @@ -39,13 +39,31 @@ set(test_autoclear_LIBS gtest ) +set(test_list_model_container_SOURCES + test-list-model-container.cpp +) + +set(test_list_model_container_INCLUDE_DIRS +) + +set(test_list_model_container_LIBS + gnc-gnome-utils + gtest +) + gnc_add_test(test-autoclear "${test_autoclear_SOURCES}" test_autoclear_INCLUDE_DIRS test_autoclear_LIBS ) +gnc_add_test(test-list-model-container "${test_list_model_container_SOURCES}" + test_list_model_container_INCLUDE_DIRS + test_list_model_container_LIBS +) + gnc_add_scheme_tests(test-load-gnome-utils-module.scm) set_dist_list(test_gnome_utils_DIST CMakeLists.txt test-load-gnome-utils-module.scm + ${test_list_model_container_SOURCES} ${test_autoclear_SOURCES}) diff --git a/gnucash/gnome-utils/test/test-list-model-container.cpp b/gnucash/gnome-utils/test/test-list-model-container.cpp new file mode 100644 index 0000000000..86dfca3614 --- /dev/null +++ b/gnucash/gnome-utils/test/test-list-model-container.cpp @@ -0,0 +1,124 @@ +/******************************************************************** + * test-tree-container.cpp: * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, you can retrieve it from * + * https://www.gnu.org/licenses/old-licenses/gpl-2.0.html * + * or contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + ********************************************************************/ + +#include "config.h" +#include +#include +#include "../gnc-list-model-container.hpp" +#include +#include + +enum { + COLUMN_STRING, + COLUMN_INT, + COLUMN_BOOLEAN, + N_COLUMNS +}; + + +TEST(GncListModelContainer, Equality) +{ + auto store1 = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN); + auto store2 = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN); + + GncListModelContainer container1{GTK_TREE_MODEL(store1)}; + GncListModelContainer container2{GTK_TREE_MODEL(store2)}; + + // these are null tests + EXPECT_TRUE (container1.begin() == container1.begin()); + EXPECT_TRUE (container1.end() == container1.end()); + + EXPECT_TRUE (container1.begin() == container1.end()); + EXPECT_TRUE (container1.size() == 0); + EXPECT_TRUE (container2.size() == 0); + EXPECT_TRUE (container1.empty()); + EXPECT_TRUE (container1.empty()); + + EXPECT_FALSE (container1.begin() == container2.begin()); + EXPECT_FALSE (container1.end() == container2.end()); + + // both containers have identical contents + container1.append()->set_column (COLUMN_STRING, "1"); + container2.append()->set_column (COLUMN_STRING, "1"); + + // the containers are now no longer empty + EXPECT_FALSE (container1.begin() == container1.end()); + EXPECT_FALSE (container2.begin() == container2.end()); + EXPECT_TRUE (container1.size() == 1); + EXPECT_TRUE (container2.size() == 1); + + // however the iterators behave as expected -- iterators from + // store1 must differ from iterators from store2 + EXPECT_FALSE (container1.begin() == container2.begin()); + EXPECT_FALSE (container1.end() == container2.end()); + + g_object_unref (store1); + g_object_unref (store2); +} + +TEST(GncListModelContainer, Basic) +{ + auto store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_BOOLEAN); + GncListModelContainer container{GTK_TREE_MODEL(store)}; + + // test empty + EXPECT_TRUE (container.empty()); + + for (size_t i = 0; i < 10; i++) + { + auto str = std::string("string ") + std::to_string(i); + auto iter = container.append (); + iter->set_columns (0, + COLUMN_STRING, str.c_str(), + COLUMN_INT, i, + COLUMN_BOOLEAN, (i % 2) == 0, + -1); + } + + // test non-empty + EXPECT_FALSE (container.empty()); + + // test size + EXPECT_TRUE (10 == container.size()); + + auto int_is_five = [](auto it){ return it.get_column_int(COLUMN_INT) == 5; }; + auto iter_found = std::find_if (container.begin(), container.end(), int_is_five); + EXPECT_TRUE (iter_found.has_value()); + EXPECT_EQ ("string 5", iter_found->get_column_string (COLUMN_STRING)); + + g_object_unref (store); +} + +int main(int argc, char** argv) +{ + if (gtk_init_check (nullptr, nullptr)) + std::cout << "gtk init completed!" << std::endl; + else + std::cout << "no display present!" << std::endl; + + // Initialize the Google Test framework + ::testing::InitGoogleTest(&argc, argv); + + // Run tests + return RUN_ALL_TESTS(); +} diff --git a/gnucash/import-export/import-match-picker.cpp b/gnucash/import-export/import-match-picker.cpp index 732012b698..738578f9d6 100644 --- a/gnucash/import-export/import-match-picker.cpp +++ b/gnucash/import-export/import-match-picker.cpp @@ -31,12 +31,15 @@ #include #include +#include "gnc-list-model-container.hpp" #include "import-match-picker.h" #include "qof.h" #include "gnc-ui-util.h" #include "dialog-utils.h" #include "gnc-prefs.h" +#include + /********************************************************************\ * Constants * \********************************************************************/ @@ -98,32 +101,22 @@ downloaded_transaction_append(GNCImportMatchPicker * matcher, g_return_if_fail (matcher); g_return_if_fail (transaction_info); - auto found = false; auto store = GTK_LIST_STORE(gtk_tree_view_get_model(matcher->downloaded_view)); auto split = gnc_import_TransInfo_get_fsplit(transaction_info); auto trans = gnc_import_TransInfo_get_trans(transaction_info); + GncListModelContainer container{GTK_TREE_MODEL(store)}; - /* Has the transaction already been added? */ - GtkTreeIter iter; - if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) - { - do - { - GNCImportTransInfo *local_info; - gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, - DOWNLOADED_COL_INFO_PTR, &local_info, - -1); - if (local_info == transaction_info) - { - found = TRUE; - break; - } - } - while (gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter)); - } - if (!found) - gtk_list_store_append(store, &iter); + auto it_matches = [&transaction_info](auto it) + { return it.template get_column(DOWNLOADED_COL_INFO_PTR) == transaction_info; }; + // find the GncTreeIter whose DOWNLOADED_COL_INFO_PTR matches transaction_info + auto iter = std::find_if (container.begin(), container.end(), it_matches); + + // not found. append a new GtkTreeIter in container. + if (iter == container.end()) + iter = container.append(); + + // now iter is a GncTreeIter; iter->get_iter() is the GtkTreeIter auto account = xaccAccountGetName(xaccSplitGetAccount(split)); auto date = qof_print_date(xaccTransGetDate(trans)); auto amount = g_strdup (xaccPrintAmount(xaccSplitGetAmount(split), gnc_split_amount_print_info(split, TRUE))); @@ -137,17 +130,17 @@ downloaded_transaction_append(GNCImportMatchPicker * matcher, auto imbalance = g_strdup (xaccPrintAmount (xaccTransGetImbalanceValue(trans), gnc_commodity_print_info (xaccTransGetCurrency (trans), TRUE))); - gtk_list_store_set (store, &iter, - DOWNLOADED_COL_ACCOUNT, account, - DOWNLOADED_COL_DATE, date, - DOWNLOADED_COL_AMOUNT, amount, - DOWNLOADED_COL_DESCRIPTION, desc, - DOWNLOADED_COL_MEMO, memo, - DOWNLOADED_COL_BALANCED, imbalance, - DOWNLOADED_COL_INFO_PTR, transaction_info, - -1); + iter->set_columns (0, + DOWNLOADED_COL_ACCOUNT, account, + DOWNLOADED_COL_DATE, date, + DOWNLOADED_COL_AMOUNT, amount, + DOWNLOADED_COL_DESCRIPTION, desc, + DOWNLOADED_COL_MEMO, memo, + DOWNLOADED_COL_BALANCED, imbalance, + DOWNLOADED_COL_INFO_PTR, transaction_info, + -1); - gtk_tree_selection_select_iter (gtk_tree_view_get_selection(matcher->downloaded_view), &iter); + gtk_tree_selection_select_iter (gtk_tree_view_get_selection(matcher->downloaded_view), &iter->get_iter()); g_free (date); g_free (amount);