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
This commit is contained in:
Kyle Reed 2023-07-11 13:48:36 -07:00 committed by GitHub
parent 0c599f7d3a
commit 29e495a17f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 979 additions and 660 deletions

View File

@ -1,6 +1,7 @@
/* /*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
* *
* This file is part of PortaPack. * This file is part of PortaPack.
* *
@ -22,278 +23,284 @@
#include "ui_freqman.hpp" #include "ui_freqman.hpp"
#include "portapack.hpp"
#include "event_m0.hpp" #include "event_m0.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
#include "utility.hpp"
#include <memory> #include <memory>
using namespace portapack; using namespace portapack;
namespace fs = std::filesystem;
namespace ui { namespace ui {
static int32_t current_category_id = 0; /* FreqManBaseView ***************************************/
size_t FreqManBaseView::current_category_index = 0;
FreqManBaseView::FreqManBaseView( FreqManBaseView::FreqManBaseView(
NavigationView& nav) NavigationView& nav)
: nav_(nav) { : nav_(nav) {
add_children({&options_category, add_children(
&label_category, {&label_category,
&button_exit}); &options_category,
&button_exit});
// initialize options_category.on_change = [this](size_t new_index, int32_t) {
refresh_list(); change_category(new_index);
options_category.on_change = [this](size_t category_id, int32_t) {
change_category(category_id);
}; };
options_category.set_selected_index(current_category_id);
button_exit.on_select = [this, &nav](Button&) { button_exit.on_select = [this, &nav](Button&) {
nav.pop(); nav.pop();
}; };
refresh_categories();
}; };
void FreqManBaseView::focus() { void FreqManBaseView::focus() {
button_exit.focus(); button_exit.focus();
// TODO: Shouldn't be on focus.
if (error_ == ERROR_ACCESS) { 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) { } 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 { } else {
options_category.focus(); options_category.focus();
} }
} }
void FreqManBaseView::get_freqman_files() { void FreqManBaseView::change_category(size_t new_index) {
// Assume this does change much, clear will preserve the existing alloc. if (categories().empty())
file_list.clear(); return;
auto files = scan_root_files(u"FREQMAN", u"*.TXT"); current_category_index = new_index;
if (!db_.open(get_freqman_path(current_category()))) {
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, {})) {
error_ = ERROR_ACCESS; error_ = ERROR_ACCESS;
} }
freqlist_view.set_db(database);
text_empty.hidden(!database.empty()); freqlist_view.set_db(db_);
set_dirty();
} }
void FreqManBaseView::refresh_list() { void FreqManBaseView::refresh_categories() {
categories.clear(); OptionsField::options_t new_categories;
get_freqman_files();
for (size_t n = 0; n < file_list.size(); n++) scan_root_files(
categories.emplace_back(std::make_pair(file_list[n].substr(0, 14), n)); freqman_dir, u"*.TXT", [&new_categories](const fs::path& path) {
// Skip temp/hidden files.
if (path.empty() || path.native()[0] == u'.')
return;
// Alphabetical sort // The UI layer will truncate long file names when displaying.
std::sort(categories.begin(), categories.end(), [](auto& left, auto& right) { 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; return left.first < right.first;
}); });
options_category.set_options(categories); // Preserve last selection; ensure in range.
if ((unsigned)current_category_id >= categories.size()) current_category_index = clip(current_category_index, 0u, new_categories.size());
current_category_id = categories.size() - 1; 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() { void FreqManBaseView::refresh_list(int delta_selected) {
save_freqman_file(file_list[categories[current_category_id].second], database); // Update the index and ensures in bounds.
nav_.pop(); freqlist_view.set_index(freqlist_view.get_index() + delta_selected);
freqlist_view.set_dirty();
} }
void FrequencySaveView::on_save_name() { /* FrequencySaveView *************************************/
text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) {
database.push_back(std::make_unique<freqman_entry>(freqman_entry{value_, 0, buffer, freqman_type::Single}));
save_current_file();
});
}
void FrequencySaveView::on_save_timestamp() {
database.push_back(std::make_unique<freqman_entry>(freqman_entry{value_, 0, live_timestamp.string(), freqman_type::Single}));
save_current_file();
}
FrequencySaveView::FrequencySaveView( FrequencySaveView::FrequencySaveView(
NavigationView& nav, NavigationView& nav,
const rf::Frequency value) const rf::Frequency value)
: FreqManBaseView(nav), : FreqManBaseView(nav) {
value_(value) { add_children(
desc_buffer.reserve(28); {&labels,
&big_display,
&button_clear,
&button_edit,
&button_save,
&text_description});
// Todo: add back ? entry_.type = freqman_type::Single;
/*for (size_t n = 0; n < database.size(); n++) { entry_.frequency_a = value;
if (database[n].value == value_) { entry_.description = to_string_timestamp(rtc_time::now());
error_ = ERROR_DUPLICATE; refresh_ui();
break;
}
}*/
add_children({&labels, button_clear.on_select = [this, &nav](Button&) {
&big_display, entry_.description = "";
&button_save_name, refresh_ui();
&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();
}; };
options_category.on_change = [this, value](size_t category_id, int32_t) { button_edit.on_select = [this, &nav](Button&) {
change_category(category_id); temp_buffer_ = entry_.description;
big_display.set(value); 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) { void FrequencySaveView::refresh_ui() {
freqlist_view.hidden(v); big_display.set(entry_.frequency_a);
text_empty.hidden(!v); text_description.set(entry_.description);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black());
set_dirty();
} }
/* FrequencyLoadView *************************************/
FrequencyLoadView::FrequencyLoadView( FrequencyLoadView::FrequencyLoadView(
NavigationView& nav) NavigationView& nav)
: FreqManBaseView(nav) { : FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) { add_children({&freqlist_view});
refresh_widgets(v);
};
add_children({&freqlist_view, // Resize to fill screen. +2 keeps text out of border.
&text_empty}); freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2});
// Resize menu view to fill screen freqlist_view.on_select = [&nav, this](size_t index) {
freqlist_view.set_parent_rect({0, 3 * 8, 240, 30 * 8}); 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&) { if (on_range_loaded && has_range)
auto& entry = database[freqlist_view.get_index()]; on_range_loaded(entry.frequency_a, entry.frequency_b);
if (entry->type == freqman_type::Range) { else if (on_frequency_loaded)
if (on_range_loaded) on_frequency_loaded(entry.frequency_a);
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);
}
nav_.pop(); // NB: this will call dtor. nav_.pop(); // NB: this will call dtor.
}; };
freqlist_view.on_leave = [this]() {
button_exit.focus();
};
} }
void FrequencyManagerView::on_edit_freq(rf::Frequency f) { /* FrequencyManagerView **********************************/
database[freqlist_view.get_index()]->frequency_a = f;
save_freqman_file(file_list[categories[current_category_id].second], database); void FrequencyManagerView::on_edit_freq() {
change_category(current_category_id); // TODO: range edit support?
auto freq_edit_view = nav_.push<FrequencyKeypadView>(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) { void FrequencyManagerView::on_edit_desc() {
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) { temp_buffer_ = current_entry().description;
database[freqlist_view.get_index()]->description = std::move(buffer); text_prompt(nav_, temp_buffer_, 28, [this](std::string& new_desc) {
save_freqman_file(file_list[categories[current_category_id].second], database); auto entry = current_entry();
change_category(current_category_id); entry.description = std::move(new_desc);
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
}); });
} }
void FrequencyManagerView::on_new_category(NavigationView& nav) { void FrequencyManagerView::on_add_category() {
text_prompt(nav, desc_buffer, 12, [this](std::string& buffer) { temp_buffer_.clear();
File freqman_file; text_prompt(nav_, temp_buffer_, 12, [this](std::string& new_name) {
create_freqman_file(buffer, freqman_file); if (!new_name.empty()) {
refresh_list(); create_freqman_file(new_name);
change_category(current_category_id); refresh_categories();
}
}); });
} }
void FrequencyManagerView::on_delete() { void FrequencyManagerView::on_del_category() {
if (database.empty()) { nav_.push<ModalMessageView>(
delete_freqman_file(file_list[categories[current_category_id].second]); "Delete", "Delete " + current_category() + "\nAre you sure?", YESNO,
refresh_list(); [this](bool choice) {
} else { if (choice) {
database.erase(database.begin() + freqlist_view.get_index()); db_.close(); // Ensure file is closed.
save_freqman_file(file_list[categories[current_category_id].second], database); auto path = get_freqman_path(current_category());
} delete_file(path);
change_category(current_category_id); refresh_categories();
}
});
} }
void FrequencyManagerView::refresh_widgets(const bool v) { void FrequencyManagerView::on_add_entry() {
button_edit_freq.hidden(v); freqman_entry entry{
button_edit_desc.hidden(v); .frequency_a = 100'000'000,
button_delete.hidden(v); .description = std::string{"Entry "} + to_string_dec_uint(db_.entry_count()),
text_empty.hidden(!v); .type = freqman_type::Single,
freqlist_view.hidden(v); };
labels.hidden(v);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black()); // Add will insert below the currently selected item.
set_dirty(); db_.insert_entry(entry, current_index() + 1);
refresh_list(1);
} }
FrequencyManagerView::~FrequencyManagerView() { void FrequencyManagerView::on_del_entry() {
// save_freqman_file(file_list[categories[current_category_id].second], database); if (db_.empty())
return;
nav_.push<ModalMessageView>(
"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( FrequencyManagerView::FrequencyManagerView(
NavigationView& nav) NavigationView& nav)
: FreqManBaseView(nav) { : FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) { add_children(
refresh_widgets(v); {&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();
}; };
// Allows for quickly exiting control.
add_children({&labels, freqlist_view.on_leave = [this]() {
&button_new_category,
&freqlist_view,
&text_empty,
&button_edit_freq,
&button_edit_desc,
&button_delete});
freqlist_view.on_select = [this](FreqManUIList&) {
button_edit_freq.focus(); button_edit_freq.focus();
}; };
button_new_category.on_select = [this, &nav](Button&) { button_add_category.on_select = [this]() {
desc_buffer = ""; on_add_category();
on_new_category(nav);
}; };
button_edit_freq.on_select = [this, &nav](Button&) { button_del_category.on_select = [this]() {
if (database.empty()) on_del_category();
database.push_back(std::make_unique<freqman_entry>(freqman_entry{0, 0, "", freqman_type::Single}));
auto new_view = nav.push<FrequencyKeypadView>(database[freqlist_view.get_index()]->frequency_a);
new_view->on_changed = [this](rf::Frequency f) {
on_edit_freq(f);
};
}; };
button_edit_desc.on_select = [this, &nav](Button&) { button_edit_freq.on_select = [this](Button&) {
if (database.empty()) on_edit_freq();
database.push_back(std::make_unique<freqman_entry>(freqman_entry{0, 0, "", freqman_type::Single}));
desc_buffer = database[freqlist_view.get_index()]->description;
on_edit_desc(nav);
}; };
button_delete.on_select = [this, &nav](Button&) { button_edit_desc.on_select = [this](Button&) {
on_delete(); on_edit_desc();
};
button_add_entry.on_select = [this]() {
on_add_entry();
};
button_del_entry.on_select = [this]() {
on_del_entry();
}; };
} }

View File

@ -1,6 +1,7 @@
/* /*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
* *
* This file is part of PortaPack. * This file is part of PortaPack.
* *
@ -20,15 +21,16 @@
* Boston, MA 02110-1301, USA. * Boston, MA 02110-1301, USA.
*/ */
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "ui.hpp" #include "ui.hpp"
#include "ui_widget.hpp" #include "ui_freqlist.hpp"
#include "ui_painter.hpp"
#include "ui_menu.hpp" #include "ui_menu.hpp"
#include "ui_navigation.hpp" #include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp" #include "ui_receiver.hpp"
#include "ui_textentry.hpp" #include "ui_textentry.hpp"
#include "freqman.hpp" #include "ui_widget.hpp"
#include "ui_freqlist.hpp"
namespace ui { namespace ui {
@ -40,74 +42,77 @@ class FreqManBaseView : public View {
void focus() override; void focus() override;
protected: protected:
using option_t = std::pair<std::string, int32_t>; using options_t = OptionsField::options_t;
using options_t = std::vector<option_t>;
NavigationView& nav_; NavigationView& nav_;
freqman_error error_{NO_ERROR}; freqman_error error_{NO_ERROR};
options_t categories{};
std::function<void(void)> on_select_frequency{nullptr}; std::function<void(void)> on_select_frequency{nullptr};
std::function<void(bool)> on_refresh_widgets{nullptr};
void get_freqman_files(); void change_category(size_t new_index);
void change_category(int32_t category_id); /* Access the categories directly from the OptionsField.
void refresh_list(); * 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{}; FreqmanDB db_{};
std::vector<std::string> file_list{};
/* The top section (category) is 20px tall. */
Labels label_category{ Labels label_category{
{{0, 4}, "Category:", Color::light_grey()}}; {{0, 2}, "Category:", Color::light_grey()}};
OptionsField options_category{ OptionsField options_category{
{9 * 8, 4}, {9 * 8, 2},
14, 14 /* length */,
{}}; {}};
FreqManUIList freqlist_view{ FreqManUIList freqlist_view{
{0, 3 * 8, 240, 23 * 8}}; {0, 3 * 8, screen_width, 12 * 16 + 2 /* 2 Keeps text out of border. */}};
Text text_empty{
{7 * 8, 12 * 8, 16 * 8, 16},
"Empty category !",
};
Button button_exit{ Button button_exit{
{16 * 8, 34 * 8, 14 * 8, 4 * 8}, {15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Exit"}; "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 { class FrequencySaveView : public FreqManBaseView {
public: public:
FrequencySaveView(NavigationView& nav, const rf::Frequency value); FrequencySaveView(NavigationView& nav, const rf::Frequency value);
std::string title() const override { return "Save freq"; };
std::string title() const override { return "Save freq."; };
private: private:
std::string desc_buffer{}; std::string temp_buffer_{};
rf::Frequency value_{}; freqman_entry entry_{};
void on_save_name(); void refresh_ui();
void on_save_timestamp();
void save_current_file();
BigFrequency big_display{ BigFrequency big_display{
{4, 2 * 16, 28 * 8, 32}, {0, 2 * 16, 28 * 8, 4 * 16},
0}; 0};
Labels labels{ Labels labels{
{{1 * 8, 12 * 8}, "Save as:", Color::white()}}; {{0 * 8, 6 * 16}, "Description:", Color::white()}};
Button button_save_name{ Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}};
{1 * 8, 17 * 8, 12 * 8, 48},
"Name (set)"}; Button button_clear{
Button button_save_timestamp{ {4 * 8, 10 * 16, 10 * 8, 2 * 16},
{1 * 8, 25 * 8, 12 * 8, 48}, "Clear"};
"Timestamp:"};
LiveDateTime live_timestamp{ Button button_edit{
{14 * 8, 27 * 8, 16 * 8, 16}}; {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 { class FrequencyLoadView : public FreqManBaseView {
@ -116,46 +121,62 @@ class FrequencyLoadView : public FreqManBaseView {
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{}; std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
FrequencyLoadView(NavigationView& nav); FrequencyLoadView(NavigationView& nav);
std::string title() const override { return "Load freq"; };
std::string title() const override { return "Load freq."; };
private:
void refresh_widgets(const bool v);
}; };
class FrequencyManagerView : public FreqManBaseView { class FrequencyManagerView : public FreqManBaseView {
public: public:
FrequencyManagerView(NavigationView& nav); FrequencyManagerView(NavigationView& nav);
~FrequencyManagerView();
std::string title() const override { return "Freqman"; }; std::string title() const override { return "Freqman"; };
private: private:
std::string desc_buffer{}; std::string temp_buffer_{};
void refresh_widgets(const bool v); void on_edit_freq();
void on_edit_freq(rf::Frequency f); void on_edit_desc();
void on_edit_desc(NavigationView& nav); void on_add_category();
void on_new_category(NavigationView& nav); void on_del_category();
void on_delete(); void on_add_entry();
void on_del_entry();
Labels labels{ Labels labels{
{{4 * 8 + 4, 26 * 8}, "Edit:", Color::light_grey()}}; {{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}};
Button button_new_category{ NewButton button_add_category{
{23 * 8, 2, 7 * 8, 20}, {23 * 8, 0 * 16, 7 * 4, 20},
"New"}; {},
&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{ Button button_edit_freq{
{0 * 8, 29 * 8, 14 * 8, 32}, {0 * 8, 15 * 16, 15 * 8, 2 * 16},
"Frequency"}; "Frequency"};
Button button_edit_desc{ Button button_edit_desc{
{0 * 8, 34 * 8, 14 * 8, 32}, {0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Description"}; "Description"};
Button button_delete{ NewButton button_add_entry{
{16 * 8, 29 * 8, 14 * 8, 32}, {15 * 8, 15 * 16, 7 * 8 + 4, 2 * 16},
"Delete"}; {},
&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 */ } /* namespace ui */

View File

@ -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.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single; entry.type = freqman_type::Single;
std::string frequency_to_add; // TODO: Use FreqmanDB
get_freq_string(entry, frequency_to_add); auto frequency_to_add = to_freqman_string(entry);
auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) { if (!result.is_valid()) {
@ -607,6 +607,7 @@ ReconView::ReconView(NavigationView& nav)
}; };
button_remove.on_select = [this](ButtonWithEncoder&) { button_remove.on_select = [this](ButtonWithEncoder&) {
// TODO: Use FreqmanDB
if (frequency_list.size() > 0) { if (frequency_list.size() > 0) {
if (!manual_mode) { if (!manual_mode) {
// scanner or recon (!scanner) mode // scanner or recon (!scanner) mode
@ -629,8 +630,7 @@ ReconView::ReconView(NavigationView& nav)
auto result = freqman_file.create(freq_file_path); auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) { if (!result.is_valid()) {
for (size_t n = 0; n < frequency_list.size(); n++) { for (size_t n = 0; n < frequency_list.size(); n++) {
std::string line; auto line = to_freqman_string(*frequency_list[n]);
get_freq_string(*frequency_list[n], line);
freqman_file.write_line(line); freqman_file.write_line(line);
} }
} }
@ -640,7 +640,6 @@ ReconView::ReconView(NavigationView& nav)
File recon_file{}; File recon_file{};
File tmp_recon_file{}; File tmp_recon_file{};
std::string tmp_freq_file_path{freq_file_path + ".TMP"}; std::string tmp_freq_file_path{freq_file_path + ".TMP"};
std::string frequency_to_add{};
freqman_entry entry = current_entry(); freqman_entry entry = current_entry();
entry.frequency_a = freq; entry.frequency_a = freq;
@ -649,7 +648,7 @@ ReconView::ReconView(NavigationView& nav)
entry.bandwidth = last_entry.bandwidth; entry.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single; 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); delete_file(tmp_freq_file_path);
auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt 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<std::string> result) { open_view->on_changed = [this](std::vector<std::string> result) {
input_file = result[0]; input_file = result[0];
output_file = result[1]; 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(); recon_save_config_to_sd();
autosave = persistent_memory::recon_autosave_freqs(); autosave = persistent_memory::recon_autosave_freqs();
@ -892,7 +891,7 @@ ReconView::ReconView(NavigationView& nav)
// Loading input and output file from settings // Loading input and output file from settings
recon_load_config_from_sd(); 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_recon_match_mode.set_selected_index(recon_match_mode);
field_squelch.set_value(squelch); field_squelch.set_value(squelch);

View File

@ -21,12 +21,13 @@
* Boston, MA 02110-1301, USA. * Boston, MA 02110-1301, USA.
*/ */
#include "ui_recon_settings.hpp"
#include "ui_navigation.hpp"
#include "ui_fileman.hpp" #include "ui_fileman.hpp"
#include "ui_navigation.hpp"
#include "ui_recon_settings.hpp"
#include "ui_textentry.hpp" #include "ui_textentry.hpp"
#include "file.hpp" #include "file.hpp"
#include "freqman_db.hpp"
#include "portapack.hpp" #include "portapack.hpp"
#include "portapack_persistent_memory.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&) { button_load_freqs.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT"); auto open_view = nav.push<FileLoadView>(".TXT");
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) { open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/"; if (new_file_path.native().find(freqman_dir.native()) == 0) {
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
_input_file = new_file_path.stem().string(); _input_file = new_file_path.stem().string();
text_input_file.set(_input_file); text_input_file.set(_input_file);
} else { } else {

View File

@ -314,17 +314,13 @@ ScannerView::ScannerView(
// Button to load txt files from the FREQMAN folder // Button to load txt files from the FREQMAN folder
button_load.on_select = [this, &nav](Button&) { button_load.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT"); auto open_view = nav.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) { open_view->push_dir(freqman_dir);
std::string dir_filter = "FREQMAN/"; open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string str_file_path = new_file_path.string(); if (new_file_path.native().find(freqman_dir.native()) == 0) {
if (str_file_path.find(dir_filter) != std::string::npos) { // assert file from the FREQMAN folder
scan_pause(); scan_pause();
// get the filename without txt extension so we can use load_freqman_file fcn frequency_file_load(new_file_path.stem().string(), true);
std::string str_file_name = new_file_path.stem().string();
frequency_file_load(str_file_name, true);
} else { } 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 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 to add current frequency (found during Search) to the Scan Frequency List
button_add.on_select = [this](Button&) { button_add.on_select = [this](Button&) {
File scanner_file; File scanner_file;

View File

@ -3819,6 +3819,44 @@ static constexpr Bitmap bitmap_icon_looking{
{16, 16}, {16, 16},
bitmap_icon_looking_data}; 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[] = { static constexpr uint8_t bitmap_icon_delete_data[] = {
0x00, 0x00,
0x00, 0x00,

View File

@ -252,12 +252,9 @@ std::filesystem::path next_filename_matching_pattern(const std::filesystem::path
std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory, std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory,
const std::filesystem::path& extension) { const std::filesystem::path& extension) {
std::vector<std::filesystem::path> file_list{}; std::vector<std::filesystem::path> file_list{};
scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) {
for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) { file_list.push_back(p);
if (std::filesystem::is_regular_file(entry.status())) { });
file_list.push_back(entry.path());
}
}
return file_list; return file_list;
} }
@ -287,7 +284,7 @@ std::filesystem::filesystem_error rename_file(
std::filesystem::filesystem_error copy_file( std::filesystem::filesystem_error copy_file(
const std::filesystem::path& file_path, const std::filesystem::path& file_path,
const std::filesystem::path& dest_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; constexpr size_t buffer_size = 512;
uint8_t buffer[buffer_size]; uint8_t buffer[buffer_size];
File src; File src;
@ -324,10 +321,11 @@ FATTimestamp file_created_date(const std::filesystem::path& file_path) {
std::filesystem::filesystem_error make_new_file( std::filesystem::filesystem_error make_new_file(
const std::filesystem::path& file_path) { const std::filesystem::path& file_path) {
File f; File f;
auto result = f.create(file_path); auto error = f.create(file_path);
return result.is_valid() if (error)
? result.value() return *error;
: std::filesystem::filesystem_error{};
return {};
} }
std::filesystem::filesystem_error make_new_directory( std::filesystem::filesystem_error make_new_directory(
@ -507,13 +505,12 @@ bool path_iequal(
} }
directory_iterator::directory_iterator( directory_iterator::directory_iterator(
std::filesystem::path path, const std::filesystem::path& path,
std::filesystem::path wild) const std::filesystem::path& wild)
: pattern{wild} { : path_{path}, wild_{wild} {
impl = std::make_shared<Impl>(); impl = std::make_shared<Impl>();
const auto result = f_findfirst(&impl->dir, &impl->filinfo, auto result = f_findfirst(&impl->dir, &impl->filinfo,
reinterpret_cast<const TCHAR*>(path.c_str()), path_.tchar(), wild_.tchar());
reinterpret_cast<const TCHAR*>(pattern.c_str()));
if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') { if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') {
impl.reset(); impl.reset();
// TODO: Throw exception if/when I enable exceptions... // TODO: Throw exception if/when I enable exceptions...

View File

@ -76,36 +76,30 @@ struct path {
: _s{} { : _s{} {
} }
path( path(const path& p)
const path& p)
: _s{p._s} { : _s{p._s} {
} }
path( path(path&& p)
path&& p)
: _s{std::move(p._s)} { : _s{std::move(p._s)} {
} }
template <class Source> template <class Source>
path( path(const Source& source)
const Source& source)
: path{std::begin(source), std::end(source)} { : path{std::begin(source), std::end(source)} {
} }
template <class InputIt> template <class InputIt>
path( path(InputIt first,
InputIt first, InputIt last)
InputIt last)
: _s{first, last} { : _s{first, last} {
} }
path( path(const char16_t* const s)
const char16_t* const s)
: _s{s} { : _s{s} {
} }
path( path(const TCHAR* const s)
const TCHAR* const s)
: _s{reinterpret_cast<const std::filesystem::path::value_type*>(s)} { : _s{reinterpret_cast<const std::filesystem::path::value_type*>(s)} {
} }
@ -132,6 +126,10 @@ struct path {
return native().c_str(); return native().c_str();
} }
const TCHAR* tchar() const {
return reinterpret_cast<const TCHAR*>(native().c_str());
}
const string_type& native() const { const string_type& native() const {
return _s; return _s;
} }
@ -149,7 +147,7 @@ struct path {
} }
path& operator/=(const path& p) { path& operator/=(const path& p) {
if (_s.back() != preferred_separator) if (_s.back() != preferred_separator && p._s.front() != preferred_separator)
_s += preferred_separator; _s += preferred_separator;
_s += p._s; _s += p._s;
return *this; return *this;
@ -207,7 +205,8 @@ class directory_iterator {
}; };
std::shared_ptr<Impl> impl{}; std::shared_ptr<Impl> impl{};
const path pattern{}; std::filesystem::path path_{};
std::filesystem::path wild_{};
friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs); 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; using iterator_category = std::input_iterator_tag;
directory_iterator() noexcept {}; 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() {} ~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 make_new_directory(const std::filesystem::path& dir_path);
std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path); std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path);
template <typename TCallback>
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<std::filesystem::path> scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension); std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension);
std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::path& directory); std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::path& directory);

View File

@ -59,6 +59,7 @@ class BufferWrapper {
using Offset = uint32_t; using Offset = uint32_t;
using Line = uint32_t; using Line = uint32_t;
using Column = uint32_t; using Column = uint32_t;
using Size = File::Size;
using Range = struct { using Range = struct {
// Offset of the start, inclusive. // Offset of the start, inclusive.
Offset start; Offset start;
@ -108,7 +109,7 @@ class BufferWrapper {
} }
/* Gets the size of the buffer in bytes. */ /* 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. */ /* Get the count of the lines in the buffer. */
uint32_t line_count() const { return line_count_; } uint32_t line_count() const { return line_count_; }

View File

@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc. * Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
* *
* This file is part of PortaPack. * 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;
extern options_t freqman_steps_short; extern options_t freqman_steps_short;
const option_t* find_by_index(const options_t& options, freqman_index_t index) { extern const option_t* find_by_index(const options_t& options, freqman_index_t index);
if (index < options.size())
return &options[index];
else
return nullptr;
}
// TODO: move into FreqmanDB type // TODO: remove in favor of FreqmanDB
/* Freqman file handling. */ /* Freqman file handling. */
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) { bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
fs::path path{u"FREQMAN/"}; return parse_freqman_file(get_freqman_path(file_stem), db, options);
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;
} }
bool delete_freqman_file(const std::string& file_stem) { bool delete_freqman_file(const std::string& file_stem) {
File freqman_file; delete_file(get_freqman_path(file_stem));
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; return true;
delete_file(freq_file_path);
return false;
} }
bool save_freqman_file(const std::string& file_stem, freqman_db& db) { 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; File freqman_file;
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT"; auto error = freqman_file.create(path);
delete_file(freq_file_path); if (error)
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())
return false; return false;
for (size_t n = 0; n < db.size(); n++)
freqman_file.write_line(to_freqman_string(*db[n]));
return true; return true;
} }
std::string freqman_item_string(freqman_entry& entry, size_t max_length) { bool create_freqman_file(const std::string& file_stem) {
std::string item_string; auto fs_error = make_new_file(get_freqman_path(file_stem));
return fs_error.ok();
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;
} }
/* Set options. */ /* Set options. */
@ -164,33 +92,6 @@ void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_steps_short); 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. */ /* Option value lookup. */
// TODO: use Optional instead of magic values. // TODO: use Optional instead of magic values.
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) { int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) {

View File

@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc. * Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
* *
* This file is part of PortaPack. * This file is part of PortaPack.
* *
@ -49,13 +50,11 @@ enum freqman_entry_modulation : uint8_t {
SPEC_MODULATION SPEC_MODULATION
}; };
// TODO: Replace with FreqmanDB.
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options); 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 delete_freqman_file(const std::string& file_stem);
bool save_freqman_file(const std::string& file_stem, freqman_db& db); bool save_freqman_file(const std::string& file_stem, freqman_db& db);
bool create_freqman_file(const std::string& file_stem, File& freqman_file); bool create_freqman_file(const std::string& file_stem);
std::string freqman_item_string(freqman_entry& item, size_t max_length);
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option); void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(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_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option); void freqman_set_tone_option(ui::OptionsField& option);
std::string freqman_entry_get_modulation_string(freqman_index_t modulation); // TODO: Can these be removed after Recon is migrated to FreqmanDB?
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);
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation); 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_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth);
int32_t freqman_entry_get_step_value(freqman_index_t step); int32_t freqman_entry_get_step_value(freqman_index_t step);

View File

@ -28,6 +28,7 @@
#include "freqman_db.hpp" #include "freqman_db.hpp"
#include "string_format.hpp" #include "string_format.hpp"
#include "tone_key.hpp" #include "tone_key.hpp"
#include "utility.hpp"
#include <array> #include <array>
#include <cctype> #include <cctype>
@ -36,6 +37,14 @@
namespace fs = std::filesystem; 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<std::string, int32_t>; using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>; using options_t = std::vector<option_t>;
@ -134,6 +143,13 @@ uint8_t find_by_name(const options_t& options, std::string_view name) {
return freqman_invalid_index; 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. /* Impl for next round of changes.
*template <typename T, size_t N> *template <typename T, size_t N>
*const T* find_by_name(const std::array<T, N>& info, std::string_view name) { *const T* find_by_name(const std::array<T, N>& 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<freqman_index_t>(tonekey::tone_key_index_by_value(tone_freq));
}
bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
if (str.empty() || str[0] == '#') if (str.empty() || str[0] == '#')
return false; 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); entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value);
} }
} else if (key == "c") { } else if (key == "c") {
// Split into whole and fractional parts. entry.tone = parse_tone_key(value);
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<freqman_index_t>(
tonekey::tone_key_index_by_value(tone_freq));
} else if (key == "d") { } else if (key == "d") {
entry.description = trim(value); entry.description = trim(value);
} else if (key == "f") { } else if (key == "f") {
@ -233,6 +353,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
return true; return true;
} }
// TODO: Use FreqmanDB iterator.
bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) { bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
File f; File f;
auto error = f.open(path); 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(); db.shrink_to_fit();
return true; 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<uint32_t>(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;
} }

View File

@ -1,7 +1,8 @@
/* /*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek * 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. * This file is part of PortaPack.
* *
@ -25,6 +26,7 @@
#define __FREQMAN_DB_H__ #define __FREQMAN_DB_H__
#include "file.hpp" #include "file.hpp"
#include "file_wrapper.hpp"
#include "utility.hpp" #include "utility.hpp"
#include <array> #include <array>
@ -33,6 +35,10 @@
#include <string_view> #include <string_view>
#include <vector> #include <vector>
/* 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; using freqman_index_t = uint8_t;
constexpr freqman_index_t freqman_invalid_index = static_cast<freqman_index_t>(-1); constexpr freqman_index_t freqman_invalid_index = static_cast<freqman_index_t>(-1);
@ -46,6 +52,9 @@ constexpr bool is_invalid(freqman_index_t index) {
return index == freqman_invalid_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 { enum class freqman_type : uint8_t {
Single, // f= Single, // f=
Range, // a=,b= Range, // a=,b=
@ -141,10 +150,16 @@ struct freqman_entry {
freqman_index_t tone{freqman_invalid_index}; 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. /* A reasonable maximum number of items to load from a freqman file.
* Apps using freqman_db should be tested and this value tuned to * Apps using freqman_db should be tested and this value tuned to
* ensure app memory stability. */ * ensure app memory stability. */
constexpr size_t freqman_default_max_entries = 90; constexpr size_t freqman_default_max_entries = 150;
struct freqman_load_options { struct freqman_load_options {
/* Loads all entries when set to 0. */ /* Loads all entries when set to 0. */
@ -157,23 +172,63 @@ struct freqman_load_options {
using freqman_entry_ptr = std::unique_ptr<freqman_entry>; using freqman_entry_ptr = std::unique_ptr<freqman_entry>;
using freqman_db = std::vector<freqman_entry_ptr>; using freqman_db = std::vector<freqman_entry_ptr>;
/* 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_entry(std::string_view str, freqman_entry& entry);
bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options); bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options);
/* Type for next round of changes. /* The tricky part of using the file directly is that there can be comments
*class FreqmanDB { * and empty lines in the file. This messes up the 'count' calculation.
* public: * Either have to live with 'count' being an upper bound have the callers
* FreqmanDB(); * know to expect that entries may be empty. */
* FreqmanDB(const FreqmanDB&) = delete; // NB: This won't apply implicit mod/bandwidth.
* FreqmanDB(FreqmanDB&&) = delete; // TODO: Reuse for parse_freqman_file
* FreqmanDB& operator=(const FreqmanDB&) = delete; class FreqmanDB {
* FreqmanDB& operator=(FreqmanDB&&) = delete; public:
* class iterator {
* size_t size() const { return 0; }; public:
* iterator(FreqmanDB& db, FileWrapper::Offset line)
* private: : db_{db}, line_{line} {}
* freqman_db entries_; 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<FileWrapper> wrapper_{};
};
#endif /* __FREQMAN_DB_H__ */ #endif /* __FREQMAN_DB_H__ */

View File

@ -25,16 +25,15 @@
* and fills it backwards towards the front. * and fills it backwards towards the front.
* The return value 'q' is a pointer to the start. * The return value 'q' is a pointer to the start.
* TODO: use std::array for all this. */ * TODO: use std::array for all this. */
template <typename Int>
static char* to_string_dec_uint_internal( static char* to_string_dec_uint_internal(
char* p, char* p,
uint32_t n) { Int n) {
*p = 0; *p = 0;
auto q = p; auto q = p;
do { do {
const uint32_t d = n % 10; *(--q) = n % 10 + '0';
const char c = d + 48;
*(--q) = c;
n /= 10; n /= 10;
} while (n != 0); } while (n != 0);
@ -60,13 +59,36 @@ static char* to_string_dec_uint_pad_internal(
return q; return q;
} }
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length) { template <typename Int>
char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) {
auto end = &buffer.back(); auto end = &buffer.back();
auto start = to_string_dec_uint_internal(end, n); auto start = to_string_dec_uint_internal(end, n);
length = end - start; length = end - start;
return 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( std::string to_string_bin(
const uint32_t n, const uint32_t n,
const uint8_t l) { const uint8_t l) {
@ -285,6 +307,9 @@ static const char* whitespace_str = " \t\r\n";
std::string trim(std::string_view str) { std::string trim(std::string_view str) {
auto first = str.find_first_not_of(whitespace_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); auto last = str.find_last_not_of(whitespace_str);
return std::string{str.substr(first, last - first + 1)}; return std::string{str.substr(first, last - first + 1)};
} }

View File

@ -41,20 +41,25 @@ enum TimeFormat {
const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'}; const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 16>; using StringFormatBuffer = std::array<char, 24>;
/* uint32_t conversion without memory allocations. */ /* uint conversion without memory allocations. */
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length); 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... // 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_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_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_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(const uint64_t n, const int32_t l = 0);
std::string to_string_hex_array(uint8_t* const array, 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_freq(const uint64_t f);
std::string to_string_short_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); std::string to_string_time_ms(const uint32_t ms);

View File

@ -1,5 +1,7 @@
/* /*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * 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. * This file is part of PortaPack.
* *
@ -24,154 +26,143 @@
#include "baseband_api.hpp" #include "baseband_api.hpp"
#include "utility.hpp" #include "utility.hpp"
#include <algorithm>
namespace ui { namespace ui {
FreqManUIList::FreqManUIList(
Rect parent_rect, FreqManUIList::FreqManUIList(Rect parent_rect)
bool instant_exec)
: Widget{parent_rect}, : Widget{parent_rect},
instant_exec_{instant_exec} { visible_lines_{(unsigned)parent_rect.height() / char_height} {
this->set_focusable(true); 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) { void FreqManUIList::paint(Painter& painter) {
freqlist_nb_lines = 0; auto rect = screen_rect();
const auto r = screen_rect();
uint8_t focused = has_focus(); if (!db_ || db_->empty()) {
const Rect r_widget_screen{r.left() + focused, r.top() + focused, r.width() - 2 * focused, r.height() - 2 * +focused}; auto line_position = rect.location() + Point{7 * 8, 6 * 16};
painter.fill_rectangle( painter.fill_rectangle(rect, Color::black());
r_widget_screen, painter.draw_string(line_position, Styles::white, "Empty Category");
Color::black());
// only return after clearing the screen so previous entries are not shown anymore
if (freqlist_db == nullptr || freqlist_db->size() == 0)
return; 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++; // Indicate when a file is too large by drawing in yellow.
} else { auto over_max = db_->entry_count() > freqman_default_max_entries;
// rect is filled, we can break auto base_style = over_max ? &Styles::yellow : &Styles::white;
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());
}
}
void FreqManUIList::set_db(freqman_db& db) { // TODO: could minimize redraw/re-read if necessary
freqlist_db = &db; // with better change tracking.
if (db.size() == 0) { for (auto offset = 0u; offset < visible_lines_; ++offset) {
current_index = 0; // The whole frame needs to be cleared so every line 'slot'
highlighted_index = 0; // is redrawn even when `text` just left empty.
} else { auto text = std::string{};
if ((unsigned)(current_index + highlighted_index) >= db.size()) { auto index = start_index_ + offset;
current_index = db.size() - 1 - highlighted_index; auto line_position = rect.location() + Point{4, 1 + (int)offset * char_height};
}
if (current_index < 0) { // Highlight the selected item.
current_index = 0; auto style = (offset == selected_index_) ? &Styles::bg_white : base_style;
if (highlighted_index > 0)
highlighted_index--; 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() { void FreqManUIList::on_focus() {
if (on_highlight) set_dirty();
on_highlight(*this); }
void FreqManUIList::on_blur() {
set_dirty();
} }
bool FreqManUIList::on_key(const KeyEvent key) { bool FreqManUIList::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) { if (!db_ || db_->empty())
if (on_select) { return false;
on_select(*this);
return true;
}
} else {
if (on_dir) {
return on_dir(*this, key);
}
}
return false;
}
bool FreqManUIList::on_touch(const TouchEvent event) { if (key == KeyEvent::Select && on_select) {
switch (event.type) { on_select(get_index());
case TouchEvent::Type::Start: return true;
set_highlighted(true); } else if (key == KeyEvent::Right && on_leave) {
set_dirty(); on_leave();
if (on_touch_press) { return true;
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;
} }
}
bool FreqManUIList::on_encoder(EncoderEvent delta) { auto delta = 0;
set_highlighted_index((int)highlighted_index + delta); 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(); set_dirty();
return true; 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<int32_t>(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<int32_t>(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<int32_t>(new_index, db_->entry_count() - 1);
}
}
} /* namespace ui */ } /* namespace ui */

View File

@ -1,5 +1,7 @@
/* /*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * 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. * This file is part of PortaPack.
* *
@ -23,53 +25,48 @@
#define __UI_FREQLIST_H__ #define __UI_FREQLIST_H__
#include "ui.hpp" #include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_painter.hpp" #include "ui_painter.hpp"
#include "ui_styles.hpp" #include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "event_m0.hpp" #include "event_m0.hpp"
#include "message.hpp"
#include "freqman.hpp" #include "freqman.hpp"
#include "freqman_db.hpp"
#include "message.hpp"
#include <cstdint> #include <cstdint>
namespace ui { namespace ui {
class FreqManUIList : public Widget { class FreqManUIList : public Widget {
public: public:
std::function<void(FreqManUIList&)> on_select{}; std::function<void(size_t)> on_select{};
std::function<void(FreqManUIList&)> on_touch_release{}; // Executed when releasing touch, after on_select. std::function<void()> on_leave{}; // Called when Right is pressed.
std::function<void(FreqManUIList&)> on_touch_press{}; // Executed when touching, before on_select.
std::function<bool(FreqManUIList&, KeyEvent)> on_dir{};
std::function<void(FreqManUIList&)> on_highlight{};
FreqManUIList(Rect parent_rect, bool instant_exec); // instant_exec: Execute on_select when you touching instead of releasing FreqManUIList(Rect parent_rect);
FreqManUIList(
Rect parent_rect)
: FreqManUIList{parent_rect, false} {
}
FreqManUIList()
: FreqManUIList{{}, {}} {
}
FreqManUIList(const FreqManUIList& other) = delete; FreqManUIList(const FreqManUIList& other) = delete;
FreqManUIList& operator=(const FreqManUIList& other) = delete; FreqManUIList& operator=(const FreqManUIList& other) = delete;
void paint(Painter& painter) override; void paint(Painter& painter) override;
void on_focus() override; void on_focus() override;
void on_blur() override;
bool on_key(const KeyEvent key) override; bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(EncoderEvent delta) 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 void set_index(size_t index);
uint8_t get_index(); // return highlighed + index size_t get_index() const;
uint8_t set_index(uint8_t index); // try to set current_index + highlighed from index, return capped index void set_db(FreqmanDB& db);
void set_db(freqman_db& db);
private: private:
void adjust_selected_index(int index);
static constexpr int8_t char_height = 16; static constexpr int8_t char_height = 16;
bool instant_exec_{false}; static constexpr int8_t char_width = 8;
freqman_db* freqlist_db{nullptr}; static constexpr int8_t line_max_length = 29;
int current_index{0}; size_t visible_lines_{0};
int highlighted_index{0};
int freqlist_nb_lines{0}; FreqmanDB* db_{nullptr};
size_t start_index_{0};
size_t selected_index_{0};
}; };
} // namespace ui } // namespace ui

View File

@ -1112,11 +1112,13 @@ NewButton::NewButton(
Rect parent_rect, Rect parent_rect,
std::string text, std::string text,
const Bitmap* bitmap, const Bitmap* bitmap,
Color color) Color color,
bool vertical_center)
: Widget{parent_rect}, : Widget{parent_rect},
text_{text}, text_{text},
bitmap_{bitmap}, bitmap_{bitmap},
color_{color} { color_{color},
vertical_center_{vertical_center} {
set_focusable(true); set_focusable(true);
} }
@ -1143,6 +1145,11 @@ void NewButton::set_color(Color color) {
set_dirty(); set_dirty();
} }
void NewButton::set_vertical_center(bool value) {
vertical_center_ = value;
set_dirty();
}
ui::Color NewButton::color() { ui::Color NewButton::color() {
return color_; return color_;
} }
@ -1164,28 +1171,34 @@ void NewButton::paint(Painter& painter) {
const Style paint_style = {style().font, bg, fg}; 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(), {r.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.left(), r.top() + r.height() - 1, r.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.left() + r.width() - 1, r.top(), 1, r.height()}, Color::dark_grey());
painter.fill_rectangle( 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); paint_style.background);
int y = r.location().y(); int y = r.top();
if (bitmap_) { 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( painter.draw_bitmap(
{r.location().x() + (r.size().width() / 2) - 8, r.location().y() + 6}, bmp_pos,
*bitmap_, *bitmap_,
color_, // Color::green(), //fg, color_, // Color::green(), //fg,
bg); bg);
y += 10;
} }
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string( if (!text_.empty()) {
{r.location().x() + (r.size().width() - label_r.width()) / 2, y + (r.size().height() - label_r.height()) / 2}, const auto label_r = paint_style.font.size_of(text_);
paint_style, painter.draw_string(
text_); {r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
paint_style,
text_);
}
} }
void NewButton::on_focus() { void NewButton::on_focus() {
@ -1482,11 +1495,11 @@ bool ImageOptionsField::on_touch(const TouchEvent event) {
OptionsField::OptionsField( OptionsField::OptionsField(
Point parent_pos, Point parent_pos,
int length, size_t length,
options_t options) options_t options)
: Widget{{parent_pos, {8 * length, 16}}}, : Widget{{parent_pos, {8 * (int)length, 16}}},
length_{length}, length_{length},
options{std::move(options)} { options_{std::move(options)} {
set_focusable(true); set_focusable(true);
} }
@ -1494,16 +1507,20 @@ size_t OptionsField::selected_index() const {
return selected_index_; return selected_index_;
} }
size_t OptionsField::selected_index_value() const { const OptionsField::name_t& OptionsField::selected_index_name() const {
return options[selected_index_].second; 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) { 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) { if (new_index != selected_index() || trigger_change) {
selected_index_ = new_index; selected_index_ = new_index;
if (on_change) { if (on_change) {
on_change(selected_index(), options[selected_index()].second); on_change(selected_index(), options_[selected_index()].second);
} }
set_dirty(); 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) { void OptionsField::set_by_value(value_t v) {
size_t new_index = 0; size_t new_index = 0;
for (const auto& option : options) { for (const auto& option : options_) {
if (option.second == v) { if (option.second == v) {
set_selected_index(new_index); set_selected_index(new_index);
return; return;
@ -1529,7 +1546,7 @@ void OptionsField::set_by_nearest_value(value_t v) {
size_t curr_index = 0; size_t curr_index = 0;
int32_t min_diff = INT32_MAX; int32_t min_diff = INT32_MAX;
for (const auto& option : options) { for (const auto& option : options_) {
auto diff = abs(v - option.second); auto diff = abs(v - option.second);
if (diff < min_diff) { if (diff < min_diff) {
min_diff = 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) { 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. // Set an invalid index to force on_change.
selected_index_ = (size_t)-1; 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()); painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, ui::Color::black());
if (selected_index() < options.size()) { if (selected_index() < options_.size()) {
const auto text = options[selected_index()].first; std::string_view temp = selected_index_name();
if (temp.length() > length_)
temp = temp.substr(0, length_);
painter.draw_string( painter.draw_string(
screen_pos(), screen_pos(),
paint_style, paint_style,
text); temp);
} }
} }
@ -1574,8 +1593,8 @@ void OptionsField::on_focus() {
bool OptionsField::on_encoder(const EncoderEvent delta) { bool OptionsField::on_encoder(const EncoderEvent delta) {
int32_t new_value = selected_index() + delta; int32_t new_value = selected_index() + delta;
if (new_value < 0) if (new_value < 0)
new_value = options.size() - 1; new_value = options_.size() - 1;
else if ((size_t)new_value >= options.size()) else if ((size_t)new_value >= options_.size())
new_value = 0; new_value = 0;
set_selected_index(new_value); set_selected_index(new_value);

View File

@ -455,7 +455,7 @@ class NewButton : public Widget {
NewButton(const NewButton&) = delete; NewButton(const NewButton&) = delete;
NewButton& operator=(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);
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()
: NewButton{{}, {}, {}} { : NewButton{{}, {}, {}} {
} }
@ -463,6 +463,7 @@ class NewButton : public Widget {
void set_bitmap(const Bitmap* bitmap); void set_bitmap(const Bitmap* bitmap);
void set_text(const std::string value); void set_text(const std::string value);
void set_color(Color value); void set_color(Color value);
void set_vertical_center(bool value);
std::string text() const; std::string text() const;
const Bitmap* bitmap(); const Bitmap* bitmap();
ui::Color color(); ui::Color color();
@ -477,6 +478,7 @@ class NewButton : public Widget {
std::string text_; std::string text_;
const Bitmap* bitmap_; const Bitmap* bitmap_;
Color color_; Color color_;
bool vertical_center_{false};
}; };
class Image : public Widget { class Image : public Widget {
@ -615,12 +617,14 @@ class OptionsField : public Widget {
std::function<void(size_t, value_t)> on_change{}; std::function<void(size_t, value_t)> on_change{};
std::function<void(void)> on_show_options{}; std::function<void(void)> 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); void set_options(options_t new_options);
size_t selected_index() const; 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_selected_index(const size_t new_index, bool trigger_change = true);
void set_by_value(value_t v); void set_by_value(value_t v);
@ -633,8 +637,8 @@ class OptionsField : public Widget {
bool on_touch(const TouchEvent event) override; bool on_touch(const TouchEvent event) override;
private: private:
const int length_; const size_t length_;
options_t options; options_t options_;
size_t selected_index_{0}; size_t selected_index_{0};
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

@ -42,14 +42,15 @@ add_executable(application_test EXCLUDE_FROM_ALL
${PROJECT_SOURCE_DIR}/test_freqman_db.cpp ${PROJECT_SOURCE_DIR}/test_freqman_db.cpp
${PROJECT_SOURCE_DIR}/test_mock_file.cpp ${PROJECT_SOURCE_DIR}/test_mock_file.cpp
${PROJECT_SOURCE_DIR}/test_optional.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp
${PROJECT_SOURCE_DIR}/test_string_format.cpp
${PROJECT_SOURCE_DIR}/test_utility.cpp ${PROJECT_SOURCE_DIR}/test_utility.cpp
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp ${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp ${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
${PROJECT_SOURCE_DIR}/../../application/string_format.cpp
# Dependencies # Dependencies
${PROJECT_SOURCE_DIR}/../../application/file.cpp ${PROJECT_SOURCE_DIR}/../../application/file.cpp
${PROJECT_SOURCE_DIR}/../../application/string_format.cpp
${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp ${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp
${PROJECT_SOURCE_DIR}/linker_stubs.cpp ${PROJECT_SOURCE_DIR}/linker_stubs.cpp
) )

View File

@ -166,14 +166,44 @@ TEST_CASE("It can parse tone freq") {
CHECK_EQ(e.tone, 3); 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") { TEST_CASE("It can parse modulation") {
freqman_entry e; freqman_entry e;
REQUIRE( REQUIRE(
parse_freqman_entry( parse_freqman_entry(
"f=123000000,d=This is the description.,m=AM", e)); "f=123000000,d=This is the description.,m=AM", e));
CHECK_EQ(e.modulation, freqman_modulation::AM); CHECK_EQ(e.modulation, freqman_modulation::AM);
REQUIRE( REQUIRE(
parse_freqman_entry( parse_freqman_entry(
"f=123000000,d=This is the description.,m=NFM", e)); "f=123000000,d=This is the description.,m=NFM", e));
@ -201,7 +231,7 @@ TEST_CASE("It can parse frequency step") {
parse_freqman_entry( parse_freqman_entry(
"f=123000000,d=This is the description.,s=0.1kHz", e)); "f=123000000,d=This is the description.,s=0.1kHz", e));
CHECK_EQ(e.step, freqman_step::_100Hz); CHECK_EQ(e.step, freqman_step::_100Hz);
REQUIRE( REQUIRE(
parse_freqman_entry( parse_freqman_entry(
"f=123000000,d=This is the description.,s=50kHz", e)); "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)); "f=123000000,d=This is the description.,s=FOO", e));
CHECK_EQ(e.step, freqman_step::Unknown); CHECK_EQ(e.step, freqman_step::Unknown);
} }
#endif */
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -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());
}