diff --git a/firmware/application/apps/gps_sim_app.hpp b/firmware/application/apps/gps_sim_app.hpp index a05f0f29..c99c2548 100644 --- a/firmware/application/apps/gps_sim_app.hpp +++ b/firmware/application/apps/gps_sim_app.hpp @@ -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}, diff --git a/firmware/application/apps/replay_app.cpp b/firmware/application/apps/replay_app.cpp index 279f35dd..bc7d8b9f 100644 --- a/firmware/application/apps/replay_app.cpp +++ b/firmware/application/apps/replay_app.cpp @@ -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(); } diff --git a/firmware/application/apps/replay_app.hpp b/firmware/application/apps/replay_app.hpp index b5970bbb..5f493b1e 100644 --- a/firmware/application/apps/replay_app.hpp +++ b/firmware/application/apps/replay_app.hpp @@ -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}, diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index 5fce87d5..0a45e15b 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -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(); }}, + {"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(); }}, // 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(); }}, + {"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push(); }}, + {"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push(); }}, //{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push(); } }, {"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push(); }}, - {"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push(); }}, {"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push(); }}, - {"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push(); }}, {"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push(); }}, - {"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push(); }}, - {"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(); }}, // temporarily disabled to conserve ROM space }); set_max_rows(2); // allow wider buttons } diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index fb4cd886..90bd8e2e 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -27,6 +27,7 @@ #include #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(path); reload_current(); return true; + } else if (path_iequal(rem_ext, ext)) { + nav_.push(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(path); + nav_.push(path); } else nav_.display_modal("Open in Notepad", "Can't open that in Notepad."); }; diff --git a/firmware/application/apps/ui_fileman.hpp b/firmware/application/apps/ui_fileman.hpp index 2ada96b7..dd110641 100644 --- a/firmware/application/apps/ui_fileman.hpp +++ b/firmware/application/apps/ui_fileman.hpp @@ -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 */ diff --git a/firmware/application/apps/ui_mictx.cpp b/firmware/application/apps/ui_mictx.cpp index 4dfdd280..441e4956 100644 --- a/firmware/application/apps/ui_mictx.cpp +++ b/firmware/application/apps/ui_mictx.cpp @@ -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); diff --git a/firmware/application/apps/ui_mictx.hpp b/firmware/application/apps/ui_mictx.hpp index 822a9118..aa4d967b 100644 --- a/firmware/application/apps/ui_mictx.hpp +++ b/firmware/application/apps/ui_mictx.hpp @@ -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}, diff --git a/firmware/application/apps/ui_playlist.cpp b/firmware/application/apps/ui_playlist.cpp index c4edbaac..84df769b 100644 --- a/firmware/application/apps/ui_playlist.cpp +++ b/firmware/application/apps/ui_playlist.cpp @@ -386,6 +386,7 @@ PlaylistView::PlaylistView( &waterfall, }); + ensure_directory(u"PLAYLIST"); waterfall.show_audio_spectrum_view(false); field_frequency.set_value(100'000'000); diff --git a/firmware/application/apps/ui_playlist.hpp b/firmware/application/apps/ui_playlist.hpp index 7d09207b..e77dcc66 100644 --- a/firmware/application/apps/ui_playlist.hpp +++ b/firmware/application/apps/ui_playlist.hpp @@ -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}, diff --git a/firmware/application/apps/ui_remote.cpp b/firmware/application/apps/ui_remote.cpp index 7572285e..603dabe6 100644 --- a/firmware/application/apps/ui_remote.cpp +++ b/firmware/application/apps/ui_remote.cpp @@ -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::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(".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(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( + 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({{}, "", 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(*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(); + 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( + 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(".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_ = {"", {}}; + 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 */ diff --git a/firmware/application/apps/ui_remote.hpp b/firmware/application/apps/ui_remote.hpp index 0a990c89..7c91cfde 100644 --- a/firmware/application/apps/ui_remote.hpp +++ b/firmware/application/apps/ui_remote.hpp @@ -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 +#include +#include +#include +#include +#include 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 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 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 parse(std::string_view line); +}; + +/* Data model for a remote. */ +struct RemoteModel { + std::string name{}; + std::vector 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 on_select2{}; + std::function 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 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 { };*/ + // 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> buttons_{}; + + std::unique_ptr 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(p); + handle_replay_thread_done(message.return_code); + }}; + + MessageHandlerRegistration message_handler_fifo_signal{ + Message::ID::RequestSignal, + [this](const Message* p) { + auto message = static_cast(p); + if (message->signal == RequestSignalMessage::Signal::FillRequest) { + ready_signal_ = true; + } + }}; }; } /* namespace ui */ diff --git a/firmware/application/apps/ui_settings.cpp b/firmware/application/apps/ui_settings.cpp index 94b7bd4d..361e8bbb 100644 --- a/firmware/application/apps/ui_settings.cpp +++ b/firmware/application/apps/ui_settings.cpp @@ -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(); }}, {"Audio", ui::Color::dark_cyan(), &bitmap_icon_speaker, [&nav]() { nav.push(); }}, + {"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push(); }}, + {"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, + {"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push(); }}, + {"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push(); }}, + {"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, + {"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push(); }}, + {"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push(); }}, {"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, {"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push(); }}, - {"Date/Time", ui::Color::dark_cyan(), &bitmap_icon_options_datetime, [&nav]() { nav.push(); }}, - {"Calibration", ui::Color::dark_cyan(), &bitmap_icon_options_touch, [&nav]() { nav.push(); }}, - {"App Settings", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push(); }}, - {"Converter", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, - {"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, - {"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push(); }}, - {"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push(); }}, - {"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push(); }}, }); set_max_rows(2); // allow wider buttons } diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp index ad39d4ca..5e3addd8 100644 --- a/firmware/application/apps/ui_text_editor.hpp +++ b/firmware/application/apps/ui_text_editor.hpp @@ -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}, diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index 3f49a805..2f971c5a 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -311,10 +311,10 @@ std::string to_string_file_size(uint32_t file_size) { return to_string_dec_uint(file_size) + suffix[suffix_index]; } -std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision) { +std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) { const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000}; std::string string{""}; - uint32_t prefix_index = base_nano; + uint32_t prefix_index = base_unit; double integer_part; double fractional_part; diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 2c043d06..72c326c6 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -76,7 +76,9 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp); // Gets a human readable file size string. std::string to_string_file_size(uint32_t file_size); -std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision); +/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from + * 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */ +std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision); double get_decimals(double num, int16_t mult, bool round = false); std::string trim(std::string_view str); // Remove whitespace at ends. diff --git a/firmware/application/ui/ui_alphanum.hpp b/firmware/application/ui/ui_alphanum.hpp index feb1eb20..f93dae24 100644 --- a/firmware/application/ui/ui_alphanum.hpp +++ b/firmware/application/ui/ui_alphanum.hpp @@ -82,7 +82,8 @@ class AlphanumView : public TextEntryView { {192, 214, screen_width / 5, 38}, {}, &bitmap_icon_shift, - Color::dark_grey()}; + Color::dark_grey(), + /*vcenter*/ true}; Labels labels{ {{1 * 8, 33 * 8}, "Raw:", Color::light_grey()}, diff --git a/firmware/application/ui/ui_transmitter.cpp b/firmware/application/ui/ui_transmitter.cpp index bd47da54..7b324f29 100644 --- a/firmware/application/ui/ui_transmitter.cpp +++ b/firmware/application/ui/ui_transmitter.cpp @@ -32,8 +32,25 @@ using namespace portapack; +#define POWER_THRESHOLD_HIGH 47 +#define POWER_THRESHOLD_MED 38 +#define POWER_THRESHOLD_LOW 17 + namespace ui { +/* Gets a style indicating total TX gain level. */ +static const Style* get_style_for_gain(uint8_t tot_gain) { + if (tot_gain > POWER_THRESHOLD_HIGH) return &Styles::red; + + if (tot_gain > POWER_THRESHOLD_MED) + return &Styles::orange; + + if (tot_gain > POWER_THRESHOLD_LOW) + return &Styles::yellow; + + return nullptr; // Uses default. +} + /* TransmitterView *******************************************************/ void TransmitterView::paint(Painter& painter) { @@ -73,21 +90,13 @@ void TransmitterView::on_tx_amp_changed(bool rf_amp) { } void TransmitterView::update_gainlevel_styles() { - const Style* new_style_ptr = NULL; int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0); + auto style = get_style_for_gain(tot_gain); - if (tot_gain > POWER_THRESHOLD_HIGH) { - new_style_ptr = &style_power_high; - } else if (tot_gain > POWER_THRESHOLD_MED) { - new_style_ptr = &style_power_med; - } else if (tot_gain > POWER_THRESHOLD_LOW) { - new_style_ptr = &style_power_low; - } - - field_gain.set_style(new_style_ptr); - text_gain.set_style(new_style_ptr); - field_amp.set_style(new_style_ptr); - text_amp.set_style(new_style_ptr); + field_gain.set_style(style); + text_gain.set_style(style); + field_amp.set_style(style); + text_amp.set_style(style); } void TransmitterView::set_transmitting(const bool transmitting) { @@ -186,89 +195,66 @@ TransmitterView::TransmitterView( } TransmitterView::~TransmitterView() { + // TODO: Does this make sense? Seems wrong to have + // what's basically a widget control the radio. audio::output::stop(); transmitter_model.disable(); baseband::shutdown(); } /* TransmitterView2 *******************************************************/ -/* Simpler transmitter view that only renders TX Gain and Amp. - * There are two modes, NORMAL_UI and SHORT_UI. SHORT_UI abbreviates control labels. */ -void TransmitterView2::paint(Painter&) { - // All widgets paint themselves. Don't let base paint. -} -void TransmitterView2::on_tx_gain_changed(int32_t tx_gain) { - transmitter_model.set_tx_gain(tx_gain); - update_gainlevel_styles(); -} +TransmitterView2::TransmitterView2(Point pos, bool short_ui) { + // There are two modes, short and !short + // Short: "G:XX A:YY" + // !Short: "Gain:XX Amp:YY" + + Dim width = short_ui ? (9 * 8) : (14 * 8); + set_parent_rect({pos, {width, 16}}); + + add_children({ + &text_labels, + &field_gain, + &field_amp, + }); + + // Set up controls depending UI mode. + text_labels.set(short_ui ? "G: A:" : "Gain: Amp:"); + text_labels.set_parent_rect( + short_ui + ? Rect{0 * 8, 0 * 16, 7 * 8, 1 * 16} + : Rect{0 * 8, 0 * 16, 12 * 8, 1 * 16}); + field_gain.set_parent_rect( + short_ui + ? Rect{2 * 8, 0 * 16, 2 * 8, 1 * 16} + : Rect{5 * 8, 0 * 16, 2 * 8, 1 * 16}); + field_amp.set_parent_rect( + short_ui + ? Rect{7 * 8, 0 * 16, 2 * 8, 1 * 16} + : Rect{12 * 8, 0 * 16, 2 * 8, 1 * 16}); + + field_gain.set_value(transmitter_model.tx_gain()); + field_gain.on_change = [this](uint32_t tx_gain) { + transmitter_model.set_tx_gain(tx_gain); + update_gainlevel_styles(); + }; + + field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0); + field_amp.on_change = [this](uint32_t rf_amp) { + transmitter_model.set_rf_amp(rf_amp > 0); + update_gainlevel_styles(); + }; -void TransmitterView2::on_tx_amp_changed(bool rf_amp) { - transmitter_model.set_rf_amp(rf_amp); update_gainlevel_styles(); } void TransmitterView2::update_gainlevel_styles() { - const Style* new_style_ptr = NULL; int8_t tot_gain = transmitter_model.tx_gain() + (transmitter_model.rf_amp() ? 14 : 0); + auto style = get_style_for_gain(tot_gain); - if (tot_gain > POWER_THRESHOLD_HIGH) { - new_style_ptr = &style_power_high; - } else if (tot_gain > POWER_THRESHOLD_MED) { - new_style_ptr = &style_power_med; - } else if (tot_gain > POWER_THRESHOLD_LOW) { - new_style_ptr = &style_power_low; - } - - field_gain.set_style(new_style_ptr); - text_gain_amp.set_style(new_style_ptr); - field_amp.set_style(new_style_ptr); - - field_gain_short_UI.set_style(new_style_ptr); - text_gain_amp_short_UI.set_style(new_style_ptr); - field_amp_short_UI.set_style(new_style_ptr); -} - -void TransmitterView2::on_show() { - field_gain.set_value(transmitter_model.tx_gain()); - field_amp.set_value(transmitter_model.rf_amp() ? 14 : 0); - - field_gain_short_UI.set_value(transmitter_model.tx_gain()); - field_amp_short_UI.set_value(transmitter_model.rf_amp() ? 14 : 0); - - update_gainlevel_styles(); -} - -TransmitterView2::TransmitterView2(const Coord x, const Coord y, bool short_UI) { - set_parent_rect({x, y, 20 * 8, 1 * 8}); - - add_children({ - &(short_UI ? text_gain_amp_short_UI : text_gain_amp), - &(short_UI ? field_gain_short_UI : field_gain), - &(short_UI ? field_amp_short_UI : field_amp), - }); - - if (short_UI) { - field_gain_short_UI.on_change = [this](uint32_t tx_gain) { - on_tx_gain_changed(tx_gain); - }; - field_amp_short_UI.on_change = [this](uint32_t rf_amp) { - on_tx_amp_changed((bool)rf_amp); - }; - } else { - field_gain.on_change = [this](uint32_t tx_gain) { - on_tx_gain_changed(tx_gain); - }; - field_amp.on_change = [this](uint32_t rf_amp) { - on_tx_amp_changed((bool)rf_amp); - }; - } -} - -TransmitterView2::~TransmitterView2() { - audio::output::stop(); - transmitter_model.disable(); - baseband::shutdown(); + text_labels.set_style(style); + field_gain.set_style(style); + field_amp.set_style(style); } } /* namespace ui */ diff --git a/firmware/application/ui/ui_transmitter.hpp b/firmware/application/ui/ui_transmitter.hpp index d023058c..d93108ae 100644 --- a/firmware/application/ui/ui_transmitter.hpp +++ b/firmware/application/ui/ui_transmitter.hpp @@ -26,9 +26,9 @@ #include "ui.hpp" #include "ui_navigation.hpp" #include "ui_painter.hpp" +#include "ui_receiver.hpp" #include "ui_styles.hpp" #include "ui_widget.hpp" -#include "ui_receiver.hpp" #include "rf_path.hpp" @@ -37,10 +37,6 @@ #include #include -#define POWER_THRESHOLD_HIGH 47 -#define POWER_THRESHOLD_MED 38 -#define POWER_THRESHOLD_LOW 17 - namespace ui { class TXGainField : public NumberField { @@ -74,11 +70,8 @@ class TransmitterView : public View { private: const Style& style_start = Styles::green; - const Style style_stop = Styles::red; - const Style style_locked = Styles::dark_grey; - const Style style_power_low = Styles::yellow; - const Style style_power_med = Styles::orange; - const Style style_power_high = Styles::red; + const Style& style_stop = Styles::red; + const Style& style_locked = Styles::dark_grey; bool lock_{false}; bool transmitting_{false}; @@ -134,60 +127,32 @@ class TransmitterView : public View { void update_gainlevel_styles(void); }; +/* Simpler transmitter view that only renders TX Gain and Amp. + * When short_UI is set it abbreviates control labels. */ class TransmitterView2 : public View { public: - TransmitterView2(const Coord x, const Coord y, bool short_UI); - - ~TransmitterView2(); - - void on_show() override; - void paint(Painter& painter) override; + TransmitterView2(Point pos, bool short_ui); private: - const Style& style_power_low = Styles::yellow; - const Style& style_power_med = Styles::orange; - const Style& style_power_high = Styles::red; - - Text text_gain_amp{ - {0, 3 * 8, 5 * 8, 1 * 16}, - "Gain: Amp:"}; + Text text_labels{ + {}, // Set in ctor. + {}}; NumberField field_gain{ - {5 * 8, 3 * 8}, + {}, // Set in ctor. 2, {max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum}, max2837::tx::gain_db_step, ' '}; NumberField field_amp{ - {12 * 8, 3 * 8}, + {}, // Set in ctor. 2, {0, 14}, 14, ' '}; - Text text_gain_amp_short_UI{ - {0, (3 * 8), 5 * 8, 1 * 16}, - "Gain A:"}; - - NumberField field_gain_short_UI{ - {(4 * 8) + 2, 3 * 8}, - 2, - {max2837::tx::gain_db_range.minimum, max2837::tx::gain_db_range.maximum}, - max2837::tx::gain_db_step, - ' '}; - - NumberField field_amp_short_UI{ - {(9 * 8) - 2, 3 * 8}, - 2, - {0, 14}, - 14, - ' '}; - - void on_tx_gain_changed(int32_t tx_gain); - void on_tx_amp_changed(bool rf_amp); - - void update_gainlevel_styles(void); + void update_gainlevel_styles(); }; } /* namespace ui */ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index a3fcf292..ee881c31 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -302,17 +302,18 @@ void SystemStatusView::on_converter() { void SystemStatusView::on_bias_tee() { if (!portapack::get_antenna_bias()) { - nav_.display_modal("Bias voltage", - "Enable DC voltage on\nantenna connector?", - YESNO, - [this](bool v) { - if (v) { - portapack::set_antenna_bias(true); - receiver_model.set_antenna_bias(); - transmitter_model.set_antenna_bias(); - refresh(); - } - }); + nav_.display_modal( + "Bias voltage", + "Enable DC voltage on\nantenna connector?", + YESNO, + [this](bool v) { + if (v) { + portapack::set_antenna_bias(true); + receiver_model.set_antenna_bias(); + transmitter_model.set_antenna_bias(); + refresh(); + } + }); } else { portapack::set_antenna_bias(false); receiver_model.set_antenna_bias(); @@ -498,30 +499,28 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) { add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}}); } add_items({ + // {"ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push(); }}, {"ADS-B", Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push(); }}, - //{ "ACARS", Color::yellow(), &bitmap_icon_adsb, [&nav](){ nav.push(); }}, - {"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push(); }}, {"AFSK", Color::yellow(), &bitmap_icon_modem, [&nav]() { nav.push(); }}, - {"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push(); }}, - {"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push(); }}, - {"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push(); }}, + {"AIS Boats", Color::green(), &bitmap_icon_ais, [&nav]() { nav.push(); }}, {"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push(); }}, + {"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push(); }}, + {"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push(); }}, + {"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push(); }}, {"ERT Meter", Color::green(), &bitmap_icon_ert, [&nav]() { nav.push(); }}, + {"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, + {"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push(); }}, {"POCSAG", Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push(); }}, {"Radiosnde", Color::green(), &bitmap_icon_sonde, [&nav]() { nav.push(); }}, - {"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push(); }}, {"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, - {"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push(); }}, - {"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push(); }} - /* - { "DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push(); } }, - { "SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push(); } }, // SIGFRXView - { "LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push(); } }, - { "SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push(); } }, - { "TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push(); } },*/ + {"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push(); }}, + {"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push(); }}, + // {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push(); }}, + // {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push(); }}, + // {"LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push(); }}, + // {"SSTV", Color::dark_grey(), &bitmap_icon_sstv, [&nav](){ nav.push(); }}, + // {"TETRA", Color::dark_grey(), &bitmap_icon_tetra, [&nav](){ nav.push(); }}, }); - - // set_highlighted(0); // Default selection is "Audio" } /* TransmittersMenuView **************************************************/ @@ -531,26 +530,24 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) { add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}}); } add_items({ - {"ADS-B [S]", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push(); }}, + {"ADS-B", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push(); }}, {"APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push(); }}, {"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push(); }}, + {"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push(); }}, {"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push(); }}, {"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push(); }}, - //{ "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push(); } }, - {"LGE tool", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push(); }}, + // { "Key fob", ui::Color::orange(), &bitmap_icon_keyfob, [&nav](){ nav.push(); }}, + {"LGE", ui::Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push(); }}, {"Morse", ui::Color::green(), &bitmap_icon_morse, [&nav]() { nav.push(); }}, - {"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push(); }}, - //{ "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push(); } }, + // { "Nuoptix DTMF", ui::Color::green(), &bitmap_icon_nuoptix, [&nav](){ nav.push(); }}, {"OOK", ui::Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push(); }}, {"POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav]() { nav.push(); }}, {"RDS", ui::Color::green(), &bitmap_icon_rds, [&nav]() { nav.push(); }}, {"Soundbrd", ui::Color::green(), &bitmap_icon_soundboard, [&nav]() { nav.push(); }}, + {"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push(); }}, {"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push(); }}, {"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push(); }}, {"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push(); }}, - //{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, - {"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push(); }}, - //{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push(); } }, }); } @@ -561,41 +558,47 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) { add_items({{"..", Color::light_grey(), &bitmap_icon_previous, [&nav]() { nav.pop(); }}}); } add_items({ - //{ "Test app", Color::dark_grey(), nullptr, [&nav](){ nav.push(); } }, - {"Freq. manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push(); }}, - {"File manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push(); }}, + {"Antenna Length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push(); }}, + {"File Manager", Color::green(), &bitmap_icon_dir, [&nav]() { nav.push(); }}, + {"Freq. Manager", Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push(); }}, {"Notepad", Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push(); }}, - {"Signal gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push(); }}, - //{ "Tone search", Color::dark_grey(), nullptr, [&nav](){ nav.push(); } }, - {"Wav viewer", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push(); }}, - {"Antenna length", Color::green(), &bitmap_icon_tools_antenna, [&nav]() { nav.push(); }}, + {"SD Over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push(); }}, + {"Signal Gen", Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push(); }}, + // {"Test App", Color::dark_grey(), nullptr, [&nav](){ nav.push(); }}, + // {"Tone Search", Color::dark_grey(), nullptr, [&nav](){ nav.push(); }}, + {"Wav View", Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push(); }}, - {"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push(); }}, + // Dangerous apps. {"Flash Utility", Color::red(), &bitmap_icon_temperature, [&nav]() { nav.push(); }}, - {"SD over USB", Color::yellow(), &bitmap_icon_hackrf, [&nav]() { nav.push(); }}, + {"Wipe SD card", Color::red(), &bitmap_icon_tools_wipesd, [&nav]() { nav.push(); }}, }); + set_max_rows(2); // allow wider buttons } /* SystemMenuView ********************************************************/ void SystemMenuView::hackrf_mode(NavigationView& nav) { - nav.push("HackRF mode", " This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?", YESNO, - [this](bool choice) { - if (choice) { - EventDispatcher::request_stop(); - } - }); + nav.push( + "HackRF mode", + " This mode enables HackRF\n functionality. To return,\n press the reset button.\n\n Switch to HackRF mode?", + YESNO, + [this](bool choice) { + if (choice) { + EventDispatcher::request_stop(); + } + }); } SystemMenuView::SystemMenuView(NavigationView& nav) { add_items({ - //{ "Play dead", Color::red(), &bitmap_icon_playdead, [&nav](){ nav.push(); } }, + // {"Play dead", Color::red(), &bitmap_icon_playdead, [&nav]() { nav.push(); }}, {"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push(); }}, {"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push(); }}, {"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push(); }}, {"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push(); }}, - {"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push(); }}, + // {"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push(); }}, + {"Remote", ui::Color::green(), &bitmap_icon_remote, [&nav]() { nav.push(); }}, {"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, {"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push(); }}, {"Looking Glass", Color::green(), &bitmap_icon_looking, [&nav]() { nav.push(); }}, @@ -603,11 +606,11 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { {"Settings", Color::cyan(), &bitmap_icon_setup, [&nav]() { nav.push(); }}, {"Debug", Color::light_grey(), &bitmap_icon_debug, [&nav]() { nav.push(); }}, {"HackRF", Color::cyan(), &bitmap_icon_hackrf, [this, &nav]() { hackrf_mode(nav); }}, - //{ "About", Color::cyan(), nullptr, [&nav](){ nav.push(); } } + // {"About", Color::cyan(), nullptr, [&nav]() { nav.push(); }}, }); + set_max_rows(2); // allow wider buttons set_arrow_enabled(false); - // set_highlighted(1); // Startup selection } /* SystemView ************************************************************/ @@ -623,19 +626,22 @@ SystemView::SystemView( constexpr Dim info_view_height = 16; add_child(&status_view); - status_view.set_parent_rect({{0, 0}, - {parent_rect.width(), status_view_height}}); + status_view.set_parent_rect( + {{0, 0}, + {parent_rect.width(), status_view_height}}); status_view.on_back = [this]() { this->navigation_view.pop(); }; add_child(&navigation_view); - navigation_view.set_parent_rect({{0, status_view_height}, - {parent_rect.width(), static_cast(parent_rect.height() - status_view_height)}}); + navigation_view.set_parent_rect( + {{0, status_view_height}, + {parent_rect.width(), static_cast(parent_rect.height() - status_view_height)}}); add_child(&info_view); - info_view.set_parent_rect({{0, 19 * 16}, - {parent_rect.width(), info_view_height}}); + info_view.set_parent_rect( + {{0, 19 * 16}, + {parent_rect.width(), info_view_height}}); navigation_view.on_view_changed = [this](const View& new_view) { if (!this->navigation_view.is_top()) { @@ -651,14 +657,6 @@ SystemView::SystemView( this->status_view.set_dirty(); }; - // pmem::set_playdead_sequence(0x8D1); - - // Initial view - /*if ((pmem::playing_dead() == 0x5920C1DF) || // Enable code - (pmem::ui_config() & 16)) { // Login option - navigation_view.push(); - } else {*/ - navigation_view.push(); if (pmem::config_splash()) { @@ -667,10 +665,6 @@ SystemView::SystemView( status_view.set_back_enabled(false); status_view.set_title_image_enabled(true); status_view.set_dirty(); - // else - // navigation_view.push(); - - //} } Context& SystemView::context() const { @@ -812,7 +806,7 @@ void ModalMessageView::paint(Painter& painter) { portapack::display.drawBMP({100, 48}, modal_warning_bmp, false); - // Terrible... + // Break on lines. while ((pos = message_.find("\n", start)) != std::string::npos) { painter.draw_string( {1 * 8, (Coord)(120 + (i * 16))}, diff --git a/firmware/common/ui.hpp b/firmware/common/ui.hpp index 49ec3c43..06636c9b 100644 --- a/firmware/common/ui.hpp +++ b/firmware/common/ui.hpp @@ -97,6 +97,14 @@ struct Color { return (v ^ 0xffff); } + /* Converts a 32-bit color into a 16-bit color. + * High byte is ignored. */ + static constexpr Color RGB(uint32_t rgb) { + return {static_cast((rgb >> 16) & 0xff), + static_cast((rgb >> 8) & 0xff), + static_cast(rgb & 0xff)}; + } + static constexpr Color black() { return {0, 0, 0}; } diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index d80e2cb5..d6c4722e 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1139,9 +1139,9 @@ NewButton::NewButton( Color color, bool vertical_center) : Widget{parent_rect}, + color_{color}, text_{text}, bitmap_{bitmap}, - color_{color}, vertical_center_{vertical_center} { set_focusable(true); } @@ -1182,18 +1182,8 @@ void NewButton::paint(Painter& painter) { if (!bitmap_ && text_.empty()) return; - Color bg, fg; const auto r = screen_rect(); - - if (has_focus() || highlighted()) { - bg = style().foreground; - fg = Color::black(); - } else { - bg = Color::grey(); - fg = style().foreground; - } - - const Style paint_style = {style().font, bg, fg}; + const Style style = paint_style(); painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey()); painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey()); @@ -1201,7 +1191,7 @@ void NewButton::paint(Painter& painter) { painter.fill_rectangle( {r.left(), r.top() + 1, r.width() - 1, r.height() - 2}, - paint_style.background); + style.background); int y = r.top(); if (bitmap_) { @@ -1213,18 +1203,32 @@ void NewButton::paint(Painter& painter) { bmp_pos, *bitmap_, color_, - bg); + style.background); } if (!text_.empty()) { - const auto label_r = paint_style.font.size_of(text_); + const auto label_r = style.font.size_of(text_); painter.draw_string( {r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, - paint_style, + style, text_); } } +Style NewButton::paint_style() { + MutableStyle s{style()}; + + if (has_focus() || highlighted()) { + s.background = style().foreground; + s.foreground = Color::black(); + } else { + s.background = Color::grey(); + s.foreground = style().foreground; + } + + return s; +} + void NewButton::on_focus() { if (on_highlight) on_highlight(*this); diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index b433978b..fcf24ca7 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -479,10 +479,13 @@ class NewButton : public Widget { void paint(Painter& painter) override; + protected: + virtual Style paint_style(); + Color color_; + private: std::string text_; const Bitmap* bitmap_; - Color color_; bool vertical_center_{false}; }; diff --git a/firmware/common/utility.cpp b/firmware/common/utility.cpp index 788de90d..d4cbdc26 100644 --- a/firmware/common/utility.cpp +++ b/firmware/common/utility.cpp @@ -213,3 +213,25 @@ static constexpr uint32_t gcd_top(const uint32_t u, const uint32_t v) { uint32_t gcd(const uint32_t u, const uint32_t v) { return gcd_top(u, v); } + +std::string join(char c, std::initializer_list strings) { + std::string result; + size_t total_size = strings.size(); + + for (auto s : strings) + total_size += s.size(); + + result.reserve(total_size); + bool first = true; + + for (auto s : strings) { + if (!first) + result += c; + else + first = false; + + result += s; + } + + return result; +} diff --git a/firmware/common/utility.hpp b/firmware/common/utility.hpp index 50960615..b1688caf 100644 --- a/firmware/common/utility.hpp +++ b/firmware/common/utility.hpp @@ -27,7 +27,9 @@ #include #include #include +#include #include +#include #include #define LOCATE_IN_RAM __attribute__((section(".ramtext"))) @@ -213,4 +215,6 @@ struct range_t { } }; +std::string join(char c, std::initializer_list strings); + #endif /*__UTILITY_H__*/ diff --git a/firmware/test/application/CMakeLists.txt b/firmware/test/application/CMakeLists.txt index fbbe7864..c25e93e8 100644 --- a/firmware/test/application/CMakeLists.txt +++ b/firmware/test/application/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(application_test EXCLUDE_FROM_ALL ${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp ${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp + ${PROJECT_SOURCE_DIR}/../../common/utility.cpp # Dependencies ${PROJECT_SOURCE_DIR}/../../application/file.cpp diff --git a/firmware/test/application/test_utility.cpp b/firmware/test/application/test_utility.cpp index d6b6f678..f49bbb20 100644 --- a/firmware/test/application/test_utility.cpp +++ b/firmware/test/application/test_utility.cpp @@ -89,4 +89,11 @@ TEST_CASE("to_byte_array returns correct size and values.") { CHECK_EQ(arr4[1], 0xCD); CHECK_EQ(arr4[2], 0x12); CHECK_EQ(arr4[3], 0x34); +} + +TEST_CASE("join will join strings") { + CHECK_EQ(join(',', {}), ""); + CHECK_EQ(join(',', {"a"}), "a"); + CHECK_EQ(join('-', {"a", "b"}), "a-b"); + CHECK_EQ(join(',', {"a", "b", "c"}), "a,b,c"); } \ No newline at end of file