mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 10:39:30 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e50d8dc148 | |||
| 23c24355ab | |||
| 0dcf2af50d | |||
| cf07428a5d | |||
| 35b381ae77 | |||
| 0fc748a234 | |||
| 638ea78db9 | |||
| e81747ef7b | |||
| 89e24cb358 | |||
| 00667cecf9 | |||
| 53fcdedb88 | |||
| 5bdf9363e9 | |||
| 6b44a77ef6 |
@@ -218,6 +218,7 @@ set(CPPSRC
|
||||
ui/ui_btngrid.cpp
|
||||
ui/ui_receiver.cpp
|
||||
ui/ui_rssi.cpp
|
||||
ui/ui_freqlist.cpp
|
||||
ui/ui_tv.cpp
|
||||
ui/ui_spectrum.cpp
|
||||
ui/ui_tabview.cpp
|
||||
|
||||
@@ -52,7 +52,7 @@ class AMOptionsView : public View {
|
||||
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
5,
|
||||
6, // number of blanking characters
|
||||
{
|
||||
// using common messages from freqman.cpp
|
||||
}};
|
||||
|
||||
@@ -97,8 +97,10 @@ void FreqManBaseView::change_category(int32_t category_id) {
|
||||
|
||||
if (!load_freqman_file(file_list[categories[current_category_id].second], database))
|
||||
error_ = ERROR_ACCESS;
|
||||
else
|
||||
else {
|
||||
menu_view.set_db(database);
|
||||
refresh_list();
|
||||
}
|
||||
}
|
||||
|
||||
void FreqManBaseView::refresh_list() {
|
||||
@@ -108,20 +110,6 @@ void FreqManBaseView::refresh_list() {
|
||||
} else {
|
||||
if (on_refresh_widgets)
|
||||
on_refresh_widgets(false);
|
||||
|
||||
menu_view.clear();
|
||||
|
||||
for (size_t n = 0; n < database.size(); n++) {
|
||||
menu_view.add_item({freqman_item_string(database[n], 30),
|
||||
ui::Color::white(),
|
||||
nullptr,
|
||||
[this](KeyEvent) {
|
||||
if (on_select_frequency)
|
||||
on_select_frequency();
|
||||
}});
|
||||
}
|
||||
|
||||
menu_view.set_highlighted(0); // Refresh
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,18 +194,13 @@ FrequencyLoadView::FrequencyLoadView(
|
||||
// Resize menu view to fill screen
|
||||
menu_view.set_parent_rect({0, 3 * 8, 240, 30 * 8});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
|
||||
on_select_frequency = [&nav, this]() {
|
||||
menu_view.on_select = [&nav, this](FreqManUIList&) {
|
||||
nav_.pop();
|
||||
|
||||
auto& entry = database[menu_view.highlighted_index()];
|
||||
auto& entry = database[menu_view.get_index()];
|
||||
|
||||
if (entry.type == RANGE) {
|
||||
// User chose a frequency range entry
|
||||
@@ -235,14 +218,14 @@ FrequencyLoadView::FrequencyLoadView(
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_freq(rf::Frequency f) {
|
||||
database[menu_view.highlighted_index()].frequency_a = f;
|
||||
database[menu_view.get_index()].frequency_a = f;
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_edit_desc(NavigationView& nav) {
|
||||
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) {
|
||||
database[menu_view.highlighted_index()].description = buffer;
|
||||
database[menu_view.get_index()].description = buffer;
|
||||
refresh_list();
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
});
|
||||
@@ -258,7 +241,7 @@ void FrequencyManagerView::on_new_category(NavigationView& nav) {
|
||||
}
|
||||
|
||||
void FrequencyManagerView::on_delete() {
|
||||
database.erase(database.begin() + menu_view.highlighted_index());
|
||||
database.erase(database.begin() + menu_view.get_index());
|
||||
save_freqman_file(file_list[categories[current_category_id].second], database);
|
||||
refresh_list();
|
||||
}
|
||||
@@ -292,15 +275,10 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
&button_edit_desc,
|
||||
&button_delete});
|
||||
|
||||
// Just to allow exit on left
|
||||
menu_view.on_left = [&nav, this]() {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
change_category(last_category_id);
|
||||
refresh_list();
|
||||
|
||||
on_select_frequency = [this]() {
|
||||
menu_view.on_select = [this](FreqManUIList&) {
|
||||
button_edit_freq.focus();
|
||||
};
|
||||
|
||||
@@ -310,14 +288,14 @@ FrequencyManagerView::FrequencyManagerView(
|
||||
};
|
||||
|
||||
button_edit_freq.on_select = [this, &nav](Button&) {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(database[menu_view.highlighted_index()].frequency_a);
|
||||
auto new_view = nav.push<FrequencyKeypadView>(database[menu_view.get_index()].frequency_a);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
on_edit_freq(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_edit_desc.on_select = [this, &nav](Button&) {
|
||||
desc_buffer = database[menu_view.highlighted_index()].description;
|
||||
desc_buffer = database[menu_view.get_index()].description;
|
||||
on_edit_desc(nav);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include "ui_freqlist.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -65,9 +66,9 @@ class FreqManBaseView : public View {
|
||||
14,
|
||||
{}};
|
||||
|
||||
MenuView menu_view{
|
||||
{0, 3 * 8, 240, 23 * 8},
|
||||
true};
|
||||
FreqManUIList menu_view{
|
||||
{0, 3 * 8, 240, 23 * 8}};
|
||||
|
||||
Text text_empty{
|
||||
{7 * 8, 12 * 8, 16 * 8, 16},
|
||||
"Empty category !",
|
||||
|
||||
@@ -70,7 +70,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
marker = f_min + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
marker = f_min + (8 * each_bin_size) + (marker_pixel_index * (looking_glass_range - 16 * each_bin_size)) / SCREEN_W;
|
||||
} else // if( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
marker = f_min + (offset * each_bin_size) + (marker_pixel_index * looking_glass_range) / SCREEN_W;
|
||||
@@ -234,7 +234,6 @@ void GlassView::on_range_changed() {
|
||||
} else {
|
||||
mode = scan_type.selected_index_value();
|
||||
}
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// if the view is done in one pass, show it like in analog_audio_app
|
||||
offset = 0;
|
||||
@@ -251,17 +250,24 @@ void GlassView::on_range_changed() {
|
||||
bin_length = 80;
|
||||
ignore_dc = 0;
|
||||
}
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
if (mode != LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_step = (bin_length + (ignore_dc * 12)) * each_bin_size;
|
||||
} else {
|
||||
looking_glass_step = SPEC_NB_BINS * each_bin_size;
|
||||
}
|
||||
|
||||
adjust_range(&f_min, &f_max, SCREEN_W);
|
||||
looking_glass_range = f_max - f_min;
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_bandwidth = looking_glass_range;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth / 2;
|
||||
each_bin_size = looking_glass_bandwidth / bin_length;
|
||||
looking_glass_step = bin_length * each_bin_size;
|
||||
} else { // if ( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
looking_glass_bandwidth = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = LOOKING_GLASS_SLICE_WIDTH_MAX / SPEC_NB_BINS;
|
||||
looking_glass_step = (bin_length + (ignore_dc * 12)) * each_bin_size;
|
||||
}
|
||||
search_span = looking_glass_range / MHZ_DIV;
|
||||
button_range.set_text(" "); // clear up to 6 chars
|
||||
marker_pixel_step = looking_glass_range / SCREEN_W; // Each pixel value in Hz
|
||||
//
|
||||
button_range.set_text(" "); // clear up to 6 chars
|
||||
if (locked_range) {
|
||||
button_range.set_text(">" + to_string_dec_uint(search_span) + "<");
|
||||
} else {
|
||||
@@ -271,18 +277,6 @@ void GlassView::on_range_changed() {
|
||||
pixel_index = 0; // reset pixel counter
|
||||
max_power = 0; // reset save max power level
|
||||
bins_Hz_size = 0; // reset amount of Hz filled up by pixels
|
||||
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
looking_glass_bandwidth = looking_glass_range;
|
||||
looking_glass_sampling_rate = looking_glass_bandwidth / 2;
|
||||
each_bin_size = looking_glass_bandwidth / SCREEN_W;
|
||||
} else // if ( mode == LOOKING_GLASS_SLOWSCAN || mode == LOOKING_GLASS_FASTSCAN )
|
||||
{
|
||||
looking_glass_sampling_rate = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
looking_glass_bandwidth = LOOKING_GLASS_SLICE_WIDTH_MAX;
|
||||
each_bin_size = looking_glass_bandwidth / SPEC_NB_BINS;
|
||||
}
|
||||
|
||||
on_marker_change();
|
||||
|
||||
// set the sample rate and bandwidth
|
||||
|
||||
@@ -123,14 +123,15 @@ class MicTXView : public View {
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{7 * 8, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{5 * 8, (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
Labels labels_AK4951{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()},
|
||||
@@ -141,14 +142,15 @@ class MicTXView : public View {
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{4 * 8, (13 * 8) - 2}, "TONE KEY:", Color::light_grey()},
|
||||
{{(6 * 8) + 4, 23 * 8}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, 23 * 8}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, 25 * 8}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, 25 * 8}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, 27 * 8}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, 27 * 8}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, 27 * 8}, "AMP:", Color::light_grey()}};
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{(5 * 8), (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
|
||||
VuMeter vumeter{
|
||||
{0 * 8, 1 * 8, 2 * 8, 33 * 8},
|
||||
@@ -210,7 +212,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField options_mode{
|
||||
{24 * 8, 5 * 8},
|
||||
4,
|
||||
6,
|
||||
{
|
||||
{"NFM/FM", 0},
|
||||
{" WFM ", 1},
|
||||
@@ -230,7 +232,7 @@ class MicTXView : public View {
|
||||
|
||||
OptionsField field_va{
|
||||
{17 * 8, 8 * 8},
|
||||
3,
|
||||
4,
|
||||
{{" OFF", 0},
|
||||
{" PTT", 1},
|
||||
{"AUTO", 2}}};
|
||||
@@ -255,30 +257,30 @@ class MicTXView : public View {
|
||||
' '};
|
||||
|
||||
OptionsField options_tone_key{
|
||||
{10 * 8, (15 * 8) - 2},
|
||||
{12 * 8, (13 * 8) - 5},
|
||||
23,
|
||||
{}};
|
||||
|
||||
Checkbox check_rogerbeep{
|
||||
{3 * 8, (16 * 8) + 7},
|
||||
{3 * 8, (14 * 8) + 4},
|
||||
10,
|
||||
"Roger beep",
|
||||
false};
|
||||
|
||||
Checkbox check_rxactive{
|
||||
{3 * 8, (21 * 8) - 4},
|
||||
18, // it was 8, but if it is string size should be 18
|
||||
"RX audio listening",
|
||||
{3 * 8, (21 * 8) - 7},
|
||||
8, // it was 18, but if it is string size should be 8
|
||||
"RX audio",
|
||||
false};
|
||||
|
||||
Checkbox check_common_freq_tx_rx{
|
||||
{18 * 8, (16 * 8) + 7},
|
||||
{18 * 8, (21 * 8) - 7},
|
||||
8,
|
||||
"F = F_RX",
|
||||
"F TX=RX",
|
||||
false};
|
||||
|
||||
NumberField field_volume{
|
||||
{11 * 8, 23 * 8},
|
||||
{9 * 8, (23 * 8) + 2},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
@@ -286,7 +288,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
OptionsField field_rxbw{
|
||||
{19 * 8, 23 * 8},
|
||||
{19 * 8, (23 * 8) + 2},
|
||||
3,
|
||||
{
|
||||
{" NFM1:8k5 ", 0},
|
||||
@@ -295,7 +297,7 @@ class MicTXView : public View {
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{23 * 8, 25 * 8},
|
||||
{23 * 8, (25 * 8) + 2},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
@@ -303,11 +305,11 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
FrequencyField field_rxfrequency{
|
||||
{10 * 8, 25 * 8},
|
||||
{10 * 8, (25 * 8) + 2},
|
||||
};
|
||||
|
||||
NumberField field_rxlna{
|
||||
{9 * 8, 27 * 8},
|
||||
{9 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 40},
|
||||
8,
|
||||
@@ -315,7 +317,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxvga{
|
||||
{16 * 8, 27 * 8},
|
||||
{16 * 8, (27 * 8) + 2},
|
||||
2,
|
||||
{0, 62},
|
||||
2,
|
||||
@@ -323,7 +325,7 @@ class MicTXView : public View {
|
||||
};
|
||||
|
||||
NumberField field_rxamp{
|
||||
{24 * 8, 27 * 8},
|
||||
{24 * 8, (27 * 8) + 2},
|
||||
1,
|
||||
{0, 1},
|
||||
1,
|
||||
|
||||
@@ -146,13 +146,15 @@ void PlaylistView::toggle() {
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
} else {
|
||||
} else { // Thanks kallanreed for providing this logic!
|
||||
total_tracks = 0;
|
||||
track_number = 0;
|
||||
playlist_db.clear();
|
||||
playlist_masterdb.clear();
|
||||
load_file(now_play_list_file);
|
||||
start();
|
||||
if (!playlist_db.empty()) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +209,7 @@ void PlaylistView::stop(const bool do_loop) {
|
||||
// Notes of the logic here in case if it needed to be changed in the future:
|
||||
// 1. check_loop.value() is for the LOOP checkbox
|
||||
// 2. do_loop is a part of the replay thread, not a user - control thing.
|
||||
// 3. when (total_tracks >= track_number) is true, it means that the current track is not the last track.
|
||||
// 3. when (total_tracks > track_number) is true, it means that the current track is not the last track.
|
||||
// Thus, (do_loop && (total_tracks != track_number)) is for the case when the start() func were called with true AND not the last track.
|
||||
// Which means it do loop until the last track.
|
||||
|
||||
@@ -224,7 +226,7 @@ void PlaylistView::stop(const bool do_loop) {
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
} else if (!check_loop.value()) {
|
||||
if (do_loop && (total_tracks >= track_number)) {
|
||||
if (do_loop && (total_tracks > track_number)) {
|
||||
if (playlist_db.size() > 0) {
|
||||
start();
|
||||
} else {
|
||||
@@ -316,7 +318,8 @@ void PlaylistView::on_hide() {
|
||||
void PlaylistView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(),
|
||||
new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,13 @@ namespace ui {
|
||||
class PlaylistView : public View {
|
||||
public:
|
||||
PlaylistView(NavigationView& nav);
|
||||
|
||||
~PlaylistView();
|
||||
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Playlist"; };
|
||||
@@ -66,15 +69,14 @@ class PlaylistView : public View {
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
|
||||
void load_file(std::filesystem::path playlist_path);
|
||||
void txtline_process(std::string&);
|
||||
void on_file_changed(std::filesystem::path new_file_path, rf::Frequency replay_frequency, uint32_t replay_sample_rate);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
@@ -87,8 +89,8 @@ class PlaylistView : public View {
|
||||
std::filesystem::path file_path{};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool ready_signal{false};
|
||||
int track_number{0};
|
||||
int total_tracks{0};
|
||||
size_t track_number{0};
|
||||
size_t total_tracks{0};
|
||||
std::filesystem::path now_play_list_file{};
|
||||
|
||||
Button button_open{
|
||||
|
||||
@@ -36,7 +36,11 @@ void ReconView::clear_freqlist_for_ui_action() {
|
||||
freqlist_cleared_for_ui_action = true;
|
||||
// if in manual mode, there is enough memory to load freqman files, else we have to unload/reload
|
||||
if (!manual_mode) {
|
||||
frequency_list.clear();
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list);
|
||||
} else {
|
||||
frequency_list.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +691,10 @@ ReconView::ReconView(NavigationView& nav)
|
||||
} else {
|
||||
audio::output::stop();
|
||||
|
||||
frequency_list.clear();
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list);
|
||||
|
||||
freqman_entry manual_freq_entry;
|
||||
|
||||
def_step = step_mode.selected_index(); // max range val
|
||||
@@ -895,8 +902,10 @@ void ReconView::frequency_file_load(bool stop_all_before) {
|
||||
audio::output::stop();
|
||||
|
||||
def_step = step_mode.selected_index(); // use def_step from manual selector
|
||||
frequency_list.clear(); // clear the existing frequency list (expected behavior)
|
||||
std::string file_input = input_file; // default recon mode
|
||||
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
std::vector<freqman_entry>().swap(frequency_list); // clear the existing frequency list (expected behavior)
|
||||
std::string file_input = input_file; // default recon mode
|
||||
if (scanner_mode) {
|
||||
file_input = output_file;
|
||||
file_name.set_style(&style_red);
|
||||
|
||||
@@ -529,9 +529,9 @@ void SetPersistentMemoryView::focus() {
|
||||
button_return.focus();
|
||||
}
|
||||
|
||||
//
|
||||
// Audio settings
|
||||
//
|
||||
// ---------------------------------------------------------
|
||||
// Audio Settings
|
||||
// ---------------------------------------------------------
|
||||
SetAudioView::SetAudioView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&field_tone_mix,
|
||||
@@ -554,6 +554,9 @@ void SetAudioView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// QR Code Settings
|
||||
// ------------------------------------------------------
|
||||
SetQRCodeView::SetQRCodeView(NavigationView& nav) {
|
||||
add_children({&checkbox_bigger_qr,
|
||||
&button_save,
|
||||
@@ -575,6 +578,31 @@ void SetQRCodeView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Rotary Encoder Dial Settings
|
||||
// ---------------------------------------------------------
|
||||
SetEncoderDialView::SetEncoderDialView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&field_encoder_dial_sensitivity,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
field_encoder_dial_sensitivity.set_by_value(persistent_memory::config_encoder_dial_sensitivity());
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
persistent_memory::set_encoder_dial_sensitivity(field_encoder_dial_sensitivity.selected_index_value());
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetEncoderDialView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Settings main menu
|
||||
// ---------------------------------------------------------
|
||||
@@ -593,6 +621,7 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
||||
{"FreqCorrection", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetFrequencyCorrectionView>(); }},
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<SetPersistentMemoryView>(); }},
|
||||
{"Encoder Dial", ui::Color::dark_cyan(), &bitmap_icon_setup, [&nav]() { nav.push<SetEncoderDialView>(); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
@@ -432,6 +432,38 @@ class SetQRCodeView : public View {
|
||||
};
|
||||
};
|
||||
|
||||
using portapack::persistent_memory::encoder_dial_sensitivity;
|
||||
|
||||
class SetEncoderDialView : public View {
|
||||
public:
|
||||
SetEncoderDialView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Encoder Dial"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{2 * 8, 3 * 16}, "Dial sensitivity:", Color::light_grey()},
|
||||
};
|
||||
|
||||
OptionsField field_encoder_dial_sensitivity{
|
||||
{20 * 8, 3 * 16},
|
||||
6,
|
||||
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
|
||||
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_MEDIUM},
|
||||
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Cancel",
|
||||
};
|
||||
};
|
||||
|
||||
class SetPersistentMemoryView : public View {
|
||||
public:
|
||||
SetPersistentMemoryView(NavigationView& nav);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "string_format.hpp"
|
||||
@@ -29,11 +30,6 @@ using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
template <typename T>
|
||||
T mid(const T& val1, const T& val2, const T& val3) {
|
||||
return std::max(val1, std::min(val2, val3));
|
||||
}
|
||||
|
||||
/*void log(const std::string& msg) {
|
||||
LogFile log{};
|
||||
log.append("LOGS/Notepad.txt");
|
||||
@@ -43,253 +39,20 @@ T mid(const T& val1, const T& val2, const T& val3) {
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* FileWrapper ******************************************************/
|
||||
/* TextViewer *******************************************************/
|
||||
|
||||
FileWrapper::FileWrapper() {
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Error> FileWrapper::open(const fs::path& path) {
|
||||
file_ = File();
|
||||
auto result = file_.open(path);
|
||||
|
||||
if (!result.is_valid()) // No error
|
||||
initialize();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string FileWrapper::get_text(Offset line, Offset col, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range.is_valid())
|
||||
return "[UNCACHED LINE]";
|
||||
|
||||
// Don't read past end of line.
|
||||
if (range->start + col + to_read >= range->end)
|
||||
to_read = range->end - col - range->start;
|
||||
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Range> FileWrapper::line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset.is_valid())
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
|
||||
return {Range{start, end}};
|
||||
}
|
||||
|
||||
FileWrapper::Offset FileWrapper::line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range.is_valid())
|
||||
return range->end - range->start;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FileWrapper::initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
newlines_.clear();
|
||||
line_ending_ = LineEnding::LF;
|
||||
|
||||
Offset offset = 0;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result.is_valid()) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
std::string FileWrapper::read(Offset offset, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
if (offset + length > file_.size())
|
||||
return {"[BAD OFFSET]"};
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(&buffer[0], length);
|
||||
if (result.is_ok())
|
||||
buffer.resize(*result);
|
||||
else
|
||||
return result.error().what();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::offset_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual < newlines_.size()) // NB: underflow wrap.
|
||||
return {actual};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void FileWrapper::ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result.is_valid())
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
while (line < start_line_ && start_offset_ >= 2) {
|
||||
// start_offset_ - 1 should be a newline. Need to
|
||||
// find the new value for start_offset_. start_line_
|
||||
// has to be > 0 to get into this block so there should
|
||||
// always be one newline before start_offset_.
|
||||
auto offset = previous_newline(start_offset_ - 2);
|
||||
newlines_.push_front(start_offset_ - 1);
|
||||
|
||||
if (!offset.is_valid()) {
|
||||
// Must be at beginning.
|
||||
start_line_ = 0;
|
||||
start_offset_ = 0;
|
||||
} else {
|
||||
// Found an previous newline, the new start_line_
|
||||
// starts at the newline offset + 1.
|
||||
start_line_--;
|
||||
start_offset_ = *offset + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (line >= start_line_ + newlines_.size()) {
|
||||
auto offset = next_newline(newlines_.back() + 1);
|
||||
if (offset.is_valid()) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::previous_newline(Offset start) {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
auto to_read = buffer_size;
|
||||
|
||||
do {
|
||||
if (offset < to_read) {
|
||||
// NB: Char at 'offset' was read in the previous iteration.
|
||||
to_read = offset;
|
||||
offset = 0;
|
||||
} else
|
||||
offset -= to_read;
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
// Find newlines in the buffer backwards.
|
||||
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::next_newline(Offset start) {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
|
||||
// EOF, nothing to do.
|
||||
if (start >= size())
|
||||
return {};
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = file_.read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
// Find newlines in the buffer.
|
||||
for (Offset i = 0; i < *result; ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
// Fake a newline at the end for consistency.
|
||||
return {offset};
|
||||
}
|
||||
|
||||
/* TextEditorView ***************************************************/
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children(
|
||||
{
|
||||
&button_open,
|
||||
&text_position,
|
||||
&text_size,
|
||||
});
|
||||
TextViewer::TextViewer(Rect parent_rect)
|
||||
: Widget(parent_rect),
|
||||
max_line{static_cast<uint8_t>(parent_rect.height() / char_height)},
|
||||
max_col{static_cast<uint8_t>(parent_rect.width() / char_width)} {
|
||||
set_focusable(true);
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>("");
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
|
||||
: TextEditorView(nav) {
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
void TextEditorView::on_focus() {
|
||||
refresh_ui();
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void TextEditorView::paint(Painter& painter) {
|
||||
void TextViewer::paint(Painter& painter) {
|
||||
auto first_line = paint_state_.first_line;
|
||||
auto first_col = paint_state_.first_col;
|
||||
|
||||
if (!paint_state_.has_file)
|
||||
if (!has_file())
|
||||
return;
|
||||
|
||||
// Move the viewport vertically.
|
||||
@@ -320,7 +83,7 @@ void TextEditorView::paint(Painter& painter) {
|
||||
paint_cursor(painter);
|
||||
}
|
||||
|
||||
bool TextEditorView::on_key(const KeyEvent key) {
|
||||
bool TextViewer::on_key(const KeyEvent key) {
|
||||
int16_t delta_col = 0;
|
||||
int16_t delta_line = 0;
|
||||
|
||||
@@ -332,19 +95,22 @@ bool TextEditorView::on_key(const KeyEvent key) {
|
||||
delta_line = -1;
|
||||
else if (key == KeyEvent::Down)
|
||||
delta_line = 1;
|
||||
/* else if (key == KeyEvent::Select)
|
||||
; // TODO: Edit/Menu */
|
||||
else if (key == KeyEvent::Select && on_select) {
|
||||
on_select();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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();
|
||||
redraw();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
bool TextEditorView::on_encoder(EncoderEvent delta) {
|
||||
bool TextViewer::on_encoder(EncoderEvent delta) {
|
||||
bool updated = false;
|
||||
|
||||
if (cursor_.dir == ScrollDirection::Horizontal)
|
||||
@@ -353,15 +119,18 @@ bool TextEditorView::on_encoder(EncoderEvent delta) {
|
||||
updated = apply_scrolling_constraints(delta, 0);
|
||||
|
||||
if (updated)
|
||||
refresh_ui();
|
||||
redraw();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
if (!has_file())
|
||||
return false;
|
||||
|
||||
int32_t new_line = cursor_.line + delta_line;
|
||||
int32_t new_col = cursor_.col + delta_col;
|
||||
int32_t new_line_length = file_.line_length(new_line);
|
||||
int32_t new_line_length = file_->line_length(new_line);
|
||||
|
||||
if (new_col < 0)
|
||||
--new_line;
|
||||
@@ -375,9 +144,9 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del
|
||||
if (new_line < 0 && new_col > 0) {
|
||||
new_line = 0;
|
||||
new_col = 0;
|
||||
} else if (new_line >= (int32_t)file_.line_count()) {
|
||||
auto last_line = file_.line_count() - 1;
|
||||
int32_t last_col = file_.line_length(last_line) - 1;
|
||||
} else if (new_line >= (int32_t)file_->line_count()) {
|
||||
auto last_line = file_->line_count() - 1;
|
||||
int32_t last_col = file_->line_length(last_line) - 1;
|
||||
|
||||
if (new_col < last_col) {
|
||||
new_line = last_line;
|
||||
@@ -385,10 +154,10 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del
|
||||
}
|
||||
}
|
||||
|
||||
if (new_line < 0 || (uint32_t)new_line >= file_.line_count())
|
||||
if (new_line < 0 || (uint32_t)new_line >= file_->line_count())
|
||||
return false;
|
||||
|
||||
new_line_length = file_.line_length(new_line);
|
||||
new_line_length = file_->line_length(new_line);
|
||||
|
||||
// TODO: don't wrap with encoder?
|
||||
// Wrap or clamp column.
|
||||
@@ -400,75 +169,47 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del
|
||||
cursor_.line = new_line;
|
||||
cursor_.col = new_col;
|
||||
|
||||
if (on_cursor_moved)
|
||||
on_cursor_moved();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextEditorView::refresh_ui() {
|
||||
if (paint_state_.has_file) {
|
||||
text_position.set(
|
||||
"Ln " + to_string_dec_uint(cursor_.line + 1) +
|
||||
", Col " + to_string_dec_uint(cursor_.col + 1));
|
||||
text_size.set(
|
||||
"Lines:" + to_string_dec_uint(file_.line_count()) +
|
||||
" (" + to_string_file_size(file_.size()) + ")");
|
||||
} else {
|
||||
// if (!button_open.has_focus())
|
||||
// button_open.focus();
|
||||
text_position.set("");
|
||||
text_size.set("");
|
||||
}
|
||||
|
||||
void TextViewer::redraw(bool redraw_text) {
|
||||
paint_state_.redraw_text = redraw_text;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
// TODO: need a temp backing file for edits.
|
||||
auto result = file_.open(path);
|
||||
|
||||
if (result.is_valid())
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result->what());
|
||||
|
||||
paint_state_.has_file = !result.is_valid(); // Has an error.
|
||||
paint_state_.first_line = 0;
|
||||
paint_state_.first_col = 0;
|
||||
cursor_.line = 0;
|
||||
cursor_.col = 0;
|
||||
|
||||
paint_state_.redraw_text = true;
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
auto r = screen_rect();
|
||||
|
||||
// Draw the lines from the file
|
||||
for (auto i = 0u; i < max_line; ++i) {
|
||||
if (line + i >= file_.line_count())
|
||||
if (line + i >= file_->line_count())
|
||||
break;
|
||||
|
||||
auto str = file_.get_text(line + i, col, max_col);
|
||||
auto str = file_->get_text(line + i, col, max_col);
|
||||
|
||||
// Draw text.
|
||||
if (str.length() > 0)
|
||||
if (str && str->length() > 0)
|
||||
painter.draw_string(
|
||||
{0, r.location().y() + (int)i * char_height},
|
||||
style_default, str);
|
||||
{0, r.top() + (int)i * char_height},
|
||||
style_text, *str);
|
||||
|
||||
// Clear empty line sections.
|
||||
int32_t clear_width = max_col - str.length();
|
||||
// Clear empty line sections. This is less visually jarring than full clear.
|
||||
int32_t clear_width = max_col - (str ? str->length() : 0);
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
r.location().y() + (int)i * char_height,
|
||||
r.top() + (int)i * char_height,
|
||||
clear_width * char_width, char_height},
|
||||
style_default.background);
|
||||
style_text.background);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::paint_cursor(Painter& painter) {
|
||||
void TextViewer::paint_cursor(Painter& painter) {
|
||||
if (!has_focus())
|
||||
return;
|
||||
|
||||
auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) {
|
||||
auto r = screen_rect();
|
||||
line = line - paint_state_.first_line;
|
||||
@@ -476,21 +217,200 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
||||
|
||||
painter.draw_rectangle(
|
||||
{(int)col * char_width - 1,
|
||||
r.location().y() + (int)line * char_height,
|
||||
r.top() + (int)line * char_height,
|
||||
char_width + 1, char_height},
|
||||
c);
|
||||
};
|
||||
|
||||
// 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);
|
||||
// Clear old cursor. CONSIDER: XOR cursor?
|
||||
draw_cursor(paint_state_.line, paint_state_.col, style_text.background);
|
||||
draw_cursor(cursor_.line, cursor_.col, style_text.foreground);
|
||||
paint_state_.line = cursor_.line;
|
||||
paint_state_.col = cursor_.col;
|
||||
}
|
||||
|
||||
uint16_t TextEditorView::line_length() {
|
||||
return file_.line_length(cursor_.line);
|
||||
void TextViewer::reset_file(FileWrapper* file) {
|
||||
file_ = file;
|
||||
paint_state_.first_line = 0;
|
||||
paint_state_.first_col = 0;
|
||||
cursor_.line = 0;
|
||||
cursor_.col = 0;
|
||||
redraw(true);
|
||||
}
|
||||
|
||||
uint16_t TextViewer::line_length() {
|
||||
return file_->line_length(cursor_.line);
|
||||
}
|
||||
|
||||
/* TextEditorMenu ***************************************************/
|
||||
|
||||
TextEditorMenu::TextEditorMenu()
|
||||
: View{{7 * 4, 9 * 4, 25 * 8, 25 * 8}} {
|
||||
add_children(
|
||||
{
|
||||
&rect_frame,
|
||||
&button_cut,
|
||||
&button_paste,
|
||||
&button_copy,
|
||||
&button_delline,
|
||||
&button_edit,
|
||||
&button_addline,
|
||||
&button_open,
|
||||
&button_save,
|
||||
&button_exit,
|
||||
});
|
||||
}
|
||||
|
||||
void TextEditorMenu::on_show() {
|
||||
hide_children(false);
|
||||
button_edit.focus();
|
||||
}
|
||||
|
||||
void TextEditorMenu::on_hide() {
|
||||
hide_children(true);
|
||||
}
|
||||
|
||||
void TextEditorMenu::hide_children(bool hidden) {
|
||||
for (auto child : children()) {
|
||||
child->hidden(hidden);
|
||||
}
|
||||
}
|
||||
|
||||
/* TextEditorView ***************************************************/
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children(
|
||||
{
|
||||
&viewer,
|
||||
&menu,
|
||||
&button_menu,
|
||||
&text_position,
|
||||
&text_size,
|
||||
});
|
||||
|
||||
viewer.on_select = [this]() {
|
||||
// Treat as if menu button was pressed.
|
||||
if (button_menu.on_select)
|
||||
button_menu.on_select();
|
||||
};
|
||||
|
||||
viewer.on_cursor_moved = [this]() {
|
||||
update_position();
|
||||
};
|
||||
|
||||
menu.hidden(true);
|
||||
menu.on_cut() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_paste() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_copy() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_delete_line() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_edit_line() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_add_line() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_open() = [this]() {
|
||||
// TODO: confirm.
|
||||
show_file_picker();
|
||||
};
|
||||
menu.on_save() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
menu.on_exit() = [this]() {
|
||||
// TODO: confirm.
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
button_menu.on_select = [this]() {
|
||||
if (file_) {
|
||||
// Toggle menu.
|
||||
hide_menu(!menu.hidden());
|
||||
} else {
|
||||
show_file_picker();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
|
||||
: TextEditorView(nav) {
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
void TextEditorView::on_show() {
|
||||
if (file_)
|
||||
viewer.focus();
|
||||
else
|
||||
button_menu.focus();
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
auto result = FileWrapper::open(path);
|
||||
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
file_.reset();
|
||||
viewer.clear_file();
|
||||
} else {
|
||||
file_ = result.take();
|
||||
viewer.set_file(*file_);
|
||||
}
|
||||
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void TextEditorView::refresh_ui() {
|
||||
if (file_) {
|
||||
update_position();
|
||||
text_size.set(
|
||||
"Lines:" + to_string_dec_uint(file_->line_count()) +
|
||||
" (" + to_string_file_size(file_->size()) + ")");
|
||||
} else {
|
||||
text_position.set("");
|
||||
text_size.set("");
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::update_position() {
|
||||
if (viewer.has_file()) {
|
||||
text_position.set(
|
||||
"Ln " + to_string_dec_uint(viewer.line() + 1) +
|
||||
", Col " + to_string_dec_uint(viewer.col() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::hide_menu(bool hidden) {
|
||||
menu.hidden(hidden);
|
||||
|
||||
// Only let the viewer be focused when the menu is
|
||||
// not shown, otherwise menu focus gets confusing.
|
||||
viewer.set_focusable(hidden);
|
||||
|
||||
if (hidden)
|
||||
viewer.focus();
|
||||
|
||||
viewer.redraw(true);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextEditorView::show_file_picker() {
|
||||
auto open_view = nav_.push<FileLoadView>("");
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
hide_menu();
|
||||
};
|
||||
}
|
||||
|
||||
void TextEditorView::show_nyi() {
|
||||
nav_.display_modal("Soon...", "Coming soon.");
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -27,86 +27,174 @@
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
//#include "ui_textentry.hpp"
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
enum class ScrollDirection : uint8_t {
|
||||
Vertical,
|
||||
Horizontal
|
||||
};
|
||||
|
||||
/* Wraps a file and provides an API for accessing lines efficiently. */
|
||||
class FileWrapper {
|
||||
/* Control that renders a text file. */
|
||||
class TextViewer : public Widget {
|
||||
public:
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
using Offset = uint32_t; // TODO: make enums?
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
Offset end;
|
||||
};
|
||||
TextViewer(Rect parent_rect);
|
||||
|
||||
FileWrapper();
|
||||
TextViewer(const TextViewer&) = delete;
|
||||
TextViewer(TextViewer&&) = delete;
|
||||
TextViewer& operator=(const TextViewer&) = delete;
|
||||
TextViewer& operator=(TextViewer&&) = delete;
|
||||
|
||||
/* Prevent copies. */
|
||||
FileWrapper(const FileWrapper&) = delete;
|
||||
FileWrapper& operator=(const FileWrapper&) = delete;
|
||||
std::function<void()> on_select{};
|
||||
std::function<void()> on_cursor_moved{};
|
||||
|
||||
Optional<Error> open(const std::filesystem::path& path);
|
||||
std::string get_text(Line line, Column col, Offset length);
|
||||
void paint(Painter& painter) override;
|
||||
bool on_key(KeyEvent delta) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
File::Size size() const { return file_.size(); }
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
void redraw(bool redraw_text = false);
|
||||
|
||||
Optional<Range> line_range(Line line);
|
||||
Offset line_length(Line line);
|
||||
void set_file(FileWrapper& file) { reset_file(&file); }
|
||||
void clear_file() { reset_file(); }
|
||||
bool has_file() const { return file_ != nullptr; }
|
||||
|
||||
uint32_t line() const { return cursor_.line; }
|
||||
uint32_t col() const { return cursor_.col; }
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = 64;
|
||||
static constexpr int8_t char_width = 5;
|
||||
static constexpr int8_t char_height = 8;
|
||||
static constexpr Style style_text{
|
||||
.font = font::fixed_5x8,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
void initialize();
|
||||
std::string read(Offset offset, Offset length = 30);
|
||||
const uint8_t max_line = 32;
|
||||
const uint8_t max_col = 48;
|
||||
|
||||
/* Returns the offset into the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const;
|
||||
/* Returns true if the cursor was updated. */
|
||||
bool apply_scrolling_constraints(
|
||||
int16_t delta_line,
|
||||
int16_t delta_col);
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line);
|
||||
void paint_text(Painter& painter, uint32_t line, uint16_t col);
|
||||
void paint_cursor(Painter& painter);
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
Optional<Offset> previous_newline(Offset start);
|
||||
Optional<Offset> next_newline(Offset start);
|
||||
void reset_file(FileWrapper* file = nullptr);
|
||||
|
||||
File file_{};
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length();
|
||||
|
||||
/* Total number of lines in the file. */
|
||||
Offset line_count_{0};
|
||||
FileWrapper* file_{};
|
||||
|
||||
/* The offset and line of the newlines cache. */
|
||||
Offset start_offset_{0};
|
||||
Offset start_line_{0};
|
||||
struct {
|
||||
// Previous cursor state.
|
||||
uint32_t line{};
|
||||
uint16_t col{};
|
||||
|
||||
LineEnding line_ending_{LineEnding::LF};
|
||||
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||
// Previous draw state.
|
||||
uint32_t first_line{};
|
||||
uint16_t first_col{};
|
||||
bool redraw_text{true};
|
||||
} paint_state_{};
|
||||
|
||||
struct {
|
||||
uint32_t line{};
|
||||
uint16_t col{};
|
||||
ScrollDirection dir{ScrollDirection::Vertical};
|
||||
} cursor_{};
|
||||
};
|
||||
|
||||
/* Menu control for the TextEditor. */
|
||||
class TextEditorMenu : public View {
|
||||
public:
|
||||
TextEditorMenu();
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
std::function<void()>& on_cut() { return button_cut.on_select; }
|
||||
std::function<void()>& on_paste() { return button_paste.on_select; }
|
||||
std::function<void()>& on_copy() { return button_copy.on_select; }
|
||||
|
||||
std::function<void()>& on_delete_line() { return button_delline.on_select; }
|
||||
std::function<void()>& on_edit_line() { return button_edit.on_select; }
|
||||
std::function<void()>& on_add_line() { return button_addline.on_select; }
|
||||
|
||||
std::function<void()>& on_open() { return button_open.on_select; }
|
||||
std::function<void()>& on_save() { return button_save.on_select; }
|
||||
std::function<void()>& on_exit() { return button_exit.on_select; }
|
||||
|
||||
private:
|
||||
void hide_children(bool hidden);
|
||||
|
||||
Rectangle rect_frame{
|
||||
{0 * 8, 0 * 8, 23 * 8, 23 * 8},
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_cut{
|
||||
{1 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Cut",
|
||||
&bitmap_icon_cut,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_paste{
|
||||
{8 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Paste",
|
||||
&bitmap_icon_paste,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_copy{
|
||||
{15 * 8, 1 * 8, 7 * 8, 7 * 8},
|
||||
"Copy",
|
||||
&bitmap_icon_copy,
|
||||
Color::dark_grey()};
|
||||
|
||||
NewButton button_delline{
|
||||
{1 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"-Line",
|
||||
&bitmap_icon_delete,
|
||||
Color::dark_red()};
|
||||
|
||||
NewButton button_edit{
|
||||
{8 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"Edit",
|
||||
&bitmap_icon_rename,
|
||||
Color::dark_blue()};
|
||||
|
||||
NewButton button_addline{
|
||||
{15 * 8, 8 * 8, 7 * 8, 7 * 8},
|
||||
"+Line",
|
||||
&bitmap_icon_scanner,
|
||||
Color::dark_blue()};
|
||||
|
||||
NewButton button_open{
|
||||
{1 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Open",
|
||||
&bitmap_icon_load,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_save{
|
||||
{8 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Save",
|
||||
&bitmap_icon_save,
|
||||
Color::green()};
|
||||
|
||||
NewButton button_exit{
|
||||
{15 * 8, 15 * 8, 7 * 8, 7 * 8},
|
||||
"Exit",
|
||||
&bitmap_icon_previous,
|
||||
Color::dark_red()};
|
||||
};
|
||||
|
||||
/* View viewing and minor edits on a text file. */
|
||||
class TextEditorView : public View {
|
||||
public:
|
||||
TextEditorView(NavigationView& nav);
|
||||
@@ -118,72 +206,37 @@ class TextEditorView : public View {
|
||||
return "Notepad";
|
||||
};
|
||||
|
||||
void on_focus() override;
|
||||
void paint(Painter& painter) override;
|
||||
bool on_key(KeyEvent delta) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
void on_show() 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;
|
||||
|
||||
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 open_file(const std::filesystem::path& path);
|
||||
|
||||
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();
|
||||
void refresh_ui();
|
||||
void update_position();
|
||||
void hide_menu(bool hidden = true);
|
||||
void show_file_picker();
|
||||
void show_nyi();
|
||||
|
||||
NavigationView& nav_;
|
||||
std::unique_ptr<FileWrapper> file_{};
|
||||
|
||||
FileWrapper file_{};
|
||||
TextViewer viewer{
|
||||
/* 272 = 320 - 16 (top bar) - 32 (bottom controls) */
|
||||
{0, 0, 240, 272}};
|
||||
|
||||
struct {
|
||||
// Previous cursor state.
|
||||
uint32_t line{};
|
||||
uint16_t col{};
|
||||
TextEditorMenu menu{};
|
||||
|
||||
// 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_{};
|
||||
|
||||
// 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"};
|
||||
NewButton button_menu{
|
||||
{26 * 8, 34 * 8, 4 * 8, 4 * 8},
|
||||
{},
|
||||
&bitmap_icon_controls,
|
||||
Color::dark_grey()};
|
||||
|
||||
Text text_position{
|
||||
{0 * 8, 34 * 8, 24 * 8, 2 * 8},
|
||||
{0 * 8, 34 * 8, 26 * 8, 2 * 8},
|
||||
""};
|
||||
|
||||
Text text_size{
|
||||
{0 * 8, 36 * 8, 24 * 8, 2 * 8},
|
||||
{0 * 8, 36 * 8, 26 * 8, 2 * 8},
|
||||
""};
|
||||
};
|
||||
|
||||
|
||||
@@ -2714,38 +2714,38 @@ static constexpr Bitmap bitmap_icon_remote{
|
||||
bitmap_icon_remote_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_save_data[] = {
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x4E,
|
||||
0x05,
|
||||
0x91,
|
||||
0x03,
|
||||
0x3F,
|
||||
0x19,
|
||||
0x01,
|
||||
0xFC,
|
||||
0x07,
|
||||
0x0A,
|
||||
0x0A,
|
||||
0x0A,
|
||||
0x12,
|
||||
0xF2,
|
||||
0x21,
|
||||
0x02,
|
||||
0x20,
|
||||
0xF9,
|
||||
0xFF,
|
||||
0xF9,
|
||||
0xFF,
|
||||
0xFD,
|
||||
0x7F,
|
||||
0xFD,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0x3F,
|
||||
0xFF,
|
||||
0x3F,
|
||||
0xFF,
|
||||
0x1F,
|
||||
0xFF,
|
||||
0x02,
|
||||
0x20,
|
||||
0x02,
|
||||
0x20,
|
||||
0x02,
|
||||
0x20,
|
||||
0xFA,
|
||||
0x27,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0x0A,
|
||||
0x28,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0x0A,
|
||||
0x28,
|
||||
0xFA,
|
||||
0x2F,
|
||||
0xFC,
|
||||
0x1F,
|
||||
0x00,
|
||||
0x00,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_save{
|
||||
{16, 16},
|
||||
|
||||
@@ -60,7 +60,7 @@ File::~File() {
|
||||
f_close(&f);
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::read(void* const data, const Size bytes_to_read) {
|
||||
File::Result<File::Size> File::read(void* data, Size bytes_to_read) {
|
||||
UINT bytes_read = 0;
|
||||
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
|
||||
if (result == FR_OK) {
|
||||
@@ -70,7 +70,7 @@ File::Result<File::Size> File::read(void* const data, const Size bytes_to_read)
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::write(const void* const data, const Size bytes_to_write) {
|
||||
File::Result<File::Size> File::write(const void* data, Size bytes_to_write) {
|
||||
UINT bytes_written = 0;
|
||||
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
|
||||
if (result == FR_OK) {
|
||||
@@ -84,7 +84,7 @@ File::Result<File::Size> File::write(const void* const data, const Size bytes_to
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Offset> File::seek(const Offset new_position) {
|
||||
File::Result<File::Offset> File::seek(Offset new_position) {
|
||||
/* NOTE: Returns *old* position, not new position */
|
||||
const auto old_position = f_tell(&f);
|
||||
const auto result = f_lseek(&f, new_position);
|
||||
@@ -241,10 +241,11 @@ std::filesystem::filesystem_error rename_file(
|
||||
std::filesystem::filesystem_error copy_file(
|
||||
const std::filesystem::path& file_path,
|
||||
const std::filesystem::path& dest_path) {
|
||||
// Decent compromise between memory and speed.
|
||||
constexpr size_t buffer_size = 512;
|
||||
uint8_t buffer[buffer_size];
|
||||
File src;
|
||||
File dst;
|
||||
constexpr size_t buffer_size = 128;
|
||||
uint8_t buffer[buffer_size];
|
||||
|
||||
auto error = src.open(file_path);
|
||||
if (error.is_valid()) return error.value();
|
||||
|
||||
@@ -288,6 +288,7 @@ class File {
|
||||
using Timestamp = uint32_t;
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
|
||||
// TODO: move to common.
|
||||
template <typename T>
|
||||
struct Result {
|
||||
enum class Type {
|
||||
@@ -303,11 +304,15 @@ class File {
|
||||
return type == Type::Success;
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return is_ok();
|
||||
}
|
||||
|
||||
bool is_error() const {
|
||||
return type == Type::Error;
|
||||
}
|
||||
|
||||
const T& value() const {
|
||||
const T& value() const& {
|
||||
return value_;
|
||||
}
|
||||
|
||||
@@ -315,6 +320,15 @@ class File {
|
||||
return value_;
|
||||
}
|
||||
|
||||
/* Allows value to be moved out of the Result. */
|
||||
T take() {
|
||||
if (is_error())
|
||||
return {};
|
||||
T temp;
|
||||
std::swap(temp, value_);
|
||||
return temp;
|
||||
}
|
||||
|
||||
Error error() const {
|
||||
return error_;
|
||||
}
|
||||
@@ -322,9 +336,9 @@ class File {
|
||||
Result() = delete;
|
||||
|
||||
constexpr Result(
|
||||
T value)
|
||||
T&& value)
|
||||
: type{Type::Success},
|
||||
value_{value} {
|
||||
value_{std::forward<T>(value)} {
|
||||
}
|
||||
|
||||
constexpr Result(
|
||||
@@ -334,17 +348,21 @@ class File {
|
||||
}
|
||||
|
||||
~Result() {
|
||||
if (type == Type::Success) {
|
||||
if (is_ok())
|
||||
value_.~T();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File(){};
|
||||
~File();
|
||||
|
||||
File(File&&) = default;
|
||||
File& operator=(File&&) = default;
|
||||
File(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
}
|
||||
File& operator=(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/* Prevent copies */
|
||||
File(const File&) = delete;
|
||||
@@ -355,10 +373,10 @@ class File {
|
||||
Optional<Error> append(const std::filesystem::path& filename);
|
||||
Optional<Error> create(const std::filesystem::path& filename);
|
||||
|
||||
Result<Size> read(void* const data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
||||
Result<Size> read(void* data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* data, Size bytes_to_write);
|
||||
|
||||
Result<Offset> seek(const uint64_t Offset);
|
||||
Result<Offset> seek(uint64_t Offset);
|
||||
// Timestamp created_date() const;
|
||||
Size size() const;
|
||||
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* 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 __FILE_WRAPPER_HPP__
|
||||
#define __FILE_WRAPPER_HPP__
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
/* TODO:
|
||||
* - CRLF handling.
|
||||
*/
|
||||
|
||||
/* BufferType requires the following members
|
||||
* Size size()
|
||||
* Result<Size> read(void* data, Size bytes_to_read)
|
||||
* Result<Offset> seek(uint32_t offset)
|
||||
*/
|
||||
|
||||
/* Wraps a buffer and provides an API for accessing lines efficiently. */
|
||||
template <typename BufferType, uint32_t CacheSize>
|
||||
class BufferWrapper {
|
||||
public:
|
||||
using Offset = uint32_t;
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
Offset end;
|
||||
};
|
||||
|
||||
BufferWrapper(BufferType* buffer)
|
||||
: wrapped_{buffer} {
|
||||
initialize();
|
||||
}
|
||||
virtual ~BufferWrapper() {}
|
||||
|
||||
/* Prevent copies */
|
||||
BufferWrapper(const BufferWrapper&) = delete;
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
|
||||
Optional<std::string> get_text(Line line, Column col, Offset length) {
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range)
|
||||
return {};
|
||||
|
||||
// Don't read past end of line.
|
||||
if (range->start + col + to_read >= range->end)
|
||||
to_read = range->end - col - range->start;
|
||||
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
}
|
||||
|
||||
/* Gets the size of the buffer in bytes. */
|
||||
File::Size size() const { return wrapped_->size(); }
|
||||
|
||||
/* Get the count of the lines in the buffer. */
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
|
||||
/* Gets the range of the line if valid. */
|
||||
Optional<Range> line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset)
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
|
||||
return Range{start, end};
|
||||
}
|
||||
|
||||
/* Gets the length of the line, or 0 if invalid. */
|
||||
Offset line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
return range->end - range->start;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Gets the index of the first line in the cache.
|
||||
* Only really useful for unit testing or diagnostics. */
|
||||
Offset start_line() { return start_line_; };
|
||||
|
||||
protected:
|
||||
BufferWrapper() {}
|
||||
|
||||
void set_buffer(BufferType* buffer) {
|
||||
wrapped_ = buffer;
|
||||
initialize();
|
||||
}
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = CacheSize;
|
||||
|
||||
/* Size of stack buffer used for reading/writing. */
|
||||
static constexpr size_t buffer_size = 512;
|
||||
|
||||
void initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
newlines_.clear();
|
||||
|
||||
// Special case for empty files to keep them consistent.
|
||||
if (size() == 0) {
|
||||
line_count_ = 1;
|
||||
newlines_.push_back(0);
|
||||
return;
|
||||
}
|
||||
|
||||
Offset offset = 0;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<std::string> read(Offset offset, Offset length) {
|
||||
if (offset + length > size())
|
||||
return {};
|
||||
|
||||
std::string buffer;
|
||||
buffer.resize(length);
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(&buffer[0], length);
|
||||
if (result.is_error())
|
||||
// TODO: better error handling.
|
||||
return std::string{"[Bad Read]"};
|
||||
|
||||
buffer.resize(*result);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/* Returns the offset of the line in the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual >= newlines_.size()) // NB: underflow wrap.
|
||||
return {};
|
||||
|
||||
return actual;
|
||||
}
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result)
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
while (line < start_line_ && start_offset_ >= 2) {
|
||||
// start_offset_ - 1 should be a newline. Need to
|
||||
// find the new value for start_offset_. start_line_
|
||||
// has to be > 0 to get into this block so there should
|
||||
// always be one newline before start_offset_.
|
||||
auto offset = previous_newline(start_offset_ - 2);
|
||||
newlines_.push_front(start_offset_ - 1);
|
||||
|
||||
if (!offset) {
|
||||
// Must be at beginning.
|
||||
start_line_ = 0;
|
||||
start_offset_ = 0;
|
||||
} else {
|
||||
// Found an previous newline, the new start_line_
|
||||
// starts at the newline offset + 1.
|
||||
start_line_--;
|
||||
start_offset_ = *offset + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (line >= start_line_ + newlines_.size()) {
|
||||
auto offset = next_newline(newlines_.back() + 1);
|
||||
if (offset) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
Optional<Offset> previous_newline(Offset offset) {
|
||||
char buffer[buffer_size];
|
||||
auto to_read = buffer_size;
|
||||
|
||||
do {
|
||||
if (offset < to_read) {
|
||||
// NB: Char at 'offset' was read in the previous iteration.
|
||||
to_read = offset;
|
||||
offset = 0;
|
||||
} else
|
||||
offset -= to_read;
|
||||
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
// Find newlines in the buffer backwards.
|
||||
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return offset + i;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
Optional<Offset> next_newline(Offset offset) {
|
||||
// EOF, no more newlines to find.
|
||||
if (offset >= size())
|
||||
return {};
|
||||
|
||||
char buffer[buffer_size];
|
||||
wrapped_->seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = wrapped_->read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
// Find newlines in the buffer.
|
||||
for (Offset i = 0; i < *result; ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return offset + i;
|
||||
}
|
||||
}
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
// For consistency, treat the end of the file as a "newline".
|
||||
return size() - 1;
|
||||
}
|
||||
|
||||
BufferType* wrapped_{};
|
||||
|
||||
/* Total number of lines in the buffer. */
|
||||
Offset line_count_{0};
|
||||
|
||||
/* The offset and line of the newlines cache. */
|
||||
Offset start_offset_{0};
|
||||
Offset start_line_{0};
|
||||
|
||||
LineEnding line_ending_{LineEnding::LF};
|
||||
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||
};
|
||||
|
||||
/* A BufferWrapper over a file. */
|
||||
class FileWrapper : public BufferWrapper<File, 64> {
|
||||
public:
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
fw->initialize();
|
||||
return fw;
|
||||
}
|
||||
|
||||
private:
|
||||
FileWrapper() {}
|
||||
void initialize() {
|
||||
set_buffer(&file_);
|
||||
}
|
||||
|
||||
File file_{};
|
||||
};
|
||||
|
||||
template <uint32_t CacheSize = 64, typename T>
|
||||
BufferWrapper<T, CacheSize> wrap_buffer(T& buffer) {
|
||||
return {&buffer};
|
||||
}
|
||||
|
||||
#endif // __FILE_WRAPPER_HPP__
|
||||
@@ -114,35 +114,40 @@ bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freq
|
||||
char* line_end;
|
||||
std::string description;
|
||||
rf::Frequency frequency_a, frequency_b;
|
||||
char file_data[257];
|
||||
const int read_buf_size = 128;
|
||||
char file_data[read_buf_size + 1];
|
||||
freqman_entry_type type;
|
||||
freqman_index_t modulation = 0;
|
||||
freqman_index_t bandwidth = 0;
|
||||
freqman_index_t step = 0;
|
||||
freqman_index_t tone = 0;
|
||||
|
||||
db.clear();
|
||||
// these are not enough to really start with a new, clean, empty vector
|
||||
// swap is the only way to achieve a perfect memory liberation
|
||||
// db.clear();
|
||||
// db.shrink_to_fit():
|
||||
std::vector<freqman_entry>().swap(db);
|
||||
|
||||
auto result = freqman_file.open("FREQMAN/" + file_stem + ".TXT");
|
||||
if (result.is_valid())
|
||||
return false;
|
||||
|
||||
while (1) {
|
||||
// Read a 256 bytes block from file
|
||||
// Read a read_buf_size bytes block from file
|
||||
freqman_file.seek(file_position);
|
||||
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = freqman_file.read(file_data, 256);
|
||||
memset(file_data, 0, read_buf_size + 1);
|
||||
auto read_size = freqman_file.read(file_data, read_buf_size);
|
||||
if (read_size.is_error())
|
||||
return false; // Read error
|
||||
|
||||
file_position += 256;
|
||||
file_position += read_buf_size;
|
||||
|
||||
// Reset line_start to beginning of buffer
|
||||
line_start = file_data;
|
||||
|
||||
// If EOF reached, insert 0x0A after, in case the last line doesn't have a C/R
|
||||
if (read_size.value() < 256)
|
||||
if (read_size.value() < read_buf_size)
|
||||
*(line_start + read_size.value()) = 0x0A;
|
||||
|
||||
// Look for complete lines in buffer
|
||||
@@ -222,8 +227,7 @@ bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freq
|
||||
pos += 2;
|
||||
length = std::min(strcspn(pos, ",\x0A"), (size_t)FREQMAN_DESC_MAX_LEN);
|
||||
description = string(pos, length);
|
||||
} else
|
||||
description = "-";
|
||||
}
|
||||
if ((type == SINGLE && load_freqs) || (type == RANGE && load_ranges) || (type == HAMRADIO && load_hamradios)) {
|
||||
db.push_back({frequency_a, frequency_b, description, type, modulation, bandwidth, step, tone});
|
||||
n++;
|
||||
@@ -231,14 +235,14 @@ bool load_freqman_file_ex(std::string& file_stem, freqman_db& db, bool load_freq
|
||||
}
|
||||
|
||||
line_start = line_end + 1;
|
||||
if (line_start - file_data >= 256) break;
|
||||
if (line_start - file_data >= read_buf_size) break;
|
||||
}
|
||||
|
||||
if (read_size.value() != 256)
|
||||
if (read_size.value() != read_buf_size)
|
||||
break; // End of file
|
||||
|
||||
// Restart at beginning of last incomplete line
|
||||
file_position -= (file_data + 256 - line_start);
|
||||
file_position -= (file_data + read_buf_size - line_start);
|
||||
}
|
||||
|
||||
/* populate implicitly specified modulation / bandwidth */
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
#include "string_format.hpp"
|
||||
#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
|
||||
// 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
|
||||
#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 60 // 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 "60" // STRING OF FREQMAN_MAX_PER_FILE
|
||||
|
||||
using namespace ui;
|
||||
using namespace std;
|
||||
|
||||
@@ -23,23 +23,75 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
static const int8_t transition_map[] = {
|
||||
0, // 0000: noop
|
||||
0, // 0001: start
|
||||
0, // 0010: start
|
||||
0, // 0011: rate
|
||||
1, // 0100: end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: end
|
||||
-1, // 1000: end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: end
|
||||
0, // 1100: rate
|
||||
0, // 1101: start
|
||||
0, // 1110: start
|
||||
0, // 1111: noop
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
// Now supporting multiple levels of rotary encoder dial sensitivity
|
||||
//
|
||||
// Portapack H2 normally has a 30-step encoder, meaning one step (pulse) every
|
||||
// 12 degrees of rotation.
|
||||
//
|
||||
// For each encoder "pulse" there are 4 state transitions, and we can choose
|
||||
// between looking at all of them (high sensitivity), half of them (medium/default),
|
||||
// or one quarter of them (low sensitivity).
|
||||
static const int8_t transition_map[][16] = {
|
||||
// Normal (Medium) Sensitivity -- default
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// Low Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
0, // 0001: ccw start
|
||||
0, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
0, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
0, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
0, // 1101: cw start
|
||||
0, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
// High Sensitivity
|
||||
{
|
||||
0, // 0000: noop
|
||||
-1, // 0001: ccw start
|
||||
1, // 0010: cw start
|
||||
0, // 0011: rate
|
||||
1, // 0100: cw end
|
||||
0, // 0101: noop
|
||||
0, // 0110: rate
|
||||
-1, // 0111: ccw end
|
||||
-1, // 1000: ccw end
|
||||
0, // 1001: rate
|
||||
0, // 1010: noop
|
||||
1, // 1011: cw end
|
||||
0, // 1100: rate
|
||||
1, // 1101: cw start
|
||||
-1, // 1110: ccw start
|
||||
0, // 1111: noop
|
||||
},
|
||||
};
|
||||
|
||||
int_fast8_t Encoder::update(
|
||||
@@ -50,5 +102,6 @@ int_fast8_t Encoder::update(
|
||||
state <<= 1;
|
||||
state |= phase_1;
|
||||
|
||||
return transition_map[state & 0xf];
|
||||
// dial sensitivity setting is stored in pmem
|
||||
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ Samples get() {
|
||||
const auto yp_reg = LPC_ADC0->DR[portapack::adc0_touch_yp_input];
|
||||
const auto yn_reg = LPC_ADC0->DR[portapack::adc0_touch_yn_input];
|
||||
return {
|
||||
(xp_reg >> 6) & 0x3ff,
|
||||
(xn_reg >> 6) & 0x3ff,
|
||||
(yp_reg >> 6) & 0x3ff,
|
||||
(yn_reg >> 6) & 0x3ff,
|
||||
((xp_reg >> 6) & 0x3ff) * 16,
|
||||
((xn_reg >> 6) & 0x3ff) * 16,
|
||||
((yp_reg >> 6) & 0x3ff) * 16,
|
||||
((yn_reg >> 6) & 0x3ff) * 16,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@ Metrics calculate_metrics(const Frame& frame) {
|
||||
* fast *enough*?), so maybe leave it alone at least for now.
|
||||
*/
|
||||
|
||||
const auto x_max = frame.x.xp;
|
||||
const auto x_min = frame.x.xn;
|
||||
const auto x_max = frame.x.xp / 16;
|
||||
const auto x_min = frame.x.xn / 16;
|
||||
const auto x_range = x_max - x_min;
|
||||
const float x_position = (frame.x.yp + frame.x.yn) * 0.5f;
|
||||
const float x_position = (frame.x.yp / 16 + frame.x.yn / 16) * 0.5f;
|
||||
const float x_norm = (x_position - x_min) / x_range;
|
||||
|
||||
const auto y_max = frame.y.yn;
|
||||
const auto y_min = frame.y.yp;
|
||||
const auto y_max = frame.y.yn / 16;
|
||||
const auto y_min = frame.y.yp / 16;
|
||||
const auto y_range = y_max - y_min;
|
||||
const float y_position = (frame.y.xp + frame.y.xn) * 0.5f;
|
||||
const float y_position = (frame.y.xp / 16 + frame.y.xn / 16) * 0.5f;
|
||||
const float y_norm = (y_position - y_min) / y_range;
|
||||
|
||||
const auto z_max = frame.pressure.yp;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* 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_freqlist.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ui {
|
||||
FreqManUIList::FreqManUIList(
|
||||
Rect parent_rect,
|
||||
bool instant_exec)
|
||||
: Widget{parent_rect},
|
||||
instant_exec_{instant_exec} {
|
||||
this->set_focusable(true);
|
||||
}
|
||||
|
||||
void FreqManUIList::set_highlighted_index(int index) {
|
||||
if ((unsigned)(current_index + index) >= freqlist_db.size())
|
||||
return;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
if (current_index > 0)
|
||||
current_index--;
|
||||
}
|
||||
if (index >= freqlist_nb_lines) {
|
||||
index = freqlist_nb_lines - 1;
|
||||
if ((unsigned)(current_index + index) < freqlist_db.size())
|
||||
current_index++;
|
||||
else
|
||||
current_index = freqlist_db.size() - freqlist_nb_lines - 1;
|
||||
}
|
||||
highlighted_index = index;
|
||||
}
|
||||
|
||||
uint8_t FreqManUIList::get_index() {
|
||||
return current_index + highlighted_index;
|
||||
}
|
||||
|
||||
void FreqManUIList::paint(Painter& painter) {
|
||||
freqlist_nb_lines = 0;
|
||||
const auto r = screen_rect();
|
||||
uint8_t focused = has_focus();
|
||||
const Rect r_widget_screen{r.left() + focused, r.top() + focused, r.width() - 2 * focused, r.height() - 2 * +focused};
|
||||
painter.fill_rectangle(
|
||||
r_widget_screen,
|
||||
Color::black());
|
||||
// only return after clearing the screen so previous entries are not shown anymore
|
||||
if (freqlist_db.size() == 0)
|
||||
return;
|
||||
// coloration if file is too big
|
||||
Style* text_color = &style_default;
|
||||
if (freqlist_db.size() > FREQMAN_MAX_PER_FILE)
|
||||
text_color = &style_yellow;
|
||||
uint8_t nb_lines = 0;
|
||||
for (uint8_t it = current_index; it < freqlist_db.size(); it++) {
|
||||
uint8_t line_height = (int)nb_lines * char_height;
|
||||
if (line_height < (r.height() - char_height)) {
|
||||
std::string description = freqman_item_string(freqlist_db[it], 30);
|
||||
// line is within the widget
|
||||
painter.draw_string(
|
||||
{0, r.location().y() + (int)nb_lines * char_height},
|
||||
*text_color, description);
|
||||
if (nb_lines == highlighted_index) {
|
||||
const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height};
|
||||
painter.draw_rectangle(
|
||||
r_highlighted_freq,
|
||||
Color::white());
|
||||
}
|
||||
nb_lines++;
|
||||
} else {
|
||||
// rect is filled, we can break
|
||||
break;
|
||||
}
|
||||
}
|
||||
freqlist_nb_lines = nb_lines;
|
||||
if (has_focus() || highlighted()) {
|
||||
const Rect r_focus{r.left(), r.top(), r.width(), r.height()};
|
||||
painter.draw_rectangle(
|
||||
r_focus,
|
||||
Color::white());
|
||||
}
|
||||
}
|
||||
|
||||
void FreqManUIList::set_db(freqman_db& db) {
|
||||
freqlist_db = db;
|
||||
current_index = 0;
|
||||
highlighted_index = 0;
|
||||
}
|
||||
|
||||
void FreqManUIList::on_focus() {
|
||||
if (on_highlight)
|
||||
on_highlight(*this);
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_key(const KeyEvent key) {
|
||||
if (key == KeyEvent::Select) {
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (on_dir) {
|
||||
return on_dir(*this, key);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_touch(const TouchEvent event) {
|
||||
switch (event.type) {
|
||||
case TouchEvent::Type::Start:
|
||||
set_highlighted(true);
|
||||
set_dirty();
|
||||
if (on_touch_press) {
|
||||
on_touch_press(*this);
|
||||
}
|
||||
if (on_select && instant_exec_) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
case TouchEvent::Type::End:
|
||||
set_highlighted(false);
|
||||
set_dirty();
|
||||
if (on_touch_release) {
|
||||
on_touch_release(*this);
|
||||
}
|
||||
if (on_select && !instant_exec_) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool FreqManUIList::on_encoder(EncoderEvent delta) {
|
||||
set_highlighted_index((int)highlighted_index + delta);
|
||||
set_dirty();
|
||||
return true;
|
||||
}
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* 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_FREQLIST_H__
|
||||
#define __UI_FREQLIST_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_font_fixed_5x8.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "event_m0.hpp"
|
||||
#include "message.hpp"
|
||||
#include "freqman.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui {
|
||||
|
||||
class FreqManUIList : public Widget {
|
||||
public:
|
||||
std::function<void(FreqManUIList&)> on_select{};
|
||||
std::function<void(FreqManUIList&)> on_touch_release{}; // Executed when releasing touch, after on_select.
|
||||
std::function<void(FreqManUIList&)> on_touch_press{}; // Executed when touching, before on_select.
|
||||
std::function<bool(FreqManUIList&, KeyEvent)> on_dir{};
|
||||
std::function<void(FreqManUIList&)> on_highlight{};
|
||||
|
||||
FreqManUIList(Rect parent_rect, bool instant_exec); // instant_exec: Execute on_select when you touching instead of releasing
|
||||
FreqManUIList(
|
||||
Rect parent_rect)
|
||||
: FreqManUIList{parent_rect, false} {
|
||||
}
|
||||
FreqManUIList()
|
||||
: FreqManUIList{{}, {}} {
|
||||
}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void on_focus() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
void set_highlighted_index(int index); // set highlighted_index and return capped highlighted_index value to set in caller
|
||||
uint8_t get_index(); // return highlighed + index
|
||||
void set_db(freqman_db& db);
|
||||
|
||||
private:
|
||||
Style style_default{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
Style style_yellow{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::yellow(),
|
||||
};
|
||||
static constexpr int8_t char_height = 16;
|
||||
bool instant_exec_{false};
|
||||
freqman_db freqlist_db{};
|
||||
int current_index{0};
|
||||
int highlighted_index{0};
|
||||
int freqlist_nb_lines{0};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif /*__UI_FREQLIST_H__*/
|
||||
@@ -807,10 +807,6 @@ void cpu_clock_init(void) {
|
||||
// CCU2_CLK_APLL_CFG = 0;
|
||||
// CCU2_CLK_SDIO_CFG = 0;
|
||||
#endif
|
||||
|
||||
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
|
||||
clkin_detect_init();
|
||||
}
|
||||
}
|
||||
|
||||
clock_source_t activate_best_clock_source(void) {
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
#include "usb_type.h"
|
||||
#include "usb_descriptor.h"
|
||||
|
||||
#define USB_VENDOR_ID (0x1D50)
|
||||
#define USB_VENDOR_ID (0x0781) /* SanDisk Corp. */
|
||||
|
||||
#ifdef HACKRF_ONE
|
||||
#define USB_PRODUCT_ID (0x6089)
|
||||
#define USB_PRODUCT_ID (0xa7a8) /* SD card reader */
|
||||
#elif JAWBREAKER
|
||||
#define USB_PRODUCT_ID (0x604B)
|
||||
#elif RAD1O
|
||||
@@ -37,7 +37,7 @@
|
||||
#define USB_PRODUCT_ID (0xFFFF)
|
||||
#endif
|
||||
|
||||
#define USB_API_VERSION (0x0107)
|
||||
#define USB_API_VERSION (0x0127) /* hardware revision */
|
||||
|
||||
#define USB_WORD(x) (x & 0xFF), ((x >> 8) & 0xFF)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class Optional {
|
||||
: value_{std::move(value)}, valid_{true} {}
|
||||
|
||||
bool is_valid() const { return valid_; }
|
||||
operator bool() const { return valid_; }
|
||||
|
||||
// TODO: Throw if not valid?
|
||||
T& value() & { return value_; }
|
||||
|
||||
@@ -302,6 +302,9 @@ struct data_t {
|
||||
uint32_t frequency_tx_correction;
|
||||
bool updown_frequency_tx_correction;
|
||||
|
||||
// Rotary encoder dial sensitivity (encoder.cpp/hpp)
|
||||
uint8_t encoder_dial_sensitivity;
|
||||
|
||||
constexpr data_t()
|
||||
: structure_version(data_structure_version_enum::VERSION_CURRENT),
|
||||
tuned_frequency(tuned_frequency_reset_value),
|
||||
@@ -337,7 +340,8 @@ struct data_t {
|
||||
frequency_rx_correction(0),
|
||||
updown_frequency_rx_correction(0),
|
||||
frequency_tx_correction(0),
|
||||
updown_frequency_tx_correction(0) {
|
||||
updown_frequency_tx_correction(0),
|
||||
encoder_dial_sensitivity(0) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -808,6 +812,14 @@ void set_config_freq_rx_correction(uint32_t v) {
|
||||
data->frequency_rx_correction = v;
|
||||
}
|
||||
|
||||
// rotary encoder dial settings
|
||||
uint8_t config_encoder_dial_sensitivity() {
|
||||
return data->encoder_dial_sensitivity;
|
||||
}
|
||||
void set_encoder_dial_sensitivity(uint8_t v) {
|
||||
data->encoder_dial_sensitivity = v;
|
||||
}
|
||||
|
||||
// sd persisting settings
|
||||
int save_persistent_settings_to_file(std::string filename) {
|
||||
delete_file(filename);
|
||||
|
||||
@@ -99,6 +99,13 @@ struct backlight_config_t {
|
||||
bool _timeout_enabled;
|
||||
};
|
||||
|
||||
enum encoder_dial_sensitivity {
|
||||
DIAL_SENSITIVITY_MEDIUM = 0,
|
||||
DIAL_SENSITIVITY_LOW = 1,
|
||||
DIAL_SENSITIVITY_HIGH = 2,
|
||||
NUM_DIAL_SENSITIVITY
|
||||
};
|
||||
|
||||
namespace cache {
|
||||
|
||||
/* Set values in cache to sensible defaults. */
|
||||
@@ -199,6 +206,8 @@ void set_config_login(bool v);
|
||||
void set_config_speaker(bool v);
|
||||
void set_config_backlight_timer(const backlight_config_t& new_value);
|
||||
void set_disable_touchscreen(bool v);
|
||||
uint8_t config_encoder_dial_sensitivity();
|
||||
void set_encoder_dial_sensitivity(uint8_t v);
|
||||
|
||||
// uint8_t ui_config_textentry();
|
||||
// void set_config_textentry(uint8_t new_value);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 188 B |
@@ -36,11 +36,13 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
||||
${PROJECT_SOURCE_DIR}/main.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||
)
|
||||
|
||||
target_include_directories(application_test PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../../application
|
||||
${COMMON}
|
||||
${PORTINC}
|
||||
${KERNINC}
|
||||
@@ -49,6 +51,7 @@ target_include_directories(application_test PRIVATE
|
||||
${PLATFORMINC}
|
||||
${BOARDINC}
|
||||
${CHIBIOS}/os/various
|
||||
${FATFSINC}
|
||||
${BASEBAND}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright (C) 2023
|
||||
*
|
||||
* 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 "doctest.h"
|
||||
#include "file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
/* Mocks the File interface with a backing string. */
|
||||
class MockFile {
|
||||
public:
|
||||
using Error = File::Error;
|
||||
using Offset = File::Offset;
|
||||
using Size = File::Size;
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
|
||||
MockFile(std::string data)
|
||||
: data_{std::move(data)} {}
|
||||
|
||||
Size size() { return data_.size(); }
|
||||
|
||||
Result<Offset> seek(uint32_t offset) {
|
||||
if (offset >= size())
|
||||
return {static_cast<Error>(FR_BAD_SEEK)};
|
||||
|
||||
auto previous = offset_;
|
||||
offset_ = offset;
|
||||
return previous;
|
||||
}
|
||||
|
||||
Result<Size> read(void* data, Size bytes_to_read) {
|
||||
if (offset_ + bytes_to_read > size())
|
||||
bytes_to_read = size() - offset_;
|
||||
|
||||
if (bytes_to_read == 0 || bytes_to_read > size()) // NB: underflow wrap
|
||||
return 0;
|
||||
|
||||
memcpy(data, &data_[offset_], bytes_to_read);
|
||||
offset_ += bytes_to_read;
|
||||
return bytes_to_read;
|
||||
}
|
||||
|
||||
std::string data_;
|
||||
uint32_t offset_{0};
|
||||
};
|
||||
|
||||
/* Verifies correctness of MockFile. */
|
||||
TEST_SUITE("Test MockFile") {
|
||||
SCENARIO("File size") {
|
||||
GIVEN("Empty string") {
|
||||
MockFile f{""};
|
||||
|
||||
THEN("size() should be 0.") {
|
||||
CHECK_EQ(f.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Not empty string") {
|
||||
MockFile f{"abc"};
|
||||
|
||||
THEN("size() should be string length.") {
|
||||
CHECK_EQ(f.size(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File seek") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
WHEN("seek() negative offset") {
|
||||
auto r = f.seek(-1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset is size()") {
|
||||
auto r = f.seek(f.size());
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset > size()") {
|
||||
auto r = f.seek(f.size() + 1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset < size()") {
|
||||
auto r = f.seek(1);
|
||||
|
||||
THEN("Result should be ok") {
|
||||
CHECK(r.is_ok());
|
||||
}
|
||||
|
||||
r = f.seek(3);
|
||||
|
||||
THEN("Result should be previous offset") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File read") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
const auto buf_len = 10;
|
||||
std::string buf;
|
||||
buf.resize(buf_len);
|
||||
|
||||
WHEN("Reading") {
|
||||
auto r = f.read(&buf[0], 3);
|
||||
|
||||
THEN("Result should be number of bytes read") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 3);
|
||||
}
|
||||
|
||||
buf.resize(*r);
|
||||
THEN("Buffer should contain read data") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "abc");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should continue where it left off") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "\nde");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should stop at the end of the file") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
|
||||
buf.resize(*r);
|
||||
CHECK_EQ(buf.length(), 1);
|
||||
CHECK_EQ(buf, "f");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading block larger than file size") {
|
||||
auto r = f.read(&buf[0], buf_len);
|
||||
buf.resize(*r);
|
||||
|
||||
THEN("It should read to file end.") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 7);
|
||||
CHECK_EQ(buf, f.data_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_BEGIN("Test BufferWrapper");
|
||||
|
||||
TEST_CASE("It can wrap a MockFile.") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
}
|
||||
|
||||
SCENARIO("Empty file") {
|
||||
GIVEN("An empty file") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 0);
|
||||
REQUIRE_EQ(w.line_count(), 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Basic file") {
|
||||
GIVEN("A file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 7);
|
||||
REQUIRE_EQ(w.line_count(), 2);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 4);
|
||||
|
||||
r = w.line_range(1);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 4);
|
||||
CHECK_EQ(r->end, 7);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 4);
|
||||
CHECK_EQ(w.line_length(1), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading file lines.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Reading a line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 4); // Includes '\n'
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading the last line") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 3);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading past the last line") {
|
||||
auto str = w.get_text(w.line_count(), 0, 10);
|
||||
|
||||
THEN("It should return empty value.") {
|
||||
REQUIRE(!str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading with cache miss.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef\nghi\njkl\nmno"};
|
||||
constexpr uint32_t cache_size = 2;
|
||||
auto w = wrap_buffer<cache_size>(f);
|
||||
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
|
||||
WHEN("Reading a cached line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line after last cached line.") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "mno");
|
||||
}
|
||||
|
||||
THEN("It should push cache window forward to include line.") {
|
||||
CHECK_EQ(w.start_line(), w.line_count() - cache_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line before first cached line.") {
|
||||
// First move cache forward to end.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line 0 before first cached line.") {
|
||||
// First move cache forward to end, then back to beginning.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -34,9 +34,28 @@ TEST_CASE("Instance with value should be valid.") {
|
||||
REQUIRE(o.is_valid());
|
||||
}
|
||||
|
||||
TEST_CASE("Instance with value should be return true.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE((bool)o);
|
||||
}
|
||||
|
||||
TEST_CASE("value() should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(o.value() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator* should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(*o == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator-> should return value members.") {
|
||||
struct S {
|
||||
int i;
|
||||
};
|
||||
|
||||
Optional<S> o{S{1}};
|
||||
REQUIRE(o->i == 1);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
Reference in New Issue
Block a user