/* * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, * Boston, MA 02110-1301, USA. */ #include "ui.hpp" #include "ui_navigation.hpp" #include "ui_receiver.hpp" #include "ui_spectrum.hpp" #include "ui_transmitter.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::external_app::remote { /* 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_peripherals_details}; }; // 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_; void refresh_ui(); void load_path(std::filesystem::path&& path); Labels labels{ {{2 * 8, 1 * 16}, "Name:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 2 * 16}, "Path:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 3 * 16}, "Freq:", Theme::getInstance()->fg_light->foreground}, {{17 * 8, 3 * 16}, "MHz", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 4 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 5 * 16}, "Icon:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 6 * 16}, "FG Color:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 7 * 16}, "BG Color:", Theme::getInstance()->fg_light->foreground}, {{8 * 8, 9 * 16}, "Button preview", Theme::getInstance()->fg_light->foreground}, }; 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 RemoteAppView : public View { public: RemoteAppView(NavigationView& nav); RemoteAppView(NavigationView& nav, std::filesystem::path path); ~RemoteAppView(); RemoteAppView(const RemoteAppView&) = delete; RemoteAppView& operator=(const RemoteAppView&) = delete; std::string title() const override { return "Remote"; }; void focus() override; private: /* Creates the dynamic buttons. */ void create_buttons(); /* Resets all the pointers to null entries. */ void reset_buttons(); void refresh_ui(); 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; 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; // This value is mysterious... why? static constexpr uint32_t baseband_bandwidth = 2'500'000; NavigationView& nav_; RxRadioState radio_state_{}; // 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::external_app::remote