mirror of
https://github.com/eried/portapack-mayhem.git
synced 2025-08-02 11:36:38 -04:00
Add Remote App & UI updates. (#1451)
* Alpha order sub-menus * WIP Getting Remote types outlined * WIP building UI * WIP adding RemoteButton control * WIP Fix build * WIP Basic editing support * Border on the active button * Make TxView2 sane * Add easier RGB color creation from uint32 * Center some button icons * WIP Remote - main UI * WIP main UI mostly working, can send * Add 'join' utility * WIP save/load * Pre-alloc buttons to prevent focus dangling * Alpha order settings/debug pages * Add UI for picking capture and set frequency * WIP Getting really close now * Fix path for init name * Some fit & finish
This commit is contained in:
parent
537cf2e79b
commit
fca373d936
27 changed files with 1205 additions and 312 deletions
|
@ -24,9 +24,6 @@
|
|||
#ifndef __GPS_SIM_APP_HPP__
|
||||
#define __GPS_SIM_APP_HPP__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
@ -109,10 +106,8 @@ class GpsSimAppView : public View {
|
|||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Used in Replay / GPS Simul / Playlist App)
|
||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
||||
};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
|
|
|
@ -203,9 +203,6 @@ ReplayAppView::ReplayAppView(
|
|||
|
||||
ReplayAppView::~ReplayAppView() {
|
||||
transmitter_model.disable();
|
||||
|
||||
display.fill_rectangle({0, 0, 240, 320}, Color::black()); // Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
|
||||
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,9 +23,6 @@
|
|||
#ifndef __REPLAY_APP_HPP__
|
||||
#define __REPLAY_APP_HPP__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
@ -99,16 +96,13 @@ class ReplayAppView : public View {
|
|||
ProgressBar progressbar{
|
||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||
|
||||
// TODO: Does this need to be a frequency field at all?
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
74, 1 * 8, SHORT_UI // x(columns), y (line) position. (Uused in Repay / GPS Simul / Playlist App)
|
||||
// 10*8, 2*8, NORMAL_UI // x(columns), y (line) position. (Used in Mic App)
|
||||
};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
|
|
|
@ -398,17 +398,17 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
|||
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||
}
|
||||
add_items({
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
|
||||
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
|
||||
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
|
||||
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
|
||||
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
|
||||
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <algorithm>
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_playlist.hpp"
|
||||
#include "ui_remote.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
@ -44,6 +45,7 @@ static const fs::path c16_ext{u".C16"};
|
|||
static const fs::path cxx_ext{u".C*"};
|
||||
static const fs::path png_ext{u".PNG"};
|
||||
static const fs::path bmp_ext{u".BMP"};
|
||||
static const fs::path rem_ext{u".REM"};
|
||||
} // namespace ui
|
||||
|
||||
namespace {
|
||||
|
@ -525,6 +527,9 @@ bool FileManagerView::handle_file_open() {
|
|||
nav_.push<SplashViewer>(path);
|
||||
reload_current();
|
||||
return true;
|
||||
} else if (path_iequal(rem_ext, ext)) {
|
||||
nav_.push<RemoteView>(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -629,7 +634,7 @@ FileManagerView::FileManagerView(
|
|||
button_open_notepad.on_select = [this]() {
|
||||
if (selected_is_valid() && !get_selected_entry().is_directory) {
|
||||
auto path = get_selected_full_path();
|
||||
nav_.replace<TextEditorView>(path);
|
||||
nav_.push<TextEditorView>(path);
|
||||
} else
|
||||
nav_.display_modal("Open in Notepad", "Can't open that in Notepad.");
|
||||
};
|
||||
|
|
|
@ -77,7 +77,8 @@ class FileManBaseView : public View {
|
|||
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
|
||||
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
|
||||
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // PPL is the file extension for playlist app
|
||||
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
|
||||
{u".REM", &bitmap_icon_remote, ui::Color::orange()}, // Remote
|
||||
{u"", &bitmap_icon_file, ui::Color::light_grey()} // NB: Must be last.
|
||||
};
|
||||
|
||||
|
@ -269,7 +270,8 @@ class FileManagerView : public FileManBaseView {
|
|||
{4 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_options_datetime,
|
||||
Color::orange()};
|
||||
Color::orange(),
|
||||
/*vcenter*/ true};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
|
|
@ -633,7 +633,7 @@ MicTXView::MicTXView(
|
|||
|
||||
MicTXView::~MicTXView() {
|
||||
audio::input::stop();
|
||||
transmitter_model.set_target_frequency(tx_frequency); // Save Tx frequency instead of Rx. Or maybe we need some "System Wide" changes to seperate Tx and Rx frequency.
|
||||
transmitter_model.set_target_frequency(tx_frequency);
|
||||
transmitter_model.disable();
|
||||
if (rx_enabled) // Also turn off audio rx if enabled
|
||||
rxaudio(false);
|
||||
|
|
|
@ -25,9 +25,6 @@
|
|||
#ifndef __UI_MICTX_H__
|
||||
#define __UI_MICTX_H__
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui.hpp"
|
||||
|
@ -219,10 +216,8 @@ class MicTXView : public View {
|
|||
' '};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
// new handling of NumberField field_rfgain, NumberField field_rfamp
|
||||
// 3*8, 2*8, SHORT_UI // x(columns), y (line) position. (used in Replay / GPS Simul / Playlist App's)
|
||||
3 * 8, 2 * 8, NORMAL_UI // x(columns), y (line) position. (used in Mic App)
|
||||
};
|
||||
{3 * 8, 5 * 8},
|
||||
/*short_ui*/ false};
|
||||
|
||||
OptionsField options_mode{
|
||||
{24 * 8, 5 * 8},
|
||||
|
|
|
@ -386,6 +386,7 @@ PlaylistView::PlaylistView(
|
|||
&waterfall,
|
||||
});
|
||||
|
||||
ensure_directory(u"PLAYLIST");
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
|
||||
field_frequency.set_value(100'000'000);
|
||||
|
|
|
@ -21,9 +21,6 @@
|
|||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#define SHORT_UI true
|
||||
#define NORMAL_UI false
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "file.hpp"
|
||||
|
@ -63,7 +60,7 @@ class PlaylistView : public View {
|
|||
|
||||
// More header == less spectrum view.
|
||||
static constexpr ui::Dim header_height = 6 * 16;
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
static constexpr uint32_t baseband_bandwidth = 2'500'000;
|
||||
|
||||
struct playlist_entry {
|
||||
std::filesystem::path path{};
|
||||
|
@ -118,14 +115,11 @@ class PlaylistView : public View {
|
|||
Text text_sample_rate{
|
||||
{10 * 8, 1 * 16, 7 * 8, 16}};
|
||||
|
||||
/*v making there's 1px line (instead of two) between two progress bars,
|
||||
* by letting 1px overlapped.
|
||||
* So, since they overlapped 1px, they are visually same, and looks better.
|
||||
*/
|
||||
|
||||
ProgressBar progressbar_track{
|
||||
{18 * 8, 1 * 16, 12 * 8, 8 + 1}};
|
||||
|
||||
// (-1) to overlap with progressbar_track so there's
|
||||
// only 1 pixel between them instead of 2.
|
||||
ProgressBar progressbar_transmit{
|
||||
{18 * 8, 3 * 8 - 1, 12 * 8, 8}};
|
||||
|
||||
|
@ -135,7 +129,8 @@ class PlaylistView : public View {
|
|||
// TODO: delay duration field.
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
9 * 8, 1 * 8, SHORT_UI};
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
@ -22,30 +21,615 @@
|
|||
|
||||
#include "ui_remote.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "convert.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "io_convert.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void RemoteView::focus() {
|
||||
button.focus();
|
||||
static constexpr uint8_t text_edit_max = 30;
|
||||
|
||||
/* RemoteEntryModel **************************************/
|
||||
|
||||
std::string RemoteEntryModel::to_string() const {
|
||||
return join(',', {path.string(),
|
||||
name,
|
||||
to_string_dec_uint(icon),
|
||||
to_string_dec_uint(bg_color),
|
||||
to_string_dec_uint(fg_color),
|
||||
to_string_dec_uint(metadata.center_frequency),
|
||||
to_string_dec_uint(metadata.sample_rate)});
|
||||
}
|
||||
|
||||
RemoteView::~RemoteView() {
|
||||
// transmitter_model.disable();
|
||||
// baseband::shutdown();
|
||||
Optional<RemoteEntryModel> RemoteEntryModel::parse(std::string_view line) {
|
||||
auto cols = split_string(line, ',');
|
||||
|
||||
if (cols.size() < 7)
|
||||
return {};
|
||||
|
||||
RemoteEntryModel entry{};
|
||||
|
||||
entry.path = cols[0];
|
||||
entry.name = std::string{cols[1]};
|
||||
parse_int(cols[2], entry.icon);
|
||||
parse_int(cols[3], entry.bg_color);
|
||||
parse_int(cols[4], entry.fg_color);
|
||||
parse_int(cols[5], entry.metadata.center_frequency);
|
||||
parse_int(cols[6], entry.metadata.sample_rate);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
RemoteView::RemoteView(
|
||||
NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&button});
|
||||
/* RemoteModel *******************************************/
|
||||
|
||||
button.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
bool RemoteModel::delete_entry(const RemoteEntryModel* entry) {
|
||||
// NB: expecting 'entry' to be a pointer to an entry in vector.
|
||||
auto it = std::find_if(
|
||||
entries.begin(), entries.end(),
|
||||
[entry](auto& item) { return entry == &item; });
|
||||
if (it == entries.end())
|
||||
return false;
|
||||
|
||||
entries.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoteModel::load(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.open(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
entries.clear();
|
||||
|
||||
bool first = true;
|
||||
auto reader = FileLineReader(f);
|
||||
for (const auto& line : reader) {
|
||||
if (line.length() == 0 || line[0] == '#')
|
||||
continue; // Empty or comment line.
|
||||
|
||||
// First line is the "name" field.
|
||||
if (first) {
|
||||
name = trim(line);
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// All the other lines are button entries.
|
||||
auto entry = RemoteEntryModel::parse(line);
|
||||
if (entry)
|
||||
entries.push_back(*std::move(entry));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoteModel::save(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
f.write_line(name);
|
||||
for (auto& entry : entries)
|
||||
f.write_line(entry.to_string());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* RemoteButton ******************************************/
|
||||
|
||||
RemoteButton::RemoteButton(Rect parent_rect, RemoteEntryModel* entry)
|
||||
: NewButton{parent_rect, {}, nullptr},
|
||||
entry_{nullptr} {
|
||||
set_entry(entry);
|
||||
// Forward to on_select2 -- this isn't ideal, but works for now.
|
||||
on_select = [this]() {
|
||||
if (on_select2)
|
||||
on_select2(*this);
|
||||
};
|
||||
}
|
||||
|
||||
void RemoteButton::on_focus() {
|
||||
// Enable long press on "Select".
|
||||
SwitchesState config;
|
||||
config[toUType(Switch::Sel)] = true;
|
||||
set_switches_long_press_config(config);
|
||||
}
|
||||
|
||||
void RemoteButton::on_blur() {
|
||||
// Reset long press.
|
||||
SwitchesState config{};
|
||||
set_switches_long_press_config(config);
|
||||
}
|
||||
|
||||
bool RemoteButton::on_key(KeyEvent key) {
|
||||
if (key == KeyEvent::Select) {
|
||||
if (key_is_long_pressed(key) && on_long_select) {
|
||||
on_long_select(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (on_select2) {
|
||||
on_select2(*this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RemoteButton::paint(Painter& painter) {
|
||||
NewButton::paint(painter);
|
||||
|
||||
// Add a border on the highlighted button.
|
||||
if (has_focus() || highlighted()) {
|
||||
auto r = screen_rect();
|
||||
painter.draw_rectangle(r, Color::white());
|
||||
|
||||
auto p = r.location() + Point{1, 1};
|
||||
auto s = Size{r.size().width() - 2, r.size().height() - 2};
|
||||
painter.draw_rectangle({p, s}, Color::light_grey());
|
||||
}
|
||||
};
|
||||
|
||||
RemoteEntryModel* RemoteButton::entry() {
|
||||
return entry_;
|
||||
}
|
||||
|
||||
void RemoteButton::set_entry(RemoteEntryModel* entry) {
|
||||
entry_ = entry;
|
||||
set_focusable(entry_ != nullptr);
|
||||
hidden(entry_ == nullptr);
|
||||
|
||||
if (entry_) {
|
||||
set_text(entry_->name);
|
||||
set_bitmap(RemoteIcons::get(entry_->icon));
|
||||
}
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
Style RemoteButton::paint_style() {
|
||||
if (!entry_)
|
||||
return style();
|
||||
|
||||
MutableStyle s{style()};
|
||||
s.foreground = RemoteColors::get(entry_->fg_color);
|
||||
s.background = RemoteColors::get(entry_->bg_color);
|
||||
|
||||
if (has_focus() || highlighted())
|
||||
s.invert();
|
||||
|
||||
// It's kind of a hack to set 'color_' here, but the base
|
||||
// class' paint logic is kind of convoluted but isn't worth
|
||||
// the extra bytes to copy and paste a paint override.
|
||||
color_ = s.foreground;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/* RemoteEntryEditView ***********************************/
|
||||
|
||||
RemoteEntryEditView::RemoteEntryEditView(
|
||||
NavigationView& nav,
|
||||
RemoteEntryModel& entry)
|
||||
: entry_{entry} {
|
||||
add_children({
|
||||
&labels,
|
||||
&field_name,
|
||||
&field_path,
|
||||
&field_freq,
|
||||
&text_rate,
|
||||
&field_icon_index,
|
||||
&field_fg_color_index,
|
||||
&field_bg_color_index,
|
||||
&button_preview,
|
||||
&button_delete,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
// TODO: It's time to make field bindings and clean this mess up.
|
||||
field_name.on_change = [this](TextField& tf) {
|
||||
entry_.name = tf.get_text();
|
||||
button_preview.set_text(entry_.name);
|
||||
};
|
||||
field_name.on_select = [this, &nav](TextField& tf) {
|
||||
temp_buffer_ = tf.get_text();
|
||||
text_prompt(nav, temp_buffer_, text_edit_max,
|
||||
[this, &tf](std::string& str) {
|
||||
tf.set_text(str);
|
||||
});
|
||||
};
|
||||
field_name.set_text(entry_.name);
|
||||
|
||||
field_path.on_select = [this, &nav](TextField&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C*");
|
||||
open_view->push_dir(u"CAPTURES");
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
load_path(std::move(path));
|
||||
refresh_ui();
|
||||
};
|
||||
};
|
||||
|
||||
field_freq.on_edit = [this, &nav]() {
|
||||
auto freq_view = nav.push<FrequencyKeypadView>(entry_.metadata.center_frequency);
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
entry_.metadata.center_frequency = f;
|
||||
field_freq.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_icon_index.on_change = [this](int32_t v) {
|
||||
entry_.icon = v;
|
||||
button_preview.set_bitmap(RemoteIcons::get(v));
|
||||
};
|
||||
field_icon_index.set_value(entry.icon);
|
||||
|
||||
field_fg_color_index.on_change = [this](int32_t v) {
|
||||
entry_.fg_color = v;
|
||||
button_preview.set_color(RemoteColors::get(v));
|
||||
};
|
||||
field_fg_color_index.set_value(entry_.fg_color);
|
||||
|
||||
field_bg_color_index.on_change = [this](int32_t v) {
|
||||
entry_.bg_color = v;
|
||||
button_preview.set_dirty();
|
||||
};
|
||||
field_bg_color_index.set_value(entry_.bg_color);
|
||||
|
||||
button_delete.on_select = [this, &nav]() {
|
||||
nav.display_modal(
|
||||
"Delete?", " Delete this button?", YESNO,
|
||||
[this, &nav](bool choice) {
|
||||
if (choice) {
|
||||
if (on_delete)
|
||||
on_delete(entry_);
|
||||
|
||||
// Exit the edit view upon delete.
|
||||
nav.set_on_pop([&nav]() { nav.pop(); });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void RemoteEntryEditView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void RemoteEntryEditView::refresh_ui() {
|
||||
field_path.set_text(entry_.path.filename().string());
|
||||
field_freq.set_value(entry_.metadata.center_frequency);
|
||||
text_rate.set(unit_auto_scale(entry_.metadata.sample_rate, 3, 0) + "Hz");
|
||||
}
|
||||
|
||||
void RemoteEntryEditView::load_path(std::filesystem::path&& path) {
|
||||
// Read metafile if it exists.
|
||||
auto metadata_path = get_metadata_path(path);
|
||||
auto metadata = read_metadata_file(metadata_path);
|
||||
entry_.path = std::move(path);
|
||||
|
||||
// Use metadata if found, otherwise fallback to the TX frequency.
|
||||
if (metadata)
|
||||
entry_.metadata = *metadata;
|
||||
else
|
||||
entry_.metadata = {transmitter_model.target_frequency(), 500'000};
|
||||
}
|
||||
|
||||
/* RemoteView ********************************************/
|
||||
|
||||
RemoteView::RemoteView(
|
||||
NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_replay);
|
||||
|
||||
add_children({
|
||||
&field_title,
|
||||
&tx_view,
|
||||
&check_loop,
|
||||
&field_filename,
|
||||
&button_add,
|
||||
&button_new,
|
||||
&button_open,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
create_buttons();
|
||||
|
||||
field_title.on_select = [this, &nav](TextField&) {
|
||||
temp_buffer_ = model_.name;
|
||||
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
|
||||
model_.name = new_name;
|
||||
refresh_ui();
|
||||
set_needs_save();
|
||||
});
|
||||
};
|
||||
|
||||
field_filename.on_select = [this, &nav](TextField&) {
|
||||
temp_buffer_ = remote_path_.stem().string();
|
||||
text_prompt(nav_, temp_buffer_, text_edit_max, [this](std::string& new_name) {
|
||||
rename_remote(new_name);
|
||||
refresh_ui();
|
||||
});
|
||||
};
|
||||
|
||||
button_add.on_select = [this]() { add_button(); };
|
||||
button_new.on_select = [this]() { new_remote(); };
|
||||
button_open.on_select = [this]() { open_remote(); };
|
||||
|
||||
// Fill in the area between the remote buttons and bottom UI with waterfall.
|
||||
Dim waterfall_top = buttons_top_.y() + button_area_height;
|
||||
Dim waterfall_bottom = button_add.parent_rect().top();
|
||||
Dim waterfall_height = waterfall_bottom - waterfall_top;
|
||||
waterfall.set_parent_rect({0, waterfall_top, screen_width, waterfall_height});
|
||||
|
||||
ensure_directory(u"REMOTES");
|
||||
|
||||
// Load the previously loaded remote if exists.
|
||||
if (!load_remote(settings_.remote_path))
|
||||
init_remote();
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
RemoteView::RemoteView(NavigationView& nav, fs::path path)
|
||||
: RemoteView(nav) {
|
||||
load_remote(std::move(path));
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
RemoteView::~RemoteView() {
|
||||
stop();
|
||||
baseband::shutdown();
|
||||
|
||||
save_remote(/*show_error*/ false);
|
||||
}
|
||||
|
||||
void RemoteView::focus() {
|
||||
if (model_.entries.empty())
|
||||
button_add.focus();
|
||||
else
|
||||
buttons_[0]->focus();
|
||||
}
|
||||
|
||||
void RemoteView::create_buttons() {
|
||||
// Handler callbacks.
|
||||
auto handle_send = [this](RemoteButton& btn) {
|
||||
if (btn.entry()->path.empty())
|
||||
// No path set? Go to edit mode instead.
|
||||
edit_button(btn);
|
||||
else if (is_sending() && &btn == current_btn_)
|
||||
// Pressed the same button again? Stop.
|
||||
stop();
|
||||
else
|
||||
// Start sending.
|
||||
send_button(btn);
|
||||
};
|
||||
|
||||
auto handle_edit = [this](RemoteButton& btn) {
|
||||
edit_button(btn);
|
||||
};
|
||||
|
||||
// Create and add RemoteButtons for the whole grid.
|
||||
for (size_t i = 0; i < max_buttons; ++i) {
|
||||
Coord x = i % button_cols;
|
||||
Coord y = i / button_cols;
|
||||
Point pos = Point{x * button_width, y * button_height} + buttons_top_;
|
||||
|
||||
auto btn = std::make_unique<RemoteButton>(
|
||||
Rect{pos, {button_width, button_height}},
|
||||
nullptr);
|
||||
btn->on_select2 = handle_send;
|
||||
btn->on_long_select = handle_edit;
|
||||
|
||||
add_child(btn.get());
|
||||
buttons_.push_back(std::move(btn));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteView::reset_buttons() {
|
||||
// Whever the model's entries instance is invalidated,
|
||||
// all the pointers in the buttons will end up dangling.
|
||||
// TODO: This is pretty lame. Could maybe static alloc?
|
||||
for (auto& btn : buttons_)
|
||||
btn->set_entry(nullptr);
|
||||
}
|
||||
|
||||
void RemoteView::refresh_ui() {
|
||||
field_title.set_text(model_.name);
|
||||
field_filename.set_text(remote_path_.stem().string());
|
||||
|
||||
// Update buttons from the model.
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
if (i < model_.entries.size())
|
||||
buttons_[i]->set_entry(&model_.entries[i]);
|
||||
else
|
||||
buttons_[i]->set_entry(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteView::add_button() {
|
||||
if (model_.entries.size() >= max_buttons)
|
||||
return;
|
||||
|
||||
// Don't let replay thread read the model while editing.
|
||||
stop();
|
||||
|
||||
model_.entries.push_back({{}, "<EMPTY>", 0, 3, 1});
|
||||
reset_buttons();
|
||||
refresh_ui();
|
||||
set_needs_save();
|
||||
}
|
||||
|
||||
void RemoteView::edit_button(RemoteButton& btn) {
|
||||
// Don't let replay thread read the model while editing.
|
||||
stop();
|
||||
|
||||
auto edit_view = nav_.push<RemoteEntryEditView>(*btn.entry());
|
||||
nav_.set_on_pop([this]() {
|
||||
refresh_ui();
|
||||
set_needs_save();
|
||||
});
|
||||
|
||||
edit_view->on_delete = [this](RemoteEntryModel& to_delete) {
|
||||
model_.delete_entry(&to_delete);
|
||||
reset_buttons();
|
||||
};
|
||||
}
|
||||
|
||||
void RemoteView::send_button(RemoteButton& btn) {
|
||||
// TODO: If this is called while is_sending() == true,
|
||||
// it just stops and doesn't start the new button?
|
||||
|
||||
// Reset everything to prepare to send a file.
|
||||
stop();
|
||||
current_btn_ = &btn; // Stash for looping.
|
||||
|
||||
// Open the sample file to send.
|
||||
auto reader = std::make_unique<FileConvertReader>();
|
||||
auto error = reader->open(btn.entry()->path);
|
||||
if (error) {
|
||||
show_error("Can't open file:\n" + btn.entry()->path.stem().string());
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the sample rate in proc_replay baseband.
|
||||
baseband::set_sample_rate(
|
||||
btn.entry()->metadata.sample_rate,
|
||||
get_oversample_rate(btn.entry()->metadata.sample_rate));
|
||||
|
||||
// ReplayThread starts immediately on construction; must be set before creating.
|
||||
transmitter_model.set_target_frequency(btn.entry()->metadata.center_frequency);
|
||||
transmitter_model.set_sampling_rate(get_actual_sample_rate(btn.entry()->metadata.sample_rate));
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
// ReplayThread reads the file and sends to the baseband.
|
||||
replay_thread_ = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
/* read_size */ 0x4000,
|
||||
/* buffer_count */ 3,
|
||||
&ready_signal_,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message{return_code};
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
}
|
||||
|
||||
void RemoteView::stop() {
|
||||
// This terminates the underlying chThread.
|
||||
replay_thread_.reset();
|
||||
transmitter_model.disable();
|
||||
ready_signal_ = false;
|
||||
}
|
||||
|
||||
void RemoteView::new_remote() {
|
||||
save_remote();
|
||||
init_remote();
|
||||
refresh_ui();
|
||||
|
||||
// View needs to redraw to hide old buttons.
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void RemoteView::open_remote() {
|
||||
auto open_view = nav_.push<FileLoadView>(".REM");
|
||||
open_view->push_dir(u"REMOTES");
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
save_remote();
|
||||
load_remote(std::move(path));
|
||||
refresh_ui();
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
void RemoteView::init_remote() {
|
||||
model_ = {"<Unnamed Remote>", {}};
|
||||
reset_buttons();
|
||||
set_remote_path(next_filename_matching_pattern(u"/REMOTES/REMOTE_????.REM"));
|
||||
set_needs_save(false);
|
||||
|
||||
if (remote_path_.empty())
|
||||
show_error("Couldn't make new remote file.");
|
||||
}
|
||||
|
||||
bool RemoteView::load_remote(fs::path&& path) {
|
||||
set_remote_path(std::move(path));
|
||||
set_needs_save(false);
|
||||
reset_buttons();
|
||||
return model_.load(remote_path_);
|
||||
}
|
||||
|
||||
void RemoteView::save_remote(bool show_errors) {
|
||||
if (!needs_save_)
|
||||
return;
|
||||
|
||||
bool ok = model_.save(remote_path_);
|
||||
if (!ok && show_errors)
|
||||
show_error("Save failed for:\n" + remote_path_.stem().string());
|
||||
|
||||
set_needs_save(false);
|
||||
}
|
||||
|
||||
void RemoteView::rename_remote(const std::string& new_name) {
|
||||
auto folder = remote_path_.parent_path();
|
||||
auto ext = remote_path_.extension();
|
||||
auto new_path = folder / new_name + ext;
|
||||
|
||||
if (file_exists(new_path)) {
|
||||
show_error("Remote " + new_name + " already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// Rename file if there is one.
|
||||
if (fs::file_exists(remote_path_))
|
||||
rename_file(remote_path_, new_path);
|
||||
|
||||
set_remote_path(std::move(new_path));
|
||||
}
|
||||
|
||||
void RemoteView::handle_replay_thread_done(uint32_t return_code) {
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
if (check_loop.value() && current_btn_) {
|
||||
send_button(*current_btn_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: This can happen when stopping an in-progress Tx.
|
||||
if (return_code == ReplayThread::READ_ERROR)
|
||||
show_error("Bad capture file.");
|
||||
*/
|
||||
|
||||
stop();
|
||||
}
|
||||
|
||||
void RemoteView::set_remote_path(fs::path&& path) {
|
||||
// Unfortunately, have to keep these two in sync because
|
||||
// settings doesn't know about fs::path.
|
||||
remote_path_ = std::move(path);
|
||||
settings_.remote_path = remote_path_.string();
|
||||
}
|
||||
|
||||
void RemoteView::show_error(const std::string& msg) const {
|
||||
nav_.display_modal("Error", msg);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
@ -21,42 +20,370 @@
|
|||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "file.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "optional.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* Maps icon index to bitmap. */
|
||||
class RemoteIcons {
|
||||
public:
|
||||
static constexpr const Bitmap* get(uint8_t index) {
|
||||
if (index >= std::size(bitmaps_))
|
||||
return bitmaps_[0];
|
||||
|
||||
return bitmaps_[index];
|
||||
}
|
||||
|
||||
static constexpr size_t size() {
|
||||
return std::size(bitmaps_);
|
||||
}
|
||||
|
||||
private:
|
||||
// NB: Icons need to be 16x16 or they won't fit corrently.
|
||||
static constexpr std::array<const Bitmap*, 25> bitmaps_{
|
||||
nullptr,
|
||||
&bitmap_icon_fox,
|
||||
&bitmap_icon_adsb,
|
||||
&bitmap_icon_ais,
|
||||
&bitmap_icon_aprs,
|
||||
&bitmap_icon_btle,
|
||||
&bitmap_icon_burger,
|
||||
&bitmap_icon_camera,
|
||||
&bitmap_icon_cwgen,
|
||||
&bitmap_icon_dmr,
|
||||
&bitmap_icon_file_image,
|
||||
&bitmap_icon_lge,
|
||||
&bitmap_icon_looking,
|
||||
&bitmap_icon_memory,
|
||||
&bitmap_icon_morse,
|
||||
&bitmap_icon_nrf,
|
||||
&bitmap_icon_notepad,
|
||||
&bitmap_icon_rds,
|
||||
&bitmap_icon_remote,
|
||||
&bitmap_icon_setup,
|
||||
&bitmap_icon_sleep,
|
||||
&bitmap_icon_sonde,
|
||||
&bitmap_icon_stealth,
|
||||
&bitmap_icon_tetra,
|
||||
&bitmap_icon_temperature};
|
||||
};
|
||||
|
||||
// TODO: Use RGB colors instead?
|
||||
/* Maps color index to color. */
|
||||
class RemoteColors {
|
||||
public:
|
||||
static constexpr Color get(uint8_t index) {
|
||||
if (index >= std::size(colors_))
|
||||
return colors_[0];
|
||||
|
||||
return colors_[index];
|
||||
}
|
||||
|
||||
static constexpr size_t size() {
|
||||
return std::size(colors_);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::array<Color, 21> colors_{
|
||||
Color::black(), // 0
|
||||
Color::white(), // 1
|
||||
Color::darker_grey(), // 2
|
||||
Color::dark_grey(), // 3
|
||||
Color::grey(), // 4
|
||||
Color::light_grey(), // 5
|
||||
Color::red(), // 6
|
||||
Color::orange(), // 7
|
||||
Color::yellow(), // 8
|
||||
Color::green(), // 9
|
||||
Color::blue(), // 10
|
||||
Color::cyan(), // 11
|
||||
Color::magenta(), // 12
|
||||
Color::dark_red(), // 13
|
||||
Color::dark_orange(), // 14
|
||||
Color::dark_yellow(), // 15
|
||||
Color::dark_green(), // 16
|
||||
Color::dark_blue(), // 17
|
||||
Color::dark_cyan(), // 18
|
||||
Color::dark_magenta(), // 19
|
||||
Color::purple()}; // 20
|
||||
};
|
||||
|
||||
/* Data model for a remote entry. */
|
||||
struct RemoteEntryModel {
|
||||
std::filesystem::path path{};
|
||||
std::string name{};
|
||||
uint8_t icon = 0;
|
||||
uint8_t bg_color = 0;
|
||||
uint8_t fg_color = 0;
|
||||
capture_metadata metadata{};
|
||||
// TODO: start/end position for trimming.
|
||||
|
||||
std::string to_string() const;
|
||||
static Optional<RemoteEntryModel> parse(std::string_view line);
|
||||
};
|
||||
|
||||
/* Data model for a remote. */
|
||||
struct RemoteModel {
|
||||
std::string name{};
|
||||
std::vector<RemoteEntryModel> entries{};
|
||||
|
||||
bool delete_entry(const RemoteEntryModel* entry);
|
||||
bool load(const std::filesystem::path& path);
|
||||
bool save(const std::filesystem::path& path);
|
||||
};
|
||||
|
||||
/* Button for the remote UI. */
|
||||
class RemoteButton : public NewButton {
|
||||
public:
|
||||
std::function<void(RemoteButton&)> on_select2{};
|
||||
std::function<void(RemoteButton&)> on_long_select{};
|
||||
|
||||
RemoteButton(Rect parent_rect, RemoteEntryModel* entry);
|
||||
RemoteButton(const RemoteButton&) = delete;
|
||||
RemoteButton& operator=(const RemoteButton&) = delete;
|
||||
|
||||
void on_focus() override;
|
||||
void on_blur() override;
|
||||
bool on_key(KeyEvent key) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
RemoteEntryModel* entry();
|
||||
void set_entry(RemoteEntryModel* entry);
|
||||
|
||||
protected:
|
||||
Style paint_style() override;
|
||||
|
||||
private:
|
||||
// Hide because it's not used.
|
||||
using NewButton::on_select;
|
||||
RemoteEntryModel* entry_;
|
||||
};
|
||||
|
||||
/* Settings container for remote. */
|
||||
struct RemoteSettings {
|
||||
std::string remote_path{};
|
||||
};
|
||||
|
||||
/* View for editing a remote entry button. */
|
||||
class RemoteEntryEditView : public View {
|
||||
public:
|
||||
std::function<void(RemoteEntryModel&)> on_delete{};
|
||||
|
||||
RemoteEntryEditView(NavigationView& nav, RemoteEntryModel& entry);
|
||||
|
||||
std::string title() const override { return "Edit Button"; };
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
RemoteEntryModel& entry_;
|
||||
std::string temp_buffer_{};
|
||||
|
||||
void refresh_ui();
|
||||
void load_path(std::filesystem::path&& path);
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 1 * 16}, "Name:", Color::light_grey()},
|
||||
{{2 * 8, 2 * 16}, "Path:", Color::light_grey()},
|
||||
{{2 * 8, 3 * 16}, "Freq:", Color::light_grey()},
|
||||
{{17 * 8, 3 * 16}, "MHz", Color::light_grey()},
|
||||
{{2 * 8, 4 * 16}, "Rate:", Color::light_grey()},
|
||||
{{2 * 8, 5 * 16}, "Icon:", Color::light_grey()},
|
||||
{{2 * 8, 6 * 16}, "FG Color:", Color::light_grey()},
|
||||
{{2 * 8, 7 * 16}, "BG Color:", Color::light_grey()},
|
||||
{{8 * 8, 9 * 16}, "Button preview", Color::light_grey()},
|
||||
};
|
||||
|
||||
TextField field_name{{8 * 8, 1 * 16, 20 * 8, 1 * 16}, {}};
|
||||
|
||||
TextField field_path{{8 * 8, 2 * 16, 20 * 8, 1 * 16}, {}};
|
||||
|
||||
FrequencyField field_freq{{8 * 8, 3 * 16}};
|
||||
|
||||
Text text_rate{{8 * 8, 4 * 16, 20 * 8, 1 * 16}, {}};
|
||||
|
||||
NumberField field_icon_index{
|
||||
{8 * 8, 5 * 16},
|
||||
2,
|
||||
{0, RemoteIcons::size() - 1},
|
||||
/*step*/ 1,
|
||||
/*fill*/ ' ',
|
||||
/*loop*/ true};
|
||||
|
||||
NumberField field_fg_color_index{
|
||||
{11 * 8, 6 * 16},
|
||||
2,
|
||||
{0, RemoteColors::size() - 1},
|
||||
/*step*/ 1,
|
||||
/*fill*/ ' ',
|
||||
/*loop*/ true};
|
||||
|
||||
NumberField field_bg_color_index{
|
||||
{11 * 8, 7 * 16},
|
||||
2,
|
||||
{0, RemoteColors::size() - 1},
|
||||
/*step*/ 1,
|
||||
/*fill*/ ' ',
|
||||
/*loop*/ true};
|
||||
|
||||
RemoteButton button_preview{
|
||||
{10 * 8, 11 * 16 - 8, 10 * 8, 50},
|
||||
&entry_};
|
||||
|
||||
NewButton button_delete{
|
||||
{2 * 8, 16 * 16, 4 * 8, 2 * 16},
|
||||
{},
|
||||
&bitmap_icon_trash,
|
||||
Color::red()};
|
||||
|
||||
Button button_done{{11 * 8, 16 * 16, 8 * 8, 2 * 16}, "Done"};
|
||||
};
|
||||
|
||||
/* App that allows for buttons to be bound to captures for playback. */
|
||||
class RemoteView : public View {
|
||||
public:
|
||||
RemoteView(NavigationView& nav);
|
||||
RemoteView(NavigationView& nav, std::filesystem::path path);
|
||||
~RemoteView();
|
||||
|
||||
RemoteView(const RemoteView&) = delete;
|
||||
RemoteView& operator=(const RemoteView&) = delete;
|
||||
|
||||
std::string title() const override { return "Remote"; };
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Custom remote"; };
|
||||
|
||||
private:
|
||||
/*enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
/* Creates the dynamic buttons. */
|
||||
void create_buttons();
|
||||
/* Resets all the pointers to null entries. */
|
||||
void reset_buttons();
|
||||
void refresh_ui();
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
void add_button();
|
||||
void edit_button(RemoteButton& btn);
|
||||
void send_button(RemoteButton& btn);
|
||||
void stop();
|
||||
void new_remote();
|
||||
void open_remote();
|
||||
void init_remote();
|
||||
bool load_remote(std::filesystem::path&& path);
|
||||
void save_remote(bool show_errors = true);
|
||||
void rename_remote(const std::string& new_name);
|
||||
void handle_replay_thread_done(uint32_t return_code);
|
||||
void set_needs_save(bool v = true) { needs_save_ = v; }
|
||||
void set_remote_path(std::filesystem::path&& path);
|
||||
bool is_sending() const { return replay_thread_ != nullptr; }
|
||||
void show_error(const std::string& msg) const;
|
||||
|
||||
struct remote_layout_t {
|
||||
Point position;
|
||||
std::string text;
|
||||
};
|
||||
static constexpr Dim button_rows = 4;
|
||||
static constexpr Dim button_cols = 3;
|
||||
static constexpr uint8_t max_buttons = button_rows * button_cols;
|
||||
static constexpr Dim button_area_height = 200;
|
||||
static constexpr Dim button_width = screen_width / button_cols;
|
||||
static constexpr Dim button_height = button_area_height / button_rows;
|
||||
|
||||
const std::array<remote_layout_t, 32> remote_layout { };*/
|
||||
// This value is mysterious... why?
|
||||
static constexpr uint32_t baseband_bandwidth = 2'500'000;
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 0}, "Work in progress...", Color::light_grey()}};
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
Button button{
|
||||
{60, 64, 120, 32},
|
||||
"Exit"};
|
||||
// Settings
|
||||
RemoteSettings settings_{};
|
||||
app_settings::SettingsManager app_settings_{
|
||||
"tx_remote"sv,
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"remote_path"sv, &settings_.remote_path},
|
||||
}};
|
||||
|
||||
RemoteModel model_{};
|
||||
|
||||
bool needs_save_ = false;
|
||||
std::string temp_buffer_{};
|
||||
std::filesystem::path remote_path_{};
|
||||
RemoteButton* current_btn_{};
|
||||
|
||||
const Point buttons_top_{0, 20};
|
||||
std::vector<std::unique_ptr<RemoteButton>> buttons_{};
|
||||
|
||||
std::unique_ptr<ReplayThread> replay_thread_{};
|
||||
bool ready_signal_{}; // Used to signal ReplayThread ready.
|
||||
|
||||
TextField field_title{
|
||||
{0 * 8, 0 * 16 + 2, 30 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{0 * 8, 17 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{10 * 8, 17 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
/*small*/ true};
|
||||
|
||||
TextField field_filename{
|
||||
{0 * 8, 18 * 16, 17 * 8, 1 * 16},
|
||||
{}};
|
||||
|
||||
NewButton button_add{
|
||||
{17 * 8 + 4, 17 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_add,
|
||||
Color::orange(),
|
||||
/*vcenter*/ true};
|
||||
|
||||
NewButton button_new{
|
||||
{22 * 8, 17 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_new_file,
|
||||
Color::dark_blue(),
|
||||
/*vcenter*/ true};
|
||||
|
||||
NewButton button_open{
|
||||
{26 * 8, 17 * 16, 4 * 8, 2 * 16},
|
||||
"",
|
||||
&bitmap_icon_load,
|
||||
Color::dark_blue(),
|
||||
/*vcenter*/ true};
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* p) {
|
||||
auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
handle_replay_thread_done(message.return_code);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* p) {
|
||||
auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
ready_signal_ = true;
|
||||
}
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
|
|
@ -654,17 +654,17 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
|||
add_items({{"..", ui::Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}});
|
||||
}
|
||||
add_items({
|
||||
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
|
||||
{"Audio", ui::Color::dark_cyan(), &bitmap_icon_speaker, [&nav]() { nav.push<SetAudioView>(); }},
|
||||
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
|
||||
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
|
||||
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
|
||||
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetRadioView>(); }},
|
||||
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push<SetUIView>(); }},
|
||||
{"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push<SetDateTimeView>(); }},
|
||||
{"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push<TouchCalibrationView>(); }},
|
||||
{"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetAppSettingsView>(); }},
|
||||
{"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetConverterSettingsView>(); }},
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
|
||||
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
|
|
@ -261,7 +261,8 @@ class TextEditorView : public View {
|
|||
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
|
||||
{},
|
||||
&bitmap_icon_controls,
|
||||
Color::dark_grey()};
|
||||
Color::dark_grey(),
|
||||
/*vcenter*/ true};
|
||||
|
||||
Text text_position{
|
||||
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue