Files

486 lines
14 KiB
C++
Raw Permalink Normal View History

/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
2023-07-11 13:48:36 -07:00
* 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 "ui_freqman.hpp"
2023-09-29 09:55:27 -07:00
#include "binder.hpp"
#include "event_m0.hpp"
2023-07-11 13:48:36 -07:00
#include "portapack.hpp"
#include "rtc_time.hpp"
2023-07-14 18:46:39 -07:00
#include "tone_key.hpp"
#include "ui_receiver.hpp"
2023-07-11 13:48:36 -07:00
#include "utility.hpp"
#include "file_path.hpp"
2023-07-08 13:04:12 -07:00
#include <memory>
using namespace portapack;
2023-07-17 11:43:37 -07:00
using namespace ui;
2023-07-11 13:48:36 -07:00
namespace fs = std::filesystem;
2023-07-14 18:46:39 -07:00
// TODO: Clean up after moving to better lookup tables.
2023-07-17 11:43:37 -07:00
using options_t = OptionsField::options_t;
2024-11-22 09:54:34 +01:00
using option_db_t = std::pair<std::string_view, int32_t>;
using options_db_t = std::vector<option_db_t>;
extern options_db_t freqman_modulations;
2025-05-05 17:48:01 +02:00
extern options_db_t freqman_bandwidths[6];
2025-04-14 20:59:50 +02:00
// extern options_db_t freqman_steps; // now included via ui_receiver.hpp
2024-11-22 09:54:34 +01:00
extern options_db_t freqman_steps_short;
options_t dboptions_to_options(const options_db_t& dboptions) {
options_t options;
for (const auto& dboption : dboptions) {
options.emplace_back(dboption.first, dboption.second);
}
return options;
}
2023-07-17 11:43:37 -07:00
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
2024-11-22 09:54:34 +01:00
option.set_options(dboptions_to_options(freqman_modulations));
2023-07-17 11:43:37 -07:00
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
if (is_valid(modulation))
2024-11-22 09:54:34 +01:00
option.set_options(dboptions_to_options(freqman_bandwidths[modulation]));
2023-07-17 11:43:37 -07:00
}
void freqman_set_step_option(OptionsField& option) {
2024-11-22 09:54:34 +01:00
option.set_options(dboptions_to_options(freqman_steps));
2023-07-17 11:43:37 -07:00
}
void freqman_set_step_option_short(OptionsField& option) {
2024-11-22 09:54:34 +01:00
option.set_options(dboptions_to_options(freqman_steps_short));
2023-07-17 11:43:37 -07:00
}
2023-07-14 18:46:39 -07:00
namespace ui {
2016-12-06 13:31:38 +01:00
2023-07-11 13:48:36 -07:00
/* FreqManBaseView ***************************************/
size_t FreqManBaseView::current_category_index = 0;
2019-10-29 23:53:54 +02:00
FreqManBaseView::FreqManBaseView(
2023-05-19 08:16:05 +12:00
NavigationView& nav)
: nav_(nav) {
2023-07-11 13:48:36 -07:00
add_children(
{&label_category,
&options_category,
&button_exit});
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
options_category.on_change = [this](size_t new_index, int32_t) {
change_category(new_index);
2023-05-19 08:16:05 +12:00
};
button_exit.on_select = [this, &nav](Button&) {
nav.pop();
};
2023-07-11 13:48:36 -07:00
refresh_categories();
};
void FreqManBaseView::focus() {
2023-05-19 08:16:05 +12:00
button_exit.focus();
2023-07-11 13:48:36 -07:00
// TODO: Shouldn't be on focus.
2023-05-19 08:16:05 +12:00
if (error_ == ERROR_ACCESS) {
2023-09-27 12:03:02 -07:00
nav_.display_modal("Error", "File access error", ABORT);
2023-05-19 08:16:05 +12:00
} else if (error_ == ERROR_NOFILES) {
2023-09-27 12:03:02 -07:00
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT);
2023-05-19 08:16:05 +12:00
} else {
options_category.focus();
}
}
2023-07-11 13:48:36 -07:00
void FreqManBaseView::change_category(size_t new_index) {
if (categories().empty())
return;
2023-07-11 13:48:36 -07:00
current_category_index = new_index;
if (!db_.open(get_freqman_path(current_category()))) {
error_ = ERROR_ACCESS;
}
2023-07-11 13:48:36 -07:00
freqlist_view.set_db(db_);
}
2023-07-11 13:48:36 -07:00
void FreqManBaseView::refresh_categories() {
OptionsField::options_t new_categories;
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
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;
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
// 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) {
2023-05-19 08:16:05 +12:00
return left.first < right.first;
});
2023-07-11 13:48:36 -07:00
// 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);
}
2023-07-11 13:48:36 -07:00
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();
}
2023-07-11 13:48:36 -07:00
/* FrequencySaveView *************************************/
2016-12-06 13:31:38 +01:00
FrequencySaveView::FrequencySaveView(
2023-05-19 08:16:05 +12:00
NavigationView& nav,
const rf::Frequency value)
2023-07-11 13:48:36 -07:00
: FreqManBaseView(nav) {
add_children(
{&labels,
&big_display,
&button_save,
2023-10-01 09:04:37 -07:00
&field_description});
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
entry_.type = freqman_type::Single;
entry_.frequency_a = value;
entry_.description = to_string_timestamp(rtc_time::now());
2023-05-19 08:16:05 +12:00
2023-10-01 09:04:37 -07:00
bind(field_description, entry_.description, nav);
2023-07-11 13:48:36 -07:00
button_save.on_select = [this, &nav](Button&) {
2023-07-17 11:43:37 -07:00
db_.insert_entry(db_.entry_count(), entry_);
2023-07-11 13:48:36 -07:00
nav_.pop();
};
}
2023-10-01 09:04:37 -07:00
void FrequencySaveView::focus() {
refresh_ui();
FreqManBaseView::focus();
}
2023-07-11 13:48:36 -07:00
void FrequencySaveView::refresh_ui() {
big_display.set(entry_.frequency_a);
2017-06-23 00:13:13 +01:00
}
2023-07-11 13:48:36 -07:00
/* FrequencyLoadView *************************************/
FrequencyLoadView::FrequencyLoadView(
2023-05-19 08:16:05 +12:00
NavigationView& nav)
: FreqManBaseView(nav) {
2023-07-11 13:48:36 -07:00
add_children({&freqlist_view});
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
// Resize to fill screen. +2 keeps text out of border.
freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2});
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
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;
2023-05-19 08:16:05 +12:00
2023-07-11 13:48:36 -07:00
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);
2023-07-08 13:04:12 -07:00
nav_.pop(); // NB: this will call dtor.
2023-05-19 08:16:05 +12:00
};
2023-07-11 13:48:36 -07:00
freqlist_view.on_leave = [this]() {
button_exit.focus();
};
2016-12-09 18:21:47 +01:00
}
2023-07-11 13:48:36 -07:00
/* FrequencyManagerView **********************************/
2023-07-14 18:46:39 -07:00
void FrequencyManagerView::on_edit_entry() {
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
edit_view->on_save = [this](const freqman_entry& entry) {
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
2023-07-11 13:48:36 -07:00
void FrequencyManagerView::on_edit_freq() {
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();
};
}
2016-12-06 13:31:38 +01:00
2023-07-11 13:48:36 -07:00
void FrequencyManagerView::on_edit_desc() {
temp_buffer_ = current_entry().description;
text_prompt(nav_, temp_buffer_, freqman_max_desc_size, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_desc) {
2023-07-11 13:48:36 -07:00
auto entry = current_entry();
entry.description = std::move(new_desc);
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
2023-05-19 08:16:05 +12:00
});
}
2023-07-11 13:48:36 -07:00
void FrequencyManagerView::on_add_category() {
temp_buffer_.clear();
text_prompt(nav_, temp_buffer_, 20, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_name) {
2023-07-11 13:48:36 -07:00
if (!new_name.empty()) {
create_freqman_file(new_name);
refresh_categories();
}
2023-05-19 08:16:05 +12:00
});
}
2023-07-11 13:48:36 -07:00
void FrequencyManagerView::on_del_category() {
nav_.push<ModalMessageView>(
"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();
}
});
}
2023-07-11 13:48:36 -07:00
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.
2023-07-17 11:43:37 -07:00
db_.insert_entry(current_index() + 1, entry);
2023-07-11 13:48:36 -07:00
refresh_list(1);
2017-06-23 00:13:13 +01:00
}
2023-07-11 13:48:36 -07:00
void FrequencyManagerView::on_del_entry() {
if (db_.empty())
return;
nav_.push<ModalMessageView>(
"Delete", "Delete " + trim(pretty_string(current_entry(), 23)) + "\nAre you sure?", YESNO,
2023-07-11 13:48:36 -07:00
[this](bool choice) {
if (choice) {
db_.delete_entry(current_index());
refresh_list();
}
});
2017-02-03 15:10:27 +00:00
}
FrequencyManagerView::FrequencyManagerView(
2023-05-19 08:16:05 +12:00
NavigationView& nav)
: FreqManBaseView(nav) {
2023-07-11 13:48:36 -07:00
add_children(
{&freqlist_view,
&button_add_category,
&button_del_category,
2023-07-14 18:46:39 -07:00
&button_edit_entry,
&rect_padding,
2023-07-11 13:48:36 -07:00
&button_edit_freq,
&button_edit_desc,
&button_add_entry,
&button_del_entry});
freqlist_view.on_select = [this](size_t) {
2023-07-14 18:46:39 -07:00
button_edit_entry.focus();
2023-05-19 08:16:05 +12:00
};
2023-07-11 13:48:36 -07:00
// Allows for quickly exiting control.
freqlist_view.on_leave = [this]() {
2023-07-14 18:46:39 -07:00
button_edit_entry.focus();
2023-05-19 08:16:05 +12:00
};
2023-07-11 13:48:36 -07:00
button_add_category.on_select = [this]() {
on_add_category();
2023-05-19 08:16:05 +12:00
};
2023-07-11 13:48:36 -07:00
button_del_category.on_select = [this]() {
on_del_category();
2023-05-19 08:16:05 +12:00
};
2023-07-14 18:46:39 -07:00
button_edit_entry.on_select = [this](Button&) {
on_edit_entry();
};
2023-07-11 13:48:36 -07:00
button_edit_freq.on_select = [this](Button&) {
on_edit_freq();
2023-05-19 08:16:05 +12:00
};
2023-07-11 13:48:36 -07:00
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();
2023-05-19 08:16:05 +12:00
};
}
2023-07-14 18:46:39 -07:00
/* FrequencyEditView *************************************/
FrequencyEditView::FrequencyEditView(
NavigationView& nav,
freqman_entry entry)
: nav_{nav},
entry_{std::move(entry)} {
add_children({&labels,
&field_type,
&field_freq_a,
&field_freq_b,
&field_modulation,
&field_bandwidth,
&field_step,
&field_tone,
&field_description,
&text_validation,
&button_save,
&button_cancel});
freqman_set_modulation_option(field_modulation);
populate_bandwidth_options();
populate_step_options();
populate_tone_options();
// Add the "invalid/unset" option. Kind of hacky, but...
field_modulation.options().insert(
field_modulation.options().begin(), {"None", -1});
field_step.options().insert(
field_step.options().begin(), {"None", -1});
2023-09-29 09:55:27 -07:00
bind(field_type, entry_.type, [this](auto) {
2023-07-14 18:46:39 -07:00
refresh_ui();
2023-09-29 09:55:27 -07:00
});
2023-07-14 18:46:39 -07:00
2023-09-29 09:55:27 -07:00
bind(field_freq_a, entry_.frequency_a, nav, [this](auto) {
2023-07-14 18:46:39 -07:00
refresh_ui();
2023-09-29 09:55:27 -07:00
});
2023-07-14 18:46:39 -07:00
2023-09-29 09:55:27 -07:00
bind(field_freq_b, entry_.frequency_b, nav, [this](auto) {
2023-07-14 18:46:39 -07:00
refresh_ui();
2023-09-29 09:55:27 -07:00
});
2023-07-14 18:46:39 -07:00
2023-09-29 09:55:27 -07:00
bind(field_modulation, entry_.modulation, [this](auto) {
2023-07-14 18:46:39 -07:00
populate_bandwidth_options();
2023-09-29 09:55:27 -07:00
});
2023-07-14 18:46:39 -07:00
2023-09-29 09:55:27 -07:00
bind(field_bandwidth, entry_.bandwidth);
bind(field_step, entry_.step);
bind(field_tone, entry_.tone);
bind(field_description, entry_.description, nav_);
2023-07-14 18:46:39 -07:00
button_save.on_select = [this](Button&) {
if (on_save)
on_save(std::move(entry_));
nav_.pop();
};
button_cancel.on_select = [this](Button&) {
nav_.pop();
};
refresh_ui();
}
void FrequencyEditView::focus() {
button_cancel.focus();
}
void FrequencyEditView::refresh_ui() {
// This needs to be called whenever a UI option is changed
// in a way that causes fields to be unused or would make the
// entry fail validation.
auto is_range = entry_.type == freqman_type::Range;
auto is_ham = entry_.type == freqman_type::HamRadio;
2023-12-28 11:25:53 +01:00
auto is_repeater = entry_.type == freqman_type::Repeater;
auto has_freq_b = is_range || is_ham || is_repeater;
2023-07-14 18:46:39 -07:00
2024-05-27 21:02:52 +02:00
field_freq_b.set_style(has_freq_b ? Theme::getInstance()->bg_darkest : Theme::getInstance()->fg_medium);
field_step.set_style(is_range ? Theme::getInstance()->bg_darkest : Theme::getInstance()->fg_medium);
field_tone.set_style(is_ham ? Theme::getInstance()->bg_darkest : Theme::getInstance()->fg_medium);
2023-07-14 18:46:39 -07:00
if (is_valid(entry_)) {
text_validation.set("Valid");
2024-05-27 21:02:52 +02:00
text_validation.set_style(Theme::getInstance()->fg_green);
2023-07-14 18:46:39 -07:00
} else {
text_validation.set("Error");
2024-05-27 21:02:52 +02:00
text_validation.set_style(Theme::getInstance()->fg_red);
2023-07-14 18:46:39 -07:00
}
}
// TODO: The following functions shouldn't be needed once
// freqman_db lookup tables are complete.
void FrequencyEditView::populate_bandwidth_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
if (entry_.modulation < std::size(freqman_bandwidths)) {
auto& bandwidths = freqman_bandwidths[entry_.modulation];
2023-07-29 15:36:00 -07:00
for (auto i = 0u; i < bandwidths.size(); ++i) {
2023-07-14 18:46:39 -07:00
auto& item = bandwidths[i];
2024-11-22 09:54:34 +01:00
options.push_back({(std::string)item.first, (OptionsField::value_t)i});
2023-07-14 18:46:39 -07:00
}
}
field_bandwidth.set_options(std::move(options));
}
void FrequencyEditView::populate_step_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
2023-07-29 15:36:00 -07:00
for (auto i = 0u; i < freqman_steps.size(); ++i) {
2023-07-14 18:46:39 -07:00
auto& item = freqman_steps[i];
2024-11-22 09:54:34 +01:00
options.push_back({(std::string)item.first, (OptionsField::value_t)i});
2023-07-14 18:46:39 -07:00
}
field_step.set_options(std::move(options));
}
void FrequencyEditView::populate_tone_options() {
2023-07-17 11:43:37 -07:00
using namespace tonekey;
2023-07-14 18:46:39 -07:00
OptionsField::options_t options;
options.push_back({"None", -1});
2023-07-29 15:36:00 -07:00
for (auto i = 0u; i < tone_keys.size(); ++i) {
2023-07-17 11:43:37 -07:00
auto& item = tone_keys[i];
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
2023-07-14 18:46:39 -07:00
}
field_tone.set_options(std::move(options));
}
2023-05-19 08:16:05 +12:00
} // namespace ui