From 8cae9981463cb9bd7920d3d87f0a27a732bff0e7 Mon Sep 17 00:00:00 2001 From: Kyle Reed Date: Wed, 10 May 2023 09:51:09 -0700 Subject: [PATCH] Fileman copy/paste support (#970) * Add copy/paste UI instead of file save --- firmware/application/apps/ui_fileman.cpp | 231 +++++++++++++++-------- firmware/application/apps/ui_fileman.hpp | 118 +++++++++--- firmware/application/file.cpp | 29 +++ firmware/application/file.hpp | 1 + firmware/application/ui/ui_textentry.cpp | 131 ------------- firmware/application/ui/ui_textentry.hpp | 46 ----- firmware/application/ui_navigation.hpp | 2 +- firmware/common/ui_widget.cpp | 131 +++++++++++++ firmware/common/ui_widget.hpp | 49 ++++- 9 files changed, 449 insertions(+), 289 deletions(-) diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index 54ef55a97..9ce4b307a 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -192,8 +192,8 @@ const fileman_entry& FileManBaseView::get_selected_entry() const { FileManBaseView::FileManBaseView( NavigationView& nav, std::string filter -) : nav_ (nav), - extension_filter { filter } +) : nav_{ nav }, + extension_filter{ filter } { add_children({ &labels, @@ -206,7 +206,7 @@ FileManBaseView::FileManBaseView( }; if (!sdcIsCardInserted(&SDCD1)) { - empty_root = true; + empty_ = EmptyReason::NoSDC; text_current.set("NO SD CARD!"); return; } @@ -214,7 +214,7 @@ FileManBaseView::FileManBaseView( load_directory_contents(current_path); if (!entry_list.size()) { - empty_root = true; + empty_ = EmptyReason::NoFiles; text_current.set("EMPTY SD CARD!"); } else { menu_view.on_left = [this]() { @@ -224,7 +224,7 @@ FileManBaseView::FileManBaseView( } void FileManBaseView::focus() { - if (empty_root) { + if (empty_ != EmptyReason::NotEmpty) { button_exit.focus(); } else { menu_view.focus(); @@ -310,34 +310,8 @@ const FileManBaseView::file_assoc_t& FileManBaseView::get_assoc( return file_types[index]; } -/*void FileSaveView::on_save_name() { - text_prompt(nav_, &filename_buffer, 8, [this](std::string * buffer) { - nav_.pop(); - }); -} -FileSaveView::FileSaveView( - NavigationView& nav -) : FileManBaseView(nav) -{ - name_buffer.clear(); - - add_children({ - &text_save, - &button_save_name, - &live_timestamp - }); - - button_save_name.on_select = [this, &nav](Button&) { - on_save_name(); - }; -}*/ - /* FileLoadView **************************************************************/ -void FileLoadView::refresh_widgets(const bool) { - set_dirty(); -} - FileLoadView::FileLoadView( NavigationView& nav, std::string filter @@ -367,6 +341,68 @@ FileLoadView::FileLoadView( }; } +void FileLoadView::refresh_widgets(const bool) { + set_dirty(); +} + +/* FileSaveView **************************************************************/ +/* +FileSaveView::FileSaveView( + NavigationView& nav, + const fs::path& path, + const fs::path& file +) : nav_{ nav }, + path_{ path }, + file_{ file } +{ + add_children({ + &labels, + &text_path, + &button_edit_path, + &text_name, + &button_edit_name, + &button_save, + &button_cancel, + }); + + button_edit_path.on_select = [this](Button&) { + buffer_ = path_.string(); + text_prompt(nav_, buffer_, max_filename_length, + [this](std::string&) { + path_ = buffer_; + refresh_widgets(); + }); + }; + + button_edit_name.on_select = [this](Button&) { + buffer_ = file_.string(); + text_prompt(nav_, buffer_, max_filename_length, + [this](std::string&) { + file_ = buffer_; + refresh_widgets(); + }); + }; + + button_save.on_select = [this](Button&) { + if (on_save) + on_save(path_ / file_); + else + nav_.pop(); + }; + + button_cancel.on_select = [this](Button&) { + nav_.pop(); + }; + + refresh_widgets(); +} + +void FileSaveView::refresh_widgets() { + text_path.set(truncate(path_, 30)); + text_name.set(truncate(file_, 30)); + set_dirty(); +} +*/ /* FileManagerView ***********************************************************/ void FileManagerView::on_rename() { @@ -429,6 +465,30 @@ void FileManagerView::on_new_dir() { }); } +void FileManagerView::on_paste() { + auto stem = copy_path.stem(); + auto ext = copy_path.extension(); + auto serial = 1; + fs::path new_path = copy_path.filename(); + + // Create a unique name. + while (fs::file_exists(current_path / new_path)) { + new_path = stem; + new_path += fs::path{ u"_" }; + new_path += to_string_dec_int(serial++); + new_path += ext; + } + + // TODO: handle partner file. Need to fix nav stack first. + auto result = copy_file(copy_path, current_path / new_path); + if (result.code() != FR_OK) + nav_.display_modal("Paste Failed", result.what()); + + copy_path = fs::path{ }; + menu_view.focus(); + reload_current(); +} + bool FileManagerView::selected_is_valid() const { return !entry_list.empty() && get_selected_entry().path != parent_dir_path; @@ -438,62 +498,79 @@ void FileManagerView::refresh_widgets(const bool v) { button_rename.hidden(v); button_delete.hidden(v); button_new_dir.hidden(v); + button_copy.hidden(v); + button_paste.hidden(v); set_dirty(); } -FileManagerView::~FileManagerView() { -} - FileManagerView::FileManagerView( NavigationView& nav ) : FileManBaseView(nav, "") { - if (!empty_root) { - on_refresh_widgets = [this](bool v) { - refresh_widgets(v); - }; - - add_children({ - &menu_view, - &labels, - &text_date, - &button_rename, - &button_delete, - &button_new_dir, - }); - - menu_view.on_highlight = [this]() { - // TODO: enable/disable buttons. - if (selected_is_valid()) - text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path()))); - else - text_date.set(""); - }; - - refresh_list(); + // Don't bother with the UI in the case of no SDC. + if (empty_ == EmptyReason::NoSDC) + return; + + on_refresh_widgets = [this](bool v) { + refresh_widgets(v); + }; - on_select_entry = [this](KeyEvent key) { - if (key == KeyEvent::Select && get_selected_entry().is_directory) { - push_dir(get_selected_entry().path); - } else { - button_rename.focus(); - } - }; - - button_rename.on_select = [this](Button&) { - if (selected_is_valid()) - on_rename(); - }; + add_children({ + &menu_view, + &labels, + &text_date, + &button_rename, + &button_delete, + &button_new_dir, + &button_copy, + &button_paste + }); + + menu_view.on_highlight = [this]() { + // TODO: enable/disable buttons. + if (selected_is_valid()) + text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path()))); + else + text_date.set(""); + }; + + refresh_list(); - button_delete.on_select = [this](Button&) { - if (selected_is_valid()) - on_delete(); - }; + on_select_entry = [this](KeyEvent key) { + if (key == KeyEvent::Select && get_selected_entry().is_directory) { + push_dir(get_selected_entry().path); + } else { + button_rename.focus(); + } + }; + + button_rename.on_select = [this](Button&) { + if (selected_is_valid()) + on_rename(); + }; - button_new_dir.on_select = [this](Button&) { - on_new_dir(); - }; - } + button_delete.on_select = [this](Button&) { + if (selected_is_valid()) + on_delete(); + }; + + button_new_dir.on_select = [this](Button&) { + on_new_dir(); + }; + + button_copy.on_select = [this](Button&) { + if (selected_is_valid() && !get_selected_entry().is_directory) + copy_path = get_selected_full_path(); + else + nav_.display_modal("Copy", "Can't copy that."); + }; + + button_paste.on_select = [this](Button&) { + if (!copy_path.empty()) + on_paste(); + else + nav_.display_modal("Paste", "Copy a file first."); + }; } } diff --git a/firmware/application/apps/ui_fileman.hpp b/firmware/application/apps/ui_fileman.hpp index 3033cdc5d..220a7f9c9 100644 --- a/firmware/application/apps/ui_fileman.hpp +++ b/firmware/application/apps/ui_fileman.hpp @@ -36,6 +36,12 @@ struct fileman_entry { bool is_directory { }; }; +enum class EmptyReason : uint8_t { + NotEmpty, + NoFiles, + NoSDC +}; + class FileManBaseView : public View { public: FileManBaseView( @@ -43,11 +49,13 @@ class FileManBaseView : public View { std::string filter ); + virtual ~FileManBaseView() { } + void focus() override; std::string title() const override { return "Fileman"; }; protected: - static constexpr size_t max_filename_length = 50; + static constexpr size_t max_filename_length = 64; struct file_assoc_t { std::filesystem::path extension; @@ -65,7 +73,6 @@ class FileManBaseView : public View { { u"", &bitmap_icon_file, ui::Color::light_grey() } // NB: Must be last. }; - std::filesystem::path get_selected_full_path() const; const fileman_entry& get_selected_entry() const; @@ -78,7 +85,7 @@ class FileManBaseView : public View { NavigationView& nav_; - bool empty_root { false }; + EmptyReason empty_ { EmptyReason::NotEmpty }; std::function on_select_entry { nullptr }; std::function on_refresh_widgets { nullptr }; @@ -104,57 +111,96 @@ class FileManBaseView : public View { }; Button button_exit { - { 16 * 8, 34 * 8, 14 * 8, 32 }, + { 21 * 8, 34 * 8, 9 * 8, 32 }, "Exit" }; }; -/*class FileSaveView : public FileManBaseView { -public: - FileSaveView(NavigationView& nav); - ~FileSaveView(); - -private: - std::string name_buffer { }; - - void on_save_name(); - - Text text_save { - { 4 * 8, 15 * 8, 8 * 8, 16 }, - "Save as:", - }; - Button button_save_name { - { 4 * 8, 18 * 8, 12 * 8, 32 }, - "Name (set)" - }; - LiveDateTime live_timestamp { - { 17 * 8, 24 * 8, 11 * 8, 16 } - }; -};*/ - class FileLoadView : public FileManBaseView { public: std::function on_changed { }; FileLoadView(NavigationView& nav, std::string filter); + virtual ~FileLoadView() { } private: void refresh_widgets(const bool v); }; +/* +// It would be nice to be able to launch FileLoadView +// but it will OOM if launched from within FileManager. +class FileSaveView : public View { +public: + FileSaveView( + NavigationView& nav, + const std::filesystem::path& path, + const std::filesystem::path& file); + + std::function on_save { }; + +private: + static constexpr size_t max_filename_length = 64; + + void refresh_widgets(); + + NavigationView& nav_; + std::filesystem::path path_; + std::filesystem::path file_; + std::string buffer_ { }; + + Labels labels { + { { 0 * 8, 1 * 16 }, "Path:", Color::light_grey() }, + { { 0 * 8, 6 * 16 }, "Filename:", Color::light_grey() }, + }; + + Text text_path { + { 0 * 8, 2 * 16, 30 * 8, 16 }, + "", + }; + + Button button_edit_path { + { 18 * 8, 3 * 16, 11 * 8, 32 }, + "Edit Path" + }; + + Text text_name { + { 0 * 8, 7 * 16, 30 * 8, 16 }, + "", + }; + + Button button_edit_name { + { 18 * 8, 8 * 16, 11 * 8, 32 }, + "Edit Name" + }; + + Button button_save { + { 10 * 8, 16 * 16, 9 * 8, 32 }, + "Save" + }; + + Button button_cancel { + { 20 * 8, 16 * 16, 9 * 8, 32 }, + "Cancel" + }; +}; +*/ + class FileManagerView : public FileManBaseView { public: FileManagerView(NavigationView& nav); - ~FileManagerView(); + virtual ~FileManagerView() { } private: // Passed by ref to other views needing lifetime extension. std::string name_buffer { }; + std::filesystem::path copy_path { }; void refresh_widgets(const bool v); void on_rename(); void on_delete(); void on_new_dir(); + void on_paste(); // True if the selected entry is a real file item. bool selected_is_valid() const; @@ -169,19 +215,29 @@ class FileManagerView : public FileManBaseView { }; Button button_rename { - { 0 * 8, 29 * 8, 14 * 8, 32 }, + { 0 * 8, 29 * 8, 9 * 8, 32 }, "Rename" }; Button button_delete { - { 16 * 8, 29 * 8, 14 * 8, 32 }, + { 21 * 4, 29 * 8, 9 * 8, 32 }, "Delete" }; Button button_new_dir { - { 0 * 8, 34 * 8, 14 * 8, 32 }, + { 21 * 8, 29 * 8, 9 * 8, 32 }, "New Dir" }; + + Button button_copy { + { 0 * 8, 34 * 8, 9 * 8, 32 }, + "Copy" + }; + + Button button_paste { + { 21 * 4, 34 * 8, 9 * 8, 32 }, + "Paste" + }; }; } /* namespace ui */ diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index c63dc157b..49a7692d1 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -205,6 +205,35 @@ uint32_t rename_file(const std::filesystem::path& file_path, const std::filesyst return f_rename(reinterpret_cast(file_path.c_str()), reinterpret_cast(new_name.c_str())); } +std::filesystem::filesystem_error copy_file( + const std::filesystem::path& file_path, + const std::filesystem::path& dest_path) +{ + File src; + File dst; + constexpr size_t buffer_size = 512; + uint8_t buffer[buffer_size]; + + auto error = src.open(file_path); + if (error.is_valid()) return error.value(); + + error = dst.create(dest_path); + if (error.is_valid()) return error.value(); + + while (true) { + auto result = src.read(buffer, buffer_size); + if (result.is_error()) return result.error(); + + result = dst.write(buffer, result.value()); + if (result.is_error()) return result.error(); + + if (result.value() < buffer_size) + break; + } + + return { }; +} + FATTimestamp file_created_date(const std::filesystem::path& file_path) { FILINFO filinfo; diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index 6e267848e..7ea12df93 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -254,6 +254,7 @@ struct FATTimestamp { uint32_t delete_file(const std::filesystem::path& file_path); uint32_t rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name); +std::filesystem::filesystem_error copy_file(const std::filesystem::path& file_path, const std::filesystem::path& dest_path); FATTimestamp file_created_date(const std::filesystem::path& file_path); uint32_t make_new_directory(const std::filesystem::path& dir_path); diff --git a/firmware/application/ui/ui_textentry.cpp b/firmware/application/ui/ui_textentry.cpp index 71e1639bc..3718b39f2 100644 --- a/firmware/application/ui/ui_textentry.cpp +++ b/firmware/application/ui/ui_textentry.cpp @@ -61,137 +61,6 @@ void text_prompt( }*/ } -/* TextField ***********************************************************/ - -TextField::TextField( - std::string& str, - size_t max_length, - Point position, - uint32_t length -) : Widget{ { position, { 8 * static_cast(length), 16 } } }, - text_{ str }, - max_length_{ std::max(max_length, str.length()) }, - char_count_{ std::max(length, 1) }, - cursor_pos_{ text_.length() }, - insert_mode_{ true } -{ - set_focusable(true); -} - -const std::string& TextField::value() const { - return text_; -} - -void TextField::set_cursor(uint32_t pos) { - cursor_pos_ = std::min(pos, text_.length()); - set_dirty(); -} - -void TextField::set_insert_mode() { - insert_mode_ = true; -} - -void TextField::set_overwrite_mode() { - insert_mode_ = false; -} - -void TextField::char_add(char c) { - // Don't add if inserting and at max_length and - // don't overwrite if past the end of the text. - if ((text_.length() >= max_length_ && insert_mode_) || - (cursor_pos_ >= text_.length() && !insert_mode_)) - return; - - if (insert_mode_) - text_.insert(cursor_pos_, 1, c); - else - text_[cursor_pos_] = c; - - cursor_pos_++; - set_dirty(); -} - -void TextField::char_delete() { - if (cursor_pos_ == 0) - return; - - cursor_pos_--; - text_.erase(cursor_pos_, 1); - set_dirty(); -} - -void TextField::paint(Painter& painter) { - constexpr int char_width = 8; - - auto rect = screen_rect(); - auto text_style = has_focus() ? style().invert() : style(); - auto offset = 0; - - // Does the string need to be shifted? - if (cursor_pos_ >= char_count_) - offset = cursor_pos_ - char_count_ + 1; - - // Clear the control. - painter.fill_rectangle(rect, text_style.background); - - // Draw the text starting at the offset. - for (uint32_t i = 0; i < char_count_ && i + offset < text_.length(); i++) { - painter.draw_char( - { rect.location().x() + (static_cast(i) * char_width), rect.location().y() }, - text_style, - text_[i + offset] - ); - } - - // Determine cursor position on screen (either the cursor position or the last char). - int32_t cursor_x = char_width * (offset > 0 ? char_count_ - 1 : cursor_pos_); - Point cursor_point{ screen_pos().x() + cursor_x, screen_pos().y() }; - auto cursor_style = text_style.invert(); - - // Invert the cursor character when in overwrite mode. - if (!insert_mode_ && (cursor_pos_) < text_.length()) - painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]); - - // Draw the cursor. - Rect cursor_box{ cursor_point, { char_width, 16 } }; - painter.draw_rectangle(cursor_box, cursor_style.background); -} - -bool TextField::on_key(const KeyEvent key) { - if (key == KeyEvent::Left && cursor_pos_ > 0) - cursor_pos_--; - else if (key == KeyEvent::Right && cursor_pos_ < text_.length()) - cursor_pos_++; - else if (key == KeyEvent::Select) - insert_mode_ = !insert_mode_; - else - return false; - - set_dirty(); - return true; -} - -bool TextField::on_encoder(const EncoderEvent delta) { - int32_t new_pos = cursor_pos_ + delta; - - // Let the encoder wrap around the ends of the text. - if (new_pos < 0) - new_pos = text_.length(); - else if (static_cast(new_pos) > text_.length()) - new_pos = 0; - - set_cursor(new_pos); - return true; -} - -bool TextField::on_touch(const TouchEvent event) { - if (event.type == TouchEvent::Type::Start) - focus(); - - set_dirty(); - return true; -} - /* TextEntryView ***********************************************************/ void TextEntryView::char_delete() { diff --git a/firmware/application/ui/ui_textentry.hpp b/firmware/application/ui/ui_textentry.hpp index f948aedf7..02240847f 100644 --- a/firmware/application/ui/ui_textentry.hpp +++ b/firmware/application/ui/ui_textentry.hpp @@ -28,52 +28,6 @@ namespace ui { -// A TextField is bound to a string reference and allows the string -// to be manipulated. The field itself does not provide the UI for -// setting the value. It provides the UI of rendering the text, -// a cursor, and an API to edit the string content. -class TextField : public Widget { -public: - TextField(std::string& str, Point position, uint32_t length = 30) - : TextField{str, 64, position, length} { } - - // Str: the string containing the content to edit. - // Max_length: max length the string is allowed to use. - // Position: the top-left corner of the control. - // Length: the number of characters to display. - // - Characters are 8 pixels wide. - // - The screen can show 30 characters max. - // - The control is 16 pixels tall. - TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30); - - TextField(const TextField&) = delete; - TextField(TextField&&) = delete; - TextField& operator=(const TextField&) = delete; - TextField& operator=(TextField&&) = delete; - - const std::string& value() const; - - void set_cursor(uint32_t pos); - void set_insert_mode(); - void set_overwrite_mode(); - - void char_add(char c); - void char_delete(); - - void paint(Painter& painter) override; - - bool on_key(const KeyEvent key) override; - bool on_encoder(const EncoderEvent delta) override; - bool on_touch(const TouchEvent event) override; - -protected: - std::string& text_; - size_t max_length_; - uint32_t char_count_; - uint32_t cursor_pos_; - bool insert_mode_; -}; - class TextEntryView : public View { public: std::function on_changed { }; diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index da827ec14..737cd38ec 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -135,7 +135,7 @@ namespace ui Color::dark_grey()}; ImageButton button_back{ - {0, 0 * 16, 12 * 8, 16},//back button is long enough to cover the title area to make it easier to touch + {0, 0 * 16, 12 * 8, 16}, // Back button also covers the title for easier touch. &bitmap_icon_previous, Color::white(), Color::dark_grey()}; diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 2c1d93319..e9dad8fcc 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1535,6 +1535,137 @@ bool OptionsField::on_touch(const TouchEvent event) { return true; } +/* TextField ***********************************************************/ + +TextField::TextField( + std::string& str, + size_t max_length, + Point position, + uint32_t length +) : Widget{ { position, { 8 * static_cast(length), 16 } } }, + text_{ str }, + max_length_{ std::max(max_length, str.length()) }, + char_count_{ std::max(length, 1) }, + cursor_pos_{ text_.length() }, + insert_mode_{ true } +{ + set_focusable(true); +} + +const std::string& TextField::value() const { + return text_; +} + +void TextField::set_cursor(uint32_t pos) { + cursor_pos_ = std::min(pos, text_.length()); + set_dirty(); +} + +void TextField::set_insert_mode() { + insert_mode_ = true; +} + +void TextField::set_overwrite_mode() { + insert_mode_ = false; +} + +void TextField::char_add(char c) { + // Don't add if inserting and at max_length and + // don't overwrite if past the end of the text. + if ((text_.length() >= max_length_ && insert_mode_) || + (cursor_pos_ >= text_.length() && !insert_mode_)) + return; + + if (insert_mode_) + text_.insert(cursor_pos_, 1, c); + else + text_[cursor_pos_] = c; + + cursor_pos_++; + set_dirty(); +} + +void TextField::char_delete() { + if (cursor_pos_ == 0) + return; + + cursor_pos_--; + text_.erase(cursor_pos_, 1); + set_dirty(); +} + +void TextField::paint(Painter& painter) { + constexpr int char_width = 8; + + auto rect = screen_rect(); + auto text_style = has_focus() ? style().invert() : style(); + auto offset = 0; + + // Does the string need to be shifted? + if (cursor_pos_ >= char_count_) + offset = cursor_pos_ - char_count_ + 1; + + // Clear the control. + painter.fill_rectangle(rect, text_style.background); + + // Draw the text starting at the offset. + for (uint32_t i = 0; i < char_count_ && i + offset < text_.length(); i++) { + painter.draw_char( + { rect.location().x() + (static_cast(i) * char_width), rect.location().y() }, + text_style, + text_[i + offset] + ); + } + + // Determine cursor position on screen (either the cursor position or the last char). + int32_t cursor_x = char_width * (offset > 0 ? char_count_ - 1 : cursor_pos_); + Point cursor_point{ screen_pos().x() + cursor_x, screen_pos().y() }; + auto cursor_style = text_style.invert(); + + // Invert the cursor character when in overwrite mode. + if (!insert_mode_ && (cursor_pos_) < text_.length()) + painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]); + + // Draw the cursor. + Rect cursor_box{ cursor_point, { char_width, 16 } }; + painter.draw_rectangle(cursor_box, cursor_style.background); +} + +bool TextField::on_key(const KeyEvent key) { + if (key == KeyEvent::Left && cursor_pos_ > 0) + cursor_pos_--; + else if (key == KeyEvent::Right && cursor_pos_ < text_.length()) + cursor_pos_++; + else if (key == KeyEvent::Select) + insert_mode_ = !insert_mode_; + else + return false; + + set_dirty(); + return true; +} + +bool TextField::on_encoder(const EncoderEvent delta) { + int32_t new_pos = cursor_pos_ + delta; + + // Let the encoder wrap around the ends of the text. + if (new_pos < 0) + new_pos = text_.length(); + else if (static_cast(new_pos) > text_.length()) + new_pos = 0; + + set_cursor(new_pos); + return true; +} + +bool TextField::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) + focus(); + + set_dirty(); + return true; +} + /* NumberField ***********************************************************/ NumberField::NumberField( diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index db198bf33..4192d47a3 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -414,7 +414,6 @@ class Button : public Widget { bool instant_exec_ { false }; }; - class ButtonWithEncoder : public Widget { public: std::function on_select { }; @@ -457,8 +456,6 @@ class ButtonWithEncoder : public Widget { bool instant_exec_ { false }; }; - - class NewButton : public Widget { public: std::function on_select { }; @@ -610,6 +607,52 @@ class OptionsField : public Widget { size_t selected_index_ { 0 }; }; +// A TextField is bound to a string reference and allows the string +// to be manipulated. The field itself does not provide the UI for +// setting the value. It provides the UI of rendering the text, +// a cursor, and an API to edit the string content. +class TextField : public Widget { +public: + TextField(std::string& str, Point position, uint32_t length = 30) + : TextField{str, 64, position, length} { } + + // Str: the string containing the content to edit. + // Max_length: max length the string is allowed to use. + // Position: the top-left corner of the control. + // Length: the number of characters to display. + // - Characters are 8 pixels wide. + // - The screen can show 30 characters max. + // - The control is 16 pixels tall. + TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30); + + TextField(const TextField&) = delete; + TextField(TextField&&) = delete; + TextField& operator=(const TextField&) = delete; + TextField& operator=(TextField&&) = delete; + + const std::string& value() const; + + void set_cursor(uint32_t pos); + void set_insert_mode(); + void set_overwrite_mode(); + + void char_add(char c); + void char_delete(); + + void paint(Painter& painter) override; + + bool on_key(const KeyEvent key) override; + bool on_encoder(const EncoderEvent delta) override; + bool on_touch(const TouchEvent event) override; + +protected: + std::string& text_; + size_t max_length_; + uint32_t char_count_; + uint32_t cursor_pos_; + bool insert_mode_; +}; + class NumberField : public Widget { public: std::function on_select { };