From 29e495a17ffa6e2d50b6d4cb8ae40aa7c877efdb Mon Sep 17 00:00:00 2001 From: Kyle Reed <3761006+kallanreed@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:48:36 -0700 Subject: [PATCH] Freqman UI (#1255) * FreqmanDB direct file * Clear UI for short lists * Final touches on freqlist UI. * Support vertical alignment in NewButton * New buttons in FreqMan * Wiring up UI to filewrapper actions * Work around empty file --- firmware/application/apps/ui_freqman.cpp | 361 +++++++++--------- firmware/application/apps/ui_freqman.hpp | 147 ++++--- firmware/application/apps/ui_recon.cpp | 15 +- .../application/apps/ui_recon_settings.cpp | 11 +- firmware/application/apps/ui_scanner.cpp | 15 +- firmware/application/bitmap.hpp | 38 ++ firmware/application/file.cpp | 31 +- firmware/application/file.hpp | 39 +- firmware/application/file_wrapper.hpp | 3 +- firmware/application/freqman.cpp | 133 +------ firmware/application/freqman.hpp | 13 +- firmware/application/freqman_db.cpp | 221 ++++++++++- firmware/application/freqman_db.hpp | 89 ++++- firmware/application/string_format.cpp | 35 +- firmware/application/string_format.hpp | 13 +- firmware/application/ui/ui_freqlist.cpp | 245 ++++++------ firmware/application/ui/ui_freqlist.hpp | 47 ++- firmware/common/ui_widget.cpp | 77 ++-- firmware/common/ui_widget.hpp | 14 +- firmware/graphics/icon_add.png | Bin 0 -> 234 bytes firmware/test/application/CMakeLists.txt | 5 +- firmware/test/application/test_freqman_db.cpp | 38 +- .../test/application/test_string_format.cpp | 49 +++ 23 files changed, 979 insertions(+), 660 deletions(-) create mode 100644 firmware/graphics/icon_add.png create mode 100644 firmware/test/application/test_string_format.cpp diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index 9c7cad38..60645819 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -22,278 +23,284 @@ #include "ui_freqman.hpp" -#include "portapack.hpp" #include "event_m0.hpp" +#include "portapack.hpp" +#include "rtc_time.hpp" +#include "utility.hpp" #include using namespace portapack; +namespace fs = std::filesystem; namespace ui { -static int32_t current_category_id = 0; +/* FreqManBaseView ***************************************/ + +size_t FreqManBaseView::current_category_index = 0; FreqManBaseView::FreqManBaseView( NavigationView& nav) : nav_(nav) { - add_children({&options_category, - &label_category, - &button_exit}); + add_children( + {&label_category, + &options_category, + &button_exit}); - // initialize - refresh_list(); - options_category.on_change = [this](size_t category_id, int32_t) { - change_category(category_id); + options_category.on_change = [this](size_t new_index, int32_t) { + change_category(new_index); }; - options_category.set_selected_index(current_category_id); button_exit.on_select = [this, &nav](Button&) { nav.pop(); }; + + refresh_categories(); }; void FreqManBaseView::focus() { button_exit.focus(); + // TODO: Shouldn't be on focus. if (error_ == ERROR_ACCESS) { - nav_.display_modal("Error", "File acces error", ABORT, nullptr); + nav_.display_modal("Error", "File access error", ABORT, nullptr); } else if (error_ == ERROR_NOFILES) { - nav_.display_modal("Error", "No database files\nin /freqman", ABORT, nullptr); + nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT, nullptr); } else { options_category.focus(); } } -void FreqManBaseView::get_freqman_files() { - // Assume this does change much, clear will preserve the existing alloc. - file_list.clear(); +void FreqManBaseView::change_category(size_t new_index) { + if (categories().empty()) + return; - auto files = scan_root_files(u"FREQMAN", u"*.TXT"); - - for (auto file : files) { - std::string file_name = file.stem().string(); - // don't propose tmp / hidden files in freqman's list - if (file_name.length() && file_name[0] != '.') { - file_list.emplace_back(std::move(file_name)); - } - } -}; - -void FreqManBaseView::change_category(int32_t category_id) { - current_category_id = category_id; - - if (file_list.empty()) return; - - if (!load_freqman_file(file_list[categories[category_id].second], database, {})) { + current_category_index = new_index; + if (!db_.open(get_freqman_path(current_category()))) { error_ = ERROR_ACCESS; } - freqlist_view.set_db(database); - text_empty.hidden(!database.empty()); - set_dirty(); + + freqlist_view.set_db(db_); } -void FreqManBaseView::refresh_list() { - categories.clear(); - get_freqman_files(); +void FreqManBaseView::refresh_categories() { + OptionsField::options_t new_categories; - for (size_t n = 0; n < file_list.size(); n++) - categories.emplace_back(std::make_pair(file_list[n].substr(0, 14), n)); + scan_root_files( + freqman_dir, u"*.TXT", [&new_categories](const fs::path& path) { + // Skip temp/hidden files. + if (path.empty() || path.native()[0] == u'.') + return; - // Alphabetical sort - std::sort(categories.begin(), categories.end(), [](auto& left, auto& right) { + // The UI layer will truncate long file names when displaying. + new_categories.emplace_back(path.stem().string(), new_categories.size()); + }); + + // Alphabetically sort the categories. + std::sort(new_categories.begin(), new_categories.end(), [](auto& left, auto& right) { return left.first < right.first; }); - options_category.set_options(categories); - if ((unsigned)current_category_id >= categories.size()) - current_category_id = categories.size() - 1; + // Preserve last selection; ensure in range. + current_category_index = clip(current_category_index, 0u, new_categories.size()); + auto saved_index = current_category_index; + options_category.set_options(std::move(new_categories)); + options_category.set_selected_index(saved_index); } -void FrequencySaveView::save_current_file() { - save_freqman_file(file_list[categories[current_category_id].second], database); - nav_.pop(); +void FreqManBaseView::refresh_list(int delta_selected) { + // Update the index and ensures in bounds. + freqlist_view.set_index(freqlist_view.get_index() + delta_selected); + freqlist_view.set_dirty(); } -void FrequencySaveView::on_save_name() { - text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) { - database.push_back(std::make_unique(freqman_entry{value_, 0, buffer, freqman_type::Single})); - save_current_file(); - }); -} - -void FrequencySaveView::on_save_timestamp() { - database.push_back(std::make_unique(freqman_entry{value_, 0, live_timestamp.string(), freqman_type::Single})); - save_current_file(); -} +/* FrequencySaveView *************************************/ FrequencySaveView::FrequencySaveView( NavigationView& nav, const rf::Frequency value) - : FreqManBaseView(nav), - value_(value) { - desc_buffer.reserve(28); + : FreqManBaseView(nav) { + add_children( + {&labels, + &big_display, + &button_clear, + &button_edit, + &button_save, + &text_description}); - // Todo: add back ? - /*for (size_t n = 0; n < database.size(); n++) { - if (database[n].value == value_) { - error_ = ERROR_DUPLICATE; - break; - } - }*/ + entry_.type = freqman_type::Single; + entry_.frequency_a = value; + entry_.description = to_string_timestamp(rtc_time::now()); + refresh_ui(); - add_children({&labels, - &big_display, - &button_save_name, - &button_save_timestamp, - &live_timestamp}); - - big_display.set(value); - - button_save_name.on_select = [this, &nav](Button&) { - on_save_name(); - }; - button_save_timestamp.on_select = [this, &nav](Button&) { - on_save_timestamp(); + button_clear.on_select = [this, &nav](Button&) { + entry_.description = ""; + refresh_ui(); }; - options_category.on_change = [this, value](size_t category_id, int32_t) { - change_category(category_id); - big_display.set(value); + button_edit.on_select = [this, &nav](Button&) { + temp_buffer_ = entry_.description; + text_prompt(nav_, temp_buffer_, 30, [this](std::string& new_desc) { + entry_.description = new_desc; + refresh_ui(); + }); + }; + + button_save.on_select = [this, &nav](Button&) { + db_.insert_entry(entry_, db_.entry_count()); + nav_.pop(); }; } -void FrequencyLoadView::refresh_widgets(const bool v) { - freqlist_view.hidden(v); - text_empty.hidden(!v); - // display.fill_rectangle(freqlist_view.screen_rect(), Color::black()); - set_dirty(); +void FrequencySaveView::refresh_ui() { + big_display.set(entry_.frequency_a); + text_description.set(entry_.description); } +/* FrequencyLoadView *************************************/ + FrequencyLoadView::FrequencyLoadView( NavigationView& nav) : FreqManBaseView(nav) { - on_refresh_widgets = [this](bool v) { - refresh_widgets(v); - }; + add_children({&freqlist_view}); - add_children({&freqlist_view, - &text_empty}); + // Resize to fill screen. +2 keeps text out of border. + freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2}); - // Resize menu view to fill screen - freqlist_view.set_parent_rect({0, 3 * 8, 240, 30 * 8}); + freqlist_view.on_select = [&nav, this](size_t index) { + auto entry = db_[index]; + // TODO: Maybe return center of range if user choses a range when the app + // needs a unique frequency, instead of frequency_a? + auto has_range = entry.type == freqman_type::Range || + entry.type == freqman_type::HamRadio; - freqlist_view.on_select = [&nav, this](FreqManUIList&) { - auto& entry = database[freqlist_view.get_index()]; - if (entry->type == freqman_type::Range) { - if (on_range_loaded) - on_range_loaded(entry->frequency_a, entry->frequency_b); - else if (on_frequency_loaded) - on_frequency_loaded(entry->frequency_a); - // TODO: Maybe return center of range if user choses a range when the app - // needs a unique frequency, instead of frequency_a? - // TODO: HamRadio? - } else { - if (on_frequency_loaded) - on_frequency_loaded(entry->frequency_a); - } + if (on_range_loaded && has_range) + on_range_loaded(entry.frequency_a, entry.frequency_b); + else if (on_frequency_loaded) + on_frequency_loaded(entry.frequency_a); nav_.pop(); // NB: this will call dtor. }; + freqlist_view.on_leave = [this]() { + button_exit.focus(); + }; } -void FrequencyManagerView::on_edit_freq(rf::Frequency f) { - database[freqlist_view.get_index()]->frequency_a = f; - save_freqman_file(file_list[categories[current_category_id].second], database); - change_category(current_category_id); +/* FrequencyManagerView **********************************/ + +void FrequencyManagerView::on_edit_freq() { + // TODO: range edit support? + auto freq_edit_view = nav_.push(current_entry().frequency_a); + freq_edit_view->on_changed = [this](rf::Frequency f) { + auto entry = current_entry(); + entry.frequency_a = f; + db_.replace_entry(current_index(), entry); + freqlist_view.set_dirty(); + }; } -void FrequencyManagerView::on_edit_desc(NavigationView& nav) { - text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) { - database[freqlist_view.get_index()]->description = std::move(buffer); - save_freqman_file(file_list[categories[current_category_id].second], database); - change_category(current_category_id); +void FrequencyManagerView::on_edit_desc() { + temp_buffer_ = current_entry().description; + text_prompt(nav_, temp_buffer_, 28, [this](std::string& new_desc) { + auto entry = current_entry(); + entry.description = std::move(new_desc); + db_.replace_entry(current_index(), entry); + freqlist_view.set_dirty(); }); } -void FrequencyManagerView::on_new_category(NavigationView& nav) { - text_prompt(nav, desc_buffer, 12, [this](std::string& buffer) { - File freqman_file; - create_freqman_file(buffer, freqman_file); - refresh_list(); - change_category(current_category_id); +void FrequencyManagerView::on_add_category() { + temp_buffer_.clear(); + text_prompt(nav_, temp_buffer_, 12, [this](std::string& new_name) { + if (!new_name.empty()) { + create_freqman_file(new_name); + refresh_categories(); + } }); } -void FrequencyManagerView::on_delete() { - if (database.empty()) { - delete_freqman_file(file_list[categories[current_category_id].second]); - refresh_list(); - } else { - database.erase(database.begin() + freqlist_view.get_index()); - save_freqman_file(file_list[categories[current_category_id].second], database); - } - change_category(current_category_id); +void FrequencyManagerView::on_del_category() { + nav_.push( + "Delete", "Delete " + current_category() + "\nAre you sure?", YESNO, + [this](bool choice) { + if (choice) { + db_.close(); // Ensure file is closed. + auto path = get_freqman_path(current_category()); + delete_file(path); + refresh_categories(); + } + }); } -void FrequencyManagerView::refresh_widgets(const bool v) { - button_edit_freq.hidden(v); - button_edit_desc.hidden(v); - button_delete.hidden(v); - text_empty.hidden(!v); - freqlist_view.hidden(v); - labels.hidden(v); - // display.fill_rectangle(freqlist_view.screen_rect(), Color::black()); - set_dirty(); +void FrequencyManagerView::on_add_entry() { + freqman_entry entry{ + .frequency_a = 100'000'000, + .description = std::string{"Entry "} + to_string_dec_uint(db_.entry_count()), + .type = freqman_type::Single, + }; + + // Add will insert below the currently selected item. + db_.insert_entry(entry, current_index() + 1); + refresh_list(1); } -FrequencyManagerView::~FrequencyManagerView() { - // save_freqman_file(file_list[categories[current_category_id].second], database); +void FrequencyManagerView::on_del_entry() { + if (db_.empty()) + return; + + nav_.push( + "Delete", "Delete" + pretty_string(current_entry(), 23) + "\nAre you sure?", YESNO, + [this](bool choice) { + if (choice) { + db_.delete_entry(current_index()); + refresh_list(); + } + }); } FrequencyManagerView::FrequencyManagerView( NavigationView& nav) : FreqManBaseView(nav) { - on_refresh_widgets = [this](bool v) { - refresh_widgets(v); + add_children( + {&freqlist_view, + &labels, + &button_add_category, + &button_del_category, + &button_edit_freq, + &button_edit_desc, + &button_add_entry, + &button_del_entry}); + + freqlist_view.on_select = [this](size_t) { + button_edit_freq.focus(); }; - - add_children({&labels, - &button_new_category, - &freqlist_view, - &text_empty, - &button_edit_freq, - &button_edit_desc, - &button_delete}); - - freqlist_view.on_select = [this](FreqManUIList&) { + // Allows for quickly exiting control. + freqlist_view.on_leave = [this]() { button_edit_freq.focus(); }; - button_new_category.on_select = [this, &nav](Button&) { - desc_buffer = ""; - on_new_category(nav); + button_add_category.on_select = [this]() { + on_add_category(); }; - button_edit_freq.on_select = [this, &nav](Button&) { - if (database.empty()) - database.push_back(std::make_unique(freqman_entry{0, 0, "", freqman_type::Single})); - - auto new_view = nav.push(database[freqlist_view.get_index()]->frequency_a); - new_view->on_changed = [this](rf::Frequency f) { - on_edit_freq(f); - }; + button_del_category.on_select = [this]() { + on_del_category(); }; - button_edit_desc.on_select = [this, &nav](Button&) { - if (database.empty()) - database.push_back(std::make_unique(freqman_entry{0, 0, "", freqman_type::Single})); - - desc_buffer = database[freqlist_view.get_index()]->description; - on_edit_desc(nav); + button_edit_freq.on_select = [this](Button&) { + on_edit_freq(); }; - button_delete.on_select = [this, &nav](Button&) { - on_delete(); + button_edit_desc.on_select = [this](Button&) { + on_edit_desc(); + }; + + button_add_entry.on_select = [this]() { + on_add_entry(); + }; + + button_del_entry.on_select = [this]() { + on_del_entry(); }; } diff --git a/firmware/application/apps/ui_freqman.hpp b/firmware/application/apps/ui_freqman.hpp index be54e467..dbf6e923 100644 --- a/firmware/application/apps/ui_freqman.hpp +++ b/firmware/application/apps/ui_freqman.hpp @@ -1,6 +1,7 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -20,15 +21,16 @@ * Boston, MA 02110-1301, USA. */ +#include "freqman.hpp" +#include "freqman_db.hpp" #include "ui.hpp" -#include "ui_widget.hpp" -#include "ui_painter.hpp" +#include "ui_freqlist.hpp" #include "ui_menu.hpp" #include "ui_navigation.hpp" +#include "ui_painter.hpp" #include "ui_receiver.hpp" #include "ui_textentry.hpp" -#include "freqman.hpp" -#include "ui_freqlist.hpp" +#include "ui_widget.hpp" namespace ui { @@ -40,74 +42,77 @@ class FreqManBaseView : public View { void focus() override; protected: - using option_t = std::pair; - using options_t = std::vector; + using options_t = OptionsField::options_t; NavigationView& nav_; freqman_error error_{NO_ERROR}; - options_t categories{}; std::function on_select_frequency{nullptr}; - std::function on_refresh_widgets{nullptr}; - void get_freqman_files(); - void change_category(int32_t category_id); - void refresh_list(); + void change_category(size_t new_index); + /* Access the categories directly from the OptionsField. + * This avoids holding multiple copies of the file list. */ + const options_t& categories() const { return options_category.options(); } + const auto& current_category() const { return options_category.selected_index_name(); } + auto current_index() const { return freqlist_view.get_index(); } + freqman_entry current_entry() const { return db_[current_index()]; } + void refresh_categories(); + void refresh_list(int delta_selected = 0); - freqman_db database{}; - std::vector file_list{}; + FreqmanDB db_{}; + /* The top section (category) is 20px tall. */ Labels label_category{ - {{0, 4}, "Category:", Color::light_grey()}}; + {{0, 2}, "Category:", Color::light_grey()}}; OptionsField options_category{ - {9 * 8, 4}, - 14, + {9 * 8, 2}, + 14 /* length */, {}}; FreqManUIList freqlist_view{ - {0, 3 * 8, 240, 23 * 8}}; - - Text text_empty{ - {7 * 8, 12 * 8, 16 * 8, 16}, - "Empty category !", - }; + {0, 3 * 8, screen_width, 12 * 16 + 2 /* 2 Keeps text out of border. */}}; Button button_exit{ - {16 * 8, 34 * 8, 14 * 8, 4 * 8}, + {15 * 8, 17 * 16, 15 * 8, 2 * 16}, "Exit"}; - private: + protected: + /* Static so selected category is persisted across UI instances. */ + static size_t current_category_index; }; +// TODO: support for new category. class FrequencySaveView : public FreqManBaseView { public: FrequencySaveView(NavigationView& nav, const rf::Frequency value); - - std::string title() const override { return "Save freq."; }; + std::string title() const override { return "Save freq"; }; private: - std::string desc_buffer{}; - rf::Frequency value_{}; + std::string temp_buffer_{}; + freqman_entry entry_{}; - void on_save_name(); - void on_save_timestamp(); - void save_current_file(); + void refresh_ui(); BigFrequency big_display{ - {4, 2 * 16, 28 * 8, 32}, + {0, 2 * 16, 28 * 8, 4 * 16}, 0}; Labels labels{ - {{1 * 8, 12 * 8}, "Save as:", Color::white()}}; + {{0 * 8, 6 * 16}, "Description:", Color::white()}}; - Button button_save_name{ - {1 * 8, 17 * 8, 12 * 8, 48}, - "Name (set)"}; - Button button_save_timestamp{ - {1 * 8, 25 * 8, 12 * 8, 48}, - "Timestamp:"}; - LiveDateTime live_timestamp{ - {14 * 8, 27 * 8, 16 * 8, 16}}; + Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}}; + + Button button_clear{ + {4 * 8, 10 * 16, 10 * 8, 2 * 16}, + "Clear"}; + + Button button_edit{ + {16 * 8, 10 * 16, 10 * 8, 2 * 16}, + "Edit"}; + + Button button_save{ + {0 * 8, 17 * 16, 15 * 8, 2 * 16}, + "Save"}; }; class FrequencyLoadView : public FreqManBaseView { @@ -116,46 +121,62 @@ class FrequencyLoadView : public FreqManBaseView { std::function on_range_loaded{}; FrequencyLoadView(NavigationView& nav); - - std::string title() const override { return "Load freq."; }; - - private: - void refresh_widgets(const bool v); + std::string title() const override { return "Load freq"; }; }; class FrequencyManagerView : public FreqManBaseView { public: FrequencyManagerView(NavigationView& nav); - ~FrequencyManagerView(); - std::string title() const override { return "Freqman"; }; private: - std::string desc_buffer{}; + std::string temp_buffer_{}; - void refresh_widgets(const bool v); - void on_edit_freq(rf::Frequency f); - void on_edit_desc(NavigationView& nav); - void on_new_category(NavigationView& nav); - void on_delete(); + void on_edit_freq(); + void on_edit_desc(); + void on_add_category(); + void on_del_category(); + void on_add_entry(); + void on_del_entry(); Labels labels{ - {{4 * 8 + 4, 26 * 8}, "Edit:", Color::light_grey()}}; + {{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}}; - Button button_new_category{ - {23 * 8, 2, 7 * 8, 20}, - "New"}; + NewButton button_add_category{ + {23 * 8, 0 * 16, 7 * 4, 20}, + {}, + &bitmap_icon_new_file, + Color::white(), + true}; + + NewButton button_del_category{ + {26 * 8 + 4, 0 * 16, 7 * 4, 20}, + {}, + &bitmap_icon_trash, + Color::red(), + true}; Button button_edit_freq{ - {0 * 8, 29 * 8, 14 * 8, 32}, + {0 * 8, 15 * 16, 15 * 8, 2 * 16}, "Frequency"}; + Button button_edit_desc{ - {0 * 8, 34 * 8, 14 * 8, 32}, + {0 * 8, 17 * 16, 15 * 8, 2 * 16}, "Description"}; - Button button_delete{ - {16 * 8, 29 * 8, 14 * 8, 32}, - "Delete"}; + NewButton button_add_entry{ + {15 * 8, 15 * 16, 7 * 8 + 4, 2 * 16}, + {}, + &bitmap_icon_add, + Color::white(), + true}; + + NewButton button_del_entry{ + {22 * 8 + 4, 15 * 16, 7 * 8 + 4, 2 * 16}, + {}, + &bitmap_icon_delete, + Color::red(), + true}; }; } /* namespace ui */ diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index 4ab4966b..a469205b 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -137,8 +137,8 @@ bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_i entry.bandwidth = last_entry.bandwidth; entry.type = freqman_type::Single; - std::string frequency_to_add; - get_freq_string(entry, frequency_to_add); + // TODO: Use FreqmanDB + auto frequency_to_add = to_freqman_string(entry); auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt if (!result.is_valid()) { @@ -607,6 +607,7 @@ ReconView::ReconView(NavigationView& nav) }; button_remove.on_select = [this](ButtonWithEncoder&) { + // TODO: Use FreqmanDB if (frequency_list.size() > 0) { if (!manual_mode) { // scanner or recon (!scanner) mode @@ -629,8 +630,7 @@ ReconView::ReconView(NavigationView& nav) auto result = freqman_file.create(freq_file_path); if (!result.is_valid()) { for (size_t n = 0; n < frequency_list.size(); n++) { - std::string line; - get_freq_string(*frequency_list[n], line); + auto line = to_freqman_string(*frequency_list[n]); freqman_file.write_line(line); } } @@ -640,7 +640,6 @@ ReconView::ReconView(NavigationView& nav) File recon_file{}; File tmp_recon_file{}; std::string tmp_freq_file_path{freq_file_path + ".TMP"}; - std::string frequency_to_add{}; freqman_entry entry = current_entry(); entry.frequency_a = freq; @@ -649,7 +648,7 @@ ReconView::ReconView(NavigationView& nav) entry.bandwidth = last_entry.bandwidth; entry.type = freqman_type::Single; - get_freq_string(entry, frequency_to_add); + auto frequency_to_add = to_freqman_string(entry); delete_file(tmp_freq_file_path); auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt @@ -833,7 +832,7 @@ ReconView::ReconView(NavigationView& nav) open_view->on_changed = [this](std::vector result) { input_file = result[0]; output_file = result[1]; - freq_file_path = "/FREQMAN/" + output_file + ".TXT"; + freq_file_path = get_freqman_path(output_file).string(); recon_save_config_to_sd(); autosave = persistent_memory::recon_autosave_freqs(); @@ -892,7 +891,7 @@ ReconView::ReconView(NavigationView& nav) // Loading input and output file from settings recon_load_config_from_sd(); - freq_file_path = "/FREQMAN/" + output_file + ".TXT"; + freq_file_path = get_freqman_path(output_file).string(); field_recon_match_mode.set_selected_index(recon_match_mode); field_squelch.set_value(squelch); diff --git a/firmware/application/apps/ui_recon_settings.cpp b/firmware/application/apps/ui_recon_settings.cpp index 1d3251d4..390d3724 100644 --- a/firmware/application/apps/ui_recon_settings.cpp +++ b/firmware/application/apps/ui_recon_settings.cpp @@ -21,12 +21,13 @@ * Boston, MA 02110-1301, USA. */ -#include "ui_recon_settings.hpp" -#include "ui_navigation.hpp" #include "ui_fileman.hpp" +#include "ui_navigation.hpp" +#include "ui_recon_settings.hpp" #include "ui_textentry.hpp" #include "file.hpp" +#include "freqman_db.hpp" #include "portapack.hpp" #include "portapack_persistent_memory.hpp" @@ -55,11 +56,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st button_load_freqs.on_select = [this, &nav](Button&) { auto open_view = nav.push(".TXT"); + open_view->push_dir(freqman_dir); open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) { - std::string dir_filter = "FREQMAN/"; - std::string str_file_path = new_file_path.string(); - if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder - // get the filename without txt extension so we can use load_freqman_file fcn + if (new_file_path.native().find(freqman_dir.native()) == 0) { _input_file = new_file_path.stem().string(); text_input_file.set(_input_file); } else { diff --git a/firmware/application/apps/ui_scanner.cpp b/firmware/application/apps/ui_scanner.cpp index 65fe50c1..1e6e15e5 100644 --- a/firmware/application/apps/ui_scanner.cpp +++ b/firmware/application/apps/ui_scanner.cpp @@ -314,17 +314,13 @@ ScannerView::ScannerView( // Button to load txt files from the FREQMAN folder button_load.on_select = [this, &nav](Button&) { auto open_view = nav.push(".TXT"); - open_view->on_changed = [this](std::filesystem::path new_file_path) { - std::string dir_filter = "FREQMAN/"; - std::string str_file_path = new_file_path.string(); - - if (str_file_path.find(dir_filter) != std::string::npos) { // assert file from the FREQMAN folder + open_view->push_dir(freqman_dir); + open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) { + if (new_file_path.native().find(freqman_dir.native()) == 0) { scan_pause(); - // get the filename without txt extension so we can use load_freqman_file fcn - std::string str_file_name = new_file_path.stem().string(); - frequency_file_load(str_file_name, true); + frequency_file_load(new_file_path.stem().string(), true); } else { - nav_.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired."); + nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired."); } }; }; @@ -494,6 +490,7 @@ ScannerView::ScannerView( bigdisplay_update(BDC_GREY); // Back to grey color }; + // TODO: remove this parsing? // Button to add current frequency (found during Search) to the Scan Frequency List button_add.on_select = [this](Button&) { File scanner_file; diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp index 9e2a23b3..0db62dda 100644 --- a/firmware/application/bitmap.hpp +++ b/firmware/application/bitmap.hpp @@ -3819,6 +3819,44 @@ static constexpr Bitmap bitmap_icon_looking{ {16, 16}, bitmap_icon_looking_data}; +static constexpr uint8_t bitmap_icon_add_data[] = { + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0xF8, + 0x1F, + 0xF8, + 0x1F, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, +}; +static constexpr Bitmap bitmap_icon_add{ + {16, 16}, + bitmap_icon_add_data}; + static constexpr uint8_t bitmap_icon_delete_data[] = { 0x00, 0x00, diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index 4aaa2fd3..9589c408 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -252,12 +252,9 @@ std::filesystem::path next_filename_matching_pattern(const std::filesystem::path std::vector scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension) { std::vector file_list{}; - - for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) { - if (std::filesystem::is_regular_file(entry.status())) { - file_list.push_back(entry.path()); - } - } + scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) { + file_list.push_back(p); + }); return file_list; } @@ -287,7 +284,7 @@ std::filesystem::filesystem_error rename_file( std::filesystem::filesystem_error copy_file( const std::filesystem::path& file_path, const std::filesystem::path& dest_path) { - // Decent compromise between memory and speed. + // 512 seems to be the largest block size FatFS likes. constexpr size_t buffer_size = 512; uint8_t buffer[buffer_size]; File src; @@ -324,10 +321,11 @@ FATTimestamp file_created_date(const std::filesystem::path& file_path) { std::filesystem::filesystem_error make_new_file( const std::filesystem::path& file_path) { File f; - auto result = f.create(file_path); - return result.is_valid() - ? result.value() - : std::filesystem::filesystem_error{}; + auto error = f.create(file_path); + if (error) + return *error; + + return {}; } std::filesystem::filesystem_error make_new_directory( @@ -507,13 +505,12 @@ bool path_iequal( } directory_iterator::directory_iterator( - std::filesystem::path path, - std::filesystem::path wild) - : pattern{wild} { + const std::filesystem::path& path, + const std::filesystem::path& wild) + : path_{path}, wild_{wild} { impl = std::make_shared(); - const auto result = f_findfirst(&impl->dir, &impl->filinfo, - reinterpret_cast(path.c_str()), - reinterpret_cast(pattern.c_str())); + auto result = f_findfirst(&impl->dir, &impl->filinfo, + path_.tchar(), wild_.tchar()); if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') { impl.reset(); // TODO: Throw exception if/when I enable exceptions... diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index 2b057ecd..3c990e9a 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -76,36 +76,30 @@ struct path { : _s{} { } - path( - const path& p) + path(const path& p) : _s{p._s} { } - path( - path&& p) + path(path&& p) : _s{std::move(p._s)} { } template - path( - const Source& source) + path(const Source& source) : path{std::begin(source), std::end(source)} { } template - path( - InputIt first, - InputIt last) + path(InputIt first, + InputIt last) : _s{first, last} { } - path( - const char16_t* const s) + path(const char16_t* const s) : _s{s} { } - path( - const TCHAR* const s) + path(const TCHAR* const s) : _s{reinterpret_cast(s)} { } @@ -132,6 +126,10 @@ struct path { return native().c_str(); } + const TCHAR* tchar() const { + return reinterpret_cast(native().c_str()); + } + const string_type& native() const { return _s; } @@ -149,7 +147,7 @@ struct path { } path& operator/=(const path& p) { - if (_s.back() != preferred_separator) + if (_s.back() != preferred_separator && p._s.front() != preferred_separator) _s += preferred_separator; _s += p._s; return *this; @@ -207,7 +205,8 @@ class directory_iterator { }; std::shared_ptr impl{}; - const path pattern{}; + std::filesystem::path path_{}; + std::filesystem::path wild_{}; friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs); @@ -219,7 +218,8 @@ class directory_iterator { using iterator_category = std::input_iterator_tag; directory_iterator() noexcept {}; - directory_iterator(std::filesystem::path path, std::filesystem::path wild); + directory_iterator(const std::filesystem::path& path, + const std::filesystem::path& wild); ~directory_iterator() {} @@ -266,6 +266,13 @@ std::filesystem::filesystem_error make_new_file(const std::filesystem::path& fil std::filesystem::filesystem_error make_new_directory(const std::filesystem::path& dir_path); std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path); +template +void scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension, const TCallback& fn) { + for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) { + if (std::filesystem::is_regular_file(entry.status())) + fn(entry.path()); + } +} std::vector scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension); std::vector scan_root_directories(const std::filesystem::path& directory); diff --git a/firmware/application/file_wrapper.hpp b/firmware/application/file_wrapper.hpp index 73a8a6d6..07d3b407 100644 --- a/firmware/application/file_wrapper.hpp +++ b/firmware/application/file_wrapper.hpp @@ -59,6 +59,7 @@ class BufferWrapper { using Offset = uint32_t; using Line = uint32_t; using Column = uint32_t; + using Size = File::Size; using Range = struct { // Offset of the start, inclusive. Offset start; @@ -108,7 +109,7 @@ class BufferWrapper { } /* Gets the size of the buffer in bytes. */ - File::Size size() const { return wrapped_->size(); } + Size size() const { return wrapped_->size(); } /* Get the count of the lines in the buffer. */ uint32_t line_count() const { return line_count_; } diff --git a/firmware/application/freqman.cpp b/firmware/application/freqman.cpp index 21b1da7c..4ec6f7ca 100644 --- a/firmware/application/freqman.cpp +++ b/firmware/application/freqman.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -40,110 +41,37 @@ extern options_t freqman_bandwidths[4]; extern options_t freqman_steps; extern options_t freqman_steps_short; -const option_t* find_by_index(const options_t& options, freqman_index_t index) { - if (index < options.size()) - return &options[index]; - else - return nullptr; -} +extern const option_t* find_by_index(const options_t& options, freqman_index_t index); -// TODO: move into FreqmanDB type +// TODO: remove in favor of FreqmanDB /* Freqman file handling. */ bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) { - fs::path path{u"FREQMAN/"}; - path += file_stem + ".TXT"; - return parse_freqman_file(path, db, options); -} - -bool get_freq_string(freqman_entry& entry, std::string& item_string) { - rf::Frequency frequency_a, frequency_b; - - frequency_a = entry.frequency_a; - if (entry.type == freqman_type::Single) { - // Single - item_string = "f=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); - } else if (entry.type == freqman_type::Range) { - // Range - frequency_b = entry.frequency_b; - item_string = "a=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); - item_string += ",b=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0'); - if (is_valid(entry.step)) { - item_string += ",s=" + freqman_entry_get_step_string_short(entry.step); - } - } else if (entry.type == freqman_type::HamRadio) { - frequency_b = entry.frequency_b; - item_string = "r=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0'); - item_string += ",t=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0'); - if (is_valid(entry.tone)) { - item_string += ",c=" + tone_key_value_string(entry.tone); - } - } - if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) { - item_string += ",m=" + freqman_entry_get_modulation_string(entry.modulation); - if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size()) { - item_string += ",bw=" + freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth); - } - } - if (entry.description.size()) - item_string += ",d=" + entry.description; - - return true; + return parse_freqman_file(get_freqman_path(file_stem), db, options); } bool delete_freqman_file(const std::string& file_stem) { - File freqman_file; - std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; - delete_file(freq_file_path); - return false; + delete_file(get_freqman_path(file_stem)); + return true; } bool save_freqman_file(const std::string& file_stem, freqman_db& db) { + auto path = get_freqman_path(file_stem); + delete_file(path); + File freqman_file; - std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; - delete_file(freq_file_path); - auto result = freqman_file.create(freq_file_path); - if (!result.is_valid()) { - for (size_t n = 0; n < db.size(); n++) { - std::string line; - get_freq_string(*db[n], line); - freqman_file.write_line(line); - } - return true; - } - return false; -} - -bool create_freqman_file(const std::string& file_stem, File& freqman_file) { - auto result = freqman_file.create("FREQMAN/" + file_stem + ".TXT"); - - if (result.is_valid()) + auto error = freqman_file.create(path); + if (error) return false; + for (size_t n = 0; n < db.size(); n++) + freqman_file.write_line(to_freqman_string(*db[n])); + return true; } -std::string freqman_item_string(freqman_entry& entry, size_t max_length) { - std::string item_string; - - switch (entry.type) { - case freqman_type::Single: - item_string = to_string_short_freq(entry.frequency_a) + "M: " + entry.description; - break; - case freqman_type::Range: - item_string = "R: " + entry.description; - break; - case freqman_type::HamRadio: - item_string = "H: " + entry.description; - break; - default: - item_string = "!UNKNOWN TYPE " + entry.description; - break; - } - - if (item_string.size() > max_length) - return item_string.substr(0, max_length - 3) + "..."; - - return item_string; +bool create_freqman_file(const std::string& file_stem) { + auto fs_error = make_new_file(get_freqman_path(file_stem)); + return fs_error.ok(); } /* Set options. */ @@ -164,33 +92,6 @@ void freqman_set_step_option_short(OptionsField& option) { option.set_options(freqman_steps_short); } -/* Option name lookup. */ -std::string freqman_entry_get_modulation_string(freqman_index_t modulation) { - if (auto opt = find_by_index(freqman_modulations, modulation)) - return opt->first; - return {}; -} - -std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) { - if (modulation < freqman_modulations.size()) { - if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth)) - return opt->first; - } - return {}; -} - -std::string freqman_entry_get_step_string(freqman_index_t step) { - if (auto opt = find_by_index(freqman_steps, step)) - return opt->first; - return {}; -} - -std::string freqman_entry_get_step_string_short(freqman_index_t step) { - if (auto opt = find_by_index(freqman_steps_short, step)) - return opt->first; - return {}; -} - /* Option value lookup. */ // TODO: use Optional instead of magic values. int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) { diff --git a/firmware/application/freqman.hpp b/firmware/application/freqman.hpp index ad55cd9d..da4498fa 100644 --- a/firmware/application/freqman.hpp +++ b/firmware/application/freqman.hpp @@ -2,6 +2,7 @@ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -49,13 +50,11 @@ enum freqman_entry_modulation : uint8_t { SPEC_MODULATION }; +// TODO: Replace with FreqmanDB. bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options); -bool get_freq_string(freqman_entry& entry, std::string& item_string); bool delete_freqman_file(const std::string& file_stem); bool save_freqman_file(const std::string& file_stem, freqman_db& db); -bool create_freqman_file(const std::string& file_stem, File& freqman_file); - -std::string freqman_item_string(freqman_entry& item, size_t max_length); +bool create_freqman_file(const std::string& file_stem); void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option); void freqman_set_modulation_option(ui::OptionsField& option); @@ -63,11 +62,7 @@ void freqman_set_step_option(ui::OptionsField& option); void freqman_set_step_option_short(ui::OptionsField& option); void freqman_set_tone_option(ui::OptionsField& option); -std::string freqman_entry_get_modulation_string(freqman_index_t modulation); -std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth); -std::string freqman_entry_get_step_string(freqman_index_t step); -std::string freqman_entry_get_step_string_short(freqman_index_t step); - +// TODO: Can these be removed after Recon is migrated to FreqmanDB? int32_t freqman_entry_get_modulation_value(freqman_index_t modulation); int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth); int32_t freqman_entry_get_step_value(freqman_index_t step); diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index e57731a5..09c67fab 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -28,6 +28,7 @@ #include "freqman_db.hpp" #include "string_format.hpp" #include "tone_key.hpp" +#include "utility.hpp" #include #include @@ -36,6 +37,14 @@ namespace fs = std::filesystem; +const std::filesystem::path freqman_dir{u"/FREQMAN"}; +const std::filesystem::path freqman_extension{u".TXT"}; + +const std::filesystem::path get_freqman_path(const std::string& stem) { + return freqman_dir / stem + freqman_extension; +} + +// NB: Don't include UI headers to keep this code unit testable. using option_t = std::pair; using options_t = std::vector; @@ -134,6 +143,13 @@ uint8_t find_by_name(const options_t& options, std::string_view name) { return freqman_invalid_index; } +const option_t* find_by_index(const options_t& options, freqman_index_t index) { + if (index < options.size()) + return &options[index]; + else + return nullptr; +} + /* Impl for next round of changes. *template *const T* find_by_name(const std::array& info, std::string_view name) { @@ -146,6 +162,129 @@ uint8_t find_by_name(const options_t& options, std::string_view name) { *} */ +std::string freqman_entry_get_modulation_string(freqman_index_t modulation) { + if (auto opt = find_by_index(freqman_modulations, modulation)) + return opt->first; + return {}; +} + +std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) { + if (modulation < freqman_modulations.size()) { + if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth)) + return opt->first; + } + return {}; +} + +std::string freqman_entry_get_step_string(freqman_index_t step) { + if (auto opt = find_by_index(freqman_steps, step)) + return opt->first; + return {}; +} + +std::string freqman_entry_get_step_string_short(freqman_index_t step) { + if (auto opt = find_by_index(freqman_steps_short, step)) + return opt->first; + return {}; +} + +std::string pretty_string(const freqman_entry& entry, size_t max_length) { + std::string str; + + switch (entry.type) { + case freqman_type::Single: + str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description; + break; + case freqman_type::Range: + str = to_string_dec_uint(entry.frequency_a / 1'000'000) + "M-" + + to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description; + break; + case freqman_type::HamRadio: + str = "" + to_string_dec_uint(entry.frequency_a / 1'000'000) + "M," + + to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description; + break; + default: + str = "UNK:" + entry.description; + break; + } + + // Truncate. '+' indicates if string has been truncated. + if (str.size() > max_length) + return str.substr(0, max_length - 1) + "+"; + + return str; +} + +std::string to_freqman_string(const freqman_entry& entry) { + std::string serialized; + serialized.reserve(0x80); + + // Append a key=value to the string. + auto append_field = [&serialized](std::string_view name, std::string_view value) { + if (!serialized.empty()) + serialized += ","; + serialized += std::string{name} + "=" + std::string{value}; + }; + + switch (entry.type) { + case freqman_type::Single: + append_field("f", to_string_dec_uint64(entry.frequency_a)); + break; + case freqman_type::Range: + append_field("a", to_string_dec_uint64(entry.frequency_a)); + append_field("b", to_string_dec_uint64(entry.frequency_b)); + + if (is_valid(entry.step)) + append_field("s", freqman_entry_get_step_string_short(entry.step)); + break; + case freqman_type::HamRadio: + append_field("r", to_string_dec_uint64(entry.frequency_a)); + append_field("t", to_string_dec_uint64(entry.frequency_b)); + + if (is_valid(entry.tone)) + append_field("c", tonekey::tone_key_value_string(entry.tone)); + break; + default: + return {}; // TODO: Comment type with description? + }; + + if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) { + append_field("m", freqman_entry_get_modulation_string(entry.modulation)); + + if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size()) + append_field("bw", freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth)); + } + + if (entry.description.size() > 0) + append_field("d", entry.description); + + serialized.shrink_to_fit(); + return serialized; +} + +freqman_index_t parse_tone_key(std::string_view value) { + // Split into whole and fractional parts. + auto parts = split_string(value, '.'); + int32_t tone_freq = 0; + int32_t whole_part = 0; + parse_int(parts[0], whole_part); + + // Tones are stored as frequency / 100 for some reason. + // E.g. 14572 would be 145.7 (NB: 1s place is dropped). + // TODO: Might be easier to just store the codes? + // Multiply the whole part by 100 to get the tone frequency. + tone_freq = whole_part * 100; + + // Add the fractional part, if present. + if (parts.size() > 1) { + auto c = parts[1].front(); + auto digit = std::isdigit(c) ? c - '0' : 0; + tone_freq += digit * 10; + } + + return static_cast(tonekey::tone_key_index_by_value(tone_freq)); +} + bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { if (str.empty() || str[0] == '#') return false; @@ -175,26 +314,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value); } } else if (key == "c") { - // Split into whole and fractional parts. - auto parts = split_string(value, '.'); - int32_t tone_freq = 0; - int32_t whole_part = 0; - parse_int(parts[0], whole_part); - - // Tones are stored as frequency / 100 for some reason. - // E.g. 14572 would be 145.7 (NB: 1s place is dropped). - // TODO: Might be easier to just store the codes? - // Multiply the whole part by 100 to get the tone frequency. - tone_freq = whole_part * 100; - - // Add the fractional part, if present. - if (parts.size() > 1) { - auto c = parts[1].front(); - auto digit = std::isdigit(c) ? c - '0' : 0; - tone_freq += digit * 10; - } - entry.tone = static_cast( - tonekey::tone_key_index_by_value(tone_freq)); + entry.tone = parse_tone_key(value); } else if (key == "d") { entry.description = trim(value); } else if (key == "f") { @@ -233,6 +353,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { return true; } +// TODO: Use FreqmanDB iterator. bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) { File f; auto error = f.open(path); @@ -276,4 +397,64 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio db.shrink_to_fit(); return true; +} + +/* FreqmanDB ***********************************/ + +bool FreqmanDB::open(const std::filesystem::path& path) { + auto result = FileWrapper::open(path); + if (!result) + return false; + + wrapper_ = *std::move(result); + return true; +} + +void FreqmanDB::close() { + wrapper_.reset(); +} + +freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const { + auto length = wrapper_->line_length(line); + auto line_text = wrapper_->get_text(line, 0, length); + + if (line_text) { + freqman_entry entry; + if (parse_freqman_entry(*line_text, entry)) + return entry; + } + + return {}; +} + +void FreqmanDB::insert_entry(const freqman_entry& entry, FileWrapper::Line line) { + // TODO: Can be more efficient. + line = clip(line, 0u, entry_count()); + wrapper_->insert_line(line); + replace_entry(line, entry); +} + +void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry) { + auto range = wrapper_->line_range(line); + if (!range) + return; // TODO: Message? + + // Don't overwrite the '\n'. + range->end--; + wrapper_->replace_range(*range, to_freqman_string(entry)); +} + +void FreqmanDB::delete_entry(FileWrapper::Line line) { + wrapper_->delete_line(line); +} + +uint32_t FreqmanDB::entry_count() const { + // FileWrapper always presents a single line even for empty files. + return empty() ? 0u : wrapper_->line_count(); +} + +bool FreqmanDB::empty() const { + // FileWrapper always presents a single line even for empty files. + // A DB is only really empty if the file size is 0. + return !wrapper_ || wrapper_->size() == 0; } \ No newline at end of file diff --git a/firmware/application/freqman_db.hpp b/firmware/application/freqman_db.hpp index 83c1d39b..974d96fe 100644 --- a/firmware/application/freqman_db.hpp +++ b/firmware/application/freqman_db.hpp @@ -1,7 +1,8 @@ /* * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek - * Copyright (C) 2023 gullradriel, Nilorea Studio Inc., Kyle Reed + * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -25,6 +26,7 @@ #define __FREQMAN_DB_H__ #include "file.hpp" +#include "file_wrapper.hpp" #include "utility.hpp" #include @@ -33,6 +35,10 @@ #include #include +/* Defined in freqman_db.cpp */ +extern const std::filesystem::path freqman_dir; +extern const std::filesystem::path freqman_extension; + using freqman_index_t = uint8_t; constexpr freqman_index_t freqman_invalid_index = static_cast(-1); @@ -46,6 +52,9 @@ constexpr bool is_invalid(freqman_index_t index) { return index == freqman_invalid_index; } +/* Gets the full path for a given file stem (no extension). */ +const std::filesystem::path get_freqman_path(const std::string& stem); + enum class freqman_type : uint8_t { Single, // f= Range, // a=,b= @@ -141,10 +150,16 @@ struct freqman_entry { freqman_index_t tone{freqman_invalid_index}; }; +// TODO: These shouldn't be exported. +std::string freqman_entry_get_modulation_string(freqman_index_t modulation); +std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth); +std::string freqman_entry_get_step_string(freqman_index_t step); +std::string freqman_entry_get_step_string_short(freqman_index_t step); + /* A reasonable maximum number of items to load from a freqman file. * Apps using freqman_db should be tested and this value tuned to * ensure app memory stability. */ -constexpr size_t freqman_default_max_entries = 90; +constexpr size_t freqman_default_max_entries = 150; struct freqman_load_options { /* Loads all entries when set to 0. */ @@ -157,23 +172,63 @@ struct freqman_load_options { using freqman_entry_ptr = std::unique_ptr; using freqman_db = std::vector; +/* Gets a pretty string representation for an entry. */ +std::string pretty_string(const freqman_entry& item, size_t max_length = 30); + +/* Gets the freqman file representation for an entry. */ +std::string to_freqman_string(const freqman_entry& entry); + bool parse_freqman_entry(std::string_view str, freqman_entry& entry); bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options); -/* Type for next round of changes. - *class FreqmanDB { - * public: - * FreqmanDB(); - * FreqmanDB(const FreqmanDB&) = delete; - * FreqmanDB(FreqmanDB&&) = delete; - * FreqmanDB& operator=(const FreqmanDB&) = delete; - * FreqmanDB& operator=(FreqmanDB&&) = delete; - * - * size_t size() const { return 0; }; - * - * private: - * freqman_db entries_; - *}; - */ +/* The tricky part of using the file directly is that there can be comments + * and empty lines in the file. This messes up the 'count' calculation. + * Either have to live with 'count' being an upper bound have the callers + * know to expect that entries may be empty. */ +// NB: This won't apply implicit mod/bandwidth. +// TODO: Reuse for parse_freqman_file +class FreqmanDB { + public: + class iterator { + public: + iterator(FreqmanDB& db, FileWrapper::Offset line) + : db_{db}, line_{line} {} + iterator& operator++() { + line_++; + return *this; + } + freqman_entry operator*() const { + return db_[line_]; + } + + bool operator!=(const iterator& other) { + return &db_ != &other.db_ || line_ != other.line_; + } + + private: + FreqmanDB& db_; + FileWrapper::Line line_; + }; + + bool open(const std::filesystem::path& path); + void close(); + freqman_entry operator[](FileWrapper::Line line) const; + void insert_entry(const freqman_entry& entry, FileWrapper::Line line); + void replace_entry(FileWrapper::Line line, const freqman_entry& entry); + void delete_entry(FileWrapper::Line line); + + uint32_t entry_count() const; + bool empty() const; + + iterator begin() { + return {*this, 0}; + } + iterator end() { + return {*this, entry_count()}; + } + + private: + std::unique_ptr wrapper_{}; +}; #endif /* __FREQMAN_DB_H__ */ \ No newline at end of file diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index f7ad5fbe..9d267e8e 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -25,16 +25,15 @@ * and fills it backwards towards the front. * The return value 'q' is a pointer to the start. * TODO: use std::array for all this. */ +template static char* to_string_dec_uint_internal( char* p, - uint32_t n) { + Int n) { *p = 0; auto q = p; do { - const uint32_t d = n % 10; - const char c = d + 48; - *(--q) = c; + *(--q) = n % 10 + '0'; n /= 10; } while (n != 0); @@ -60,13 +59,36 @@ static char* to_string_dec_uint_pad_internal( return q; } -char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length) { +template +char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) { auto end = &buffer.back(); auto start = to_string_dec_uint_internal(end, n); length = end - start; return start; } +char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length) { + return to_string_dec_uint_internal(n, buffer, length); +} + +char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) { + return to_string_dec_uint_internal(n, buffer, length); +} + +std::string to_string_dec_uint(uint32_t n) { + StringFormatBuffer b{}; + size_t len{}; + char* str = to_string_dec_uint(n, b, len); + return std::string(str, len); +} + +std::string to_string_dec_uint64(uint64_t n) { + StringFormatBuffer b{}; + size_t len{}; + char* str = to_string_dec_uint(n, b, len); + return std::string(str, len); +} + std::string to_string_bin( const uint32_t n, const uint8_t l) { @@ -285,6 +307,9 @@ static const char* whitespace_str = " \t\r\n"; std::string trim(std::string_view str) { auto first = str.find_first_not_of(whitespace_str); + if (first == std::string::npos) + return {}; + auto last = str.find_last_not_of(whitespace_str); return std::string{str.substr(first, last - first + 1)}; } diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 50c4741b..c3f8e912 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -41,20 +41,25 @@ enum TimeFormat { const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'}; -using StringFormatBuffer = std::array; +using StringFormatBuffer = std::array; -/* uint32_t conversion without memory allocations. */ -char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length); +/* uint conversion without memory allocations. */ +char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length); +char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length); + +std::string to_string_dec_uint(uint32_t n); +std::string to_string_dec_uint64(uint64_t n); // TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp... std::string to_string_bin(const uint32_t n, const uint8_t l = 0); -std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = ' '); +std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' '); std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0); std::string to_string_decimal(float decimal, int8_t precision); std::string to_string_hex(const uint64_t n, const int32_t l = 0); std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0); +// NB: These pad-left and don't work correctly for values less than 1M. std::string to_string_freq(const uint64_t f); std::string to_string_short_freq(const uint64_t f); std::string to_string_time_ms(const uint32_t ms); diff --git a/firmware/application/ui/ui_freqlist.cpp b/firmware/application/ui/ui_freqlist.cpp index 7286bf3a..6c112e5e 100644 --- a/firmware/application/ui/ui_freqlist.cpp +++ b/firmware/application/ui/ui_freqlist.cpp @@ -1,5 +1,7 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -24,154 +26,143 @@ #include "baseband_api.hpp" #include "utility.hpp" -#include - namespace ui { -FreqManUIList::FreqManUIList( - Rect parent_rect, - bool instant_exec) + +FreqManUIList::FreqManUIList(Rect parent_rect) : Widget{parent_rect}, - instant_exec_{instant_exec} { + visible_lines_{(unsigned)parent_rect.height() / char_height} { this->set_focusable(true); } -void FreqManUIList::set_highlighted_index(int index) { - if (freqlist_db == nullptr || (unsigned)(current_index + index) >= freqlist_db->size()) - return; - if (index < 0) { - index = 0; - if (current_index > 0) - current_index--; - } - if (index >= freqlist_nb_lines) { - index = freqlist_nb_lines - 1; - if ((unsigned)(current_index + index) < freqlist_db->size()) - current_index++; - else - current_index = freqlist_db->size() - freqlist_nb_lines - 1; - } - highlighted_index = index; -} - -uint8_t FreqManUIList::get_index() { - return current_index + highlighted_index; -} - void FreqManUIList::paint(Painter& painter) { - freqlist_nb_lines = 0; - const auto r = screen_rect(); - uint8_t focused = has_focus(); - const Rect r_widget_screen{r.left() + focused, r.top() + focused, r.width() - 2 * focused, r.height() - 2 * +focused}; - painter.fill_rectangle( - r_widget_screen, - Color::black()); - // only return after clearing the screen so previous entries are not shown anymore - if (freqlist_db == nullptr || freqlist_db->size() == 0) + auto rect = screen_rect(); + + if (!db_ || db_->empty()) { + auto line_position = rect.location() + Point{7 * 8, 6 * 16}; + painter.fill_rectangle(rect, Color::black()); + painter.draw_string(line_position, Styles::white, "Empty Category"); return; - // coloration if file is too big - auto text_color = &Styles::white; - if (freqlist_db->size() > FREQMAN_MAX_PER_FILE) - text_color = &Styles::yellow; - uint8_t nb_lines = 0; - for (uint8_t it = current_index; it < freqlist_db->size(); it++) { - uint8_t line_height = (int)nb_lines * char_height; - if (line_height < (r.height() - char_height)) { // line is within the widget - std::string description = freqman_item_string(*freqlist_db->at(it), 30); - if (nb_lines == highlighted_index) { - const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height}; - painter.fill_rectangle( - r_highlighted_freq, - Color::white()); - painter.draw_string( - {0, r.location().y() + (int)nb_lines * char_height}, - Styles::bg_white, description); - } else { - painter.draw_string( - {0, r.location().y() + (int)nb_lines * char_height}, - *text_color, description); - } + } - nb_lines++; - } else { - // rect is filled, we can break - break; - } - } - freqlist_nb_lines = nb_lines; - if (has_focus() || highlighted()) { - const Rect r_focus{r.left(), r.top(), r.width(), r.height()}; - painter.draw_rectangle( - r_focus, - Color::white()); - } -} + // Indicate when a file is too large by drawing in yellow. + auto over_max = db_->entry_count() > freqman_default_max_entries; + auto base_style = over_max ? &Styles::yellow : &Styles::white; -void FreqManUIList::set_db(freqman_db& db) { - freqlist_db = &db; - if (db.size() == 0) { - current_index = 0; - highlighted_index = 0; - } else { - if ((unsigned)(current_index + highlighted_index) >= db.size()) { - current_index = db.size() - 1 - highlighted_index; - } - if (current_index < 0) { - current_index = 0; - if (highlighted_index > 0) - highlighted_index--; + // TODO: could minimize redraw/re-read if necessary + // with better change tracking. + for (auto offset = 0u; offset < visible_lines_; ++offset) { + // The whole frame needs to be cleared so every line 'slot' + // is redrawn even when `text` just left empty. + auto text = std::string{}; + auto index = start_index_ + offset; + auto line_position = rect.location() + Point{4, 1 + (int)offset * char_height}; + + // Highlight the selected item. + auto style = (offset == selected_index_) ? &Styles::bg_white : base_style; + + if (index < db_->entry_count()) { + auto entry = (*db_)[index]; + // db_ is directly backed by a file, so invalid lines cannot be + // pre-filtered. Just show an empty 'slot' in this case. + if (entry.type != freqman_type::Unknown) + text = pretty_string(entry, line_max_length); } + + // Pad right with ' ' so trailing chars are cleaned up. + // draw_glyph has less flicker than fill_rect when drawing. + if (text.length() < line_max_length) + text.resize(line_max_length, ' '); + + painter.draw_string(line_position, *style, text); } + + // Draw a bounding rectangle when focused. + painter.draw_rectangle(rect, (has_focus() ? Color::white() : Color::black())); } void FreqManUIList::on_focus() { - if (on_highlight) - on_highlight(*this); + set_dirty(); +} + +void FreqManUIList::on_blur() { + set_dirty(); } bool FreqManUIList::on_key(const KeyEvent key) { - if (key == KeyEvent::Select) { - if (on_select) { - on_select(*this); - return true; - } - } else { - if (on_dir) { - return on_dir(*this, key); - } - } - return false; -} + if (!db_ || db_->empty()) + return false; -bool FreqManUIList::on_touch(const TouchEvent event) { - switch (event.type) { - case TouchEvent::Type::Start: - set_highlighted(true); - set_dirty(); - if (on_touch_press) { - on_touch_press(*this); - } - if (on_select && instant_exec_) { - on_select(*this); - } - return true; - case TouchEvent::Type::End: - set_highlighted(false); - set_dirty(); - if (on_touch_release) { - on_touch_release(*this); - } - if (on_select && !instant_exec_) { - on_select(*this); - } - return true; - default: - return false; + if (key == KeyEvent::Select && on_select) { + on_select(get_index()); + return true; + } else if (key == KeyEvent::Right && on_leave) { + on_leave(); + return true; } -} -bool FreqManUIList::on_encoder(EncoderEvent delta) { - set_highlighted_index((int)highlighted_index + delta); + auto delta = 0; + if (key == KeyEvent::Up && get_index() > 0) + delta = -1; + else if (key == KeyEvent::Down && get_index() < db_->entry_count() - 1) + delta = 1; + else + return false; + + adjust_selected_index(delta); set_dirty(); return true; } + +bool FreqManUIList::on_encoder(EncoderEvent delta) { + if (!db_ || db_->empty()) + return false; + + adjust_selected_index(delta); + set_dirty(); + return true; +} + +void FreqManUIList::set_parent_rect(Rect new_parent_rect) { + visible_lines_ = new_parent_rect.height() / char_height; + Widget::set_parent_rect(new_parent_rect); +} + +void FreqManUIList::set_index(size_t index) { + start_index_ = 0; + selected_index_ = 0; + adjust_selected_index(index); +} + +size_t FreqManUIList::get_index() const { + return start_index_ + selected_index_; +} + +void FreqManUIList::set_db(FreqmanDB& db) { + db_ = &db; + start_index_ = 0; + selected_index_ = 0; + set_dirty(); +} + +void FreqManUIList::adjust_selected_index(int delta) { + int32_t new_index = selected_index_ + delta; + + // The selection went off the top of the screen, move up. + if (new_index < 0) { + start_index_ = std::max(start_index_ + new_index, 0); + selected_index_ = 0; + } + + // Selection is off the bottom of the screen, move down. + else if (new_index >= (int32_t)visible_lines_) { + start_index_ = std::min(start_index_ + delta, db_->entry_count() - visible_lines_); + selected_index_ = visible_lines_ - 1; + } + + // Otherwise, scroll within the screen, but not past the end. + else { + selected_index_ = std::min(new_index, db_->entry_count() - 1); + } +} + } /* namespace ui */ diff --git a/firmware/application/ui/ui_freqlist.hpp b/firmware/application/ui/ui_freqlist.hpp index b18b8a38..de09aa13 100644 --- a/firmware/application/ui/ui_freqlist.hpp +++ b/firmware/application/ui/ui_freqlist.hpp @@ -1,5 +1,7 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2023 gullradriel, Nilorea Studio Inc. + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -23,53 +25,48 @@ #define __UI_FREQLIST_H__ #include "ui.hpp" -#include "ui_widget.hpp" #include "ui_painter.hpp" #include "ui_styles.hpp" +#include "ui_widget.hpp" #include "event_m0.hpp" -#include "message.hpp" #include "freqman.hpp" +#include "freqman_db.hpp" +#include "message.hpp" #include namespace ui { class FreqManUIList : public Widget { public: - std::function on_select{}; - std::function on_touch_release{}; // Executed when releasing touch, after on_select. - std::function on_touch_press{}; // Executed when touching, before on_select. - std::function on_dir{}; - std::function on_highlight{}; + std::function on_select{}; + std::function on_leave{}; // Called when Right is pressed. - FreqManUIList(Rect parent_rect, bool instant_exec); // instant_exec: Execute on_select when you touching instead of releasing - FreqManUIList( - Rect parent_rect) - : FreqManUIList{parent_rect, false} { - } - FreqManUIList() - : FreqManUIList{{}, {}} { - } + FreqManUIList(Rect parent_rect); FreqManUIList(const FreqManUIList& other) = delete; FreqManUIList& operator=(const FreqManUIList& other) = delete; void paint(Painter& painter) override; void on_focus() override; + void on_blur() override; bool on_key(const KeyEvent key) override; - bool on_touch(const TouchEvent event) override; bool on_encoder(EncoderEvent delta) override; + void set_parent_rect(Rect new_parent_rect) override; - void set_highlighted_index(int index); // internal set highlighted_index in list handler - uint8_t get_index(); // return highlighed + index - uint8_t set_index(uint8_t index); // try to set current_index + highlighed from index, return capped index - void set_db(freqman_db& db); + void set_index(size_t index); + size_t get_index() const; + void set_db(FreqmanDB& db); private: + void adjust_selected_index(int index); + static constexpr int8_t char_height = 16; - bool instant_exec_{false}; - freqman_db* freqlist_db{nullptr}; - int current_index{0}; - int highlighted_index{0}; - int freqlist_nb_lines{0}; + static constexpr int8_t char_width = 8; + static constexpr int8_t line_max_length = 29; + size_t visible_lines_{0}; + + FreqmanDB* db_{nullptr}; + size_t start_index_{0}; + size_t selected_index_{0}; }; } // namespace ui diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index d2c11b3c..744d3f5f 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1112,11 +1112,13 @@ NewButton::NewButton( Rect parent_rect, std::string text, const Bitmap* bitmap, - Color color) + Color color, + bool vertical_center) : Widget{parent_rect}, text_{text}, bitmap_{bitmap}, - color_{color} { + color_{color}, + vertical_center_{vertical_center} { set_focusable(true); } @@ -1143,6 +1145,11 @@ void NewButton::set_color(Color color) { set_dirty(); } +void NewButton::set_vertical_center(bool value) { + vertical_center_ = value; + set_dirty(); +} + ui::Color NewButton::color() { return color_; } @@ -1164,28 +1171,34 @@ void NewButton::paint(Painter& painter) { const Style paint_style = {style().font, bg, fg}; - painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Color::light_grey()); - painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Color::dark_grey()); - painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Color::dark_grey()); + painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey()); + painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey()); + painter.draw_rectangle({r.left() + r.width() - 1, r.top(), 1, r.height()}, Color::dark_grey()); painter.fill_rectangle( - {r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2}, + {r.left(), r.top() + 1, r.width() - 1, r.height() - 2}, paint_style.background); - int y = r.location().y(); + int y = r.top(); if (bitmap_) { + int offset_y = vertical_center_ ? (r.height() / 2) - (bitmap_->size.height() / 2) : 6; + Point bmp_pos = {r.left() + (r.width() / 2) - (bitmap_->size.width() / 2), r.top() + offset_y}; + y += bitmap_->size.height() - offset_y; + painter.draw_bitmap( - {r.location().x() + (r.size().width() / 2) - 8, r.location().y() + 6}, + bmp_pos, *bitmap_, color_, // Color::green(), //fg, bg); - y += 10; } - const auto label_r = paint_style.font.size_of(text_); - painter.draw_string( - {r.location().x() + (r.size().width() - label_r.width()) / 2, y + (r.size().height() - label_r.height()) / 2}, - paint_style, - text_); + + if (!text_.empty()) { + const auto label_r = paint_style.font.size_of(text_); + painter.draw_string( + {r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, + paint_style, + text_); + } } void NewButton::on_focus() { @@ -1482,11 +1495,11 @@ bool ImageOptionsField::on_touch(const TouchEvent event) { OptionsField::OptionsField( Point parent_pos, - int length, + size_t length, options_t options) - : Widget{{parent_pos, {8 * length, 16}}}, + : Widget{{parent_pos, {8 * (int)length, 16}}}, length_{length}, - options{std::move(options)} { + options_{std::move(options)} { set_focusable(true); } @@ -1494,16 +1507,20 @@ size_t OptionsField::selected_index() const { return selected_index_; } -size_t OptionsField::selected_index_value() const { - return options[selected_index_].second; +const OptionsField::name_t& OptionsField::selected_index_name() const { + return options_[selected_index_].first; +} + +const OptionsField::value_t& OptionsField::selected_index_value() const { + return options_[selected_index_].second; } void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) { - if (new_index < options.size()) { + if (new_index < options_.size()) { if (new_index != selected_index() || trigger_change) { selected_index_ = new_index; if (on_change) { - on_change(selected_index(), options[selected_index()].second); + on_change(selected_index(), options_[selected_index()].second); } set_dirty(); } @@ -1512,7 +1529,7 @@ void OptionsField::set_selected_index(const size_t new_index, bool trigger_chang void OptionsField::set_by_value(value_t v) { size_t new_index = 0; - for (const auto& option : options) { + for (const auto& option : options_) { if (option.second == v) { set_selected_index(new_index); return; @@ -1529,7 +1546,7 @@ void OptionsField::set_by_nearest_value(value_t v) { size_t curr_index = 0; int32_t min_diff = INT32_MAX; - for (const auto& option : options) { + for (const auto& option : options_) { auto diff = abs(v - option.second); if (diff < min_diff) { min_diff = diff; @@ -1543,7 +1560,7 @@ void OptionsField::set_by_nearest_value(value_t v) { } void OptionsField::set_options(options_t new_options) { - options = std::move(new_options); + options_ = std::move(new_options); // Set an invalid index to force on_change. selected_index_ = (size_t)-1; @@ -1556,12 +1573,14 @@ void OptionsField::paint(Painter& painter) { painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, ui::Color::black()); - if (selected_index() < options.size()) { - const auto text = options[selected_index()].first; + if (selected_index() < options_.size()) { + std::string_view temp = selected_index_name(); + if (temp.length() > length_) + temp = temp.substr(0, length_); painter.draw_string( screen_pos(), paint_style, - text); + temp); } } @@ -1574,8 +1593,8 @@ void OptionsField::on_focus() { bool OptionsField::on_encoder(const EncoderEvent delta) { int32_t new_value = selected_index() + delta; if (new_value < 0) - new_value = options.size() - 1; - else if ((size_t)new_value >= options.size()) + new_value = options_.size() - 1; + else if ((size_t)new_value >= options_.size()) new_value = 0; set_selected_index(new_value); diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 77d26a78..819f8f0b 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -455,7 +455,7 @@ class NewButton : public Widget { NewButton(const NewButton&) = delete; NewButton& operator=(const NewButton&) = delete; NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap); - NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color); + NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color, bool vertical_center = false); NewButton() : NewButton{{}, {}, {}} { } @@ -463,6 +463,7 @@ class NewButton : public Widget { void set_bitmap(const Bitmap* bitmap); void set_text(const std::string value); void set_color(Color value); + void set_vertical_center(bool value); std::string text() const; const Bitmap* bitmap(); ui::Color color(); @@ -477,6 +478,7 @@ class NewButton : public Widget { std::string text_; const Bitmap* bitmap_; Color color_; + bool vertical_center_{false}; }; class Image : public Widget { @@ -615,12 +617,14 @@ class OptionsField : public Widget { std::function on_change{}; std::function on_show_options{}; - OptionsField(Point parent_pos, int length, options_t options); + OptionsField(Point parent_pos, size_t length, options_t options); + const options_t& options() const { return options_; } void set_options(options_t new_options); size_t selected_index() const; - size_t selected_index_value() const; + const name_t& selected_index_name() const; + const value_t& selected_index_value() const; void set_selected_index(const size_t new_index, bool trigger_change = true); void set_by_value(value_t v); @@ -633,8 +637,8 @@ class OptionsField : public Widget { bool on_touch(const TouchEvent event) override; private: - const int length_; - options_t options; + const size_t length_; + options_t options_; size_t selected_index_{0}; }; diff --git a/firmware/graphics/icon_add.png b/firmware/graphics/icon_add.png new file mode 100644 index 0000000000000000000000000000000000000000..6a565fdc602dd93edc99f816820e9b595ce18caa GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z~GV^ z;hE;^%b>-;z`()4*q+J20#d@jz#zo{0?Z2-8JNK$j0_A5nBcMk3z!jXkV2Et!cPng z3?ZH_jv*F;M*~lDF(`1%{`=oPI;eY>NaQ64n-g&w3|o4nPoB*=62TbSdX{S=XV|$t zvFZ~RUGaYR>r&{E4AFeyfT^=P-Q4f(STFoXe8#>5aq7S8TX{B%3JCj~`L==V@pScb JS?83{1OPS^Jjnn6 literal 0 HcmV?d00001 diff --git a/firmware/test/application/CMakeLists.txt b/firmware/test/application/CMakeLists.txt index 0ce4213e..ffe37765 100644 --- a/firmware/test/application/CMakeLists.txt +++ b/firmware/test/application/CMakeLists.txt @@ -42,14 +42,15 @@ add_executable(application_test EXCLUDE_FROM_ALL ${PROJECT_SOURCE_DIR}/test_freqman_db.cpp ${PROJECT_SOURCE_DIR}/test_mock_file.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp + ${PROJECT_SOURCE_DIR}/test_string_format.cpp ${PROJECT_SOURCE_DIR}/test_utility.cpp ${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp ${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp - ${PROJECT_SOURCE_DIR}/../../application/string_format.cpp - + # Dependencies ${PROJECT_SOURCE_DIR}/../../application/file.cpp + ${PROJECT_SOURCE_DIR}/../../application/string_format.cpp ${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp ${PROJECT_SOURCE_DIR}/linker_stubs.cpp ) diff --git a/firmware/test/application/test_freqman_db.cpp b/firmware/test/application/test_freqman_db.cpp index 1e93b1a4..5d3b308b 100644 --- a/firmware/test/application/test_freqman_db.cpp +++ b/firmware/test/application/test_freqman_db.cpp @@ -166,14 +166,44 @@ TEST_CASE("It can parse tone freq") { CHECK_EQ(e.tone, 3); } -#if 0 // New tables for a future PR. +TEST_CASE("It can serialize basic Single entry") { + auto str = to_freqman_string(freqman_entry{ + .frequency_a = 123'456'000, + .description = "Foobar", + .type = freqman_type::Single, + }); + CHECK(str == "f=123456000,d=Foobar"); +} + +TEST_CASE("It can serialize basic Range entry") { + auto str = to_freqman_string(freqman_entry{ + .frequency_a = 123'456'000, + .frequency_b = 423'456'000, + .description = "Foobar", + .type = freqman_type::Range, + }); + CHECK(str == "a=123456000,b=423456000,d=Foobar"); +} + +TEST_CASE("It can serialize basic HamRadio entry") { + auto str = to_freqman_string(freqman_entry{ + .frequency_a = 123'456'000, + .frequency_b = 423'456'000, + .description = "Foobar", + .type = freqman_type::HamRadio, + }); + CHECK(str == "r=123456000,t=423456000,d=Foobar"); +} + +// New tables for a future PR. +/* TEST_CASE("It can parse modulation") { freqman_entry e; REQUIRE( parse_freqman_entry( "f=123000000,d=This is the description.,m=AM", e)); CHECK_EQ(e.modulation, freqman_modulation::AM); - + REQUIRE( parse_freqman_entry( "f=123000000,d=This is the description.,m=NFM", e)); @@ -201,7 +231,7 @@ TEST_CASE("It can parse frequency step") { parse_freqman_entry( "f=123000000,d=This is the description.,s=0.1kHz", e)); CHECK_EQ(e.step, freqman_step::_100Hz); - + REQUIRE( parse_freqman_entry( "f=123000000,d=This is the description.,s=50kHz", e)); @@ -212,6 +242,6 @@ TEST_CASE("It can parse frequency step") { "f=123000000,d=This is the description.,s=FOO", e)); CHECK_EQ(e.step, freqman_step::Unknown); } -#endif +*/ TEST_SUITE_END(); diff --git a/firmware/test/application/test_string_format.cpp b/firmware/test/application/test_string_format.cpp new file mode 100644 index 00000000..99c79ab7 --- /dev/null +++ b/firmware/test/application/test_string_format.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * 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, 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "doctest.h" +#include "string_format.hpp" + +/* TODO: Tests for all string_format functions. */ + +TEST_CASE("to_string_dec_uint64 returns correct value.") { + CHECK_EQ(to_string_dec_uint64(0), "0"); + CHECK_EQ(to_string_dec_uint64(1), "1"); + CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000"); + CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890"); + CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891"); +} + +/*TEST_CASE("to_string_freq returns correct value.") { + CHECK_EQ(to_string_freq(0), "0"); + CHECK_EQ(to_string_freq(1), "1"); + CHECK_EQ(to_string_freq(1'000'000), "1000000"); + CHECK_EQ(to_string_freq(1'234'567'890), "1234567890"); + CHECK_EQ(to_string_freq(1'234'567'891), "1234567891"); +}*/ + +TEST_CASE("trim removes whitespace.") { + CHECK(trim(" foo\n") == "foo"); +} + +TEST_CASE("trim returns empty for only whitespace.") { + CHECK(trim(" \n").empty()); +}