diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index 90bd8e2e..9aad8515 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -108,14 +108,17 @@ bool partner_file_prompt( if (partner.empty()) return false; - nav.push_under_current( - "Partner File", - partner.filename().string() + "\n" + action_name + " this file too?", - YESNO, - [&nav, partner, on_partner_action](bool choice) { - if (on_partner_action) - on_partner_action(partner, choice); - }); + // Display the continuation UI once the current has been popped. + nav.set_on_pop([&nav, partner, action_name, on_partner_action] { + nav.display_modal( + "Partner File", + partner.filename().string() + "\n" + action_name + " this file too?", + YESNO, + [&nav, partner, on_partner_action](bool choice) { + if (on_partner_action) + on_partner_action(partner, choice); + }); + }); return true; } @@ -189,8 +192,8 @@ FileManBaseView::FileManBaseView( &text_current, &button_exit}); - button_exit.on_select = [this, &nav](Button&) { - nav.pop(); + button_exit.on_select = [this](Button&) { + nav_.pop(); }; if (!sdcIsCardInserted(&SDCD1)) { @@ -325,9 +328,9 @@ FileLoadView::FileLoadView( if (get_selected_entry().is_directory) { push_dir(get_selected_entry().path); } else { - nav_.pop(); if (on_changed) on_changed(get_selected_full_path()); + nav_.pop(); } }; } diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index e8d12f16..5fb3cc74 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -92,9 +92,9 @@ void FreqManBaseView::focus() { // TODO: Shouldn't be on focus. if (error_ == ERROR_ACCESS) { - nav_.display_modal("Error", "File access error", ABORT, nullptr); + nav_.display_modal("Error", "File access error", ABORT); } else if (error_ == ERROR_NOFILES) { - nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT, nullptr); + nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT); } else { options_category.focus(); } diff --git a/firmware/application/apps/ui_numbers.cpp b/firmware/application/apps/ui_numbers.cpp index 63e20e3d..6c0edafe 100644 --- a/firmware/application/apps/ui_numbers.cpp +++ b/firmware/application/apps/ui_numbers.cpp @@ -39,7 +39,7 @@ namespace ui { void NumbersStationView::focus() { if (file_error) - nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT, nullptr); + nav_.display_modal("No voices", "No valid voices found in\nthe /numbers directory.", ABORT); else button_exit.focus(); } diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index 4df0698d..37d30f21 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -459,8 +459,7 @@ ReconView::ReconView(NavigationView& nav) rssi.set_focusable(true); rssi.set_peak(true, 500); rssi.on_select = [this](RSSI&) { - nav_.pop(); - nav_.push(); + nav_.replace(); }; // TODO: *BUG* Both transmitter_model and receiver_model share the same pmem setting for target_frequency. diff --git a/firmware/application/apps/ui_remote.cpp b/firmware/application/apps/ui_remote.cpp index 603dabe6..d4c973b5 100644 --- a/firmware/application/apps/ui_remote.cpp +++ b/firmware/application/apps/ui_remote.cpp @@ -555,7 +555,6 @@ void RemoteView::open_remote() { save_remote(); load_remote(std::move(path)); refresh_ui(); - ; }; } diff --git a/firmware/application/apps/ui_sd_wipe.cpp b/firmware/application/apps/ui_sd_wipe.cpp index 3139b4d6..c035691b 100644 --- a/firmware/application/apps/ui_sd_wipe.cpp +++ b/firmware/application/apps/ui_sd_wipe.cpp @@ -44,15 +44,21 @@ void WipeSDView::focus() { dummy.focus(); if (!confirmed) { - nav_.push("Warning !", "Wipe FAT of SD card?", YESCANCEL, [this](bool choice) { - if (choice) - confirmed = true; - }); + nav_.push( + "Warning !", + "Wipe FAT of SD card?", + YESNO, + [this](bool choice) { + if (choice) + confirmed = true; + else + nav_.pop(false); // Pop w/o update so the modal will pop off the app. + }); } else { if (sdcGetInfo(&SDCD1, &block_device_info) == CH_SUCCESS) { thread = chThdCreateFromHeap(NULL, 2048, NORMALPRIO, WipeSDView::static_fn, this); } else { - nav_.pop(); // Just silently abort for now + nav_.pop(); // Just silently abort for now. } } } diff --git a/firmware/application/apps/ui_sd_wipe.hpp b/firmware/application/apps/ui_sd_wipe.hpp index 8f8eebe8..d3b87347 100644 --- a/firmware/application/apps/ui_sd_wipe.hpp +++ b/firmware/application/apps/ui_sd_wipe.hpp @@ -38,7 +38,7 @@ class WipeSDView : public View { ~WipeSDView(); void focus() override; - std::string title() const override { return "Wipe sdcard"; }; + std::string title() const override { return "Wipe SD Card"; }; private: NavigationView& nav_; diff --git a/firmware/application/apps/ui_spectrum_painter_image.cpp b/firmware/application/apps/ui_spectrum_painter_image.cpp index b04559eb..4a788443 100644 --- a/firmware/application/apps/ui_spectrum_painter_image.cpp +++ b/firmware/application/apps/ui_spectrum_painter_image.cpp @@ -40,17 +40,13 @@ SpectrumInputImageView::SpectrumInputImageView(NavigationView& nav) { auto open_view = nav.push(".bmp"); constexpr auto data_directory = u"SPECTRUM"; - if (std::filesystem::is_directory(data_directory) == false) { - if (make_new_directory(data_directory).ok()) - open_view->push_dir(data_directory); - } else - open_view->push_dir(data_directory); + ensure_directory(data_directory); + open_view->push_dir(data_directory); open_view->on_changed = [this](std::filesystem::path new_file_path) { this->file = new_file_path.string(); painted = false; this->set_dirty(); - this->on_input_avaliable(); }; }; diff --git a/firmware/application/apps/ui_sstvtx.cpp b/firmware/application/apps/ui_sstvtx.cpp index 90d1a1d3..b5acae04 100644 --- a/firmware/application/apps/ui_sstvtx.cpp +++ b/firmware/application/apps/ui_sstvtx.cpp @@ -35,7 +35,7 @@ namespace ui { void SSTVTXView::focus() { if (file_error) - nav_.display_modal("No files", "No valid bitmaps\nin /sstv directory.", ABORT, nullptr); + nav_.display_modal("No files", "No valid bitmaps\nin /sstv directory.", ABORT); else options_bitmaps.focus(); } diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp index 130f76c9..797a715d 100644 --- a/firmware/application/apps/ui_text_editor.cpp +++ b/firmware/application/apps/ui_text_editor.cpp @@ -29,14 +29,6 @@ using namespace portapack; namespace fs = std::filesystem; -namespace { -/*void log(const std::string& msg) { - LogFile log{}; - log.append("LOGS/Notepad.txt"); - log.write_entry(msg); -}*/ -} // namespace - namespace ui { /* TextViewer *******************************************************/ @@ -413,16 +405,9 @@ TextEditorView::TextEditorView(NavigationView& nav) }; menu.on_open() = [this]() { - /*show_save_prompt([this]() { + show_save_prompt([this]() { show_file_picker(); - });*/ - // HACK: above should work but it's faulting. - if (!file_dirty_) { - show_file_picker(); - } else { - show_save_prompt(nullptr); - show_file_picker(false); - } + }); }; menu.on_save() = [this]() { @@ -530,16 +515,16 @@ void TextEditorView::hide_menu(bool hidden) { set_dirty(); } -void TextEditorView::show_file_picker(bool immediate) { - // TODO: immediate is a hack until nav_.on_pop is fixed. - auto open_view = immediate ? nav_.push("") : nav_.push_under_current(""); - - if (open_view) { - open_view->on_changed = [this](std::filesystem::path path) { - open_file(path); +void TextEditorView::show_file_picker() { + auto open_view = nav_.push(""); + open_view->on_changed = [this](std::filesystem::path path) { + // Can't update the UI focus while the FileLoadView is still up. + // Do this on a continuation instead of in on_changed. + nav_.set_on_pop([this, p = std::move(path)]() { + open_file(p); hide_menu(); - }; - } + }); + }; } void TextEditorView::show_edit_line() { diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp index 5e3addd8..e393ef1f 100644 --- a/firmware/application/apps/ui_text_editor.hpp +++ b/firmware/application/apps/ui_text_editor.hpp @@ -19,10 +19,6 @@ * Boston, MA 02110-1301, USA. */ -/* TODO: - * - Busy indicator while reading files. - */ - #ifndef __UI_TEXT_EDITOR_H__ #define __UI_TEXT_EDITOR_H__ @@ -238,7 +234,7 @@ class TextEditorView : public View { void refresh_ui(); void update_position(); void hide_menu(bool hidden = true); - void show_file_picker(bool immediate = true); + void show_file_picker(); void show_edit_line(); void show_save_prompt(std::function continuation); @@ -252,8 +248,8 @@ class TextEditorView : public View { bool has_temp_file_{false}; TextViewer viewer{ - /* 272 = 320 - 16 (top bar) - 32 (bottom controls) */ - {0, 0, 240, 272}}; + /* 272 = screen_height - 16 (top bar) - 32 (bottom controls) */ + {0, 0, screen_width, 272}}; TextEditorMenu menu{}; diff --git a/firmware/application/apps/ui_view_wav.cpp b/firmware/application/apps/ui_view_wav.cpp index 325566e7..8cdc4231 100644 --- a/firmware/application/apps/ui_view_wav.cpp +++ b/firmware/application/apps/ui_view_wav.cpp @@ -138,17 +138,20 @@ ViewWavView::ViewWavView( reset_controls(); button_open.on_select = [this, &nav](Button&) { auto open_view = nav.push(".WAV"); - open_view->on_changed = [this](std::filesystem::path file_path) { - if (!wav_reader->open(file_path)) { - nav_.display_modal("Error", "Couldn't open file."); - return; - } - if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) { - nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files."); - return; - } - load_wav(file_path); - field_pos_seconds.focus(); + open_view->on_changed = [this, &nav](std::filesystem::path file_path) { + // Can't show new dialogs in an on_changed handler, so use continuation. + nav.set_on_pop([this, &nav, file_path]() { + if (!wav_reader->open(file_path)) { + nav_.display_modal("Error", "Couldn't open file."); + return; + } + if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) { + nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files."); + return; + } + load_wav(file_path); + field_pos_seconds.focus(); + }); }; }; diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 3d657560..d85273f4 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -413,7 +413,7 @@ void GeoMapView::focus() { geopos.focus(); if (!map_opened) - nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT, nullptr); + nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT); } void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t altitude) { diff --git a/firmware/application/ui/ui_receiver.cpp b/firmware/application/ui/ui_receiver.cpp index c484a707..5bfbcf1a 100644 --- a/firmware/application/ui/ui_receiver.cpp +++ b/firmware/application/ui/ui_receiver.cpp @@ -273,9 +273,8 @@ FrequencyKeypadView::FrequencyKeypadView( }; button_close.on_select = [this, &nav](Button&) { - if (on_changed) { + if (on_changed) on_changed(this->value()); - } nav.pop(); }; diff --git a/firmware/application/ui/ui_textentry.cpp b/firmware/application/ui/ui_textentry.cpp index 27ffef9a..669fc226 100644 --- a/firmware/application/ui/ui_textentry.cpp +++ b/firmware/application/ui/ui_textentry.cpp @@ -41,20 +41,12 @@ void text_prompt( uint32_t cursor_pos, size_t max_length, std::function on_done) { - // if (persistent_memory::ui_config_textentry() == 0) { auto te_view = nav.push(str, max_length); te_view->set_cursor(cursor_pos); te_view->on_changed = [on_done](std::string& value) { if (on_done) on_done(value); }; - /*} else { - auto te_view = nav.push(str, max_length); - te_view->on_changed = [on_done](std::string * value) { - if (on_done) - on_done(value); - }; - }*/ } /* TextEntryView ***********************************************************/ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 196dedb2..d9bb0c47 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -96,6 +96,7 @@ #include "core_control.hpp" #include "file.hpp" +#include "file_reader.hpp" #include "png_writer.hpp" using portapack::receiver_model; @@ -406,15 +407,21 @@ View* NavigationView::push_view(std::unique_ptr new_view) { return p; } -void NavigationView::pop() { - pop(true); -} +void NavigationView::pop(bool trigger_update) { + // Don't pop off the NavView. + if (view_stack.size() <= 1) + return; -void NavigationView::pop_modal() { - // Pop modal view + underlying app view. - // TODO: this shouldn't be necessary. - pop(false); - pop(true); + auto on_pop = view_stack.back().on_pop; + + free_view(); + view_stack.pop_back(); + + // NB: These are executed _after_ the view has been + // destroyed. The old view MUST NOT be referenced in + // these callbacks or it will cause crashes. + if (trigger_update) update_view(); + if (on_pop) on_pop(); } void NavigationView::display_modal( @@ -426,49 +433,33 @@ void NavigationView::display_modal( void NavigationView::display_modal( const std::string& title, const std::string& message, - const modal_t type, - const std::function on_choice) { - /* If a modal view is already visible, don't display another */ - if (!modal_view) { - modal_view = push(title, message, type, on_choice); - } -} - -void NavigationView::pop(bool update) { - if (view() == modal_view) { - modal_view = nullptr; - } - - // Can't pop last item from stack. - if (view_stack.size() > 1) { - auto on_pop = view_stack.back().on_pop; - - free_view(); - view_stack.pop_back(); - - if (update) - update_view(); - - if (on_pop) on_pop(); - } + modal_t type, + std::function on_choice) { + push(title, message, type, on_choice); } void NavigationView::free_view() { + // The focus_manager holds a raw pointer to the currently focused Widget. + // It then tries to call blur() on that instance when the focus is changed. + // This causes crashes if focused_widget has been deleted (as is the case + // when a view is popped). Calling blur() here resets the focus_manager's + // focus_widget pointer so focus can be called safely. + this->blur(); remove_child(view()); } void NavigationView::update_view() { - const auto new_view = view_stack.back().view.get(); + const auto& top = view_stack.back(); + auto top_view = top.view.get(); - add_child(new_view); - new_view->set_parent_rect({{0, 0}, size()}); + add_child(top_view); + top_view->set_parent_rect({{0, 0}, size()}); focus(); set_dirty(); - if (on_view_changed) { - on_view_changed(*new_view); - } + if (on_view_changed) + on_view_changed(*top_view); } Widget* NavigationView::view() const { @@ -476,9 +467,8 @@ Widget* NavigationView::view() const { } void NavigationView::focus() { - if (view()) { + if (view()) view()->focus(); - } } bool NavigationView::set_on_pop(std::function on_pop) { @@ -753,21 +743,19 @@ ModalMessageView::ModalMessageView( NavigationView& nav, const std::string& title, const std::string& message, - const modal_t type, - const std::function on_choice) + modal_t type, + std::function on_choice) : title_{title}, message_{message}, type_{type}, on_choice_{on_choice} { if (type == INFO) { add_child(&button_ok); - button_ok.on_select = [this, &nav](Button&) { - if (on_choice_) - on_choice_(true); // Assumes handler will pop. - else - nav.pop(); + if (on_choice_) on_choice_(true); + nav.pop(); }; + } else if (type == YESNO) { add_children({&button_yes, &button_no}); @@ -780,50 +768,33 @@ ModalMessageView::ModalMessageView( if (on_choice_) on_choice_(false); nav.pop(); }; - } else if (type == YESCANCEL) { - add_children({&button_yes, - &button_no}); - button_yes.on_select = [this, &nav](Button&) { - if (on_choice_) on_choice_(true); - nav.pop(); - }; - button_no.on_select = [this, &nav](Button&) { - // if (on_choice_) on_choice_(false); - nav.pop_modal(); - }; } else { // ABORT add_child(&button_ok); button_ok.on_select = [this, &nav](Button&) { if (on_choice_) on_choice_(true); - nav.pop_modal(); + nav.pop(false); // Pop the modal. + nav.pop(); // Pop the underlying view. }; } } void ModalMessageView::paint(Painter& painter) { - size_t pos, i = 0, start = 0; - portapack::display.drawBMP({100, 48}, modal_warning_bmp, false); - // Break on lines. - while ((pos = message_.find("\n", start)) != std::string::npos) { + // Break lines. + auto lines = split_string(message_, '\n'); + for (size_t i = 0; i < lines.size(); ++i) { painter.draw_string( {1 * 8, (Coord)(120 + (i * 16))}, style(), - message_.substr(start, pos - start)); - i++; - start = pos + 1; + lines[i]); } - painter.draw_string( - {1 * 8, (Coord)(120 + (i * 16))}, - style(), - message_.substr(start, pos)); } void ModalMessageView::focus() { - if ((type_ == YESNO) || (type_ == YESCANCEL)) { + if ((type_ == YESNO)) { button_yes.focus(); } else { button_ok.focus(); diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 4123864d..d51c7329 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -51,7 +51,6 @@ namespace ui { enum modal_t { INFO = 0, YESNO, - YESCANCEL, ABORT }; @@ -73,15 +72,6 @@ class NavigationView : public View { return reinterpret_cast(push_view(std::unique_ptr(new T(*this, std::forward(args)...)))); } - // Pushes a new view under the current on the stack so the current view returns into this new one. - template - T* push_under_current(Args&&... args) { - auto new_view = std::unique_ptr(new T(*this, std::forward(args)...)); - auto new_view_ptr = new_view.get(); - view_stack.insert(view_stack.end() - 1, ViewState{std::move(new_view), {}}); - return reinterpret_cast(new_view_ptr); - } - template T* replace(Args&&... args) { pop(); @@ -90,12 +80,14 @@ class NavigationView : public View { void push(View* v); void replace(View* v); - - void pop(); - void pop_modal(); + void pop(bool trigger_update = true); void display_modal(const std::string& title, const std::string& message); - void display_modal(const std::string& title, const std::string& message, const modal_t type, const std::function on_choice = nullptr); + void display_modal( + const std::string& title, + const std::string& message, + modal_t type, + std::function on_choice = nullptr); void focus() override; @@ -110,11 +102,9 @@ class NavigationView : public View { }; std::vector view_stack{}; - Widget* modal_view{nullptr}; Widget* view() const; - void pop(bool update); void free_view(); void update_view(); View* push_view(std::unique_ptr new_view); @@ -362,8 +352,8 @@ class ModalMessageView : public View { NavigationView& nav, const std::string& title, const std::string& message, - const modal_t type, - const std::function on_choice); + modal_t type, + std::function on_choice); void paint(Painter& painter) override; void focus() override; diff --git a/firmware/common/ui_focus.cpp b/firmware/common/ui_focus.cpp index 8dcd55d7..2ef155a8 100644 --- a/firmware/common/ui_focus.cpp +++ b/firmware/common/ui_focus.cpp @@ -43,18 +43,12 @@ void FocusManager::set_focus_widget(Widget* const new_focus_widget) { } if (new_focus_widget) { - // if( !new_focus_widget->visible() ) { - // if( new_focus_widget->hidden() ) { - // // New widget is not visible. Do nothing. - // // TODO: Should this be a debug assertion? - // return; - // } - if (!new_focus_widget->focusable()) { + if (!new_focus_widget->focusable()) return; - } } // Blur old widget. + // NB: This will crash if the focus_widget is a destroyed instance. if (focus_widget()) { focus_widget()->on_blur(); focus_widget()->set_dirty(); diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 2ed9348f..6a2e7c15 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -134,7 +134,6 @@ void Widget::focus() { } void Widget::on_focus() { - // set_dirty(); } void Widget::blur() { @@ -142,7 +141,6 @@ void Widget::blur() { } void Widget::on_blur() { - // set_dirty(); } bool Widget::focusable() const {