mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2024-12-15 04:28:10 +00:00
Fileman copy/paste support (#970)
* Add copy/paste UI instead of file save
This commit is contained in:
parent
9a22a760ad
commit
8cae998146
@ -192,8 +192,8 @@ const fileman_entry& FileManBaseView::get_selected_entry() const {
|
|||||||
FileManBaseView::FileManBaseView(
|
FileManBaseView::FileManBaseView(
|
||||||
NavigationView& nav,
|
NavigationView& nav,
|
||||||
std::string filter
|
std::string filter
|
||||||
) : nav_ (nav),
|
) : nav_{ nav },
|
||||||
extension_filter { filter }
|
extension_filter{ filter }
|
||||||
{
|
{
|
||||||
add_children({
|
add_children({
|
||||||
&labels,
|
&labels,
|
||||||
@ -206,7 +206,7 @@ FileManBaseView::FileManBaseView(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!sdcIsCardInserted(&SDCD1)) {
|
if (!sdcIsCardInserted(&SDCD1)) {
|
||||||
empty_root = true;
|
empty_ = EmptyReason::NoSDC;
|
||||||
text_current.set("NO SD CARD!");
|
text_current.set("NO SD CARD!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@ FileManBaseView::FileManBaseView(
|
|||||||
load_directory_contents(current_path);
|
load_directory_contents(current_path);
|
||||||
|
|
||||||
if (!entry_list.size()) {
|
if (!entry_list.size()) {
|
||||||
empty_root = true;
|
empty_ = EmptyReason::NoFiles;
|
||||||
text_current.set("EMPTY SD CARD!");
|
text_current.set("EMPTY SD CARD!");
|
||||||
} else {
|
} else {
|
||||||
menu_view.on_left = [this]() {
|
menu_view.on_left = [this]() {
|
||||||
@ -224,7 +224,7 @@ FileManBaseView::FileManBaseView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileManBaseView::focus() {
|
void FileManBaseView::focus() {
|
||||||
if (empty_root) {
|
if (empty_ != EmptyReason::NotEmpty) {
|
||||||
button_exit.focus();
|
button_exit.focus();
|
||||||
} else {
|
} else {
|
||||||
menu_view.focus();
|
menu_view.focus();
|
||||||
@ -310,34 +310,8 @@ const FileManBaseView::file_assoc_t& FileManBaseView::get_assoc(
|
|||||||
return file_types[index];
|
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 **************************************************************/
|
/* FileLoadView **************************************************************/
|
||||||
|
|
||||||
void FileLoadView::refresh_widgets(const bool) {
|
|
||||||
set_dirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
FileLoadView::FileLoadView(
|
FileLoadView::FileLoadView(
|
||||||
NavigationView& nav,
|
NavigationView& nav,
|
||||||
std::string filter
|
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 ***********************************************************/
|
/* FileManagerView ***********************************************************/
|
||||||
|
|
||||||
void FileManagerView::on_rename() {
|
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 {
|
bool FileManagerView::selected_is_valid() const {
|
||||||
return !entry_list.empty() &&
|
return !entry_list.empty() &&
|
||||||
get_selected_entry().path != parent_dir_path;
|
get_selected_entry().path != parent_dir_path;
|
||||||
@ -438,17 +498,19 @@ void FileManagerView::refresh_widgets(const bool v) {
|
|||||||
button_rename.hidden(v);
|
button_rename.hidden(v);
|
||||||
button_delete.hidden(v);
|
button_delete.hidden(v);
|
||||||
button_new_dir.hidden(v);
|
button_new_dir.hidden(v);
|
||||||
|
button_copy.hidden(v);
|
||||||
|
button_paste.hidden(v);
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
FileManagerView::~FileManagerView() {
|
|
||||||
}
|
|
||||||
|
|
||||||
FileManagerView::FileManagerView(
|
FileManagerView::FileManagerView(
|
||||||
NavigationView& nav
|
NavigationView& nav
|
||||||
) : FileManBaseView(nav, "")
|
) : FileManBaseView(nav, "")
|
||||||
{
|
{
|
||||||
if (!empty_root) {
|
// Don't bother with the UI in the case of no SDC.
|
||||||
|
if (empty_ == EmptyReason::NoSDC)
|
||||||
|
return;
|
||||||
|
|
||||||
on_refresh_widgets = [this](bool v) {
|
on_refresh_widgets = [this](bool v) {
|
||||||
refresh_widgets(v);
|
refresh_widgets(v);
|
||||||
};
|
};
|
||||||
@ -460,6 +522,8 @@ FileManagerView::FileManagerView(
|
|||||||
&button_rename,
|
&button_rename,
|
||||||
&button_delete,
|
&button_delete,
|
||||||
&button_new_dir,
|
&button_new_dir,
|
||||||
|
&button_copy,
|
||||||
|
&button_paste
|
||||||
});
|
});
|
||||||
|
|
||||||
menu_view.on_highlight = [this]() {
|
menu_view.on_highlight = [this]() {
|
||||||
@ -493,7 +557,20 @@ FileManagerView::FileManagerView(
|
|||||||
button_new_dir.on_select = [this](Button&) {
|
button_new_dir.on_select = [this](Button&) {
|
||||||
on_new_dir();
|
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.");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,12 @@ struct fileman_entry {
|
|||||||
bool is_directory { };
|
bool is_directory { };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class EmptyReason : uint8_t {
|
||||||
|
NotEmpty,
|
||||||
|
NoFiles,
|
||||||
|
NoSDC
|
||||||
|
};
|
||||||
|
|
||||||
class FileManBaseView : public View {
|
class FileManBaseView : public View {
|
||||||
public:
|
public:
|
||||||
FileManBaseView(
|
FileManBaseView(
|
||||||
@ -43,11 +49,13 @@ public:
|
|||||||
std::string filter
|
std::string filter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
virtual ~FileManBaseView() { }
|
||||||
|
|
||||||
void focus() override;
|
void focus() override;
|
||||||
std::string title() const override { return "Fileman"; };
|
std::string title() const override { return "Fileman"; };
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static constexpr size_t max_filename_length = 50;
|
static constexpr size_t max_filename_length = 64;
|
||||||
|
|
||||||
struct file_assoc_t {
|
struct file_assoc_t {
|
||||||
std::filesystem::path extension;
|
std::filesystem::path extension;
|
||||||
@ -65,7 +73,6 @@ protected:
|
|||||||
{ u"", &bitmap_icon_file, ui::Color::light_grey() } // NB: Must be last.
|
{ u"", &bitmap_icon_file, ui::Color::light_grey() } // NB: Must be last.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
std::filesystem::path get_selected_full_path() const;
|
std::filesystem::path get_selected_full_path() const;
|
||||||
const fileman_entry& get_selected_entry() const;
|
const fileman_entry& get_selected_entry() const;
|
||||||
|
|
||||||
@ -78,7 +85,7 @@ protected:
|
|||||||
|
|
||||||
NavigationView& nav_;
|
NavigationView& nav_;
|
||||||
|
|
||||||
bool empty_root { false };
|
EmptyReason empty_ { EmptyReason::NotEmpty };
|
||||||
std::function<void(KeyEvent)> on_select_entry { nullptr };
|
std::function<void(KeyEvent)> on_select_entry { nullptr };
|
||||||
std::function<void(bool)> on_refresh_widgets { nullptr };
|
std::function<void(bool)> on_refresh_widgets { nullptr };
|
||||||
|
|
||||||
@ -104,57 +111,96 @@ protected:
|
|||||||
};
|
};
|
||||||
|
|
||||||
Button button_exit {
|
Button button_exit {
|
||||||
{ 16 * 8, 34 * 8, 14 * 8, 32 },
|
{ 21 * 8, 34 * 8, 9 * 8, 32 },
|
||||||
"Exit"
|
"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 {
|
class FileLoadView : public FileManBaseView {
|
||||||
public:
|
public:
|
||||||
std::function<void(std::filesystem::path)> on_changed { };
|
std::function<void(std::filesystem::path)> on_changed { };
|
||||||
|
|
||||||
FileLoadView(NavigationView& nav, std::string filter);
|
FileLoadView(NavigationView& nav, std::string filter);
|
||||||
|
virtual ~FileLoadView() { }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh_widgets(const bool v);
|
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<void(std::filesystem::path)> 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 {
|
class FileManagerView : public FileManBaseView {
|
||||||
public:
|
public:
|
||||||
FileManagerView(NavigationView& nav);
|
FileManagerView(NavigationView& nav);
|
||||||
~FileManagerView();
|
virtual ~FileManagerView() { }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Passed by ref to other views needing lifetime extension.
|
// Passed by ref to other views needing lifetime extension.
|
||||||
std::string name_buffer { };
|
std::string name_buffer { };
|
||||||
|
std::filesystem::path copy_path { };
|
||||||
|
|
||||||
void refresh_widgets(const bool v);
|
void refresh_widgets(const bool v);
|
||||||
void on_rename();
|
void on_rename();
|
||||||
void on_delete();
|
void on_delete();
|
||||||
void on_new_dir();
|
void on_new_dir();
|
||||||
|
void on_paste();
|
||||||
|
|
||||||
// True if the selected entry is a real file item.
|
// True if the selected entry is a real file item.
|
||||||
bool selected_is_valid() const;
|
bool selected_is_valid() const;
|
||||||
@ -169,19 +215,29 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
Button button_rename {
|
Button button_rename {
|
||||||
{ 0 * 8, 29 * 8, 14 * 8, 32 },
|
{ 0 * 8, 29 * 8, 9 * 8, 32 },
|
||||||
"Rename"
|
"Rename"
|
||||||
};
|
};
|
||||||
|
|
||||||
Button button_delete {
|
Button button_delete {
|
||||||
{ 16 * 8, 29 * 8, 14 * 8, 32 },
|
{ 21 * 4, 29 * 8, 9 * 8, 32 },
|
||||||
"Delete"
|
"Delete"
|
||||||
};
|
};
|
||||||
|
|
||||||
Button button_new_dir {
|
Button button_new_dir {
|
||||||
{ 0 * 8, 34 * 8, 14 * 8, 32 },
|
{ 21 * 8, 29 * 8, 9 * 8, 32 },
|
||||||
"New Dir"
|
"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 */
|
} /* namespace ui */
|
||||||
|
@ -205,6 +205,35 @@ uint32_t rename_file(const std::filesystem::path& file_path, const std::filesyst
|
|||||||
return f_rename(reinterpret_cast<const TCHAR*>(file_path.c_str()), reinterpret_cast<const TCHAR*>(new_name.c_str()));
|
return f_rename(reinterpret_cast<const TCHAR*>(file_path.c_str()), reinterpret_cast<const TCHAR*>(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) {
|
FATTimestamp file_created_date(const std::filesystem::path& file_path) {
|
||||||
FILINFO filinfo;
|
FILINFO filinfo;
|
||||||
|
|
||||||
|
@ -254,6 +254,7 @@ struct FATTimestamp {
|
|||||||
|
|
||||||
uint32_t delete_file(const std::filesystem::path& file_path);
|
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);
|
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);
|
FATTimestamp file_created_date(const std::filesystem::path& file_path);
|
||||||
uint32_t make_new_directory(const std::filesystem::path& dir_path);
|
uint32_t make_new_directory(const std::filesystem::path& dir_path);
|
||||||
|
|
||||||
|
@ -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<int>(length), 16 } } },
|
|
||||||
text_{ str },
|
|
||||||
max_length_{ std::max<size_t>(max_length, str.length()) },
|
|
||||||
char_count_{ std::max<uint32_t>(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<size_t>(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<int>(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<size_t>(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 ***********************************************************/
|
/* TextEntryView ***********************************************************/
|
||||||
|
|
||||||
void TextEntryView::char_delete() {
|
void TextEntryView::char_delete() {
|
||||||
|
@ -28,52 +28,6 @@
|
|||||||
|
|
||||||
namespace ui {
|
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 {
|
class TextEntryView : public View {
|
||||||
public:
|
public:
|
||||||
std::function<void(std::string&)> on_changed { };
|
std::function<void(std::string&)> on_changed { };
|
||||||
|
@ -135,7 +135,7 @@ namespace ui
|
|||||||
Color::dark_grey()};
|
Color::dark_grey()};
|
||||||
|
|
||||||
ImageButton button_back{
|
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,
|
&bitmap_icon_previous,
|
||||||
Color::white(),
|
Color::white(),
|
||||||
Color::dark_grey()};
|
Color::dark_grey()};
|
||||||
|
@ -1535,6 +1535,137 @@ bool OptionsField::on_touch(const TouchEvent event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TextField ***********************************************************/
|
||||||
|
|
||||||
|
TextField::TextField(
|
||||||
|
std::string& str,
|
||||||
|
size_t max_length,
|
||||||
|
Point position,
|
||||||
|
uint32_t length
|
||||||
|
) : Widget{ { position, { 8 * static_cast<int>(length), 16 } } },
|
||||||
|
text_{ str },
|
||||||
|
max_length_{ std::max<size_t>(max_length, str.length()) },
|
||||||
|
char_count_{ std::max<uint32_t>(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<size_t>(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<int>(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<size_t>(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::NumberField(
|
NumberField::NumberField(
|
||||||
|
@ -414,7 +414,6 @@ private:
|
|||||||
bool instant_exec_ { false };
|
bool instant_exec_ { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class ButtonWithEncoder : public Widget {
|
class ButtonWithEncoder : public Widget {
|
||||||
public:
|
public:
|
||||||
std::function<void(ButtonWithEncoder&)> on_select { };
|
std::function<void(ButtonWithEncoder&)> on_select { };
|
||||||
@ -457,8 +456,6 @@ private:
|
|||||||
bool instant_exec_ { false };
|
bool instant_exec_ { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NewButton : public Widget {
|
class NewButton : public Widget {
|
||||||
public:
|
public:
|
||||||
std::function<void(void)> on_select { };
|
std::function<void(void)> on_select { };
|
||||||
@ -610,6 +607,52 @@ private:
|
|||||||
size_t selected_index_ { 0 };
|
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 {
|
class NumberField : public Widget {
|
||||||
public:
|
public:
|
||||||
std::function<void(NumberField&)> on_select { };
|
std::function<void(NumberField&)> on_select { };
|
||||||
|
Loading…
Reference in New Issue
Block a user