diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index e8de6eaa..95035c7b 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -210,6 +210,7 @@ set(CPPSRC ui/ui_alphanum.cpp ui/ui_audio.cpp ui/ui_channel.cpp + ui/ui_font_fixed_5x8.cpp ui/ui_font_fixed_8x16.cpp ui/ui_geomap.cpp ui/ui_qrcode.cpp @@ -259,6 +260,7 @@ set(CPPSRC apps/ui_sonde.cpp apps/ui_sstvtx.cpp # apps/ui_test.cpp + apps/ui_text_editor.cpp apps/ui_tone_search.cpp apps/ui_touch_calibration.cpp apps/ui_touchtunes.cpp diff --git a/firmware/application/apps/capture_app.hpp b/firmware/application/apps/capture_app.hpp index 93acffe8..8dab7dff 100644 --- a/firmware/application/apps/capture_app.hpp +++ b/firmware/application/apps/capture_app.hpp @@ -44,7 +44,7 @@ {"1750k", 1750000}, \ {"2000k", 2000000}, \ {"2500k", 2500000}, \ - { "2750k", 2750000 } // That is our max Capture option , to keep using later / 8 decimation (22Mhz sampling ADC) + {"2750k", 2750000}, // That is our max Capture option , to keep using later / 8 decimation (22Mhz sampling ADC) namespace ui { diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 516ec637..ca0c2698 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -76,9 +76,7 @@ void RecentEntriesTable::draw( } void ADSBLogger::log_str(std::string& logline) { - rtc::RTC datetime; - rtcGetTime(&RTCD1, &datetime); - log_file.write_entry(datetime, logline); + log_file.write_entry(logline); } // Aircraft Details diff --git a/firmware/application/apps/ui_afsk_rx.cpp b/firmware/application/apps/ui_afsk_rx.cpp index 95011cec..c94768f0 100644 --- a/firmware/application/apps/ui_afsk_rx.cpp +++ b/firmware/application/apps/ui_afsk_rx.cpp @@ -34,10 +34,7 @@ using namespace portapack; using namespace modems; void AFSKLogger::log_raw_data(const std::string& data) { - rtc::RTC datetime; - rtcGetTime(&RTCD1, &datetime); - - log_file.write_entry(datetime, data); + log_file.write_entry(data); } namespace ui { diff --git a/firmware/application/apps/ui_aprs_rx.cpp b/firmware/application/apps/ui_aprs_rx.cpp index db40066d..1dd4a017 100644 --- a/firmware/application/apps/ui_aprs_rx.cpp +++ b/firmware/application/apps/ui_aprs_rx.cpp @@ -31,10 +31,7 @@ using namespace portapack; void APRSLogger::log_raw_data(const std::string& data) { - rtc::RTC datetime; - rtcGetTime(&RTCD1, &datetime); - - log_file.write_entry(datetime, data); + log_file.write_entry(data); } namespace ui { diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index bbfee137..ade8e9ab 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -45,22 +45,6 @@ std::string truncate(const fs::path& path, size_t max_length) { return ::truncate(path.string(), max_length); } -// Gets a human readable file size string. -std::string get_pretty_size(uint32_t file_size) { - static const std::string suffix[5] = {"B", "kB", "MB", "GB", "??"}; - size_t suffix_index = 0; - - while (file_size >= 1024) { - file_size /= 1024; - suffix_index++; - } - - if (suffix_index > 4) - suffix_index = 4; - - return to_string_dec_uint(file_size) + suffix[suffix_index]; -} - // Case insensitive path equality on underlying "native" string. bool iequal( const fs::path& lhs, @@ -272,25 +256,27 @@ void FileManBaseView::refresh_list() { auto entry_name = truncate(entry.path, 20); if (entry.is_directory) { - menu_view.add_item({entry_name, - ui::Color::yellow(), - &bitmap_icon_dir, - [this](KeyEvent key) { - if (on_select_entry) - on_select_entry(key); - }}); + menu_view.add_item( + {entry_name, + ui::Color::yellow(), + &bitmap_icon_dir, + [this](KeyEvent key) { + if (on_select_entry) + on_select_entry(key); + }}); } else { const auto& assoc = get_assoc(entry.path.extension()); - auto size_str = get_pretty_size(entry.size); + auto size_str = to_string_file_size(entry.size); - menu_view.add_item({entry_name + std::string(21 - entry_name.length(), ' ') + size_str, - assoc.color, - assoc.icon, - [this](KeyEvent key) { - if (on_select_entry) - on_select_entry(key); - }}); + menu_view.add_item( + {entry_name + std::string(21 - entry_name.length(), ' ') + size_str, + assoc.color, + assoc.icon, + [this](KeyEvent key) { + if (on_select_entry) + on_select_entry(key); + }}); } // HACK: Should page menu items instead of limiting the number. diff --git a/firmware/application/apps/ui_morse.cpp b/firmware/application/apps/ui_morse.cpp index bc7b1212..c0f3dc70 100644 --- a/firmware/application/apps/ui_morse.cpp +++ b/firmware/application/apps/ui_morse.cpp @@ -118,7 +118,7 @@ bool MorseView::start_tx() { update_tx_duration(); if (!symbol_count) { - nav_.display_modal("Error", "Message too long,\nmust be < 256 symbols.", INFO, nullptr); + nav_.display_modal("Error", "Message too long,\nmust be < 256 symbols."); return false; } diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp new file mode 100644 index 00000000..7e291c82 --- /dev/null +++ b/firmware/application/apps/ui_text_editor.cpp @@ -0,0 +1,322 @@ +/* + * 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_fileman.hpp" +#include "ui_text_editor.hpp" +#include "string_format.hpp" + +using namespace portapack; +namespace fs = std::filesystem; + +namespace { +template +T mid(const T& val1, const T& val2, const T& val3) { + return std::max(val1, std::min(val2, val3)); +} +} // namespace + +namespace ui { + +TextEditorView::TextEditorView(NavigationView& nav) + : nav_{nav} { + add_children( + {&button_open, + &text_position}); + set_focusable(true); + + // log_.append("LOGS/NOTEPAD.TXT"); + + button_open.on_select = [this](Button&) { + auto open_view = nav_.push(".TXT"); + open_view->on_changed = [this](std::filesystem::path path) { + open_file(path); + }; + }; +} + +void TextEditorView::on_focus() { + refresh_ui(); +} + +void TextEditorView::paint(Painter& painter) { + auto first_line = paint_state_.first_line; + auto first_col = paint_state_.first_col; + + if (!paint_state_.has_file) + return; + + if (cursor_.line < first_line) + first_line = cursor_.line; + else if (cursor_.line >= first_line + max_line) + first_line = cursor_.line - max_line + 1; + + if (cursor_.col < first_col) + first_col = cursor_.col; + if (cursor_.col >= first_col + max_col) + first_col = cursor_.col - max_col + 1; + + if (first_line != paint_state_.first_line || + first_col != paint_state_.first_col) { + paint_state_.first_line = first_line; + paint_state_.first_col = first_col; + paint_state_.redraw_text = true; + } + + if (paint_state_.redraw_text) { + paint_text(painter, first_line, first_col); + paint_state_.redraw_text = false; + } + + paint_cursor(painter); +} + +bool TextEditorView::on_key(const KeyEvent key) { + int16_t delta_col = 0; + int16_t delta_line = 0; + + if (key == KeyEvent::Left) + delta_col = -1; + else if (key == KeyEvent::Right) + delta_col = 1; + else if (key == KeyEvent::Up) + delta_line = -1; + else if (key == KeyEvent::Down) + delta_line = 1; + /* else if (key == KeyEvent::Select) + ; // TODO: Edit/Menu */ + + // Always allow cursor direction to be updated. + cursor_.dir = delta_col != 0 ? ScrollDirection::Horizontal : ScrollDirection::Vertical; + auto updated = apply_scrolling_constraints(delta_line, delta_col); + + if (updated) + refresh_ui(); + return updated; +} + +bool TextEditorView::on_encoder(EncoderEvent delta) { + bool updated = false; + + if (cursor_.dir == ScrollDirection::Horizontal) + updated = apply_scrolling_constraints(0, delta); + else + updated = apply_scrolling_constraints(delta, 0); + + if (updated) + refresh_ui(); + + return updated; +} + +bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) { + int32_t new_line = cursor_.line + delta_line; + int32_t new_col = cursor_.col + delta_col; + + auto new_line_length = info_.line_length(new_line); + + if (new_col < 0) + --new_line; + else if (new_col > new_line_length && delta_line == 0) { + // Only want to wrap if moving horizontally. + new_col = 0; + ++new_line; + } + + if (new_line < 0 || (uint32_t)new_line >= info_.line_count()) + return false; + + new_line_length = info_.line_length(new_line); + + // TODO: don't wrap with encoder? + // Wrap or clamp column. + if (new_line_length == 0) + new_col = 0; + else if (new_col > new_line_length || new_col < 0) + new_col = new_line_length; + + cursor_.line = new_line; + cursor_.col = new_col; + + return true; +} + +void TextEditorView::refresh_ui() { + if (paint_state_.has_file) { + text_position.set( + to_string_dec_uint(cursor_.col + 1) + ":" + + to_string_dec_uint(cursor_.line + 1) + "/" + + to_string_dec_uint(info_.line_count()) + + (info_.truncated ? "*" : "") + + " Size: " + + to_string_file_size(info_.size)); + focus(); + } else { + button_open.focus(); + } + + set_dirty(); +} + +void TextEditorView::refresh_file_info() { + constexpr size_t buffer_size = 128; + char buffer[buffer_size]; + uint32_t base_offset = 0; + + file_.seek(0); + info_.newlines.clear(); + info_.line_ending = LineEnding::LF; + info_.size = file_.size(); + info_.truncated = false; + + while (true) { + auto result = file_.read(buffer, buffer_size); + if (result.is_error()) + break; // TODO: report error? + + // TODO: CRLF state machine for cross block. + for (uint32_t i = 0; i < result.value(); ++i) { + switch (buffer[i]) { + case '\n': + info_.newlines.push_back(base_offset + i); + } + } + + base_offset += result.value(); + + // Fake a newline at the end for consistency. + // Could check if there already is a trailing newline, but it doesn't hurt. + if (result.value() < buffer_size) { + info_.newlines.push_back(base_offset); + break; + } + + // HACK HACK: only show first 1000 lines for now. + if (info_.newlines.size() >= 1000) { + info_.truncated = true; + break; + } + } + + refresh_ui(); +} + +void TextEditorView::open_file(const fs::path& path) { + // TODO: need a temp backing file for edits. + + auto result = file_.open(path); + paint_state_.has_file = !result.is_valid(); /* Has an error. */ + + if (paint_state_.has_file) { + refresh_file_info(); + paint_state_.first_line = 0; + paint_state_.first_col = 0; + cursor_.line = 0; + cursor_.col = 0; + } else { + nav_.display_modal("Read Error", "Cannot open file:\n" + result.value().what()); + paint_state_.has_file = false; + } + + paint_state_.redraw_text = true; + refresh_ui(); +} + +std::string TextEditorView::read(uint32_t offset, uint32_t length) { + if (offset >= info_.size) + return {"[BAD OFFSET]"}; + + std::string buffer(length + 1, '\0'); + file_.seek(offset); + + auto result = file_.read(&buffer[0], length); + if (result.is_ok()) + /* resize? */; + else + return result.error().what(); + + return buffer; +} + +void TextEditorView::paint_text(Painter& painter, uint32_t line, uint16_t col) { + // TODO: A line cache would use more memory but save a lot of IO. + // Only the new lines/characters would need to be refetched. + + auto r = screen_rect(); + auto& lines = info_.newlines; + auto line_start = info_.line_start(line); + + // Draw the lines from the file + for (uint32_t i = 0; i < max_line && i < lines.size(); ++i) { + auto line_end = lines[line + i]; + int32_t read_length = max_col; + + // Don't read past end of the line. + if (line_start + col + (uint32_t)read_length > line_end) + read_length = line_end - col - line_start; + + if (read_length > 0) + painter.draw_string( + {0, r.location().y() + (int)i * char_height}, + style_default, read(line_start + col, read_length)); + + // Erase empty line sectons. + if (read_length >= 0) { + int32_t clear_width = max_col - read_length; + if (clear_width > 0) + painter.fill_rectangle( + {(max_col - clear_width) * char_width, + r.location().y() + (int)i * char_height, + clear_width * char_width, char_height}, + style_default.background); + } + + line_start = lines[line + i] + 1 /* newline */; + } +} + +void TextEditorView::paint_cursor(Painter& painter) { + auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) { + auto r = screen_rect(); + line = line - paint_state_.first_line; + col = col - paint_state_.first_col; + + painter.draw_rectangle( + {(int)col * char_width - 1, + r.location().y() + (int)line * char_height, + char_width + 1, char_height}, + c); + }; + + // TOOD: bug where cursor doesn't clear at EOF. + // TODO: XOR cursor? + + // Clear old cursor. + draw_cursor(paint_state_.line, paint_state_.col, style_default.background); + draw_cursor(cursor_.line, cursor_.col, style_default.foreground); + paint_state_.line = cursor_.line; + paint_state_.col = cursor_.col; +} + +uint16_t TextEditorView::line_length() const { + return info_.line_length(cursor_.line); +} + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp new file mode 100644 index 00000000..865a183a --- /dev/null +++ b/firmware/application/apps/ui_text_editor.hpp @@ -0,0 +1,170 @@ +/* + * 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. + */ + +#ifndef __UI_TEXT_EDITOR_H__ +#define __UI_TEXT_EDITOR_H__ + +#include "ui.hpp" +#include "ui_font_fixed_5x8.hpp" +#include "ui_navigation.hpp" +#include "ui_painter.hpp" +#include "ui_widget.hpp" +//#include "ui_textentry.hpp" + +#include "file.hpp" +#include "log_file.hpp" + +#include +#include + +namespace ui { + +enum class LineEnding : uint8_t { + LF, + CRLF +}; + +enum class ScrollDirection : uint8_t { + Vertical, + Horizontal +}; + +// TODO: RAM is _very_ limited. Need to +// rework this to not store every line. +// Should be able to get away with only +// abount one screen of lines so long as +// you can't scroll more than one screen +// at a time. +struct FileInfo { + /* Offsets of newlines. */ + std::vector newlines; + LineEnding line_ending; + File::Size size; + bool truncated; + + uint32_t line_count() const { + return newlines.size(); + } + + uint32_t line_start(uint32_t line) const { + return line == 0 ? 0 : (newlines[line - 1] + 1); + } + + uint16_t line_length(uint32_t line) const { + if (line >= line_count()) + return 0; + + auto start = line_start(line); + auto end = newlines[line]; + return end - start; + } +}; + +/*class TextViewer : public Widget { +};*/ + +class TextEditorView : public View { + public: + TextEditorView(NavigationView& nav); + // TextEditorView(NavigationView& nav, const std::filesystem::path& path); + + std::string title() const override { + return "Notepad"; + }; + + void on_focus() override; + void paint(Painter& painter) override; + bool on_key(KeyEvent delta) override; + bool on_encoder(EncoderEvent delta) override; + + private: + static constexpr uint8_t max_line = 32; + static constexpr uint8_t max_col = 48; + static constexpr int8_t char_width = 5; + static constexpr int8_t char_height = 8; + + // TODO: should these be common somewhere? + static constexpr Style style_default{ + .font = font::fixed_5x8, + .background = Color::black(), + .foreground = Color::white(), + }; + + /* Returns true if the cursor was updated. */ + bool apply_scrolling_constraints( + int16_t delta_line, + int16_t delta_col); + + void refresh_ui(); + void refresh_file_info(); + void open_file(const std::filesystem::path& path); + std::string read(uint32_t offset, uint32_t length = 30); + + void paint_text(Painter& painter, uint32_t line, uint16_t col); + void paint_cursor(Painter& painter); + + // Gets the length of the current line. + uint16_t line_length() const; + + NavigationView& nav_; + + File file_{}; + FileInfo info_{}; + // LogFile log_{ }; + + struct { + // Previous cursor state. + uint32_t line{}; + uint16_t col{}; + + // Previous draw state. + uint32_t first_line{}; + uint16_t first_col{}; + bool redraw_text{true}; + bool has_file{false}; + } paint_state_{}; + + struct { + uint32_t line{}; + uint16_t col{}; + ScrollDirection dir{ScrollDirection::Vertical}; + } cursor_{}; + + /* 8px grid is 30 wide, 38 tall. */ + /* 16px font height or 19 rows. */ + /* Titlebar is 16px tall, so 18 rows left. */ + /* 240 x 320, (304 with titlebar) */ + + // TODO: The scrollable view should be its own widget + // otherwise control navigation doesn't work. + + Button button_open{ + {24 * 8, 34 * 8, 6 * 8, 4 * 8}, + "Open"}; + + Text text_position{ + {0 * 8, 36 * 8, 24 * 8, 2 * 8}, + ""}; +}; + +} // namespace ui + +#endif // __UI_TEXT_EDITOR_H__ \ No newline at end of file diff --git a/firmware/application/apps/ui_view_wav.cpp b/firmware/application/apps/ui_view_wav.cpp index e6489bd2..325566e7 100644 --- a/firmware/application/apps/ui_view_wav.cpp +++ b/firmware/application/apps/ui_view_wav.cpp @@ -140,11 +140,11 @@ ViewWavView::ViewWavView( 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.", INFO, nullptr); + 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.", INFO, nullptr); + nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files."); return; } load_wav(file_path); diff --git a/firmware/application/freqman.hpp b/firmware/application/freqman.hpp index 73487c20..ebfe5d1f 100644 --- a/firmware/application/freqman.hpp +++ b/firmware/application/freqman.hpp @@ -32,7 +32,7 @@ #include "ui_widget.hpp" #define FREQMAN_DESC_MAX_LEN 24 // This is the number of characters that can be drawn in front of "R: TEXT..." before taking a full screen line -#define FREQMAN_MAX_PER_FILE 115 // Maximum of entries we can read. This is a hardware limit \ +#define FREQMAN_MAX_PER_FILE 115 // Maximum of entries we can read. This is a hardware limit // It was tested and lowered to leave a bit of space to the caller #define FREQMAN_MAX_PER_FILE_STR "115" // STRING OF FREQMAN_MAX_PER_FILE diff --git a/firmware/application/log_file.cpp b/firmware/application/log_file.cpp index 68956dae..c1126cc0 100644 --- a/firmware/application/log_file.cpp +++ b/firmware/application/log_file.cpp @@ -20,9 +20,12 @@ */ #include "log_file.hpp" - #include "string_format.hpp" +Optional LogFile::write_entry(const std::string& entry) { + return write_entry(rtc_time::now(), entry); +} + Optional LogFile::write_entry(const rtc::RTC& datetime, const std::string& entry) { std::string timestamp = to_string_timestamp(datetime); return write_line(timestamp + " " + entry); diff --git a/firmware/application/log_file.hpp b/firmware/application/log_file.hpp index 78568cea..9ec0e3fe 100644 --- a/firmware/application/log_file.hpp +++ b/firmware/application/log_file.hpp @@ -25,10 +25,7 @@ #include #include "file.hpp" - -#include "lpc43xx_cpp.hpp" - -using namespace lpc43xx; +#include "rtc_time.hpp" #define LOG_ROOT_DIR "LOGS" @@ -42,6 +39,7 @@ class LogFile { return file.append(filename); } + Optional write_entry(const std::string& entry); Optional write_entry(const rtc::RTC& datetime, const std::string& entry); private: diff --git a/firmware/application/rtc_time.cpp b/firmware/application/rtc_time.cpp index c5b803a4..37e1762b 100644 --- a/firmware/application/rtc_time.cpp +++ b/firmware/application/rtc_time.cpp @@ -29,4 +29,15 @@ void on_tick_second() { signal_tick_second.emit(); } +rtc::RTC now() { + rtc::RTC datetime; + rtcGetTime(&RTCD1, &datetime); + return datetime; +} + +rtc::RTC now(rtc::RTC& out_datetime) { + rtcGetTime(&RTCD1, &out_datetime); + return out_datetime; +} + } /* namespace rtc_time */ diff --git a/firmware/application/rtc_time.hpp b/firmware/application/rtc_time.hpp index 9a0bf568..d77a8a32 100644 --- a/firmware/application/rtc_time.hpp +++ b/firmware/application/rtc_time.hpp @@ -24,12 +24,21 @@ #include "signal.hpp" +#include "lpc43xx_cpp.hpp" +using namespace lpc43xx; + namespace rtc_time { extern Signal<> signal_tick_second; void on_tick_second(); +/* Returns the current RTCTime from the RTC. */ +rtc::RTC now(); + +/* Returns the current RTCTime from the RTC. */ +rtc::RTC now(rtc::RTC& out_datetime); + } /* namespace rtc_time */ #endif /*__RTC_TIME_H__*/ diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index 1b387b55..376b7fca 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -213,6 +213,21 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp) { to_string_dec_uint((timestamp.FAT_time >> 5) & 0x3F, 2, '0'); } +std::string to_string_file_size(uint32_t file_size) { + static const std::string suffix[5] = {"B", "kB", "MB", "GB", "??"}; + size_t suffix_index = 0; + + while (file_size >= 1024) { + file_size /= 1024; + suffix_index++; + } + + if (suffix_index > 4) + suffix_index = 4; + + 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) { const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000}; std::string string{""}; diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 43da6b3a..bc2e2fd7 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -56,6 +56,9 @@ std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = std::string to_string_timestamp(const rtc::RTC& value); 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); double get_decimals(double num, int16_t mult, bool round = false); // euquiq added diff --git a/firmware/application/ui/ui_font_fixed_5x8.cpp b/firmware/application/ui/ui_font_fixed_5x8.cpp new file mode 100644 index 00000000..22fc5aa0 --- /dev/null +++ b/firmware/application/ui/ui_font_fixed_5x8.cpp @@ -0,0 +1,710 @@ +/* + * 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_font_fixed_5x8.hpp" + +#include + +namespace ui { +namespace font { + +namespace { + +const uint8_t fixed_5x8_glyph_data[] = { + + // Index: 0 (0x00) Char: 0x0020 (' ') + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + // Index: 1 (0x01) Char: 0x0021 ('!') + 0x80, + 0x10, + 0x42, + 0x00, + 0x01, + + // Index: 2 (0x02) Char: 0x0022 ('"') + 0x40, + 0x29, + 0x00, + 0x00, + 0x00, + + // Index: 3 (0x03) Char: 0x0023 ('#') + 0x00, + 0xA8, + 0xA7, + 0x9E, + 0x02, + + // Index: 4 (0x04) Char: 0x0024 ('$') + 0x80, + 0x38, + 0x83, + 0x1C, + 0x01, + + // Index: 5 (0x05) Char: 0x0025 ('%') + 0x00, + 0x80, + 0x44, + 0x44, + 0x02, + + // Index: 6 (0x06) Char: 0x0026 ('&') + 0x40, + 0x94, + 0xA1, + 0xCA, + 0x02, + + // Index: 7 (0x07) Char: 0x0027 (''') + 0x80, + 0x10, + 0x00, + 0x00, + 0x00, + + // Index: 8 (0x08) Char: 0x0028 ('(') + 0x80, + 0x08, + 0x21, + 0x04, + 0x01, + + // Index: 9 (0x09) Char: 0x0029 (')') + 0x40, + 0x10, + 0x42, + 0x88, + 0x00, + + // Index: 10 (0x0A) Char: 0x002A ('*') + 0x00, + 0x28, + 0xE2, + 0x88, + 0x02, + + // Index: 11 (0x0B) Char: 0x002B ('+') + 0x00, + 0x00, + 0xE2, + 0x08, + 0x00, + + // Index: 12 (0x0C) Char: 0x002C (',') + 0x00, + 0x00, + 0x00, + 0x88, + 0x00, + + // Index: 13 (0x0D) Char: 0x002D ('-') + 0x00, + 0x00, + 0xE0, + 0x00, + 0x00, + + // Index: 14 (0x0E) Char: 0x002E ('.') + 0x00, + 0x00, + 0x00, + 0x80, + 0x00, + + // Index: 15 (0x0F) Char: 0x002F ('/') + 0x00, + 0x21, + 0x42, + 0x84, + 0x00, + + // Index: 16 (0x10) Char: 0x0030 ('0') + 0xC0, + 0xA4, + 0xB6, + 0x92, + 0x01, + + // Index: 17 (0x11) Char: 0x0031 ('1') + 0x80, + 0x18, + 0x42, + 0x88, + 0x03, + + // Index: 18 (0x12) Char: 0x0032 ('2') + 0xC0, + 0x24, + 0x64, + 0xC2, + 0x03, + + // Index: 19 (0x13) Char: 0x0033 ('3') + 0xC0, + 0x24, + 0x82, + 0x92, + 0x01, + + // Index: 20 (0x14) Char: 0x0034 ('4') + 0x00, + 0x31, + 0x95, + 0x1E, + 0x02, + + // Index: 21 (0x15) Char: 0x0035 ('5') + 0xE0, + 0x85, + 0x83, + 0x92, + 0x01, + + // Index: 22 (0x16) Char: 0x0036 ('6') + 0xC0, + 0x84, + 0x93, + 0x92, + 0x01, + + // Index: 23 (0x17) Char: 0x0037 ('7') + 0xE0, + 0x21, + 0x44, + 0x84, + 0x00, + + // Index: 24 (0x18) Char: 0x0038 ('8') + 0xC0, + 0x24, + 0x93, + 0x92, + 0x01, + + // Index: 25 (0x19) Char: 0x0039 ('9') + 0xC0, + 0xA4, + 0xE4, + 0x90, + 0x01, + + // Index: 26 (0x1A) Char: 0x003A (':') + 0x00, + 0x00, + 0x01, + 0x04, + 0x00, + + // Index: 27 (0x1B) Char: 0x003B (';') + 0x00, + 0x00, + 0x02, + 0x88, + 0x00, + + // Index: 28 (0x1C) Char: 0x003C ('<') + 0x00, + 0x10, + 0x11, + 0x04, + 0x01, + + // Index: 29 (0x1D) Char: 0x003D ('=') + 0x00, + 0x00, + 0x07, + 0x1C, + 0x00, + + // Index: 30 (0x1E) Char: 0x003E ('>') + 0x00, + 0x08, + 0x82, + 0x88, + 0x00, + + // Index: 31 (0x1F) Char: 0x003F ('?') + 0x80, + 0x28, + 0x44, + 0x00, + 0x01, + + // Index: 32 (0x20) Char: 0x0040 ('@') + 0xC0, + 0xA4, + 0xD6, + 0x82, + 0x01, + + // Index: 33 (0x21) Char: 0x0041 ('A') + 0xC0, + 0xA4, + 0xF4, + 0x52, + 0x02, + + // Index: 34 (0x22) Char: 0x0042 ('B') + 0xE0, + 0xA4, + 0x93, + 0xD2, + 0x01, + + // Index: 35 (0x23) Char: 0x0043 ('C') + 0xC0, + 0xA4, + 0x10, + 0x92, + 0x01, + + // Index: 36 (0x24) Char: 0x0044 ('D') + 0xE0, + 0xA4, + 0x94, + 0xD2, + 0x01, + + // Index: 37 (0x25) Char: 0x0045 ('E') + 0xE0, + 0x85, + 0x13, + 0xC2, + 0x03, + + // Index: 38 (0x26) Char: 0x0046 ('F') + 0xE0, + 0x85, + 0x13, + 0x42, + 0x00, + + // Index: 39 (0x27) Char: 0x0047 ('G') + 0xC0, + 0xA4, + 0xD0, + 0x92, + 0x01, + + // Index: 40 (0x28) Char: 0x0048 ('H') + 0x20, + 0xA5, + 0x97, + 0x52, + 0x02, + + // Index: 41 (0x29) Char: 0x0049 ('I') + 0xC0, + 0x11, + 0x42, + 0x88, + 0x03, + + // Index: 42 (0x2A) Char: 0x004A ('J') + 0xC0, + 0x21, + 0x84, + 0x92, + 0x01, + + // Index: 43 (0x2B) Char: 0x004B ('K') + 0x20, + 0x95, + 0x51, + 0x52, + 0x02, + + // Index: 44 (0x2C) Char: 0x004C ('L') + 0x20, + 0x84, + 0x10, + 0xC2, + 0x01, + + // Index: 45 (0x2D) Char: 0x004D ('M') + 0xA0, + 0xBD, + 0x95, + 0x52, + 0x02, + + // Index: 46 (0x2E) Char: 0x004E ('N') + 0x20, + 0xA5, + 0xD5, + 0x52, + 0x02, + + // Index: 47 (0x2F) Char: 0x004F ('O') + 0xC0, + 0xA4, + 0x94, + 0x92, + 0x01, + + // Index: 48 (0x30) Char: 0x0050 ('P') + 0xE0, + 0xA4, + 0x74, + 0x42, + 0x00, + + // Index: 49 (0x31) Char: 0x0051 ('Q') + 0xC0, + 0xA4, + 0x94, + 0x8A, + 0x02, + + // Index: 50 (0x32) Char: 0x0052 ('R') + 0xE0, + 0xA4, + 0x74, + 0x52, + 0x02, + + // Index: 51 (0x33) Char: 0x0053 ('S') + 0xC0, + 0x24, + 0x41, + 0x92, + 0x01, + + // Index: 52 (0x34) Char: 0x0054 ('T') + 0xC0, + 0x11, + 0x42, + 0x08, + 0x01, + + // Index: 53 (0x35) Char: 0x0055 ('U') + 0x20, + 0xA5, + 0x94, + 0x92, + 0x03, + + // Index: 54 (0x36) Char: 0x0056 ('V') + 0x20, + 0xA5, + 0x94, + 0x8A, + 0x00, + + // Index: 55 (0x37) Char: 0x0057 ('W') + 0x20, + 0xA5, + 0xB4, + 0x5E, + 0x01, + + // Index: 56 (0x38) Char: 0x0058 ('X') + 0x20, + 0x25, + 0x63, + 0x52, + 0x02, + + // Index: 57 (0x39) Char: 0x0059 ('Y') + 0x20, + 0xA5, + 0x64, + 0x08, + 0x01, + + // Index: 58 (0x3A) Char: 0x005A ('Z') + 0xE0, + 0x21, + 0x22, + 0xC2, + 0x03, + + // Index: 59 (0x3B) Char: 0x005B ('[') + 0xC0, + 0x08, + 0x21, + 0x84, + 0x01, + + // Index: 60 (0x3C) Char: 0x005C ('\') + 0x40, + 0x08, + 0x42, + 0x10, + 0x02, + + // Index: 61 (0x3D) Char: 0x005D (']') + 0xC0, + 0x10, + 0x42, + 0x88, + 0x01, + + // Index: 62 (0x3E) Char: 0x005E ('^') + 0x00, + 0x10, + 0x05, + 0x00, + 0x00, + + // Index: 63 (0x3F) Char: 0x005F ('_') + 0x00, + 0x00, + 0x00, + 0xC0, + 0x03, + + // Index: 64 (0x40) Char: 0x0060 ('`') + 0x40, + 0x10, + 0x00, + 0x00, + 0x00, + + // Index: 65 (0x41) Char: 0x0061 ('a') + 0x00, + 0x00, + 0x83, + 0x9E, + 0x03, + + // Index: 66 (0x42) Char: 0x0062 ('b') + 0x20, + 0x84, + 0x93, + 0x92, + 0x01, + + // Index: 67 (0x43) Char: 0x0063 ('c') + 0x00, + 0x00, + 0x17, + 0x82, + 0x03, + + // Index: 68 (0x44) Char: 0x0064 ('d') + 0x00, + 0x21, + 0x97, + 0x92, + 0x01, + + // Index: 69 (0x45) Char: 0x0065 ('e') + 0x00, + 0x00, + 0xF3, + 0x82, + 0x01, + + // Index: 70 (0x46) Char: 0x0066 ('f') + 0x80, + 0x89, + 0x23, + 0x84, + 0x00, + + // Index: 71 (0x47) Char: 0x0067 ('g') + 0x00, + 0x98, + 0xE4, + 0x90, + 0x01, + + // Index: 72 (0x48) Char: 0x0068 ('h') + 0x20, + 0x84, + 0x93, + 0x52, + 0x02, + + // Index: 73 (0x49) Char: 0x0069 ('i') + 0x80, + 0x00, + 0x43, + 0x08, + 0x02, + + // Index: 74 (0x4A) Char: 0x006A ('j') + 0x00, + 0x01, + 0x86, + 0x90, + 0x01, + + // Index: 75 (0x4B) Char: 0x006B ('k') + 0x40, + 0x08, + 0x65, + 0x94, + 0x02, + + // Index: 76 (0x4C) Char: 0x006C ('l') + 0xC0, + 0x10, + 0x42, + 0x08, + 0x02, + + // Index: 77 (0x4D) Char: 0x006D ('m') + 0x00, + 0x80, + 0xF6, + 0x56, + 0x02, + + // Index: 78 (0x4E) Char: 0x006E ('n') + 0x00, + 0x80, + 0x93, + 0x52, + 0x02, + + // Index: 79 (0x4F) Char: 0x006F ('o') + 0x00, + 0x00, + 0x93, + 0x92, + 0x01, + + // Index: 80 (0x50) Char: 0x0070 ('p') + 0x00, + 0x98, + 0x74, + 0x42, + 0x00, + + // Index: 81 (0x51) Char: 0x0071 ('q') + 0x00, + 0x98, + 0xE4, + 0x10, + 0x02, + + // Index: 82 (0x52) Char: 0x0072 ('r') + 0x00, + 0x00, + 0x65, + 0x84, + 0x00, + + // Index: 83 (0x53) Char: 0x0073 ('s') + 0x00, + 0x00, + 0x66, + 0x90, + 0x03, + + // Index: 84 (0x54) Char: 0x0074 ('t') + 0x80, + 0x10, + 0x47, + 0x08, + 0x02, + + // Index: 85 (0x55) Char: 0x0075 ('u') + 0x00, + 0x80, + 0x94, + 0x92, + 0x03, + + // Index: 86 (0x56) Char: 0x0076 ('v') + 0x00, + 0x80, + 0x94, + 0x8A, + 0x00, + + // Index: 87 (0x57) Char: 0x0077 ('w') + 0x00, + 0x80, + 0xB4, + 0x5E, + 0x01, + + // Index: 88 (0x58) Char: 0x0078 ('x') + 0x00, + 0x00, + 0x45, + 0x88, + 0x02, + + // Index: 89 (0x59) Char: 0x0079 ('y') + 0x00, + 0x00, + 0xE5, + 0x90, + 0x01, + + // Index: 90 (0x5A) Char: 0x007A ('z') + 0x00, + 0x00, + 0xC7, + 0x84, + 0x03, + + // Index: 91 (0x5B) Char: 0x007B ('{') + 0x80, + 0x11, + 0x41, + 0x08, + 0x03, + + // Index: 92 (0x5C) Char: 0x007C ('|') + 0x80, + 0x10, + 0x42, + 0x08, + 0x01, + + // Index: 93 (0x5D) Char: 0x007D ('}') + 0xC0, + 0x10, + 0x44, + 0x88, + 0x01, + + // Index: 94 (0x5E) Char: 0x007E ('~') + 0x00, + 0x00, + 0x55, + 0x00, + 0x00, + +}; + +} + +const ui::Font fixed_5x8{ + 5, + 8, + fixed_5x8_glyph_data, + 0x20, + 95, +}; + +} /* namespace font */ +} /* namespace ui */ diff --git a/firmware/application/ui/ui_font_fixed_5x8.hpp b/firmware/application/ui/ui_font_fixed_5x8.hpp new file mode 100644 index 00000000..afda4ac6 --- /dev/null +++ b/firmware/application/ui/ui_font_fixed_5x8.hpp @@ -0,0 +1,35 @@ +/* + * 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. + */ + +#ifndef __UI_FONT_FIXED_5X8_H__ +#define __UI_FONT_FIXED_5X8_H__ + +#include "ui_text.hpp" + +namespace ui { +namespace font { + +extern const ui::Font fixed_5x8; + +} // namespace font +} // namespace ui + +#endif /*__UI_FONT_FIXED_5X8_H__*/ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index b3ed39fc..09b30887 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -66,6 +66,7 @@ #include "ui_sonde.hpp" #include "ui_sstvtx.hpp" //#include "ui_test.hpp" +#include "ui_text_editor.hpp" #include "ui_tone_search.hpp" #include "ui_touchtunes.hpp" #include "ui_playlist.hpp" @@ -578,7 +579,7 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) { //{ "Test app", ui::Color::dark_grey(), nullptr, [&nav](){ nav.push(); } }, {"Freq. manager", ui::Color::green(), &bitmap_icon_freqman, [&nav]() { nav.push(); }}, {"File manager", ui::Color::yellow(), &bitmap_icon_dir, [&nav]() { nav.push(); }}, - //{ "Notepad", ui::Color::dark_grey(), &bitmap_icon_notepad, [&nav](){ nav.push(); } }, + {"Notepad", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push(); }}, {"Signal gen", ui::Color::green(), &bitmap_icon_cwgen, [&nav]() { nav.push(); }}, //{ "Tone search", ui::Color::dark_grey(), nullptr, [&nav](){ nav.push(); } }, {"Wav viewer", ui::Color::yellow(), &bitmap_icon_soundboard, [&nav]() { nav.push(); }}, @@ -783,8 +784,11 @@ ModalMessageView::ModalMessageView( if (type == INFO) { add_child(&button_ok); - button_ok.on_select = [&nav](Button&) { - nav.pop(); + button_ok.on_select = [this, &nav](Button&) { + if (on_choice_) + on_choice_(true); // Assumes handler will pop. + else + nav.pop(); }; } else if (type == YESNO) { add_children({&button_yes, diff --git a/firmware/common/optional.hpp b/firmware/common/optional.hpp index efe03c9d..c5561b3c 100644 --- a/firmware/common/optional.hpp +++ b/firmware/common/optional.hpp @@ -35,7 +35,7 @@ class Optional { : value_{std::move(value)}, valid_{true} {}; bool is_valid() const { return valid_; }; - T value() const { return value_; }; + const T& value() const { return value_; }; private: T value_;