Compare commits

...

13 Commits

Author SHA1 Message Date
Kyle Reed e50d8dc148 Move file_wrapper and make testable. (#1085)
* WIP Move file_wrapper and make testable.

* More tests, get text_editor compiling

* Back to working

* Run formatter

---------

Co-authored-by: kallanreed <kallanreed@outlook.com>
2023-05-28 08:44:21 -07:00
gullradriel 23c24355ab Yellow coloration for truncated freqman files (#1086) 2023-05-28 17:09:46 +02:00
Brumi-2021 0dcf2af50d Minor GUI change to the Mic App (#1084)
* Mic App GUI tidy up
2023-05-27 18:13:17 +02:00
Bernd Herzog cf07428a5d changed the vendor id of sd to usb (#1082) 2023-05-27 21:39:04 +12:00
E.T 35b381ae77 Fix SD over USB (#1081)
Remove clkin_detect_init() from cpu_clock_init() mirroring upstream change from the hackrf repo

Co-authored-by: Eisenberger Tamas <e.tamas@iwstudio.hu>
2023-05-27 11:06:05 +12:00
gullradriel 0fc748a234 precision marker tweak on single pass (#1079)
* precision marker tweaks
2023-05-26 22:42:12 +02:00
gullradriel 638ea78db9 cosmetic fix on option config (#1080) 2023-05-26 19:47:41 +02:00
gullradriel e81747ef7b fixed wrong function declaration (#1078) 2023-05-26 19:35:17 +02:00
gullradriel 89e24cb358 Freqman mem reduce (#1076)
-made frequency lists a widget to avoid a full frequency_list copy
-reduced FREQMAN_MAX_PER_FILE to a working limit
2023-05-26 18:17:47 +02:00
Kyle Reed 00667cecf9 Notepad menu (#1072)
* WIP notepad menu
2023-05-26 10:02:17 +02:00
zxkmm 53fcdedb88 init commit for fix touchscreen bad corner (#1071)
fix a rounding issue and allow more sensible touchscreen
2023-05-26 09:50:32 +02:00
zxkmm 5bdf9363e9 fix for bad track count (#1069)
* fix for bad track count
* code format
* Thanks kallanreed for providing a more clear logic!
2023-05-25 22:07:27 +02:00
Mark Thompson 6b44a77ef6 Support for Rotary Encoder Dial sensitivity levels, issue #965 (#1057)
* Support for 3 levels of rotary encoder sensitivity #965

Backend support; UI will still need to call set function to configure.

* Support for 3 levels of rotary encoder sensitivity #965

Backend support only.  UI will still need to be changed to call the set_sensitivity() function to configure.

* Removed trailing space

* Deleted blank lines to see if format checker will be happier

* Simpler support for multiple levels of encoder sensitivity, for issue #965

Removed the convoluted code :-) and instead just using a 2-dimensional array to choose which transition map to use.  For now I only have 2 (vs 3) levels enabled as well, to save code space and because high-sensitivity is very touchy.

* Simpler version of configurable encoder sensitivity, issue #965

* Formatting

* Formatting test for Clang

* Formatting test

* Formatting (removed helpful comment)

* Formatting test (remove commented-out code)

* Formatting & swapping medium/low so default mode=0

* Swapped medium/low so default mode=0

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Adding UI & PMEM support to make encoder dial sensitivity configurable, issue #965

* Removed unneeded range check (trusting in pmem checksum)
2023-05-24 19:32:12 -07:00
33 changed files with 1693 additions and 619 deletions
+1
View File
@@ -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
}};
+11 -33
View File
@@ -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);
};
+4 -3
View File
@@ -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
+34 -32
View File
@@ -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,
+8 -5
View File
@@ -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);
}
+6 -4
View File
@@ -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{
+13 -4
View File
@@ -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);
+32 -3
View File
@@ -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
}
+32
View File
@@ -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);
+232 -312
View File
@@ -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
+156 -103
View File
@@ -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},
""};
};
+30 -30
View File
@@ -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},
+6 -5
View File
@@ -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();
+28 -10
View File
@@ -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;
+342
View File
@@ -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__
+16 -12
View File
@@ -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 */
+4 -4
View File
@@ -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;
+71 -18
View File
@@ -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];
}
+4 -4
View File
@@ -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,
};
}
+6 -6
View File
@@ -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;
+162
View File
@@ -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 */
+84
View File
@@ -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)
+1
View File
@@ -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

+3
View File
@@ -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();