diff --git a/libgnucash/app-utils/gnc-option-impl.cpp b/libgnucash/app-utils/gnc-option-impl.cpp index 9ac10c0122..38b1f8e048 100644 --- a/libgnucash/app-utils/gnc-option-impl.cpp +++ b/libgnucash/app-utils/gnc-option-impl.cpp @@ -32,6 +32,9 @@ extern "C" #include "gnc-ui-util.h" } +const std::string GncOptionMultichoiceValue::c_empty_string{""}; +const std::string GncOptionMultichoiceValue::c_list_string{"multiple values"}; + bool GncOptionAccountValue::validate(const GncOptionAccountList& values) const { diff --git a/libgnucash/app-utils/gnc-option-impl.hpp b/libgnucash/app-utils/gnc-option-impl.hpp index 9716fdb262..69951dbb13 100644 --- a/libgnucash/app-utils/gnc-option-impl.hpp +++ b/libgnucash/app-utils/gnc-option-impl.hpp @@ -412,13 +412,14 @@ private: ValueType m_step; }; -using GncMultiChoiceOptionEntry = std::tuple; -using GncMultiChoiceOptionChoices = std::vector; +using GncMultichoiceOptionIndexVec = std::vector; +using GncMultichoiceOptionChoices = std::vector; -/** MultiChoice options have a vector of valid options - * (GncMultiChoiceOptionChoices) and validate the selection as being one of +/** Multichoice options have a vector of valid options + * (GncMultichoiceOptionChoices) and validate the selection as being one of * those values. The value is the index of the selected item in the vector. The * tuple contains three strings, a key, a display * name and a brief description for the tooltip. Both name and description @@ -433,22 +434,48 @@ public: GncOptionMultichoiceValue(const char* section, const char* name, const char* key, const char* doc_string, const char* value, - GncMultiChoiceOptionChoices&& choices, + GncMultichoiceOptionChoices&& choices, GncOptionUIType ui_type = GncOptionUIType::MULTICHOICE) : OptionClassifier{section, name, key, doc_string}, m_ui_type{ui_type}, - m_value{}, m_default_value{}, m_choices{std::move(choices)} { - if (value) + m_value{}, m_default_value{}, m_choices{std::move(choices)} + { + if (value) + { + if (auto index = find_key(value); + index != size_t_max) { - if (auto index = find_key(value); - index != size_t_max) - { - m_value = index; - m_default_value = index; - } + m_value.push_back(index); + m_default_value.push_back(index); } } + } + GncOptionMultichoiceValue(const char* section, const char* name, + const char* key, const char* doc_string, + size_t index, + GncMultichoiceOptionChoices&& choices, + GncOptionUIType ui_type = GncOptionUIType::MULTICHOICE) : + OptionClassifier{section, name, key, doc_string}, + m_ui_type{ui_type}, + m_value{}, m_default_value{}, m_choices{std::move(choices)} + { + if (index < m_choices.size()) + { + m_value.push_back(index); + m_default_value.push_back(index); + } + } + + GncOptionMultichoiceValue(const char* section, const char* name, + const char* key, const char* doc_string, + GncMultichoiceOptionIndexVec&& indices, + GncMultichoiceOptionChoices&& choices, + GncOptionUIType ui_type = GncOptionUIType::LIST) : + OptionClassifier{section, name, key, doc_string}, + m_ui_type{ui_type}, + m_value{indices}, m_default_value{std::move(indices)}, + m_choices{std::move(choices)} {} GncOptionMultichoiceValue(const GncOptionMultichoiceValue&) = default; GncOptionMultichoiceValue(GncOptionMultichoiceValue&&) = default; GncOptionMultichoiceValue& operator=(const GncOptionMultichoiceValue&) = default; @@ -456,27 +483,84 @@ public: const std::string& get_value() const { - return std::get<0>(m_choices.at(m_value)); + auto vec{m_value.size() > 0 ? m_value : m_default_value}; + if (vec.size() == 0) + return c_empty_string; + if (vec.size() == 1) + return std::get<0>(m_choices.at(vec[0])); + else + return c_list_string; } const std::string& get_default_value() const { - return std::get<0>(m_choices.at(m_default_value)); + if (m_default_value.size() == 1) + return std::get<0>(m_choices.at(m_default_value[0])); + else if (m_default_value.size() == 0) + return c_empty_string; + else + return c_list_string; } - bool validate(const std::string& value) const noexcept + + size_t get_index() const + { + if (m_value.size() > 0) + return m_value[0]; + if (m_default_value.size() > 0) + return m_default_value[0]; + return 0; + } + const GncMultichoiceOptionIndexVec& get_multiple() const noexcept + { + return m_value; + } + const GncMultichoiceOptionIndexVec& get_default_multiple() const noexcept + { + return m_default_value; + } + bool validate(const std::string& value) const noexcept { auto index = find_key(value); return index != size_t_max; + } + bool validate(const GncMultichoiceOptionIndexVec& indexes) const noexcept + { + for (auto index : indexes) + if (index >= m_choices.size()) + return false; + return true; + } void set_value(const std::string& value) { auto index = find_key(value); if (index != size_t_max) - m_value = index; + { + m_value.clear(); + m_value.push_back(index); + } else throw std::invalid_argument("Value not a valid choice."); } + void set_value(size_t index) + { + if (index < m_choices.size()) + { + m_value.clear(); + m_value.push_back(index); + } + else + throw std::invalid_argument("Value not a valid choice."); + + } + void set_multiple(const GncMultichoiceOptionIndexVec& indexes) + { + if (validate(indexes)) + m_value = indexes; + else + throw std::invalid_argument("One of the supplied indexes was out of range."); + } std::size_t num_permissible_values() const noexcept { return m_choices.size(); @@ -513,11 +597,119 @@ private: } GncOptionUIType m_ui_type; - std::size_t m_value; - std::size_t m_default_value; - GncMultiChoiceOptionChoices m_choices; + GncMultichoiceOptionIndexVec m_value; + GncMultichoiceOptionIndexVec m_default_value; + GncMultichoiceOptionChoices m_choices; + static const std::string c_empty_string; + static const std::string c_list_string; }; +template<> inline std::ostream& +operator<< (std::ostream& oss, + const GncOptionMultichoiceValue& opt) +{ + auto vec{opt.get_multiple()}; + bool first{true}; + for (auto index : vec) + { + if (first) + first = false; + else + oss << " "; + oss << opt.permissible_value(index); + } + return oss; +} + +template<> inline std::istream& +operator>> (std::istream& iss, + GncOptionMultichoiceValue& opt) +{ + GncMultichoiceOptionIndexVec values; + while (true) + { + std::string str; + std::getline(iss, str, ' '); + if (!str.empty()) + { + auto index = opt.permissible_value_index(str.c_str()); + if (index != size_t_max) + values.push_back(index); + else + { + std::string err = str + " is not one of "; + err += opt.m_name; + err += "'s permissible values."; + throw std::invalid_argument(err); + } + } + else + break; + } + opt.set_multiple(values); + iss.clear(); + return iss; +} + +template, GncOptionMultichoiceValue>, int> = 0> +inline std::ostream& +gnc_option_to_scheme(std::ostream& oss, const OptType& opt) +{ + auto indexes{opt.get_multiple()}; + if (indexes.size() > 1) + oss << "'("; + bool first = true; + for (auto index : indexes) + { + if (first) + first = false; + else + oss << " "; + oss << "'" << opt.permissible_value(index); + } + if (indexes.size() > 1) + oss << ')'; + return oss; +} + +template, GncOptionMultichoiceValue>, int> = 0> +inline std::istream& +gnc_option_from_scheme(std::istream& iss, OptType& opt) +{ + iss.ignore(3, '\''); + auto c{iss.peek()}; + if (static_cast(c) == '(') + { + GncMultichoiceOptionIndexVec values; + iss.ignore(3, '\''); + while (true) + { + std::string str; + std::getline(iss, str, ' '); + if (!str.empty()) + { + if (str.back() == ')') + { + str.pop_back(); + break; + } + values.push_back(opt.permissible_value_index(str.c_str())); + iss.ignore(2, '\''); + } + else + break; + } + opt.set_multiple(values); + } + else + { + std::string str; + std::getline(iss, str, ' '); + opt.set_value(str); + } + return iss; +} + using GncOptionAccountList = std::vector; using GncOptionAccountTypeList = std::vector; diff --git a/libgnucash/app-utils/gnc-option.cpp b/libgnucash/app-utils/gnc-option.cpp index 5abc02622f..b4b8e33f7b 100644 --- a/libgnucash/app-utils/gnc-option.cpp +++ b/libgnucash/app-utils/gnc-option.cpp @@ -58,6 +58,18 @@ GncOption::get_value() const std::is_same_v, size_t>) return option.get_period_index(); + if constexpr + (std::is_same_v, + GncOptionMultichoiceValue> && + std::is_same_v, + size_t>) + return option.get_index(); + if constexpr + (std::is_same_v, + GncOptionMultichoiceValue> && + std::is_same_v, + GncMultichoiceOptionIndexVec>) + return option.get_multiple(); return ValueType {}; }, *m_option); } @@ -78,6 +90,12 @@ GncOption::get_default_value() const std::is_same_v, size_t>) return option.get_default_period_index(); + if constexpr + (std::is_same_v, + GncOptionMultichoiceValue> && + std::is_same_v, + GncMultichoiceOptionIndexVec>) + return option.get_default_multiple(); return ValueType {}; }, *m_option); @@ -96,6 +114,12 @@ GncOption::set_value(ValueType value) RelativeDatePeriod> || std::is_same_v, size_t>))) option.set_value(value); + if constexpr + (std::is_same_v, + GncOptionMultichoiceValue> && + std::is_same_v, + GncMultichoiceOptionIndexVec>) + option.set_multiple(value); }, *m_option); } @@ -221,6 +245,10 @@ GncOption::validate(ValueType value) const GncOptionMultichoiceValue> && std::is_same_v, std::string>) || + (std::is_same_v, + GncOptionMultichoiceValue> && + std::is_same_v, + GncMultichoiceOptionIndexVec>) || std::is_same_v, GncOptionValidatedValue>) return option.validate(value); @@ -334,19 +362,15 @@ GncOption::to_scheme(std::ostream& oss) const { return std::visit([&oss](auto& option) ->std::ostream& { if constexpr - (std::is_same_v, - GncOptionAccountValue>) - gnc_option_to_scheme(oss, option); - else if constexpr - (std::is_same_v, - GncOptionMultichoiceValue>) - oss << "'" << option; - else if constexpr - (std::is_same_v, + ((std::is_same_v, + GncOptionAccountValue>) || + (std::is_same_v, + GncOptionMultichoiceValue>) || + std::is_same_v, GncOptionValue> || std::is_same_v, GncOptionValidatedValue>) - gnc_option_to_scheme(oss, option); + gnc_option_to_scheme(oss, option); else if constexpr (std::is_same_v, GncOptionDateValue>) @@ -366,18 +390,11 @@ GncOption::from_scheme(std::istream& iss) { return std::visit([&iss](auto& option) -> std::istream& { if constexpr - (std::is_same_v, - GncOptionAccountValue>) - gnc_option_from_scheme(iss, option); - else if constexpr ((std::is_same_v, - GncOptionMultichoiceValue>)) - { - iss.ignore(1, '\''); - iss >> option; - } - else if constexpr - (std::is_same_v, + GncOptionAccountValue>) || + (std::is_same_v, + GncOptionMultichoiceValue>) || + std::is_same_v, GncOptionValue> || std::is_same_v, GncOptionValidatedValue>) @@ -438,6 +455,7 @@ template std::string GncOption::get_value() const; template const QofInstance* GncOption::get_value() const; template RelativeDatePeriod GncOption::get_value() const; template GncOptionAccountList GncOption::get_value() const; +template GncMultichoiceOptionIndexVec GncOption::get_value() const; template bool GncOption::get_default_value() const; template int GncOption::get_default_value() const; @@ -447,6 +465,7 @@ template const char* GncOption::get_default_value() const; template std::string GncOption::get_default_value() const; template const QofInstance* GncOption::get_default_value() const; template RelativeDatePeriod GncOption::get_default_value() const; +template GncMultichoiceOptionIndexVec GncOption::get_default_value() const; template void GncOption::set_value(bool); template void GncOption::set_value(int); @@ -457,6 +476,7 @@ template void GncOption::set_value(std::string); template void GncOption::set_value(const QofInstance*); template void GncOption::set_value(RelativeDatePeriod); template void GncOption::set_value(size_t); +template void GncOption::set_value(GncMultichoiceOptionIndexVec); template bool GncOption::validate(bool) const; template bool GncOption::validate(int) const; @@ -466,3 +486,4 @@ template bool GncOption::validate(const char*) const; template bool GncOption::validate(std::string) const; template bool GncOption::validate(const QofInstance*) const; template bool GncOption::validate(RelativeDatePeriod) const; +template bool GncOption::validate(GncMultichoiceOptionIndexVec) const; diff --git a/libgnucash/app-utils/test/gtest-gnc-option.cpp b/libgnucash/app-utils/test/gtest-gnc-option.cpp index a1144a4e0d..d36b19b752 100644 --- a/libgnucash/app-utils/test/gtest-gnc-option.cpp +++ b/libgnucash/app-utils/test/gtest-gnc-option.cpp @@ -1033,6 +1033,95 @@ TEST_F(GncMultichoiceOption, test_multichoice_scheme_in) EXPECT_STREQ("pork", m_option.get_value().c_str()); } +class GncOptionListTest : public ::testing::Test +{ +protected: + GncOptionListTest() : + m_option{GncOptionMultichoiceValue{"foo", "bar", "baz", + "Phony Option", + GncMultichoiceOptionIndexVec{0, 2}, + { + {"plugh", "xyzzy", "thud"}, + {"waldo", "pepper", "salt"}, + {"pork", "sausage", "links"}, + {"corge", "grault", "garply"} + }}} {} + GncOption m_option; +}; + +using GncListOption = GncOptionListTest; + +TEST_F(GncListOption, test_option_ui_type) +{ + EXPECT_EQ(GncOptionUIType::LIST, m_option.get_ui_type()); +} + +TEST_F(GncListOption, test_validate) +{ + EXPECT_TRUE(m_option.validate(std::string{"pork"})); + EXPECT_TRUE(m_option.validate(GncMultichoiceOptionIndexVec{1, 3})); + EXPECT_FALSE(m_option.validate(GncMultichoiceOptionIndexVec{2, 6})); +} + +TEST_F(GncListOption, test_set_value) +{ + EXPECT_NO_THROW({ + m_option.set_value(GncMultichoiceOptionIndexVec{1, 3}); + EXPECT_STREQ("multiple values", + m_option.get_value().c_str()); + EXPECT_EQ(1U, m_option.get_value()); + auto vec{m_option.get_value()}; + ASSERT_EQ(2U, vec.size()); + EXPECT_EQ(1U, vec[0]); + EXPECT_EQ(3U, vec[1]); + }); + EXPECT_THROW({ m_option.set_value(GncMultichoiceOptionIndexVec{2, 5}); }, std::invalid_argument); + EXPECT_EQ(1U, m_option.get_value()); +} + +TEST_F(GncListOption, test_list_out) +{ + auto vec{m_option.get_value()}; + std::string value{m_option.permissible_value(vec[0])}; + value += " "; + value += m_option.permissible_value(vec[1]); + std::ostringstream oss; + oss << m_option; + EXPECT_EQ(oss.str(), value); +} + +TEST_F(GncListOption, test_list_in) +{ + std::istringstream iss{"grault"}; + EXPECT_THROW({ + iss >> m_option; + }, std::invalid_argument); + iss.clear(); //reset failedbit + iss.str("pork"); + iss >> m_option; + EXPECT_EQ(iss.str(), m_option.get_value()); +} + +TEST_F(GncListOption, test_list_scheme_out) +{ + std::ostringstream oss; + m_option.to_scheme(oss); + std::string value{"'('"}; + auto vec{m_option.get_value()}; + value += m_option.permissible_value(vec[0]); + value += " '"; + value += m_option.permissible_value(vec[1]); + value += ")"; + EXPECT_EQ(value, oss.str()); +} + +TEST_F(GncListOption, test_list_scheme_in) +{ + std::istringstream iss{"'('pork 'waldo)"}; + m_option.from_scheme(iss); + EXPECT_STREQ("pork", m_option.get_value().c_str()); +} + static time64 time64_from_gdate(const GDate* g_date, DayPart when) {