mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-04-23 07:11:28 +00:00
New Freqman edit UI (#1272)
* WIP new edit UI * Fix textfield highlight * WIP new edit * Wrap up first pass of freqman edit * Fix indexing of options --------- Co-authored-by: kallanreed <kallanreed@noreply.github.com>
This commit is contained in:
parent
61cb57e48d
commit
25923e82a4
@ -24,8 +24,12 @@
|
|||||||
#include "ui_freqman.hpp"
|
#include "ui_freqman.hpp"
|
||||||
|
|
||||||
#include "event_m0.hpp"
|
#include "event_m0.hpp"
|
||||||
|
#include "freqman.hpp"
|
||||||
#include "portapack.hpp"
|
#include "portapack.hpp"
|
||||||
#include "rtc_time.hpp"
|
#include "rtc_time.hpp"
|
||||||
|
#include "tone_key.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_styles.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -33,6 +37,10 @@
|
|||||||
using namespace portapack;
|
using namespace portapack;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// TODO: Clean up after moving to better lookup tables.
|
||||||
|
extern ui::OptionsField::options_t freqman_bandwidths[4];
|
||||||
|
extern ui::OptionsField::options_t freqman_steps;
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
/* FreqManBaseView ***************************************/
|
/* FreqManBaseView ***************************************/
|
||||||
@ -188,8 +196,15 @@ FrequencyLoadView::FrequencyLoadView(
|
|||||||
|
|
||||||
/* FrequencyManagerView **********************************/
|
/* FrequencyManagerView **********************************/
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void FrequencyManagerView::on_edit_freq() {
|
void FrequencyManagerView::on_edit_freq() {
|
||||||
// TODO: range edit support.
|
|
||||||
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
|
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
|
||||||
freq_edit_view->on_changed = [this](rf::Frequency f) {
|
freq_edit_view->on_changed = [this](rf::Frequency f) {
|
||||||
auto entry = current_entry();
|
auto entry = current_entry();
|
||||||
@ -263,20 +278,21 @@ FrequencyManagerView::FrequencyManagerView(
|
|||||||
: FreqManBaseView(nav) {
|
: FreqManBaseView(nav) {
|
||||||
add_children(
|
add_children(
|
||||||
{&freqlist_view,
|
{&freqlist_view,
|
||||||
&labels,
|
|
||||||
&button_add_category,
|
&button_add_category,
|
||||||
&button_del_category,
|
&button_del_category,
|
||||||
|
&button_edit_entry,
|
||||||
|
&rect_padding,
|
||||||
&button_edit_freq,
|
&button_edit_freq,
|
||||||
&button_edit_desc,
|
&button_edit_desc,
|
||||||
&button_add_entry,
|
&button_add_entry,
|
||||||
&button_del_entry});
|
&button_del_entry});
|
||||||
|
|
||||||
freqlist_view.on_select = [this](size_t) {
|
freqlist_view.on_select = [this](size_t) {
|
||||||
button_edit_freq.focus();
|
button_edit_entry.focus();
|
||||||
};
|
};
|
||||||
// Allows for quickly exiting control.
|
// Allows for quickly exiting control.
|
||||||
freqlist_view.on_leave = [this]() {
|
freqlist_view.on_leave = [this]() {
|
||||||
button_edit_freq.focus();
|
button_edit_entry.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
button_add_category.on_select = [this]() {
|
button_add_category.on_select = [this]() {
|
||||||
@ -287,6 +303,10 @@ FrequencyManagerView::FrequencyManagerView(
|
|||||||
on_del_category();
|
on_del_category();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
button_edit_entry.on_select = [this](Button&) {
|
||||||
|
on_edit_entry();
|
||||||
|
};
|
||||||
|
|
||||||
button_edit_freq.on_select = [this](Button&) {
|
button_edit_freq.on_select = [this](Button&) {
|
||||||
on_edit_freq();
|
on_edit_freq();
|
||||||
};
|
};
|
||||||
@ -304,4 +324,179 @@ FrequencyManagerView::FrequencyManagerView(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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});
|
||||||
|
|
||||||
|
field_type.set_by_value((int32_t)entry_.type);
|
||||||
|
field_type.on_change = [this](size_t, auto value) {
|
||||||
|
entry_.type = static_cast<freqman_type>(value);
|
||||||
|
refresh_ui();
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: this pattern should be able to be wrapped up.
|
||||||
|
field_freq_a.set_value(entry_.frequency_a);
|
||||||
|
field_freq_a.on_change = [this](rf::Frequency f) {
|
||||||
|
entry_.frequency_a = f;
|
||||||
|
refresh_ui();
|
||||||
|
};
|
||||||
|
field_freq_a.on_edit = [this]() {
|
||||||
|
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
|
||||||
|
freq_view->on_changed = [this](rf::Frequency f) {
|
||||||
|
field_freq_a.set_value(f);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
field_freq_b.set_value(entry_.frequency_b);
|
||||||
|
field_freq_b.on_change = [this](rf::Frequency f) {
|
||||||
|
entry_.frequency_b = f;
|
||||||
|
refresh_ui();
|
||||||
|
};
|
||||||
|
field_freq_b.on_edit = [this]() {
|
||||||
|
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
|
||||||
|
freq_view->on_changed = [this](rf::Frequency f) {
|
||||||
|
field_freq_b.set_value(f);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
field_modulation.set_by_value((int32_t)entry_.modulation);
|
||||||
|
field_modulation.on_change = [this](size_t, auto value) {
|
||||||
|
entry_.modulation = static_cast<freqman_index_t>(value);
|
||||||
|
populate_bandwidth_options();
|
||||||
|
};
|
||||||
|
|
||||||
|
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
|
||||||
|
field_bandwidth.on_change = [this](size_t, auto value) {
|
||||||
|
entry_.bandwidth = static_cast<freqman_index_t>(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_step.set_by_value((int32_t)entry_.step);
|
||||||
|
field_step.on_change = [this](size_t, auto value) {
|
||||||
|
entry_.step = static_cast<freqman_index_t>(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_tone.set_by_value((int32_t)entry_.tone);
|
||||||
|
field_tone.on_change = [this](size_t, auto value) {
|
||||||
|
entry_.tone = static_cast<freqman_index_t>(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_description.set_text(entry_.description);
|
||||||
|
field_description.on_change = [this](TextField& tf) {
|
||||||
|
entry_.description = tf.get_text();
|
||||||
|
};
|
||||||
|
field_description.on_select = [this](TextField& tf) {
|
||||||
|
temp_buffer_ = tf.get_text();
|
||||||
|
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
|
||||||
|
[this, &tf](std::string& new_desc) {
|
||||||
|
tf.set_text(new_desc);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
auto has_freq_b = is_range || is_ham;
|
||||||
|
|
||||||
|
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
|
||||||
|
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
|
||||||
|
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
|
||||||
|
|
||||||
|
if (is_valid(entry_)) {
|
||||||
|
text_validation.set("Valid");
|
||||||
|
text_validation.set_style(&Styles::green);
|
||||||
|
} else {
|
||||||
|
text_validation.set("Error");
|
||||||
|
text_validation.set_style(&Styles::red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
for (auto i = 1u; i < bandwidths.size(); ++i) {
|
||||||
|
auto& item = bandwidths[i];
|
||||||
|
options.push_back({item.first, (OptionsField::value_t)i});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field_bandwidth.set_options(std::move(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrequencyEditView::populate_step_options() {
|
||||||
|
OptionsField::options_t options;
|
||||||
|
options.push_back({"None", -1});
|
||||||
|
|
||||||
|
for (auto i = 1u; i < freqman_steps.size(); ++i) {
|
||||||
|
auto& item = freqman_steps[i];
|
||||||
|
options.push_back({item.first, (OptionsField::value_t)i});
|
||||||
|
}
|
||||||
|
|
||||||
|
field_step.set_options(std::move(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrequencyEditView::populate_tone_options() {
|
||||||
|
OptionsField::options_t options;
|
||||||
|
options.push_back({"None", -1});
|
||||||
|
|
||||||
|
for (auto i = 1u; i < tonekey::tone_keys.size(); ++i) {
|
||||||
|
auto& item = tonekey::tone_keys[i];
|
||||||
|
options.push_back({item.first, (OptionsField::value_t)i});
|
||||||
|
}
|
||||||
|
|
||||||
|
field_tone.set_options(std::move(options));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
@ -36,17 +36,18 @@ namespace ui {
|
|||||||
|
|
||||||
class FreqManBaseView : public View {
|
class FreqManBaseView : public View {
|
||||||
public:
|
public:
|
||||||
FreqManBaseView(
|
FreqManBaseView(NavigationView& nav);
|
||||||
NavigationView& nav);
|
|
||||||
|
|
||||||
void focus() override;
|
void focus() override;
|
||||||
|
|
||||||
|
static constexpr size_t desc_edit_max = 0x80;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
using options_t = OptionsField::options_t;
|
using options_t = OptionsField::options_t;
|
||||||
|
|
||||||
NavigationView& nav_;
|
NavigationView& nav_;
|
||||||
freqman_error error_{NO_ERROR};
|
freqman_error error_{NO_ERROR};
|
||||||
std::function<void(void)> on_select_frequency{nullptr};
|
std::function<void(void)> on_select_frequency{};
|
||||||
|
|
||||||
void change_category(size_t new_index);
|
void change_category(size_t new_index);
|
||||||
/* Access the categories directly from the OptionsField.
|
/* Access the categories directly from the OptionsField.
|
||||||
@ -79,14 +80,13 @@ class FreqManBaseView : public View {
|
|||||||
protected:
|
protected:
|
||||||
/* Static so selected category is persisted across UI instances. */
|
/* Static so selected category is persisted across UI instances. */
|
||||||
static size_t current_category_index;
|
static size_t current_category_index;
|
||||||
static constexpr size_t desc_edit_max = 0x80;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: support for new category.
|
// 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 temp_buffer_{};
|
std::string temp_buffer_{};
|
||||||
@ -122,17 +122,18 @@ 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"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class FrequencyManagerView : public FreqManBaseView {
|
class FrequencyManagerView : public FreqManBaseView {
|
||||||
public:
|
public:
|
||||||
FrequencyManagerView(NavigationView& nav);
|
FrequencyManagerView(NavigationView& nav);
|
||||||
std::string title() const override { return "Freqman"; };
|
std::string title() const override { return "Freqman"; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string temp_buffer_{};
|
std::string temp_buffer_{};
|
||||||
|
|
||||||
|
void on_edit_entry();
|
||||||
void on_edit_freq();
|
void on_edit_freq();
|
||||||
void on_edit_desc();
|
void on_edit_desc();
|
||||||
void on_add_category();
|
void on_add_category();
|
||||||
@ -140,9 +141,6 @@ class FrequencyManagerView : public FreqManBaseView {
|
|||||||
void on_add_entry();
|
void on_add_entry();
|
||||||
void on_del_entry();
|
void on_del_entry();
|
||||||
|
|
||||||
Labels labels{
|
|
||||||
{{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}};
|
|
||||||
|
|
||||||
NewButton button_add_category{
|
NewButton button_add_category{
|
||||||
{23 * 8, 0 * 16, 7 * 4, 20},
|
{23 * 8, 0 * 16, 7 * 4, 20},
|
||||||
{},
|
{},
|
||||||
@ -157,6 +155,14 @@ class FrequencyManagerView : public FreqManBaseView {
|
|||||||
Color::red(),
|
Color::red(),
|
||||||
true};
|
true};
|
||||||
|
|
||||||
|
Button button_edit_entry{
|
||||||
|
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||||
|
"Edit"};
|
||||||
|
|
||||||
|
Rectangle rect_padding{
|
||||||
|
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
|
||||||
|
Color::grey()};
|
||||||
|
|
||||||
Button button_edit_freq{
|
Button button_edit_freq{
|
||||||
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
|
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
|
||||||
"Frequency"};
|
"Frequency"};
|
||||||
@ -180,4 +186,73 @@ class FrequencyManagerView : public FreqManBaseView {
|
|||||||
true};
|
true};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class FrequencyEditView : public View {
|
||||||
|
public:
|
||||||
|
std::function<void(const freqman_entry&)> on_save{};
|
||||||
|
|
||||||
|
FrequencyEditView(
|
||||||
|
NavigationView& nav,
|
||||||
|
freqman_entry entry);
|
||||||
|
std::string title() const override { return "Freqman Edit"; }
|
||||||
|
|
||||||
|
void focus() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NavigationView& nav_;
|
||||||
|
freqman_entry entry_;
|
||||||
|
std::string temp_buffer_{};
|
||||||
|
|
||||||
|
void refresh_ui();
|
||||||
|
void populate_bandwidth_options();
|
||||||
|
void populate_step_options();
|
||||||
|
void populate_tone_options();
|
||||||
|
|
||||||
|
Labels labels{
|
||||||
|
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
|
||||||
|
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
|
||||||
|
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
|
||||||
|
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
|
||||||
|
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
|
||||||
|
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
|
||||||
|
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
|
||||||
|
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
|
||||||
|
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
|
||||||
|
};
|
||||||
|
|
||||||
|
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
|
||||||
|
{"Single", 0},
|
||||||
|
{"Range", 1},
|
||||||
|
{"HamRadio", 2},
|
||||||
|
{"Raw", 3},
|
||||||
|
}};
|
||||||
|
|
||||||
|
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
|
||||||
|
|
||||||
|
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
|
||||||
|
|
||||||
|
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
|
||||||
|
|
||||||
|
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
|
||||||
|
|
||||||
|
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
|
||||||
|
|
||||||
|
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
|
||||||
|
|
||||||
|
TextField field_description{
|
||||||
|
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
|
||||||
|
{}};
|
||||||
|
|
||||||
|
Text text_validation{
|
||||||
|
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
|
||||||
|
{}};
|
||||||
|
|
||||||
|
Button button_save{
|
||||||
|
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||||
|
"Save"};
|
||||||
|
|
||||||
|
Button button_cancel{
|
||||||
|
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
|
||||||
|
"Cancel"};
|
||||||
|
};
|
||||||
|
|
||||||
} /* namespace ui */
|
} /* namespace ui */
|
||||||
|
43
firmware/application/field_binder.hpp
Normal file
43
firmware/application/field_binder.hpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __FIELD_BINDER_H__
|
||||||
|
#define __FIELD_BINDER_H__
|
||||||
|
|
||||||
|
/* There's a common pattern that appears in applications that use
|
||||||
|
* field widgets. The field value is set, on_change and on_select
|
||||||
|
* handlers are registered. on_select will read the value and
|
||||||
|
* set the new value once changed. on_change will be called which
|
||||||
|
* will write the new value to the backing variable. */
|
||||||
|
|
||||||
|
struct binding_traits {
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr auto get_traits() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename TField, typename TTraits = get_traits<TField>()>
|
||||||
|
class FieldBinding {
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /*__FIELD_BINDER_H__*/
|
@ -97,6 +97,7 @@ options_t freqman_bandwidths[4] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: these should be indexes.
|
||||||
options_t freqman_steps = {
|
options_t freqman_steps = {
|
||||||
{"0.1kHz ", 100},
|
{"0.1kHz ", 100},
|
||||||
{"1kHz ", 1000},
|
{"1kHz ", 1000},
|
||||||
@ -116,6 +117,7 @@ options_t freqman_steps = {
|
|||||||
{"1MHz ", 1000000},
|
{"1MHz ", 1000000},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: these should be indexes.
|
||||||
options_t freqman_steps_short = {
|
options_t freqman_steps_short = {
|
||||||
{"0.1kHz", 100},
|
{"0.1kHz", 100},
|
||||||
{"1kHz", 1000},
|
{"1kHz", 1000},
|
||||||
@ -337,32 +339,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No valid frequency combination was set.
|
return is_valid(entry);
|
||||||
if (entry.type == freqman_type::Unknown)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Frequency A must be set for all types
|
|
||||||
if (entry.frequency_a == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Frequency B must be set for type Range or Ham Radio
|
|
||||||
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
|
|
||||||
if (entry.frequency_b == 0)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ranges should have frequencies A <= B.
|
|
||||||
if (entry.type == freqman_type::Range) {
|
|
||||||
if (entry.frequency_a > entry.frequency_b)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Consider additional validation:
|
|
||||||
// - Tone only on HamRadio.
|
|
||||||
// - Fail on failed parse_int.
|
|
||||||
// - Fail if bandwidth set before modulation.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use FreqmanDB iterator.
|
// TODO: Use FreqmanDB iterator.
|
||||||
@ -411,6 +388,35 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_valid(const freqman_entry& entry) {
|
||||||
|
// No valid frequency combination was set.
|
||||||
|
if (entry.type == freqman_type::Unknown)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Frequency A must be set for all types
|
||||||
|
if (entry.frequency_a == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Frequency B must be set for type Range or Ham Radio
|
||||||
|
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
|
||||||
|
if (entry.frequency_b == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ranges should have frequencies A <= B.
|
||||||
|
if (entry.type == freqman_type::Range) {
|
||||||
|
if (entry.frequency_a > entry.frequency_b)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider additional validation:
|
||||||
|
// - Tone only on HamRadio.
|
||||||
|
// - Fail on failed parse_int.
|
||||||
|
// - Fail if bandwidth set before modulation.
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* FreqmanDB ***********************************/
|
/* FreqmanDB ***********************************/
|
||||||
|
|
||||||
bool FreqmanDB::open(const std::filesystem::path& path) {
|
bool FreqmanDB::open(const std::filesystem::path& path) {
|
||||||
|
@ -182,6 +182,9 @@ 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);
|
||||||
|
|
||||||
|
/* Returns true if the entry is well-formed. */
|
||||||
|
bool is_valid(const freqman_entry& entry);
|
||||||
|
|
||||||
/* The tricky part of using the file directly is that there can be comments
|
/* The tricky part of using the file directly is that there can be comments
|
||||||
* and empty lines in the file. This messes up the 'count' calculation.
|
* and empty lines in the file. This messes up the 'count' calculation.
|
||||||
* Either have to live with 'count' being an upper bound have the callers
|
* Either have to live with 'count' being an upper bound have the callers
|
||||||
|
@ -48,7 +48,7 @@ class TextEntryView : public View {
|
|||||||
void char_add(const char c);
|
void char_add(const char c);
|
||||||
void char_delete();
|
void char_delete();
|
||||||
|
|
||||||
TextField text_input;
|
TextEdit text_input;
|
||||||
Button button_ok{
|
Button button_ok{
|
||||||
{22 * 8, 33 * 8, 7 * 8, 32},
|
{22 * 8, 33 * 8, 7 * 8, 32},
|
||||||
"OK"};
|
"OK"};
|
||||||
|
@ -349,7 +349,7 @@ Text::Text(
|
|||||||
Rect parent_rect,
|
Rect parent_rect,
|
||||||
std::string text)
|
std::string text)
|
||||||
: Widget{parent_rect},
|
: Widget{parent_rect},
|
||||||
text{text} {
|
text{std::move(text)} {
|
||||||
}
|
}
|
||||||
|
|
||||||
Text::Text(
|
Text::Text(
|
||||||
@ -357,14 +357,14 @@ Text::Text(
|
|||||||
: Text{parent_rect, {}} {
|
: Text{parent_rect, {}} {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Text::set(const std::string value) {
|
void Text::set(std::string_view value) {
|
||||||
text = value;
|
text = std::string{value};
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Text::paint(Painter& painter) {
|
void Text::paint(Painter& painter) {
|
||||||
const auto rect = screen_rect();
|
const auto rect = screen_rect();
|
||||||
const auto s = style();
|
auto s = has_focus() ? style().invert() : style();
|
||||||
|
|
||||||
painter.fill_rectangle(rect, s.background);
|
painter.fill_rectangle(rect, s.background);
|
||||||
|
|
||||||
@ -1608,9 +1608,9 @@ bool OptionsField::on_touch(const TouchEvent event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TextField ***********************************************************/
|
/* TextEdit ***********************************************************/
|
||||||
|
|
||||||
TextField::TextField(
|
TextEdit::TextEdit(
|
||||||
std::string& str,
|
std::string& str,
|
||||||
size_t max_length,
|
size_t max_length,
|
||||||
Point position,
|
Point position,
|
||||||
@ -1624,24 +1624,24 @@ TextField::TextField(
|
|||||||
set_focusable(true);
|
set_focusable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& TextField::value() const {
|
const std::string& TextEdit::value() const {
|
||||||
return text_;
|
return text_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::set_cursor(uint32_t pos) {
|
void TextEdit::set_cursor(uint32_t pos) {
|
||||||
cursor_pos_ = std::min<size_t>(pos, text_.length());
|
cursor_pos_ = std::min<size_t>(pos, text_.length());
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::set_insert_mode() {
|
void TextEdit::set_insert_mode() {
|
||||||
insert_mode_ = true;
|
insert_mode_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::set_overwrite_mode() {
|
void TextEdit::set_overwrite_mode() {
|
||||||
insert_mode_ = false;
|
insert_mode_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::char_add(char c) {
|
void TextEdit::char_add(char c) {
|
||||||
// Don't add if inserting and at max_length and
|
// Don't add if inserting and at max_length and
|
||||||
// don't overwrite if past the end of the text.
|
// don't overwrite if past the end of the text.
|
||||||
if ((text_.length() >= max_length_ && insert_mode_) ||
|
if ((text_.length() >= max_length_ && insert_mode_) ||
|
||||||
@ -1657,7 +1657,7 @@ void TextField::char_add(char c) {
|
|||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::char_delete() {
|
void TextEdit::char_delete() {
|
||||||
if (cursor_pos_ == 0)
|
if (cursor_pos_ == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -1666,7 +1666,7 @@ void TextField::char_delete() {
|
|||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextField::paint(Painter& painter) {
|
void TextEdit::paint(Painter& painter) {
|
||||||
constexpr int char_width = 8;
|
constexpr int char_width = 8;
|
||||||
|
|
||||||
auto rect = screen_rect();
|
auto rect = screen_rect();
|
||||||
@ -1702,7 +1702,7 @@ void TextField::paint(Painter& painter) {
|
|||||||
painter.draw_rectangle(cursor_box, cursor_style.background);
|
painter.draw_rectangle(cursor_box, cursor_style.background);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextField::on_key(const KeyEvent key) {
|
bool TextEdit::on_key(const KeyEvent key) {
|
||||||
if (key == KeyEvent::Left && cursor_pos_ > 0)
|
if (key == KeyEvent::Left && cursor_pos_ > 0)
|
||||||
cursor_pos_--;
|
cursor_pos_--;
|
||||||
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
|
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
|
||||||
@ -1716,7 +1716,7 @@ bool TextField::on_key(const KeyEvent key) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextField::on_encoder(const EncoderEvent delta) {
|
bool TextEdit::on_encoder(const EncoderEvent delta) {
|
||||||
int32_t new_pos = cursor_pos_ + delta;
|
int32_t new_pos = cursor_pos_ + delta;
|
||||||
|
|
||||||
// Let the encoder wrap around the ends of the text.
|
// Let the encoder wrap around the ends of the text.
|
||||||
@ -1729,7 +1729,7 @@ bool TextField::on_encoder(const EncoderEvent delta) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextField::on_touch(const TouchEvent event) {
|
bool TextEdit::on_touch(const TouchEvent event) {
|
||||||
if (event.type == TouchEvent::Type::Start)
|
if (event.type == TouchEvent::Type::Start)
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
@ -1737,6 +1737,32 @@ bool TextField::on_touch(const TouchEvent event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TextField *************************************************************/
|
||||||
|
|
||||||
|
TextField::TextField(Rect parent_rect, std::string text)
|
||||||
|
: Text(parent_rect, std::move(text)) {
|
||||||
|
set_focusable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& TextField::get_text() const {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextField::set_text(std::string_view value) {
|
||||||
|
set(value);
|
||||||
|
if (on_change)
|
||||||
|
on_change(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextField::on_key(KeyEvent key) {
|
||||||
|
if (key == KeyEvent::Select && on_select) {
|
||||||
|
on_select(*this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/* NumberField ***********************************************************/
|
/* NumberField ***********************************************************/
|
||||||
|
|
||||||
NumberField::NumberField(
|
NumberField::NumberField(
|
||||||
|
@ -33,10 +33,11 @@
|
|||||||
#include "portapack.hpp"
|
#include "portapack.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
@ -202,11 +203,11 @@ class Text : public Widget {
|
|||||||
Text(Rect parent_rect, std::string text);
|
Text(Rect parent_rect, std::string text);
|
||||||
Text(Rect parent_rect);
|
Text(Rect parent_rect);
|
||||||
|
|
||||||
void set(const std::string value);
|
void set(std::string_view value);
|
||||||
|
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
private:
|
protected:
|
||||||
std::string text;
|
std::string text;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -619,6 +620,7 @@ class OptionsField : public Widget {
|
|||||||
|
|
||||||
OptionsField(Point parent_pos, size_t length, options_t options);
|
OptionsField(Point parent_pos, size_t length, options_t options);
|
||||||
|
|
||||||
|
options_t& options() { return options_; }
|
||||||
const options_t& options() const { return options_; }
|
const options_t& options() const { return options_; }
|
||||||
void set_options(options_t new_options);
|
void set_options(options_t new_options);
|
||||||
|
|
||||||
@ -642,14 +644,14 @@ class OptionsField : public Widget {
|
|||||||
size_t selected_index_{0};
|
size_t selected_index_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
// A TextField is bound to a string reference and allows the string
|
// A TextEdit is bound to a string reference and allows the string
|
||||||
// to be manipulated. The field itself does not provide the UI for
|
// to be manipulated. The field itself does not provide the UI for
|
||||||
// setting the value. It provides the UI of rendering the text,
|
// setting the value. It provides the UI of rendering the text,
|
||||||
// a cursor, and an API to edit the string content.
|
// a cursor, and an API to edit the string content.
|
||||||
class TextField : public Widget {
|
class TextEdit : public Widget {
|
||||||
public:
|
public:
|
||||||
TextField(std::string& str, Point position, uint32_t length = 30)
|
TextEdit(std::string& str, Point position, uint32_t length = 30)
|
||||||
: TextField{str, 64, position, length} {}
|
: TextEdit{str, 64, position, length} {}
|
||||||
|
|
||||||
// Str: the string containing the content to edit.
|
// Str: the string containing the content to edit.
|
||||||
// Max_length: max length the string is allowed to use.
|
// Max_length: max length the string is allowed to use.
|
||||||
@ -658,12 +660,12 @@ class TextField : public Widget {
|
|||||||
// - Characters are 8 pixels wide.
|
// - Characters are 8 pixels wide.
|
||||||
// - The screen can show 30 characters max.
|
// - The screen can show 30 characters max.
|
||||||
// - The control is 16 pixels tall.
|
// - The control is 16 pixels tall.
|
||||||
TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30);
|
TextEdit(std::string& str, size_t max_length, Point position, uint32_t length = 30);
|
||||||
|
|
||||||
TextField(const TextField&) = delete;
|
TextEdit(const TextEdit&) = delete;
|
||||||
TextField(TextField&&) = delete;
|
TextEdit(TextEdit&&) = delete;
|
||||||
TextField& operator=(const TextField&) = delete;
|
TextEdit& operator=(const TextEdit&) = delete;
|
||||||
TextField& operator=(TextField&&) = delete;
|
TextEdit& operator=(TextEdit&&) = delete;
|
||||||
|
|
||||||
const std::string& value() const;
|
const std::string& value() const;
|
||||||
|
|
||||||
@ -688,6 +690,22 @@ class TextField : public Widget {
|
|||||||
bool insert_mode_;
|
bool insert_mode_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TextField : public Text {
|
||||||
|
public:
|
||||||
|
std::function<void(TextField&)> on_select{};
|
||||||
|
std::function<void(TextField&)> on_change{};
|
||||||
|
|
||||||
|
TextField(Rect parent_rect, std::string text);
|
||||||
|
|
||||||
|
const std::string& get_text() const;
|
||||||
|
void set_text(std::string_view value);
|
||||||
|
|
||||||
|
bool on_key(KeyEvent key) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Text::set;
|
||||||
|
};
|
||||||
|
|
||||||
class NumberField : public Widget {
|
class NumberField : public Widget {
|
||||||
public:
|
public:
|
||||||
std::function<void(NumberField&)> on_select{};
|
std::function<void(NumberField&)> on_select{};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user