Compare commits

...

20 Commits

Author SHA1 Message Date
gullradriel 92072b4225 removed redundant call (#1282)
Co-authored-by: GullCode <gullradriel@hotmail.com>
2023-07-17 15:31:36 -05:00
gullradriel 4e823dd3a3 Recon adjustements (#1280)
* Fix offset by 1 error in button_remove, added back load_freqfile(true) in restart
* removed badly duplicated load freqman file
2023-07-17 22:19:39 +02:00
gullradriel 53d9cecdaf Encoder default setting (#1281)
* fix default encoder value to normal
* use enum name instead of hardcoded value
2023-07-17 22:16:40 +02:00
Mark Thompson 680f1f49ff Resolve compile error (#1279) 2023-07-17 21:31:25 +02:00
Kyle Reed 6574272ca8 Freqman improvements (#1276)
* Show .1 MHz in pretty freqman string

* Refactor load to user FreqmanDB

* Recon file parsing cleanup

* use strtol for parse_int

* recon file cleanup

* Fix bugs in Recon changes

* PR feedback

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-17 11:43:37 -07:00
Brumi-2021 f5c4aa2be2 Correct_RXBW_char_7_spaces_in_Mic_App (#1278)
* Correct_RXBW_char_7_spaces_in_Mic_App

* format_issues_Mic_app

* using_freqman_functions_and_solving_other_defaults
2023-07-17 09:29:24 -05:00
Mark Thompson 2498861003 Increased number of encoder dial sensitivity setting levels (#1275)
* Encoder dial sensitivity 0-4

* Workaround for worn encoders & more sensitivity levels

* Workaround for worn encoders & more sensitivity levels

* Encoder sensitivity levels 0-4

* Encoder sensitivity levels 0-4

* Fix comment

* Clang

* Clang

* Change default encoder sensitivity in pmem

* Set default encoder sensitivity

* Set default encoder sensitivity

* Revert widget type for encoder sensitivity

* Revert widget type for encoder dial sensitivity

* Revert worn encoder workaround (didn't work)

* 5 levels of encoder dial sensitivity

* 5 levels of encoder dial sensitivity
2023-07-16 17:05:27 -05:00
Kyle Reed 25923e82a4 New Freqman edit UI (#1272)
* WIP new edit UI

* Fix textfield highlight

* WIP new edit

* Wrap up first pass of freqman edit

* Fix indexing of options

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-14 18:46:39 -07:00
Mark Thompson 61cb57e48d Added to_string_rounded_freq() function (#1271)
* Added to_string_rounded_freq function

* Added to_string_rounded_freq function

* Clang

* Clang try #2

* Use constexpr for pow10 array

* Additional string_format test cases

* Specify std=c++17 for g++ versions that default to an older standard

* Corrected string_format test cases
2023-07-14 13:38:02 -05:00
Kyle Reed 4985d836ea Support save prompt on back arrow (#1270)
* Support save prompt on back arrow
* Add lifetime warning comment
2023-07-13 18:38:40 +02:00
eried e1a79a0ff1 Fonts for pcb 2023-07-13 15:21:05 +02:00
Kyle Reed d72d935607 Support showing invalid entries in Freqman and allow minor edits (#1269)
* Support showing invalid entries in Freqman and allow minor edits
* Use light_grey instead of grey
* Fix comment
* Fix null in description bug
* Fix spacing in delete entry dialog
* trim in delete modal
2023-07-13 08:28:27 +02:00
Mark Thompson 4ed06b9eff Correct HamRadio frequency validation (#1266) 2023-07-12 20:27:02 +02:00
Mark Thompson fa4623db7c Print leading zeroes after decimal point (#1265) 2023-07-12 13:00:45 -05:00
Mark Thompson a44e8b9921 Use unicode standard 0xA0-0xFF for Latin 1 characters (#1263)
* Update chars 0xA0-0xFF to match unicode

* Update ui_text.hpp

Update chars 0xA0-0xFF to match unicode

* Use unicode 0xB0 for degree character

* Clang
2023-07-12 07:14:00 -05:00
jLynx 6b4f1a552e Changelog refactoring (#1264)
* Delete stable_changelog.py

* Update changelog.py

* Update create_stable_release.yml
2023-07-12 18:50:39 +12:00
pathetic nihilist 3d32682a67 time counting fix (#1261) 2023-07-12 13:10:52 +12:00
Kyle Reed 29e495a17f Freqman UI (#1255)
* FreqmanDB direct file
* Clear UI for short lists
* Final touches on freqlist UI.
* Support vertical alignment in NewButton
* New buttons in FreqMan
* Wiring up UI to filewrapper actions
* Work around empty file
2023-07-11 22:48:36 +02:00
Tragic nihilist 0c599f7d3a Changelog fix 3rd way (#1259)
* init_commit_for_changelog_fix_2nd_way

* 2nd_commit_for_changelog_fix_2nd_way

* methods-3

* add_more_exception
2023-07-11 09:51:22 +02:00
jLynx 006e4c9fce Smaller SD card ZIP (#1257)
* Update create_nightly_release.yml

* Update create_stable_release.yml
2023-07-10 19:43:39 +12:00
82 changed files with 2952 additions and 1268 deletions
+44 -17
View File
@@ -1,28 +1,55 @@
import os
import re
import sys
raw_git = os.popen('git log next --since="24 hours" --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
import requests
from datetime import datetime, timedelta, timezone
# Set up your personal access token and the repository details
token = os.environ.get('GH_TOKEN')
repo_owner = "eried"
repo_name = "portapack-mayhem"
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
def print_stable_changelog(previous_sha):
url = f"compare/{previous_sha}...next"
commits = handle_get_request(url)
for commit in commits["commits"]:
# Print the commit details
print(format_output(commit))
pattern = re.compile("[$@+&?].*[$@+&?]")
if pattern.match(stripped):
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
stripped = re.match(r'.+?(?=-)', stripped).group(0)
def print_nightly_changelog():
# Calculate the date and time 24 hours ago
since_time = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat() # Check that this is UTC
url = "commits"
commits = handle_get_request(url, since_time)
for commit in commits:
# Print the commit details
print(format_output(commit))
def handle_get_request(path, offset=None):
headers = {"Authorization": f"Bearer {token}"}
params = {"since": offset}
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
response = requests.get(url_base + path, headers=headers, params=params)
# Check if the request was successful (status code 200)
if response.status_code == 200:
return response.json()
else:
stripped = re.sub(r'^.*?-', "", stripped)
return "@" + stripped
print(f"Request failed with status code: {response.status_code}")
return None
def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
return line
def format_output(commit):
message_lines = commit["commit"]["message"].split("\n")
author = commit["author"]["login"] if commit["author"] and "login" in commit["author"] else commit["commit"]["author"]["name"]
return '- ' + commit["sha"][:8] + ' - @' + author + ': ' + message_lines[0]
for row in raw_git.splitlines():
print(compile_line(row))
if len(sys.argv) < 2:
print_nightly_changelog()
else:
past_version = sys.argv[1]
print_stable_changelog(past_version)
@@ -47,6 +47,9 @@ jobs:
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -60,6 +63,8 @@ jobs:
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
CHANGELOG=$(python3 .github/workflows/changelog.py)
CHANGELOG="${CHANGELOG//'%'/'%25'}"
@@ -103,3 +108,13 @@ jobs:
asset_path: ./sdcard.zip
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD.zip
asset_content_type: application/zip
- name: Upload SD Card Assets - No Map
id: upload-sd-card-asset-no-map
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sdcard-no-map.zip
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD-no-world-map.zip
asset_content_type: application/zip
+16 -1
View File
@@ -31,6 +31,9 @@ jobs:
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -44,8 +47,10 @@ jobs:
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
CHANGELOG=$(python3 .github/workflows/stable_changelog.py ${{ steps.past_version.outputs.past_version }})
CHANGELOG=$(python3 .github/workflows/changelog.py ${{ steps.past_version.outputs.past_version }})
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
@@ -96,4 +101,14 @@ jobs:
asset_path: ./sdcard.zip
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD.zip
asset_content_type: application/zip
- name: Upload SD Card Assets - No Map
id: upload-sd-card-asset-no-map
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sdcard-no-map.zip
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD-no-world-map.zip
asset_content_type: application/zip
-30
View File
@@ -1,30 +0,0 @@
import os
import re
import sys
past_version = sys.argv[1]
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
pattern = re.compile("[$@+&?].*[$@+&?]")
if pattern.match(stripped):
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
stripped = re.match(r'.+?(?=-)', stripped).group(0)
else:
stripped = re.sub(r'^.*?-', "", stripped)
return "@" + stripped
def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
return line
for row in raw_git.splitlines():
print(compile_line(row))
@@ -25,10 +25,10 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "freqman.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "ui_freqman.hpp"
#include "utility.hpp"
using namespace portapack;
@@ -23,6 +23,7 @@
#include "capture_app.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
@@ -31,7 +31,6 @@
#include "ui_spectrum.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "freqman.hpp"
namespace ui {
+401 -177
View File
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -22,279 +23,502 @@
#include "ui_freqman.hpp"
#include "portapack.hpp"
#include "event_m0.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
#include "tone_key.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "utility.hpp"
#include <memory>
using namespace portapack;
using namespace ui;
namespace fs = std::filesystem;
// TODO: Clean up after moving to better lookup tables.
using options_t = OptionsField::options_t;
extern options_t freqman_modulations;
extern options_t freqman_bandwidths[4];
extern options_t freqman_steps;
extern options_t freqman_steps_short;
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
option.set_options(freqman_modulations);
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
if (is_valid(modulation))
option.set_options(freqman_bandwidths[modulation]);
}
void freqman_set_step_option(OptionsField& option) {
option.set_options(freqman_steps);
}
void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_steps_short);
}
namespace ui {
static int32_t current_category_id = 0;
/* FreqManBaseView ***************************************/
size_t FreqManBaseView::current_category_index = 0;
FreqManBaseView::FreqManBaseView(
NavigationView& nav)
: nav_(nav) {
add_children({&options_category,
&label_category,
&button_exit});
add_children(
{&label_category,
&options_category,
&button_exit});
// initialize
refresh_list();
options_category.on_change = [this](size_t category_id, int32_t) {
change_category(category_id);
options_category.on_change = [this](size_t new_index, int32_t) {
change_category(new_index);
};
options_category.set_selected_index(current_category_id);
button_exit.on_select = [this, &nav](Button&) {
nav.pop();
};
refresh_categories();
};
void FreqManBaseView::focus() {
button_exit.focus();
// TODO: Shouldn't be on focus.
if (error_ == ERROR_ACCESS) {
nav_.display_modal("Error", "File acces error", ABORT, nullptr);
nav_.display_modal("Error", "File access error", ABORT, nullptr);
} else if (error_ == ERROR_NOFILES) {
nav_.display_modal("Error", "No database files\nin /freqman", ABORT, nullptr);
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT, nullptr);
} else {
options_category.focus();
}
}
void FreqManBaseView::get_freqman_files() {
// Assume this does change much, clear will preserve the existing alloc.
file_list.clear();
void FreqManBaseView::change_category(size_t new_index) {
if (categories().empty())
return;
auto files = scan_root_files(u"FREQMAN", u"*.TXT");
for (auto file : files) {
std::string file_name = file.stem().string();
// don't propose tmp / hidden files in freqman's list
if (file_name.length() && file_name[0] != '.') {
file_list.emplace_back(std::move(file_name));
}
}
};
void FreqManBaseView::change_category(int32_t category_id) {
current_category_id = category_id;
if (file_list.empty()) return;
if (!load_freqman_file(file_list[categories[category_id].second], database, {})) {
current_category_index = new_index;
if (!db_.open(get_freqman_path(current_category()))) {
error_ = ERROR_ACCESS;
}
freqlist_view.set_db(database);
text_empty.hidden(!database.empty());
set_dirty();
freqlist_view.set_db(db_);
}
void FreqManBaseView::refresh_list() {
categories.clear();
get_freqman_files();
void FreqManBaseView::refresh_categories() {
OptionsField::options_t new_categories;
for (size_t n = 0; n < file_list.size(); n++)
categories.emplace_back(std::make_pair(file_list[n].substr(0, 14), n));
scan_root_files(
freqman_dir, u"*.TXT", [&new_categories](const fs::path& path) {
// Skip temp/hidden files.
if (path.empty() || path.native()[0] == u'.')
return;
// Alphabetical sort
std::sort(categories.begin(), categories.end(), [](auto& left, auto& right) {
// The UI layer will truncate long file names when displaying.
new_categories.emplace_back(path.stem().string(), new_categories.size());
});
// Alphabetically sort the categories.
std::sort(new_categories.begin(), new_categories.end(), [](auto& left, auto& right) {
return left.first < right.first;
});
options_category.set_options(categories);
if ((unsigned)current_category_id >= categories.size())
current_category_id = categories.size() - 1;
// Preserve last selection; ensure in range.
current_category_index = clip(current_category_index, 0u, new_categories.size());
auto saved_index = current_category_index;
options_category.set_options(std::move(new_categories));
options_category.set_selected_index(saved_index);
}
void FrequencySaveView::save_current_file() {
save_freqman_file(file_list[categories[current_category_id].second], database);
nav_.pop();
void FreqManBaseView::refresh_list(int delta_selected) {
// Update the index and ensures in bounds.
freqlist_view.set_index(freqlist_view.get_index() + delta_selected);
freqlist_view.set_dirty();
}
void FrequencySaveView::on_save_name() {
text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) {
database.push_back(std::make_unique<freqman_entry>(freqman_entry{value_, 0, buffer, freqman_type::Single}));
save_current_file();
});
}
void FrequencySaveView::on_save_timestamp() {
database.push_back(std::make_unique<freqman_entry>(freqman_entry{value_, 0, live_timestamp.string(), freqman_type::Single}));
save_current_file();
}
/* FrequencySaveView *************************************/
FrequencySaveView::FrequencySaveView(
NavigationView& nav,
const rf::Frequency value)
: FreqManBaseView(nav),
value_(value) {
desc_buffer.reserve(28);
: FreqManBaseView(nav) {
add_children(
{&labels,
&big_display,
&button_clear,
&button_edit,
&button_save,
&text_description});
// Todo: add back ?
/*for (size_t n = 0; n < database.size(); n++) {
if (database[n].value == value_) {
error_ = ERROR_DUPLICATE;
break;
}
}*/
entry_.type = freqman_type::Single;
entry_.frequency_a = value;
entry_.description = to_string_timestamp(rtc_time::now());
refresh_ui();
add_children({&labels,
&big_display,
&button_save_name,
&button_save_timestamp,
&live_timestamp});
big_display.set(value);
button_save_name.on_select = [this, &nav](Button&) {
on_save_name();
};
button_save_timestamp.on_select = [this, &nav](Button&) {
on_save_timestamp();
button_clear.on_select = [this, &nav](Button&) {
entry_.description = "";
refresh_ui();
};
options_category.on_change = [this, value](size_t category_id, int32_t) {
change_category(category_id);
big_display.set(value);
button_edit.on_select = [this, &nav](Button&) {
temp_buffer_ = entry_.description;
text_prompt(nav_, temp_buffer_, desc_edit_max, [this](std::string& new_desc) {
entry_.description = new_desc;
refresh_ui();
});
};
button_save.on_select = [this, &nav](Button&) {
db_.insert_entry(db_.entry_count(), entry_);
nav_.pop();
};
}
void FrequencyLoadView::refresh_widgets(const bool v) {
freqlist_view.hidden(v);
text_empty.hidden(!v);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black());
set_dirty();
void FrequencySaveView::refresh_ui() {
big_display.set(entry_.frequency_a);
text_description.set(entry_.description);
}
/* FrequencyLoadView *************************************/
FrequencyLoadView::FrequencyLoadView(
NavigationView& nav)
: FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) {
refresh_widgets(v);
};
add_children({&freqlist_view});
add_children({&freqlist_view,
&text_empty});
// Resize to fill screen. +2 keeps text out of border.
freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2});
// Resize menu view to fill screen
freqlist_view.set_parent_rect({0, 3 * 8, 240, 30 * 8});
freqlist_view.on_select = [&nav, this](size_t index) {
auto entry = db_[index];
// TODO: Maybe return center of range if user choses a range when the app
// needs a unique frequency, instead of frequency_a?
auto has_range = entry.type == freqman_type::Range ||
entry.type == freqman_type::HamRadio;
freqlist_view.on_select = [&nav, this](FreqManUIList&) {
auto& entry = database[freqlist_view.get_index()];
if (entry->type == freqman_type::Range) {
if (on_range_loaded)
on_range_loaded(entry->frequency_a, entry->frequency_b);
else if (on_frequency_loaded)
on_frequency_loaded(entry->frequency_a);
// TODO: Maybe return center of range if user choses a range when the app
// needs a unique frequency, instead of frequency_a?
// TODO: HamRadio?
} else {
if (on_frequency_loaded)
on_frequency_loaded(entry->frequency_a);
}
if (on_range_loaded && has_range)
on_range_loaded(entry.frequency_a, entry.frequency_b);
else if (on_frequency_loaded)
on_frequency_loaded(entry.frequency_a);
nav_.pop(); // NB: this will call dtor.
};
freqlist_view.on_leave = [this]() {
button_exit.focus();
};
}
void FrequencyManagerView::on_edit_freq(rf::Frequency f) {
database[freqlist_view.get_index()]->frequency_a = f;
save_freqman_file(file_list[categories[current_category_id].second], database);
change_category(current_category_id);
/* FrequencyManagerView **********************************/
void FrequencyManagerView::on_edit_entry() {
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
edit_view->on_save = [this](const freqman_entry& entry) {
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
void FrequencyManagerView::on_edit_desc(NavigationView& nav) {
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) {
database[freqlist_view.get_index()]->description = std::move(buffer);
save_freqman_file(file_list[categories[current_category_id].second], database);
change_category(current_category_id);
void FrequencyManagerView::on_edit_freq() {
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
freq_edit_view->on_changed = [this](rf::Frequency f) {
auto entry = current_entry();
entry.frequency_a = f;
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
void FrequencyManagerView::on_edit_desc() {
temp_buffer_ = current_entry().description;
text_prompt(nav_, temp_buffer_, desc_edit_max, [this](std::string& new_desc) {
auto entry = current_entry();
entry.description = std::move(new_desc);
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
});
}
void FrequencyManagerView::on_new_category(NavigationView& nav) {
text_prompt(nav, desc_buffer, 12, [this](std::string& buffer) {
File freqman_file;
create_freqman_file(buffer, freqman_file);
refresh_list();
change_category(current_category_id);
void FrequencyManagerView::on_add_category() {
temp_buffer_.clear();
text_prompt(nav_, temp_buffer_, 20, [this](std::string& new_name) {
if (!new_name.empty()) {
create_freqman_file(new_name);
refresh_categories();
}
});
}
void FrequencyManagerView::on_delete() {
if (database.empty()) {
delete_freqman_file(file_list[categories[current_category_id].second]);
refresh_list();
} else {
database.erase(database.begin() + freqlist_view.get_index());
save_freqman_file(file_list[categories[current_category_id].second], database);
}
change_category(current_category_id);
void FrequencyManagerView::on_del_category() {
nav_.push<ModalMessageView>(
"Delete", "Delete " + current_category() + "\nAre you sure?", YESNO,
[this](bool choice) {
if (choice) {
db_.close(); // Ensure file is closed.
auto path = get_freqman_path(current_category());
delete_file(path);
refresh_categories();
}
});
}
void FrequencyManagerView::refresh_widgets(const bool v) {
button_edit_freq.hidden(v);
button_edit_desc.hidden(v);
button_delete.hidden(v);
text_empty.hidden(!v);
freqlist_view.hidden(v);
labels.hidden(v);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black());
set_dirty();
void FrequencyManagerView::on_add_entry() {
freqman_entry entry{
.frequency_a = 100'000'000,
.description = std::string{"Entry "} + to_string_dec_uint(db_.entry_count()),
.type = freqman_type::Single,
};
// Add will insert below the currently selected item.
db_.insert_entry(current_index() + 1, entry);
refresh_list(1);
}
FrequencyManagerView::~FrequencyManagerView() {
// save_freqman_file(file_list[categories[current_category_id].second], database);
void FrequencyManagerView::on_del_entry() {
if (db_.empty())
return;
nav_.push<ModalMessageView>(
"Delete", "Delete " + trim(pretty_string(current_entry(), 23)) + "\nAre you sure?", YESNO,
[this](bool choice) {
if (choice) {
db_.delete_entry(current_index());
refresh_list();
}
});
}
FrequencyManagerView::FrequencyManagerView(
NavigationView& nav)
: FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) {
refresh_widgets(v);
add_children(
{&freqlist_view,
&button_add_category,
&button_del_category,
&button_edit_entry,
&rect_padding,
&button_edit_freq,
&button_edit_desc,
&button_add_entry,
&button_del_entry});
freqlist_view.on_select = [this](size_t) {
button_edit_entry.focus();
};
// Allows for quickly exiting control.
freqlist_view.on_leave = [this]() {
button_edit_entry.focus();
};
add_children({&labels,
&button_new_category,
&freqlist_view,
&text_empty,
&button_edit_freq,
&button_edit_desc,
&button_delete});
freqlist_view.on_select = [this](FreqManUIList&) {
button_edit_freq.focus();
button_add_category.on_select = [this]() {
on_add_category();
};
button_new_category.on_select = [this, &nav](Button&) {
desc_buffer = "";
on_new_category(nav);
button_del_category.on_select = [this]() {
on_del_category();
};
button_edit_freq.on_select = [this, &nav](Button&) {
if (database.empty())
database.push_back(std::make_unique<freqman_entry>(freqman_entry{0, 0, "", freqman_type::Single}));
auto new_view = nav.push<FrequencyKeypadView>(database[freqlist_view.get_index()]->frequency_a);
new_view->on_changed = [this](rf::Frequency f) {
on_edit_freq(f);
};
button_edit_entry.on_select = [this](Button&) {
on_edit_entry();
};
button_edit_desc.on_select = [this, &nav](Button&) {
if (database.empty())
database.push_back(std::make_unique<freqman_entry>(freqman_entry{0, 0, "", freqman_type::Single}));
desc_buffer = database[freqlist_view.get_index()]->description;
on_edit_desc(nav);
button_edit_freq.on_select = [this](Button&) {
on_edit_freq();
};
button_delete.on_select = [this, &nav](Button&) {
on_delete();
button_edit_desc.on_select = [this](Button&) {
on_edit_desc();
};
button_add_entry.on_select = [this]() {
on_add_entry();
};
button_del_entry.on_select = [this]() {
on_del_entry();
};
}
/* FrequencyEditView *************************************/
FrequencyEditView::FrequencyEditView(
NavigationView& nav,
freqman_entry entry)
: nav_{nav},
entry_{std::move(entry)} {
add_children({&labels,
&field_type,
&field_freq_a,
&field_freq_b,
&field_modulation,
&field_bandwidth,
&field_step,
&field_tone,
&field_description,
&text_validation,
&button_save,
&button_cancel});
freqman_set_modulation_option(field_modulation);
populate_bandwidth_options();
populate_step_options();
populate_tone_options();
// Add the "invalid/unset" option. Kind of hacky, but...
field_modulation.options().insert(
field_modulation.options().begin(), {"None", -1});
field_step.options().insert(
field_step.options().begin(), {"None", -1});
field_type.set_by_value((int32_t)entry_.type);
field_type.on_change = [this](size_t, auto value) {
entry_.type = static_cast<freqman_type>(value);
refresh_ui();
};
// TODO: this pattern should be able to be wrapped up.
field_freq_a.set_value(entry_.frequency_a);
field_freq_a.on_change = [this](rf::Frequency f) {
entry_.frequency_a = f;
refresh_ui();
};
field_freq_a.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_a.set_value(f);
};
};
field_freq_b.set_value(entry_.frequency_b);
field_freq_b.on_change = [this](rf::Frequency f) {
entry_.frequency_b = f;
refresh_ui();
};
field_freq_b.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_b.set_value(f);
};
};
field_modulation.set_by_value((int32_t)entry_.modulation);
field_modulation.on_change = [this](size_t, auto value) {
entry_.modulation = static_cast<freqman_index_t>(value);
populate_bandwidth_options();
};
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
field_bandwidth.on_change = [this](size_t, auto value) {
entry_.bandwidth = static_cast<freqman_index_t>(value);
};
field_step.set_by_value((int32_t)entry_.step);
field_step.on_change = [this](size_t, auto value) {
entry_.step = static_cast<freqman_index_t>(value);
};
field_tone.set_by_value((int32_t)entry_.tone);
field_tone.on_change = [this](size_t, auto value) {
entry_.tone = static_cast<freqman_index_t>(value);
};
field_description.set_text(entry_.description);
field_description.on_change = [this](TextField& tf) {
entry_.description = tf.get_text();
};
field_description.on_select = [this](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
[this, &tf](std::string& new_desc) {
tf.set_text(new_desc);
});
};
button_save.on_select = [this](Button&) {
if (on_save)
on_save(std::move(entry_));
nav_.pop();
};
button_cancel.on_select = [this](Button&) {
nav_.pop();
};
refresh_ui();
}
void FrequencyEditView::focus() {
button_cancel.focus();
}
void FrequencyEditView::refresh_ui() {
// This needs to be called whenever a UI option is changed
// in a way that causes fields to be unused or would make the
// entry fail validation.
auto is_range = entry_.type == freqman_type::Range;
auto is_ham = entry_.type == freqman_type::HamRadio;
auto has_freq_b = is_range || is_ham;
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
if (is_valid(entry_)) {
text_validation.set("Valid");
text_validation.set_style(&Styles::green);
} else {
text_validation.set("Error");
text_validation.set_style(&Styles::red);
}
}
// TODO: The following functions shouldn't be needed once
// freqman_db lookup tables are complete.
void FrequencyEditView::populate_bandwidth_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
if (entry_.modulation < std::size(freqman_bandwidths)) {
auto& bandwidths = freqman_bandwidths[entry_.modulation];
for (auto i = 1u; i < bandwidths.size(); ++i) {
auto& item = bandwidths[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
}
field_bandwidth.set_options(std::move(options));
}
void FrequencyEditView::populate_step_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 1u; i < freqman_steps.size(); ++i) {
auto& item = freqman_steps[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
field_step.set_options(std::move(options));
}
void FrequencyEditView::populate_tone_options() {
using namespace tonekey;
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 1u; i < tone_keys.size(); ++i) {
auto& item = tone_keys[i];
options.push_back({fx100_string(item.second), (OptionsField::value_t)i});
}
field_tone.set_options(std::move(options));
}
} // namespace ui
+171 -68
View File
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -20,94 +21,99 @@
* Boston, MA 02110-1301, USA.
*/
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "ui_freqlist.hpp"
#include "ui_menu.hpp"
#include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "ui_textentry.hpp"
#include "freqman.hpp"
#include "ui_freqlist.hpp"
#include "ui_widget.hpp"
namespace ui {
class FreqManBaseView : public View {
public:
FreqManBaseView(
NavigationView& nav);
FreqManBaseView(NavigationView& nav);
void focus() override;
static constexpr size_t desc_edit_max = 0x80;
protected:
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
using options_t = OptionsField::options_t;
NavigationView& nav_;
freqman_error error_{NO_ERROR};
options_t categories{};
std::function<void(void)> on_select_frequency{nullptr};
std::function<void(bool)> on_refresh_widgets{nullptr};
std::function<void(void)> on_select_frequency{};
void get_freqman_files();
void change_category(int32_t category_id);
void refresh_list();
void change_category(size_t new_index);
/* Access the categories directly from the OptionsField.
* This avoids holding multiple copies of the file list. */
const options_t& categories() const { return options_category.options(); }
const auto& current_category() const { return options_category.selected_index_name(); }
auto current_index() const { return freqlist_view.get_index(); }
freqman_entry current_entry() const { return db_[current_index()]; }
void refresh_categories();
void refresh_list(int delta_selected = 0);
freqman_db database{};
std::vector<std::string> file_list{};
FreqmanDB db_{};
/* The top section (category) is 20px tall. */
Labels label_category{
{{0, 4}, "Category:", Color::light_grey()}};
{{0, 2}, "Category:", Color::light_grey()}};
OptionsField options_category{
{9 * 8, 4},
14,
{9 * 8, 2},
14 /* length */,
{}};
FreqManUIList freqlist_view{
{0, 3 * 8, 240, 23 * 8}};
Text text_empty{
{7 * 8, 12 * 8, 16 * 8, 16},
"Empty category !",
};
{0, 3 * 8, screen_width, 12 * 16 + 2 /* 2 Keeps text out of border. */}};
Button button_exit{
{16 * 8, 34 * 8, 14 * 8, 4 * 8},
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Exit"};
private:
protected:
/* Static so selected category is persisted across UI instances. */
static size_t current_category_index;
};
// TODO: support for new category.
class FrequencySaveView : public FreqManBaseView {
public:
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
std::string title() const override { return "Save freq."; };
std::string title() const override { return "Save freq"; }
private:
std::string desc_buffer{};
rf::Frequency value_{};
std::string temp_buffer_{};
freqman_entry entry_{};
void on_save_name();
void on_save_timestamp();
void save_current_file();
void refresh_ui();
BigFrequency big_display{
{4, 2 * 16, 28 * 8, 32},
{0, 2 * 16, 28 * 8, 4 * 16},
0};
Labels labels{
{{1 * 8, 12 * 8}, "Save as:", Color::white()}};
{{0 * 8, 6 * 16}, "Description:", Color::white()}};
Button button_save_name{
{1 * 8, 17 * 8, 12 * 8, 48},
"Name (set)"};
Button button_save_timestamp{
{1 * 8, 25 * 8, 12 * 8, 48},
"Timestamp:"};
LiveDateTime live_timestamp{
{14 * 8, 27 * 8, 16 * 8, 16}};
Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}};
Button button_clear{
{4 * 8, 10 * 16, 10 * 8, 2 * 16},
"Clear"};
Button button_edit{
{16 * 8, 10 * 16, 10 * 8, 2 * 16},
"Edit"};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Save"};
};
class FrequencyLoadView : public FreqManBaseView {
@@ -116,46 +122,143 @@ class FrequencyLoadView : public FreqManBaseView {
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
FrequencyLoadView(NavigationView& nav);
std::string title() const override { return "Load freq."; };
private:
void refresh_widgets(const bool v);
std::string title() const override { return "Load freq"; }
};
class FrequencyManagerView : public FreqManBaseView {
public:
FrequencyManagerView(NavigationView& nav);
~FrequencyManagerView();
std::string title() const override { return "Freqman"; };
std::string title() const override { return "Freqman"; }
private:
std::string desc_buffer{};
std::string temp_buffer_{};
void refresh_widgets(const bool v);
void on_edit_freq(rf::Frequency f);
void on_edit_desc(NavigationView& nav);
void on_new_category(NavigationView& nav);
void on_delete();
void on_edit_entry();
void on_edit_freq();
void on_edit_desc();
void on_add_category();
void on_del_category();
void on_add_entry();
void on_del_entry();
Labels labels{
{{4 * 8 + 4, 26 * 8}, "Edit:", Color::light_grey()}};
NewButton button_add_category{
{23 * 8, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_new_file,
Color::white(),
true};
Button button_new_category{
{23 * 8, 2, 7 * 8, 20},
"New"};
NewButton button_del_category{
{26 * 8 + 4, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_trash,
Color::red(),
true};
Button button_edit_entry{
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
"Edit"};
Rectangle rect_padding{
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
Color::grey()};
Button button_edit_freq{
{0 * 8, 29 * 8, 14 * 8, 32},
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
"Frequency"};
Button button_edit_desc{
{0 * 8, 34 * 8, 14 * 8, 32},
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Description"};
Button button_delete{
{16 * 8, 29 * 8, 14 * 8, 32},
"Delete"};
NewButton button_add_entry{
{15 * 8, 15 * 16, 7 * 8 + 4, 2 * 16},
{},
&bitmap_icon_add,
Color::white(),
true};
NewButton button_del_entry{
{22 * 8 + 4, 15 * 16, 7 * 8 + 4, 2 * 16},
{},
&bitmap_icon_delete,
Color::red(),
true};
};
class FrequencyEditView : public View {
public:
std::function<void(const freqman_entry&)> on_save{};
FrequencyEditView(
NavigationView& nav,
freqman_entry entry);
std::string title() const override { return "Freqman Edit"; }
void focus() override;
private:
NavigationView& nav_;
freqman_entry entry_;
std::string temp_buffer_{};
void refresh_ui();
void populate_bandwidth_options();
void populate_step_options();
void populate_tone_options();
Labels labels{
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
};
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
{"Single", 0},
{"Range", 1},
{"HamRadio", 2},
{"Raw", 3},
}};
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
TextField field_description{
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
{}};
Text text_validation{
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
{}};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Save"};
Button button_cancel{
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Cancel"};
};
} /* namespace ui */
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(ui::OptionsField& option);
void freqman_set_step_option(ui::OptionsField& option);
void freqman_set_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option);
+1
View File
@@ -23,6 +23,7 @@
#include "ui_level.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
#include "file.hpp"
using namespace portapack;
+12 -12
View File
@@ -24,21 +24,21 @@
#ifndef _UI_LEVEL
#define _UI_LEVEL
#include "ui.hpp"
#include "receiver_model.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "freqman.hpp"
#include "analog_audio_app.hpp"
#include "audio.hpp"
#include "ui_mictx.hpp"
#include "portapack_persistent_memory.hpp"
#include "baseband_api.hpp"
#include "ui_spectrum.hpp"
#include "string_format.hpp"
#include "file.hpp"
#include "app_settings.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "file.hpp"
#include "freqman_db.hpp"
#include "portapack_persistent_memory.hpp"
#include "radio_state.hpp"
#include "receiver_model.hpp"
#include "string_format.hpp"
#include "ui.hpp"
#include "ui_mictx.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
#include "ui_styles.hpp"
namespace ui {
+17 -22
View File
@@ -25,6 +25,7 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "irq_controls.hpp"
#include "ui_freqman.hpp"
#include "portapack_hal.hpp"
#include "string_format.hpp"
#include "tonesets.hpp"
@@ -355,13 +356,10 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
options_tone_key.hidden(0); // we are in FM mode, we should have active the Key-tones & CTCSS option.
rxbw.emplace_back(" NFM1:8k5 ", 0); // restore the original dynamic field_rxbw value.
rxbw.emplace_back(" NFM2:11k ", 1);
rxbw.emplace_back(" FM :16k ", 2);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
field_rxbw.set_by_value(2); // // bw 16k (2) default
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
break;
case 1: //{ "WFM", 1 }
enable_am = false;
@@ -375,13 +373,10 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
options_tone_key.hidden(0); // we are in WFM mode, we should have active the Key-tones & CTCSS option.
rxbw.emplace_back(" 200k-WFM ", 0); // We allow the user selection of the 3 x WFM BW filters, (0) WFM-200K, (1) WFM-180K, (2) WFM-40K.
rxbw.emplace_back(" 180k-WFM ", 1);
rxbw.emplace_back(" 40k-WFM ", 2);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
field_rxbw.set_by_value(0); // bw 200k (0) default
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
break;
case 2: //{ "AM", 2 }
enable_am = true;
@@ -390,8 +385,8 @@ MicTXView::MicTXView(
set_dirty(); // Refresh display
options_tone_key.hidden(1); // we hide that Key-tones & CTCSS input selecction, (no meaning in AM/DSB/SSB).
rxbw.emplace_back(" DSB1-9k ", 0); // we offer in AM DSB two audio BW 9k / 6k.
rxbw.emplace_back(" DSB2-6k ", 1);
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
rxbw.emplace_back("DSB 6k", 1);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
field_rxbw.hidden(0); // we show fixed RX AM BW 6Khz
@@ -404,8 +399,8 @@ MicTXView::MicTXView(
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
check_rogerbeep.hidden(1); // hide that roger beep selection.
rxbw.emplace_back(" USB+3k ", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
set_dirty(); // Refresh display
break;
@@ -415,8 +410,8 @@ MicTXView::MicTXView(
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
check_rogerbeep.hidden(1); // hide that roger beep selection.
rxbw.emplace_back(" LSB-3k ", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
set_dirty(); // Refresh display
break;
@@ -425,8 +420,8 @@ MicTXView::MicTXView(
rxaudio(rx_enabled); // Update now if we have RX audio on
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
rxbw.emplace_back("SSB1:USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-C case to the field_rxbw value.
rxbw.emplace_back("SSB2:LSB-3k", 1);
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
rxbw.emplace_back("LSB-3k", 1);
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
+6 -6
View File
@@ -180,7 +180,7 @@ class MicTXView : public View {
OptionsField options_ak4951_alc_mode{
{20 * 8, 1 * 8},
11,
10, // Label has 10 chars
{
{" OFF-12kHz", 0}, // Nothing changed from ORIGINAL, keeping ALL programmable AK4951 Digital Block->OFF, sampling 24Khz)
{"+12dB-6kHz", 1}, // ALC-> on, (+12dB's) Auto Vol max + Wind Noise cancel + LPF 6kHz + Pre-amp Mic (+21dB=original)
@@ -198,7 +198,7 @@ class MicTXView : public View {
OptionsField options_wm8731_boost_mode{
{22 * 8, 1 * 8},
5,
8, // Label has 8 chars
{
{"ON +12dB", 0}, // WM8731 Mic Boost ON, original+12dBs condition, easy to saturate ADC sat in high voice, relative G = +12 dB's respect ref level
{"ON +06dB", 1}, // WM8731 Mic Boost ON, original+6 dBs condition, easy to saturate ADC sat in high voice, relative G = +06 dB's respect ref level
@@ -298,11 +298,11 @@ class MicTXView : public View {
OptionsField field_rxbw{
{19 * 8, (23 * 8) + 2},
3,
7,
{
{" NFM1:8k5 ", 0},
{" NFM2:11k ", 1},
{" FM :16k ", 2},
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
{" 11k ", 1},
{" 16k ", 2},
}};
NumberField field_squelch{
+118 -231
View File
@@ -23,13 +23,17 @@
#include "ui_recon.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
#include "capture_app.hpp"
#include "convert.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "tone_key.hpp"
using namespace portapack;
using namespace tonekey;
using portapack::memory::map::backup_ram;
namespace fs = std::filesystem;
namespace ui {
@@ -124,152 +128,81 @@ void ReconView::colorize_waits() {
}
}
bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
File recon_file;
bool ReconView::recon_save_freq(const fs::path& path, size_t freq_index, bool warn_if_exists) {
if (frequency_list.size() == 0 || !current_is_valid())
return false;
FreqmanDB freq_db;
if (!freq_db.open(path, /*create*/ true))
return false;
freqman_entry entry = *frequency_list[freq_index]; // Makes a copy.
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single;
std::string frequency_to_add;
get_freq_string(entry, frequency_to_add);
auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
char one_char[1]{}; // Read it char by char
std::string line{}; // and put read line in here
bool found{false};
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
recon_file.seek(pointer);
recon_file.read(one_char, 1);
if ((int)one_char[0] > 31) { // ascii space upwards
line += one_char[0]; // add it to the textline
} else if (one_char[0] == '\n') { // New Line
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
found = true;
break;
}
line.clear(); // Ready for next textline
}
}
if (!found) {
result = recon_file.append(freq_file_path); // Second: append if it is not there
if (!result.is_valid()) {
recon_file.write_line(frequency_to_add);
}
}
if (found && warn_if_exists) {
nav_.display_modal("Error", "Frequency already exists");
}
} else {
result = recon_file.create(freq_file_path); // First freq if no output file
if (!result.is_valid()) {
recon_file.write_line(frequency_to_add);
}
// For ranges, save the current frequency instead.
if (entry.type == freqman_type::Range) {
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.type = freqman_type::Single;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.step = freqman_invalid_index;
}
auto it = freq_db.find_entry(entry);
auto found = (it != freq_db.end());
if (found && warn_if_exists)
nav_.display_modal("Error", "Frequency already exists");
if (!found)
freq_db.append_entry(entry);
return true;
}
bool ReconView::recon_load_config_from_sd() {
File settings_file{};
size_t length{0};
size_t file_position{0};
char* pos{NULL};
char* line_start{NULL};
char* line_end{NULL};
char file_data[257]{};
uint32_t it{0};
uint32_t nb_params{RECON_SETTINGS_NB_PARAMS};
std::string params[RECON_SETTINGS_NB_PARAMS]{};
make_new_directory(u"SETTINGS");
auto result = settings_file.open(RECON_CFG_FILE);
if (!result.is_valid()) {
while (it < nb_params) {
// Read a 256 bytes block from file
settings_file.seek(file_position);
memset(file_data, 0, 257);
auto read_size = settings_file.read(file_data, 256);
if (read_size.is_error())
break;
file_position += 256;
// Reset line_start to beginning of buffer
line_start = file_data;
pos = line_start;
while ((line_end = strstr(line_start, "\x0A"))) {
length = line_end - line_start - 1;
params[it] = std::string(pos, length);
it++;
line_start = line_end + 1;
pos = line_start;
if (line_start - file_data >= 256)
break;
if (it >= nb_params)
break;
}
if (read_size.value() != 256)
break; // End of file
// Restart at beginning of last incomplete line
file_position -= (file_data + 256 - line_start);
}
}
if (it < nb_params) {
/* bad number of params, signal defaults */
input_file = "RECON";
output_file = "RECON_RESULTS";
recon_lock_duration = RECON_MIN_LOCK_DURATION;
recon_lock_nb_match = RECON_DEF_NB_MATCH;
squelch = -14;
recon_match_mode = RECON_MATCH_CONTINUOUS;
wait = RECON_DEF_WAIT_DURATION;
File settings_file;
auto error = settings_file.open(RECON_CFG_FILE);
if (error)
return false;
auto complete = false;
auto line_nb = 0;
auto reader = FileLineReader(settings_file);
for (const auto& line : reader) {
switch (line_nb) {
case 0:
input_file = trim(line);
break;
case 1:
output_file = trim(line);
break;
case 2:
parse_int(line, recon_lock_duration);
break;
case 3:
parse_int(line, recon_lock_nb_match);
break;
case 4:
parse_int(line, squelch);
break;
case 5:
parse_int(line, recon_match_mode);
break;
case 6:
parse_int(line, recon_match_mode);
complete = true; // NB: Last entry.
break;
}
if (complete) break;
line_nb++;
}
if (it > 0)
input_file = params[0];
else
input_file = "RECON";
if (it > 1)
output_file = params[1];
else
output_file = "RECON_RESULTS";
if (it > 2)
recon_lock_duration = strtoll(params[2].c_str(), nullptr, 10);
else
recon_lock_duration = RECON_MIN_LOCK_DURATION;
if (it > 3)
recon_lock_nb_match = strtoll(params[3].c_str(), nullptr, 10);
else
recon_lock_nb_match = RECON_DEF_NB_MATCH;
if (it > 4)
squelch = strtoll(params[4].c_str(), nullptr, 10);
else
squelch = -14;
if (it > 5)
recon_match_mode = strtoll(params[5].c_str(), nullptr, 10);
else
recon_match_mode = RECON_MATCH_CONTINUOUS;
if (it > 6)
wait = strtoll(params[6].c_str(), nullptr, 10);
else
wait = RECON_DEF_WAIT_DURATION;
return true;
return complete;
}
bool ReconView::recon_save_config_to_sd() {
@@ -486,6 +419,7 @@ ReconView::ReconView(NavigationView& nav)
if (frequency_list.size() > 0) {
auto new_view = nav_.push<FrequencyKeypadView>(current_index);
new_view->on_changed = [this, &button](rf::Frequency f) {
// NB: This is using the freq keypad to select an index.
f = f / OneMHz;
if (f >= 1 && f <= frequency_list.size()) {
index_stepper = f - 1 - current_index;
@@ -496,17 +430,18 @@ ReconView::ReconView(NavigationView& nav)
};
button_manual_start.on_change = [this]() {
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * freqman_entry_get_step_value(def_step);
auto step_val = freqman_entry_get_step_value(def_step);
frequency_range.min = frequency_range.min + button_manual_start.get_encoder_delta() * step_val;
if (frequency_range.min < 0) {
frequency_range.min = 0;
}
if (frequency_range.min > (MAX_UFREQ - freqman_entry_get_step_value(def_step))) {
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
if (frequency_range.min > (MAX_UFREQ - step_val)) {
frequency_range.min = MAX_UFREQ - step_val;
}
if (frequency_range.min > (frequency_range.max - freqman_entry_get_step_value(def_step))) {
frequency_range.max = frequency_range.min + freqman_entry_get_step_value(def_step);
if (frequency_range.min > (frequency_range.max - step_val)) {
frequency_range.max = frequency_range.min + step_val;
if (frequency_range.max > MAX_UFREQ) {
frequency_range.min = MAX_UFREQ - freqman_entry_get_step_value(def_step);
frequency_range.min = MAX_UFREQ - step_val;
frequency_range.max = MAX_UFREQ;
}
}
@@ -516,18 +451,19 @@ ReconView::ReconView(NavigationView& nav)
};
button_manual_end.on_change = [this]() {
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * freqman_entry_get_step_value(def_step);
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
auto step_val = freqman_entry_get_step_value(def_step);
frequency_range.max = frequency_range.max + button_manual_end.get_encoder_delta() * step_val;
if (frequency_range.max < (step_val + 1)) {
frequency_range.max = (step_val + 1);
}
if (frequency_range.max > MAX_UFREQ) {
frequency_range.max = MAX_UFREQ;
}
if (frequency_range.max < (frequency_range.min + freqman_entry_get_step_value(def_step))) {
frequency_range.min = frequency_range.max - freqman_entry_get_step_value(def_step);
if (frequency_range.max < (freqman_entry_get_step_value(def_step) + 1)) {
if (frequency_range.max < (frequency_range.min + step_val)) {
frequency_range.min = frequency_range.max - step_val;
if (frequency_range.max < (step_val + 1)) {
frequency_range.min = 1;
frequency_range.max = (freqman_entry_get_step_value(def_step) + 1);
frequency_range.max = (step_val + 1);
}
}
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
@@ -607,88 +543,9 @@ ReconView::ReconView(NavigationView& nav)
};
button_remove.on_select = [this](ButtonWithEncoder&) {
if (frequency_list.size() > 0) {
if (!manual_mode) {
// scanner or recon (!scanner) mode
// in both we delete index from live view, but only remove in output file in scanner_mode
if (current_index >= (int32_t)frequency_list.size()) {
current_index = frequency_list.size() - 1;
}
frequency_list.erase(frequency_list.begin() + current_index);
if (current_index >= (int32_t)frequency_list.size()) {
current_index = frequency_list.size() - 1;
}
if (frequency_list.size() > 0) {
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
}
// also remove from output file if in scanner mode
if (scanner_mode) {
File freqman_file{};
delete_file(freq_file_path);
auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) {
for (size_t n = 0; n < frequency_list.size(); n++) {
std::string line;
get_freq_string(*frequency_list[n], line);
freqman_file.write_line(line);
}
}
}
} else if (manual_mode) // only remove from output
{
File recon_file{};
File tmp_recon_file{};
std::string tmp_freq_file_path{freq_file_path + ".TMP"};
std::string frequency_to_add{};
handle_remove_current_item();
freqman_entry entry = current_entry();
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = freqman_type::Single;
get_freq_string(entry, frequency_to_add);
delete_file(tmp_freq_file_path);
auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
bool found = false;
result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
char one_char[1]{}; // Read it char by char
std::string line{}; // and put read line in here
for (size_t pointer = 0; pointer < recon_file.size(); pointer++) {
recon_file.seek(pointer);
recon_file.read(one_char, 1);
if ((int)one_char[0] > 31) { // ascii space upwards
line += one_char[0]; // add it to the textline
} else if (one_char[0] == '\n') { // new Line
if (line.compare(0, frequency_to_add.size(), frequency_to_add) == 0) {
found = true;
} else {
tmp_recon_file.write_line(frequency_to_add);
}
line.clear(); // ready for next textline
}
}
if (found) {
delete_file(freq_file_path);
rename_file(tmp_freq_file_path, freq_file_path);
} else {
delete_file(tmp_freq_file_path);
}
}
}
}
receiver_model.set_target_frequency(current_entry().frequency_a); // retune
}
if (frequency_list.size() == 0) {
text_cycle.set_text(" ");
desc_cycle.set("no entries in list"); // Show new description
delete_file(freq_file_path);
}
// TODO: refresh UI.
timer = 0;
freq_lock = 0;
};
@@ -767,9 +624,8 @@ ReconView::ReconView(NavigationView& nav)
};
button_restart.on_select = [this](Button&) {
frequency_file_load(true);
if (frequency_list.size() > 0) {
def_step = step_mode.selected_index(); // Use def_step from manual selector
frequency_file_load(true);
if (fwd) {
button_dir.set_text("FW>");
} else {
@@ -833,7 +689,7 @@ ReconView::ReconView(NavigationView& nav)
open_view->on_changed = [this](std::vector<std::string> result) {
input_file = result[0];
output_file = result[1];
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
freq_file_path = get_freqman_path(output_file).string();
recon_save_config_to_sd();
autosave = persistent_memory::recon_autosave_freqs();
@@ -892,7 +748,7 @@ ReconView::ReconView(NavigationView& nav)
// Loading input and output file from settings
recon_load_config_from_sd();
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
freq_file_path = get_freqman_path(output_file).string();
field_recon_match_mode.set_selected_index(recon_match_mode);
field_squelch.set_value(squelch);
@@ -922,8 +778,7 @@ ReconView::ReconView(NavigationView& nav)
recon_redraw();
}
void ReconView::frequency_file_load(bool stop_all_before) {
(void)(stop_all_before);
void ReconView::frequency_file_load(bool) {
if (field_mode.selected_index_value() != SPEC_MODULATION)
audio::output::stop();
@@ -941,6 +796,7 @@ void ReconView::frequency_file_load(bool stop_all_before) {
desc_cycle.set_style(&Styles::blue);
button_scanner_mode.set_text("RECON");
}
freqman_load_options options{
.load_freqs = load_freqs,
.load_ranges = load_ranges,
@@ -1431,4 +1287,35 @@ void ReconView::handle_coded_squelch(const uint32_t value) {
text_ctcss.set(" ");
}
void ReconView::handle_remove_current_item() {
if (frequency_list.empty() || !current_is_valid())
return;
auto entry = current_entry(); // Copy the current entry.
// In Scanner or Recon modes, remove from the in-memory list.
if (mode() != recon_mode::Manual) {
if (current_is_valid()) {
frequency_list.erase(frequency_list.begin() + current_index);
}
if (frequency_list.size() > 0) {
current_index = clip<int32_t>(current_index, 0u, frequency_list.size() - 1);
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
} else {
current_index = 0;
}
}
// In Scanner or Manual mode, remove the entry from the output file.
if (mode() != recon_mode::Recon) {
FreqmanDB freq_db;
if (!freq_db.open(freq_file_path))
return;
freq_db.delete_entry(entry);
}
}
} /* namespace ui */
+17 -3
View File
@@ -46,6 +46,12 @@ namespace ui {
#define RECON_CFG_FILE "SETTINGS/recon.cfg"
enum class recon_mode : uint8_t {
Recon,
Scanner,
Manual
};
class ReconView : public View {
public:
ReconView(NavigationView& nav);
@@ -82,9 +88,10 @@ class ReconView : public View {
void recon_redraw();
void handle_retune();
void handle_coded_squelch(const uint32_t value);
void handle_remove_current_item();
bool recon_load_config_from_sd();
bool recon_save_config_to_sd();
bool recon_save_freq(const std::string& freq_file_path, size_t index, bool warn_if_exists);
bool recon_save_freq(const std::filesystem::path& path, size_t index, bool warn_if_exists);
// placeholder for possible void recon_start_recording();
void recon_stop_recording();
@@ -92,8 +99,15 @@ class ReconView : public View {
bool current_is_valid();
freqman_entry& current_entry();
// TODO: consolidate mode bools and use recon_mode.
recon_mode mode() const {
if (manual_mode) return recon_mode::Manual;
if (scanner_mode) return recon_mode::Scanner;
return recon_mode::Recon;
}
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
int32_t squelch{0};
int32_t squelch{RECON_DEF_SQUELCH};
int32_t db{0};
int32_t timer{0};
int32_t wait{RECON_DEF_WAIT_DURATION}; // in msec. if > 0 wait duration after a lock, if < 0 duration is set to 'wait' unless there is no more activity
@@ -117,7 +131,7 @@ class ReconView : public View {
bool user_pause{false};
bool auto_record_locked{false};
bool is_recording{false};
uint32_t recon_lock_nb_match{3};
uint32_t recon_lock_nb_match{RECON_DEF_NB_MATCH};
uint32_t recon_lock_duration{RECON_MIN_LOCK_DURATION};
uint32_t recon_match_mode{RECON_MATCH_CONTINUOUS};
bool scanner_mode{false};
@@ -21,12 +21,13 @@
* Boston, MA 02110-1301, USA.
*/
#include "ui_recon_settings.hpp"
#include "ui_navigation.hpp"
#include "ui_fileman.hpp"
#include "ui_navigation.hpp"
#include "ui_recon_settings.hpp"
#include "ui_textentry.hpp"
#include "file.hpp"
#include "freqman_db.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
@@ -55,11 +56,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_load_freqs.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
// get the filename without txt extension so we can use load_freqman_file fcn
if (new_file_path.native().find(freqman_dir.native()) == 0) {
_input_file = new_file_path.stem().string();
text_input_file.set(_input_file);
} else {
@@ -71,11 +70,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_save_freqs.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
if (new_file_path.native().find(freqman_dir.native()) == 0) {
_output_file = new_file_path.stem().string();
button_output_file.set_text(_output_file);
text_input_file.set(_output_file);
} else {
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
}
@@ -85,7 +82,7 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_output_file.on_select = [this, &nav](Button&) {
text_prompt(nav, _output_file, 28,
[this](std::string& buffer) {
_output_file = buffer;
_output_file = std::move(buffer);
button_output_file.set_text(_output_file);
});
};
@@ -50,6 +50,8 @@
// maximum lock duration
#define RECON_MAX_LOCK_DURATION 9900
#define RECON_DEF_SQUELCH -14
// default number of match to have a lock
#define RECON_DEF_NB_MATCH 3
#define RECON_MIN_LOCK_DURATION 100 // have to be >= and a multiple of STATS_UPDATE_INTERVAL
+7 -9
View File
@@ -22,6 +22,7 @@
#include "ui_scanner.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
@@ -314,17 +315,13 @@ ScannerView::ScannerView(
// Button to load txt files from the FREQMAN folder
button_load.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != std::string::npos) { // assert file from the FREQMAN folder
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
if (new_file_path.native().find(freqman_dir.native()) == 0) {
scan_pause();
// get the filename without txt extension so we can use load_freqman_file fcn
std::string str_file_name = new_file_path.stem().string();
frequency_file_load(str_file_name, true);
frequency_file_load(new_file_path.stem().string(), true);
} else {
nav_.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
}
};
};
@@ -494,6 +491,7 @@ ScannerView::ScannerView(
bigdisplay_update(BDC_GREY); // Back to grey color
};
// TODO: remove this parsing?
// Button to add current frequency (found during Search) to the Scan Frequency List
button_add.on_select = [this](Button&) {
File scanner_file;
+6 -4
View File
@@ -480,10 +480,12 @@ class SetEncoderDialView : public View {
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}}};
7,
{{"LOWEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOWEST},
{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH},
{"HIGHEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGHEST}}};
Button button_save{
{2 * 8, 16 * 16, 12 * 8, 32},
@@ -20,13 +20,14 @@
*/
#include "ui_spectrum_painter.hpp"
#include "bmp.hpp"
#include "baseband_api.hpp"
#include "ui_fileman.hpp"
#include "io_file.hpp"
#include "file.hpp"
#include "io_file.hpp"
#include "portapack_persistent_memory.hpp"
#include "ui_fileman.hpp"
#include "ui_freqman.hpp"
using namespace portapack;
+51 -31
View File
@@ -301,6 +301,38 @@ void TextEditorMenu::hide_children(bool hidden) {
/* TextEditorView ***************************************************/
static fs::path get_temp_path(const fs::path& path) {
if (!path.empty())
return path + "~";
return {};
}
static void delete_temp_file(const fs::path& path) {
auto temp_path = get_temp_path(path);
if (!temp_path.empty()) {
delete_file(temp_path);
}
}
static void save_temp_file(const fs::path& path) {
delete_file(path);
copy_file(get_temp_path(path), path);
}
static void show_save_prompt(
NavigationView& nav,
std::function<void()> on_save,
std::function<void()> continuation) {
nav.display_modal(
"Save?", "Save changes?", YESNO,
[on_save](bool choice) {
if (choice && on_save)
on_save();
});
nav.set_on_pop(continuation);
}
TextEditorView::TextEditorView(NavigationView& nav)
: nav_{nav} {
add_children(
@@ -381,9 +413,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
};
menu.on_exit() = [this]() {
show_save_prompt([this]() {
nav_.pop();
});
nav_.pop();
};
button_menu.on_select = [this]() {
@@ -402,7 +432,15 @@ TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
}
TextEditorView::~TextEditorView() {
delete_temp_file();
// NB: Be careful here. The UI will render after this instance
// has been destroyed. Everything needed to render the UI
// and perform the save actions must be value captured.
if (file_dirty_) {
ui::show_save_prompt(
nav_,
[p = path_]() { ui::save_temp_file(p); },
[p = std::move(path_)]() { delete_temp_file(p); });
}
}
void TextEditorView::on_show() {
@@ -415,7 +453,7 @@ void TextEditorView::on_show() {
void TextEditorView::open_file(const fs::path& path) {
file_.reset();
viewer.clear_file();
delete_temp_file();
delete_temp_file(path_);
path_ = {};
file_dirty_ = false;
@@ -516,13 +554,10 @@ void TextEditorView::show_save_prompt(std::function<void()> continuation) {
return;
}
nav_.display_modal(
"Save?", "Save changes?", YESNO,
[this](bool choice) {
if (choice)
save_temp_file();
});
nav_.set_on_pop(continuation);
ui::show_save_prompt(
nav_,
[this]() { save_temp_file(); },
continuation);
}
void TextEditorView::prepare_for_write() {
@@ -533,31 +568,16 @@ void TextEditorView::prepare_for_write() {
// Copy to temp file on write.
has_temp_file_ = true;
delete_temp_file();
copy_file(path_, get_temp_path());
file_->assume_file(get_temp_path());
}
void TextEditorView::delete_temp_file() const {
auto temp_path = get_temp_path();
if (!temp_path.empty()) {
delete_file(temp_path);
}
delete_temp_file(path_);
copy_file(path_, get_temp_path(path_));
file_->assume_file(get_temp_path(path_));
}
void TextEditorView::save_temp_file() {
if (file_dirty_) {
delete_file(path_);
copy_file(get_temp_path(), path_);
ui::save_temp_file(path_);
file_dirty_ = false;
}
}
fs::path TextEditorView::get_temp_path() const {
if (!path_.empty())
return path_ + "~";
return {};
}
} // namespace ui
@@ -229,10 +229,7 @@ class TextEditorView : public View {
void show_save_prompt(std::function<void()> continuation);
void prepare_for_write();
void create_temp_file() const;
void delete_temp_file() const;
void save_temp_file();
std::filesystem::path get_temp_path() const;
NavigationView& nav_;
std::unique_ptr<FileWrapper> file_{};
+38
View File
@@ -3819,6 +3819,44 @@ static constexpr Bitmap bitmap_icon_looking{
{16, 16},
bitmap_icon_looking_data};
static constexpr uint8_t bitmap_icon_add_data[] = {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0xF8,
0x1F,
0xF8,
0x1F,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
};
static constexpr Bitmap bitmap_icon_add{
{16, 16},
bitmap_icon_add_data};
static constexpr uint8_t bitmap_icon_delete_data[] = {
0x00,
0x00,
+43
View File
@@ -0,0 +1,43 @@
/*
* 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 __FIELD_BINDER_H__
#define __FIELD_BINDER_H__
/* There's a common pattern that appears in applications that use
* field widgets. The field value is set, on_change and on_select
* handlers are registered. on_select will read the value and
* set the new value once changed. on_change will be called which
* will write the new value to the backing variable. */
struct binding_traits {
};
template <typename T>
constexpr auto get_traits() {
return {};
}
template <typename TField, typename TTraits = get_traits<TField>()>
class FieldBinding {
};
#endif /*__FIELD_BINDER_H__*/
+18 -18
View File
@@ -45,8 +45,11 @@ Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BY
}
}
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only) {
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only, bool create) {
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
if (create)
mode |= FA_OPEN_ALWAYS;
return open_fatfs(filename, mode);
}
@@ -252,12 +255,9 @@ std::filesystem::path next_filename_matching_pattern(const std::filesystem::path
std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory,
const std::filesystem::path& extension) {
std::vector<std::filesystem::path> file_list{};
for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) {
if (std::filesystem::is_regular_file(entry.status())) {
file_list.push_back(entry.path());
}
}
scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) {
file_list.push_back(p);
});
return file_list;
}
@@ -287,7 +287,7 @@ 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.
// 512 seems to be the largest block size FatFS likes.
constexpr size_t buffer_size = 512;
uint8_t buffer[buffer_size];
File src;
@@ -324,10 +324,11 @@ FATTimestamp file_created_date(const std::filesystem::path& file_path) {
std::filesystem::filesystem_error make_new_file(
const std::filesystem::path& file_path) {
File f;
auto result = f.create(file_path);
return result.is_valid()
? result.value()
: std::filesystem::filesystem_error{};
auto error = f.create(file_path);
if (error)
return *error;
return {};
}
std::filesystem::filesystem_error make_new_directory(
@@ -507,13 +508,12 @@ bool path_iequal(
}
directory_iterator::directory_iterator(
std::filesystem::path path,
std::filesystem::path wild)
: pattern{wild} {
const std::filesystem::path& path,
const std::filesystem::path& wild)
: path_{path}, wild_{wild} {
impl = std::make_shared<Impl>();
const auto result = f_findfirst(&impl->dir, &impl->filinfo,
reinterpret_cast<const TCHAR*>(path.c_str()),
reinterpret_cast<const TCHAR*>(pattern.c_str()));
auto result = f_findfirst(&impl->dir, &impl->filinfo,
path_.tchar(), wild_.tchar());
if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') {
impl.reset();
// TODO: Throw exception if/when I enable exceptions...
+24 -17
View File
@@ -76,36 +76,30 @@ struct path {
: _s{} {
}
path(
const path& p)
path(const path& p)
: _s{p._s} {
}
path(
path&& p)
path(path&& p)
: _s{std::move(p._s)} {
}
template <class Source>
path(
const Source& source)
path(const Source& source)
: path{std::begin(source), std::end(source)} {
}
template <class InputIt>
path(
InputIt first,
InputIt last)
path(InputIt first,
InputIt last)
: _s{first, last} {
}
path(
const char16_t* const s)
path(const char16_t* const s)
: _s{s} {
}
path(
const TCHAR* const s)
path(const TCHAR* const s)
: _s{reinterpret_cast<const std::filesystem::path::value_type*>(s)} {
}
@@ -132,6 +126,10 @@ struct path {
return native().c_str();
}
const TCHAR* tchar() const {
return reinterpret_cast<const TCHAR*>(native().c_str());
}
const string_type& native() const {
return _s;
}
@@ -149,7 +147,7 @@ struct path {
}
path& operator/=(const path& p) {
if (_s.back() != preferred_separator)
if (_s.back() != preferred_separator && p._s.front() != preferred_separator)
_s += preferred_separator;
_s += p._s;
return *this;
@@ -207,7 +205,8 @@ class directory_iterator {
};
std::shared_ptr<Impl> impl{};
const path pattern{};
std::filesystem::path path_{};
std::filesystem::path wild_{};
friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs);
@@ -219,7 +218,8 @@ class directory_iterator {
using iterator_category = std::input_iterator_tag;
directory_iterator() noexcept {};
directory_iterator(std::filesystem::path path, std::filesystem::path wild);
directory_iterator(const std::filesystem::path& path,
const std::filesystem::path& wild);
~directory_iterator() {}
@@ -266,6 +266,13 @@ std::filesystem::filesystem_error make_new_file(const std::filesystem::path& fil
std::filesystem::filesystem_error make_new_directory(const std::filesystem::path& dir_path);
std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path);
template <typename TCallback>
void scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension, const TCallback& fn) {
for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) {
if (std::filesystem::is_regular_file(entry.status()))
fn(entry.path());
}
}
std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension);
std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::path& directory);
@@ -312,7 +319,7 @@ class File {
File& operator=(const File&) = delete;
// TODO: Return Result<>.
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true);
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true, bool create = false);
Optional<Error> append(const std::filesystem::path& filename);
Optional<Error> create(const std::filesystem::path& filename);
+4 -3
View File
@@ -59,6 +59,7 @@ class BufferWrapper {
using Offset = uint32_t;
using Line = uint32_t;
using Column = uint32_t;
using Size = File::Size;
using Range = struct {
// Offset of the start, inclusive.
Offset start;
@@ -108,7 +109,7 @@ class BufferWrapper {
}
/* Gets the size of the buffer in bytes. */
File::Size size() const { return wrapped_->size(); }
Size size() const { return wrapped_->size(); }
/* Get the count of the lines in the buffer. */
uint32_t line_count() const { return line_count_; }
@@ -462,9 +463,9 @@ class FileWrapper : public BufferWrapper<File, 64> {
template <typename T>
using Result = File::Result<T>;
using Error = File::Error;
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path, bool create = false) {
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
auto error = fw->file_.open(path, /*read_only*/ false);
auto error = fw->file_.open(path, /*read_only*/ false, create);
if (error)
return *error;
+4 -179
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -21,192 +22,16 @@
* Boston, MA 02110-1301, USA.
*/
#include "file.hpp"
#include "freqman.hpp"
#include "tone_key.hpp"
#include "ui_widget.hpp"
#include <algorithm>
using option_t = ui::OptionsField::option_t;
using options_t = ui::OptionsField::options_t;
namespace fs = std::filesystem;
using namespace tonekey;
using namespace ui;
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
extern options_t freqman_modulations;
extern options_t freqman_bandwidths[4];
extern options_t freqman_steps;
extern options_t freqman_steps_short;
const option_t* find_by_index(const options_t& options, freqman_index_t index) {
if (index < options.size())
return &options[index];
else
return nullptr;
}
// TODO: move into FreqmanDB type
/* Freqman file handling. */
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
fs::path path{u"FREQMAN/"};
path += file_stem + ".TXT";
return parse_freqman_file(path, db, options);
}
bool get_freq_string(freqman_entry& entry, std::string& item_string) {
rf::Frequency frequency_a, frequency_b;
frequency_a = entry.frequency_a;
if (entry.type == freqman_type::Single) {
// Single
item_string = "f=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
} else if (entry.type == freqman_type::Range) {
// Range
frequency_b = entry.frequency_b;
item_string = "a=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
item_string += ",b=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0');
if (is_valid(entry.step)) {
item_string += ",s=" + freqman_entry_get_step_string_short(entry.step);
}
} else if (entry.type == freqman_type::HamRadio) {
frequency_b = entry.frequency_b;
item_string = "r=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
item_string += ",t=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0');
if (is_valid(entry.tone)) {
item_string += ",c=" + tone_key_value_string(entry.tone);
}
}
if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) {
item_string += ",m=" + freqman_entry_get_modulation_string(entry.modulation);
if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size()) {
item_string += ",bw=" + freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth);
}
}
if (entry.description.size())
item_string += ",d=" + entry.description;
return true;
}
bool delete_freqman_file(const std::string& file_stem) {
File freqman_file;
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT";
delete_file(freq_file_path);
return false;
}
bool save_freqman_file(const std::string& file_stem, freqman_db& db) {
File freqman_file;
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT";
delete_file(freq_file_path);
auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) {
for (size_t n = 0; n < db.size(); n++) {
std::string line;
get_freq_string(*db[n], line);
freqman_file.write_line(line);
}
return true;
}
return false;
}
bool create_freqman_file(const std::string& file_stem, File& freqman_file) {
auto result = freqman_file.create("FREQMAN/" + file_stem + ".TXT");
if (result.is_valid())
return false;
return true;
}
std::string freqman_item_string(freqman_entry& entry, size_t max_length) {
std::string item_string;
switch (entry.type) {
case freqman_type::Single:
item_string = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
break;
case freqman_type::Range:
item_string = "R: " + entry.description;
break;
case freqman_type::HamRadio:
item_string = "H: " + entry.description;
break;
default:
item_string = "!UNKNOWN TYPE " + entry.description;
break;
}
if (item_string.size() > max_length)
return item_string.substr(0, max_length - 3) + "...";
return item_string;
}
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
option.set_options(freqman_modulations);
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
if (is_valid(modulation))
option.set_options(freqman_bandwidths[modulation]);
}
void freqman_set_step_option(OptionsField& option) {
option.set_options(freqman_steps);
}
void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_steps_short);
}
/* Option name lookup. */
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->first;
return {};
}
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->first;
}
return {};
}
std::string freqman_entry_get_step_string(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps, step))
return opt->first;
return {};
}
std::string freqman_entry_get_step_string_short(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps_short, step))
return opt->first;
return {};
}
extern const option_t* find_by_index(const options_t& options, freqman_index_t index);
/* Option value lookup. */
// TODO: use Optional instead of magic values.
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->second;
return -1;
}
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->second;
}
return -1;
}
int32_t freqman_entry_get_step_value(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps, step))
return opt->second;
+2 -27
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -24,13 +25,7 @@
#ifndef __FREQMAN_H__
#define __FREQMAN_H__
#include <cstring>
#include <string>
#include "file.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_widget.hpp"
// Defined for back-compat.
#define FREQMAN_MAX_PER_FILE freqman_default_max_entries
@@ -49,27 +44,7 @@ enum freqman_entry_modulation : uint8_t {
SPEC_MODULATION
};
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
bool get_freq_string(freqman_entry& entry, std::string& item_string);
bool delete_freqman_file(const std::string& file_stem);
bool save_freqman_file(const std::string& file_stem, freqman_db& db);
bool create_freqman_file(const std::string& file_stem, File& freqman_file);
std::string freqman_item_string(freqman_entry& item, size_t max_length);
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(ui::OptionsField& option);
void freqman_set_step_option(ui::OptionsField& option);
void freqman_set_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option);
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
std::string freqman_entry_get_step_string(freqman_index_t step);
std::string freqman_entry_get_step_string_short(freqman_index_t step);
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation);
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth);
// TODO: Can these be removed after Recon is migrated to FreqmanDB?
int32_t freqman_entry_get_step_value(freqman_index_t step);
#endif /*__FREQMAN_H__*/
+303 -53
View File
@@ -28,6 +28,7 @@
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "tone_key.hpp"
#include "utility.hpp"
#include <array>
#include <cctype>
@@ -36,6 +37,10 @@
namespace fs = std::filesystem;
const std::filesystem::path freqman_dir{u"/FREQMAN"};
const std::filesystem::path freqman_extension{u".TXT"};
// NB: Don't include UI headers to keep this code unit testable.
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
@@ -88,6 +93,7 @@ options_t freqman_bandwidths[4] = {
},
};
// TODO: these should be indexes.
options_t freqman_steps = {
{"0.1kHz ", 100},
{"1kHz ", 1000},
@@ -107,6 +113,7 @@ options_t freqman_steps = {
{"1MHz ", 1000000},
};
// TODO: these should be indexes.
options_t freqman_steps_short = {
{"0.1kHz", 100},
{"1kHz", 1000},
@@ -134,6 +141,13 @@ uint8_t find_by_name(const options_t& options, std::string_view name) {
return freqman_invalid_index;
}
const option_t* find_by_index(const options_t& options, freqman_index_t index) {
if (index < options.size())
return &options[index];
else
return nullptr;
}
/* Impl for next round of changes.
*template <typename T, size_t N>
*const T* find_by_name(const std::array<T, N>& info, std::string_view name) {
@@ -146,6 +160,172 @@ uint8_t find_by_name(const options_t& options, std::string_view name) {
*}
*/
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs) {
auto equal = lhs.type == rhs.type &&
lhs.frequency_a == rhs.frequency_a &&
lhs.description == rhs.description &&
lhs.modulation == rhs.modulation &&
lhs.bandwidth == rhs.bandwidth;
if (!equal)
return false;
if (lhs.type == freqman_type::Range) {
equal = lhs.frequency_b == rhs.frequency_b &&
lhs.step == rhs.step;
} else if (lhs.type == freqman_type::HamRadio) {
equal = lhs.frequency_b == rhs.frequency_b &&
lhs.tone == rhs.tone;
}
return equal;
}
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->first;
return {};
}
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->first;
}
return {};
}
std::string freqman_entry_get_step_string(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps, step))
return opt->first;
return {};
}
std::string freqman_entry_get_step_string_short(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps_short, step))
return opt->first;
return {};
}
const std::filesystem::path get_freqman_path(const std::string& stem) {
return freqman_dir / stem + freqman_extension;
}
bool create_freqman_file(const std::string& file_stem) {
auto fs_error = make_new_file(get_freqman_path(file_stem));
return fs_error.ok();
}
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
return parse_freqman_file(get_freqman_path(file_stem), db, options);
}
void delete_freqman_file(const std::string& file_stem) {
delete_file(get_freqman_path(file_stem));
}
std::string pretty_string(const freqman_entry& entry, size_t max_length) {
std::string str;
switch (entry.type) {
case freqman_type::Single:
str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
break;
case freqman_type::Range:
str = to_string_rounded_freq(entry.frequency_a, 1) + "M-" +
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
break;
case freqman_type::HamRadio:
str = "R:" + to_string_rounded_freq(entry.frequency_a, 1) + "M,T:" +
to_string_rounded_freq(entry.frequency_b, 1) + "M: " + entry.description;
break;
case freqman_type::Raw:
str = entry.description;
break;
default:
str = "UNK:" + entry.description;
break;
}
// Truncate. '+' indicates if string has been truncated.
if (str.size() > max_length)
return str.substr(0, max_length - 1) + "+";
return str;
}
std::string to_freqman_string(const freqman_entry& entry) {
std::string serialized;
serialized.reserve(0x80);
// Append a key=value to the string.
auto append_field = [&serialized](std::string_view name, std::string_view value) {
if (!serialized.empty())
serialized += ",";
serialized += std::string{name} + "=" + std::string{value};
};
switch (entry.type) {
case freqman_type::Single:
append_field("f", to_string_dec_uint64(entry.frequency_a));
break;
case freqman_type::Range:
append_field("a", to_string_dec_uint64(entry.frequency_a));
append_field("b", to_string_dec_uint64(entry.frequency_b));
if (is_valid(entry.step))
append_field("s", freqman_entry_get_step_string_short(entry.step));
break;
case freqman_type::HamRadio:
append_field("r", to_string_dec_uint64(entry.frequency_a));
append_field("t", to_string_dec_uint64(entry.frequency_b));
if (is_valid(entry.tone))
append_field("c", tonekey::tone_key_value_string(entry.tone));
break;
case freqman_type::Raw:
return entry.description;
default:
return {};
};
if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) {
append_field("m", freqman_entry_get_modulation_string(entry.modulation));
if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size())
append_field("bw", freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth));
}
if (entry.description.size() > 0)
append_field("d", entry.description);
serialized.shrink_to_fit();
return serialized;
}
freqman_index_t parse_tone_key(std::string_view value) {
// Split into whole and fractional parts.
auto parts = split_string(value, '.');
int32_t tone_freq = 0;
int32_t whole_part = 0;
parse_int(parts[0], whole_part);
// Tones are stored as frequency / 100 for some reason.
// E.g. 14572 would be 145.7 (NB: 1s place is dropped).
// TODO: Might be easier to just store the codes?
// Multiply the whole part by 100 to get the tone frequency.
tone_freq = whole_part * 100;
// Add the fractional part, if present.
if (parts.size() > 1) {
auto c = parts[1].front();
auto digit = std::isdigit(c) ? c - '0' : 0;
tone_freq += digit * 10;
}
return static_cast<freqman_index_t>(tonekey::tone_key_index_by_value(tone_freq));
}
bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
if (str.empty() || str[0] == '#')
return false;
@@ -175,26 +355,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value);
}
} else if (key == "c") {
// Split into whole and fractional parts.
auto parts = split_string(value, '.');
int32_t tone_freq = 0;
int32_t whole_part = 0;
parse_int(parts[0], whole_part);
// Tones are stored as frequency / 100 for some reason.
// E.g. 14572 would be 145.7 (NB: 1s place is dropped).
// TODO: Might be easier to just store the codes?
// Multiply the whole part by 100 to get the tone frequency.
tone_freq = whole_part * 100;
// Add the fractional part, if present.
if (parts.size() > 1) {
auto c = parts[1].front();
auto digit = std::isdigit(c) ? c - '0' : 0;
tone_freq += digit * 10;
}
entry.tone = static_cast<freqman_index_t>(
tonekey::tone_key_index_by_value(tone_freq));
entry.tone = parse_tone_key(value);
} else if (key == "d") {
entry.description = trim(value);
} else if (key == "f") {
@@ -212,47 +373,23 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
}
}
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Ranges should have both frequencies set and A <= B.
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_a == 0 || entry.frequency_b == 0)
return false;
if (entry.frequency_a > entry.frequency_b)
return false;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true;
return is_valid(entry);
}
bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
File f;
auto error = f.open(path);
if (error)
FreqmanDB freqman_db;
freqman_db.set_read_raw(false); // Don't return malformed lines.
if (!freqman_db.open(path))
return false;
auto reader = FileLineReader(f);
auto line_count = count_lines(reader);
// Attempt to avoid a re-alloc if possible.
db.clear();
db.reserve(line_count);
for (const auto& line : reader) {
freqman_entry entry{};
if (!parse_freqman_entry(line, entry))
continue;
db.reserve(freqman_db.entry_count());
for (auto entry : freqman_db) {
// Filter by entry type.
if ((entry.type == freqman_type::Single && !options.load_freqs) ||
if (entry.type == freqman_type::Unknown ||
(entry.type == freqman_type::Single && !options.load_freqs) ||
(entry.type == freqman_type::Range && !options.load_ranges) ||
(entry.type == freqman_type::HamRadio && !options.load_hamradios)) {
continue;
@@ -276,4 +413,117 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio
db.shrink_to_fit();
return true;
}
}
bool is_valid(const freqman_entry& entry) {
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Frequency A must be set for all types
if (entry.frequency_a == 0)
return false;
// Frequency B must be set for type Range or Ham Radio
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_b == 0)
return false;
}
// Ranges should have frequencies A <= B.
if (entry.type == freqman_type::Range) {
if (entry.frequency_a > entry.frequency_b)
return false;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
// - Step only on Range
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true;
}
/* FreqmanDB ***********************************/
bool FreqmanDB::open(const std::filesystem::path& path, bool create) {
auto result = FileWrapper::open(path, create);
if (!result)
return false;
wrapper_ = *std::move(result);
return true;
}
void FreqmanDB::close() {
wrapper_.reset();
}
freqman_entry FreqmanDB::operator[](Index index) const {
auto length = wrapper_->line_length(index);
auto line_text = wrapper_->get_text(index, 0, length);
if (line_text) {
freqman_entry entry;
if (parse_freqman_entry(*line_text, entry))
return entry;
else if (read_raw_) {
entry.type = freqman_type::Raw;
entry.description = trim(*line_text);
return entry;
}
}
return {};
}
void FreqmanDB::insert_entry(Index index, const freqman_entry& entry) {
index = clip<uint32_t>(index, 0u, entry_count());
wrapper_->insert_line(index);
replace_entry(index, entry);
}
void FreqmanDB::append_entry(const freqman_entry& entry) {
insert_entry(entry_count(), entry);
}
void FreqmanDB::replace_entry(Index index, const freqman_entry& entry) {
auto range = wrapper_->line_range(index);
if (!range)
return;
// Don't overwrite the '\n'.
range->end--;
wrapper_->replace_range(*range, to_freqman_string(entry));
}
void FreqmanDB::delete_entry(Index index) {
wrapper_->delete_line(index);
}
bool FreqmanDB::delete_entry(const freqman_entry& entry) {
auto it = find_entry(entry);
if (it == end())
return false;
delete_entry(it.index());
return true;
}
FreqmanDB::iterator FreqmanDB::find_entry(const freqman_entry& entry) {
return find_entry([&entry](const auto& other) {
return entry == other;
});
}
uint32_t FreqmanDB::entry_count() const {
// FileWrapper always presents a single line even for empty files.
return empty() ? 0u : wrapper_->line_count();
}
bool FreqmanDB::empty() const {
// FileWrapper always presents a single line even for empty files.
// A DB is only really empty if the file size is 0.
return !wrapper_ || wrapper_->size() == 0;
}
+121 -20
View File
@@ -1,7 +1,8 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc., Kyle Reed
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -25,6 +26,7 @@
#define __FREQMAN_DB_H__
#include "file.hpp"
#include "file_wrapper.hpp"
#include "utility.hpp"
#include <array>
@@ -33,6 +35,10 @@
#include <string_view>
#include <vector>
/* Defined in freqman_db.cpp */
extern const std::filesystem::path freqman_dir;
extern const std::filesystem::path freqman_extension;
using freqman_index_t = uint8_t;
constexpr freqman_index_t freqman_invalid_index = static_cast<freqman_index_t>(-1);
@@ -50,6 +56,7 @@ enum class freqman_type : uint8_t {
Single, // f=
Range, // a=,b=
HamRadio, // r=,t=
Raw, // line content in description
Unknown,
};
@@ -131,9 +138,9 @@ enum class freqman_type : uint8_t {
/* Freqman Entry *******************************/
struct freqman_entry {
int64_t frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq'
int64_t frequency_b{0}; // 'b=freq_end' or 't=tx_freq'
std::string description{0}; // 'd=desc'
int64_t frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq'
int64_t frequency_b{0}; // 'b=freq_end' or 't=tx_freq'
std::string description{}; // 'd=desc'
freqman_type type{freqman_type::Unknown};
freqman_index_t modulation{freqman_invalid_index};
freqman_index_t bandwidth{freqman_invalid_index};
@@ -141,10 +148,18 @@ struct freqman_entry {
freqman_index_t tone{freqman_invalid_index};
};
bool operator==(const freqman_entry& lhs, const freqman_entry& rhs);
// TODO: These shouldn't be exported.
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
std::string freqman_entry_get_step_string(freqman_index_t step);
std::string freqman_entry_get_step_string_short(freqman_index_t step);
/* A reasonable maximum number of items to load from a freqman file.
* Apps using freqman_db should be tested and this value tuned to
* ensure app memory stability. */
constexpr size_t freqman_default_max_entries = 90;
constexpr size_t freqman_default_max_entries = 150;
struct freqman_load_options {
/* Loads all entries when set to 0. */
@@ -157,23 +172,109 @@ struct freqman_load_options {
using freqman_entry_ptr = std::unique_ptr<freqman_entry>;
using freqman_db = std::vector<freqman_entry_ptr>;
/* Gets the full path for a given file stem (no extension). */
const std::filesystem::path get_freqman_path(const std::string& stem);
bool create_freqman_file(const std::string& file_stem);
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
void delete_freqman_file(const std::string& file_stem);
/* Gets a pretty string representation for an entry. */
std::string pretty_string(const freqman_entry& item, size_t max_length = 30);
/* Gets the freqman file representation for an entry. */
std::string to_freqman_string(const freqman_entry& entry);
bool parse_freqman_entry(std::string_view str, freqman_entry& entry);
bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options);
/* Type for next round of changes.
*class FreqmanDB {
* public:
* FreqmanDB();
* FreqmanDB(const FreqmanDB&) = delete;
* FreqmanDB(FreqmanDB&&) = delete;
* FreqmanDB& operator=(const FreqmanDB&) = delete;
* FreqmanDB& operator=(FreqmanDB&&) = delete;
*
* size_t size() const { return 0; };
*
* private:
* freqman_db entries_;
*};
*/
/* Returns true if the entry is well-formed. */
bool is_valid(const freqman_entry& entry);
/* API wrapper over a Freqman file. Provides CRUD operations
* for freqman_entry instances that are read/written directly
* to the underlying file. */
class FreqmanDB {
public:
using Index = FileWrapper::Line;
/* NB: This iterator is very basic: forward only, read-only. */
class iterator {
public:
iterator(FreqmanDB& db, Index index)
: db_{db}, index_{index} {}
iterator& operator++() {
index_++;
if (index_ >= db_.entry_count())
index_ = end_index;
return *this;
}
freqman_entry operator*() const {
return db_[index_];
}
bool operator==(const iterator& other) {
return &db_ == &other.db_ && index_ == other.index_;
}
bool operator!=(const iterator& other) {
return !(*this == other);
}
Index index() const {
return index_;
}
/* Value indicating the 'end' iterator. */
static constexpr Index end_index = (Index)-1;
private:
FreqmanDB& db_;
Index index_;
};
bool open(const std::filesystem::path& path, bool create = false);
void close();
freqman_entry operator[](Index index) const;
void insert_entry(Index index, const freqman_entry& entry);
void append_entry(const freqman_entry& entry);
void replace_entry(Index index, const freqman_entry& entry);
void delete_entry(Index index);
bool delete_entry(const freqman_entry& entry);
template <typename Fn>
iterator find_entry(const Fn& predicate) {
// TODO: use std::find, but need to make the iterator compliant.
auto it = begin();
const auto it_end = end();
while (it != it_end) {
if (predicate(*it))
return it;
++it;
}
return it_end;
}
iterator find_entry(const freqman_entry& entry);
uint32_t entry_count() const;
bool empty() const;
/* When true, Raw entries are returned instead of Unknown. */
void set_read_raw(bool v) { read_raw_ = v; }
iterator begin() {
return {*this, 0};
}
iterator end() {
return {*this, iterator::end_index};
}
private:
std::unique_ptr<FileWrapper> wrapper_{};
bool read_raw_{true};
};
#endif /* __FREQMAN_DB_H__ */
+27 -62
View File
@@ -32,66 +32,24 @@
// 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
},
// how many transitions are needed before movement is registered
static const int8_t transition_map[16] = {
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(
@@ -102,6 +60,13 @@ int_fast8_t Encoder::update(
state <<= 1;
state |= phase_1;
// dial sensitivity setting is stored in pmem
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
int_fast8_t retval = transition_map[state & 0xf];
transition_count += retval;
if (abs(transition_count) > portapack::persistent_memory::config_encoder_dial_sensitivity())
transition_count = 0;
else
retval = 0;
return retval;
}
+1
View File
@@ -32,6 +32,7 @@ class Encoder {
private:
uint_fast8_t state{0};
int_fast8_t transition_count{0};
};
#endif /*__ENCODER_H__*/
+69 -9
View File
@@ -25,16 +25,15 @@
* and fills it backwards towards the front.
* The return value 'q' is a pointer to the start.
* TODO: use std::array for all this. */
template <typename Int>
static char* to_string_dec_uint_internal(
char* p,
uint32_t n) {
Int n) {
*p = 0;
auto q = p;
do {
const uint32_t d = n % 10;
const char c = d + 48;
*(--q) = c;
*(--q) = n % 10 + '0';
n /= 10;
} while (n != 0);
@@ -60,13 +59,36 @@ static char* to_string_dec_uint_pad_internal(
return q;
}
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length) {
template <typename Int>
char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) {
auto end = &buffer.back();
auto start = to_string_dec_uint_internal(end, n);
length = end - start;
return start;
}
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
}
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
}
std::string to_string_dec_uint(uint32_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
return std::string(str, len);
}
std::string to_string_dec_uint64(uint64_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
return std::string(str, len);
}
std::string to_string_bin(
const uint32_t n,
const uint8_t l) {
@@ -89,8 +111,8 @@ std::string to_string_dec_uint(
auto term = p + sizeof(p) - 1;
auto q = to_string_dec_uint_pad_internal(term, n, l, fill);
// TODO: is this needed, seems like pad already does this.
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l) {
*(--q) = ' ';
}
@@ -115,6 +137,7 @@ std::string to_string_dec_int(
}
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l) {
*(--q) = ' ';
}
@@ -139,13 +162,47 @@ std::string to_string_decimal(float decimal, int8_t precision) {
return result;
}
// right-justified frequency in Hz, always 10 characters
std::string to_string_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
std::string final_str{""};
if (f < 1000000)
final_str = to_string_dec_int(f, 10, ' ');
else
final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
return final_str;
}
// right-justified frequency in MHz, rounded to 4 decimal places, always 9 characters
std::string to_string_short_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int((f / 100) % 10000, 4, '0');
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int(((f + 50) / 100) % 10000, 4, '0');
return final_str;
}
// non-justified non-padded frequency in MHz, rounded to specified number of decimal places
std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
std::string final_str{""};
static constexpr uint32_t pow10[7] = {
1,
10,
100,
1000,
10000,
100000,
1000000,
};
if (precision < 1) {
final_str = to_string_dec_uint64(f / 1000000);
} else {
if (precision > 6)
precision = 6;
uint32_t divisor = pow10[6 - precision];
final_str = to_string_dec_uint64(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
}
return final_str;
}
@@ -263,7 +320,7 @@ std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precisi
string = to_string_dec_int(integer_part);
if (precision)
string += '.' + to_string_dec_uint(fractional_part, precision);
string += '.' + to_string_dec_uint(fractional_part, precision, '0');
if (prefix_index != 3)
string += unit_prefix[prefix_index];
@@ -285,6 +342,9 @@ static const char* whitespace_str = " \t\r\n";
std::string trim(std::string_view str) {
auto first = str.find_first_not_of(whitespace_str);
if (first == std::string::npos)
return {};
auto last = str.find_last_not_of(whitespace_str);
return std::string{str.substr(first, last - first + 1)};
}
+9 -4
View File
@@ -41,14 +41,18 @@ enum TimeFormat {
const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 16>;
using StringFormatBuffer = std::array<char, 24>;
/* uint32_t conversion without memory allocations. */
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length);
/* uint conversion without memory allocations. */
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length);
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length);
std::string to_string_dec_uint(uint32_t n);
std::string to_string_dec_uint64(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = ' ');
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
@@ -57,6 +61,7 @@ std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
std::string to_string_rounded_freq(const uint64_t f, int8_t precision);
std::string to_string_time_ms(const uint32_t ms);
std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = YMDHMS);
+122 -127
View File
@@ -1,5 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -24,154 +26,147 @@
#include "baseband_api.hpp"
#include "utility.hpp"
#include <algorithm>
namespace ui {
FreqManUIList::FreqManUIList(
Rect parent_rect,
bool instant_exec)
FreqManUIList::FreqManUIList(Rect parent_rect)
: Widget{parent_rect},
instant_exec_{instant_exec} {
visible_lines_{(unsigned)parent_rect.height() / char_height} {
this->set_focusable(true);
}
void FreqManUIList::set_highlighted_index(int index) {
if (freqlist_db == nullptr || (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 == nullptr || freqlist_db->size() == 0)
auto rect = screen_rect();
if (!db_ || db_->empty()) {
auto line_position = rect.location() + Point{7 * 8, 6 * 16};
painter.fill_rectangle(rect, Color::black());
painter.draw_string(line_position, Styles::white, "Empty Category");
return;
// coloration if file is too big
auto text_color = &Styles::white;
if (freqlist_db->size() > FREQMAN_MAX_PER_FILE)
text_color = &Styles::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)) { // line is within the widget
std::string description = freqman_item_string(*freqlist_db->at(it), 30);
if (nb_lines == highlighted_index) {
const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height};
painter.fill_rectangle(
r_highlighted_freq,
Color::white());
painter.draw_string(
{0, r.location().y() + (int)nb_lines * char_height},
Styles::bg_white, description);
} else {
painter.draw_string(
{0, r.location().y() + (int)nb_lines * char_height},
*text_color, description);
}
}
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());
}
}
// Indicate when a file is too large by drawing in yellow.
auto over_max = db_->entry_count() > freqman_default_max_entries;
auto base_style = over_max ? &Styles::yellow : &Styles::white;
void FreqManUIList::set_db(freqman_db& db) {
freqlist_db = &db;
if (db.size() == 0) {
current_index = 0;
highlighted_index = 0;
} else {
if ((unsigned)(current_index + highlighted_index) >= db.size()) {
current_index = db.size() - 1 - highlighted_index;
}
if (current_index < 0) {
current_index = 0;
if (highlighted_index > 0)
highlighted_index--;
// TODO: could minimize redraw/re-read if necessary
// with better change tracking.
for (auto offset = 0u; offset < visible_lines_; ++offset) {
// The whole frame needs to be cleared so every line 'slot'
// is redrawn even when `text` just left empty.
auto text = std::string{};
auto index = start_index_ + offset;
auto line_position = rect.location() + Point{4, 1 + (int)offset * char_height};
auto is_selected = offset == selected_index_;
auto style = base_style;
if (index < db_->entry_count()) {
auto entry = (*db_)[index];
// db_ is directly backed by a file, so invalid lines cannot be
// pre-filtered. Show an empty entry if 'Unknown'.
if (entry.type != freqman_type::Unknown)
text = pretty_string(entry, line_max_length);
// Otherwise, if 'Raw' indicate an invalid entry by color.
if (entry.type == freqman_type::Raw)
style = &Styles::light_grey;
}
// Pad right with ' ' so trailing chars are cleaned up.
// draw_glyph has less flicker than fill_rect when drawing.
if (text.length() < line_max_length)
text.resize(line_max_length, ' ');
painter.draw_string(
line_position, (is_selected ? style->invert() : *style), text);
}
// Draw a bounding rectangle when focused.
painter.draw_rectangle(rect, (has_focus() ? Color::white() : Color::black()));
}
void FreqManUIList::on_focus() {
if (on_highlight)
on_highlight(*this);
set_dirty();
}
void FreqManUIList::on_blur() {
set_dirty();
}
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;
}
if (!db_ || db_->empty())
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;
if (key == KeyEvent::Select && on_select) {
on_select(get_index());
return true;
} else if (key == KeyEvent::Right && on_leave) {
on_leave();
return true;
}
}
bool FreqManUIList::on_encoder(EncoderEvent delta) {
set_highlighted_index((int)highlighted_index + delta);
auto delta = 0;
if (key == KeyEvent::Up && get_index() > 0)
delta = -1;
else if (key == KeyEvent::Down && get_index() < db_->entry_count() - 1)
delta = 1;
else
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
bool FreqManUIList::on_encoder(EncoderEvent delta) {
if (!db_ || db_->empty())
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
void FreqManUIList::set_parent_rect(Rect new_parent_rect) {
visible_lines_ = new_parent_rect.height() / char_height;
Widget::set_parent_rect(new_parent_rect);
}
void FreqManUIList::set_index(size_t index) {
start_index_ = 0;
selected_index_ = 0;
adjust_selected_index(index);
}
size_t FreqManUIList::get_index() const {
return start_index_ + selected_index_;
}
void FreqManUIList::set_db(FreqmanDB& db) {
db_ = &db;
start_index_ = 0;
selected_index_ = 0;
set_dirty();
}
void FreqManUIList::adjust_selected_index(int delta) {
int32_t new_index = selected_index_ + delta;
// The selection went off the top of the screen, move up.
if (new_index < 0) {
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
selected_index_ = 0;
}
// Selection is off the bottom of the screen, move down.
else if (new_index >= (int32_t)visible_lines_) {
start_index_ = std::min<int32_t>(start_index_ + delta, db_->entry_count() - visible_lines_);
selected_index_ = visible_lines_ - 1;
}
// Otherwise, scroll within the screen, but not past the end.
else {
selected_index_ = std::min<int32_t>(new_index, db_->entry_count() - 1);
}
}
} /* namespace ui */
+22 -25
View File
@@ -1,5 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -23,53 +25,48 @@
#define __UI_FREQLIST_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "message.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{};
std::function<void(size_t)> on_select{};
std::function<void()> on_leave{}; // Called when Right is pressed.
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{{}, {}} {
}
FreqManUIList(Rect parent_rect);
FreqManUIList(const FreqManUIList& other) = delete;
FreqManUIList& operator=(const FreqManUIList& other) = delete;
void paint(Painter& painter) override;
void on_focus() override;
void on_blur() override;
bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(EncoderEvent delta) override;
void set_parent_rect(Rect new_parent_rect) override;
void set_highlighted_index(int index); // internal set highlighted_index in list handler
uint8_t get_index(); // return highlighed + index
uint8_t set_index(uint8_t index); // try to set current_index + highlighed from index, return capped index
void set_db(freqman_db& db);
void set_index(size_t index);
size_t get_index() const;
void set_db(FreqmanDB& db);
private:
void adjust_selected_index(int index);
static constexpr int8_t char_height = 16;
bool instant_exec_{false};
freqman_db* freqlist_db{nullptr};
int current_index{0};
int highlighted_index{0};
int freqlist_nb_lines{0};
static constexpr int8_t char_width = 8;
static constexpr int8_t line_max_length = 29;
size_t visible_lines_{0};
FreqmanDB* db_{nullptr};
size_t start_index_{0};
size_t selected_index_{0};
};
} // namespace ui
+2 -2
View File
@@ -84,8 +84,8 @@ class GeoPos : public View {
Labels labels_position{
{{1 * 8, 0 * 16}, "Alt:", Color::light_grey()},
{{1 * 8, 1 * 16}, "Lat: \x90 ' \"", Color::light_grey()}, // 0x90 is degree ° symbol in our 8x16 font
{{1 * 8, 2 * 16}, "Lon: \x90 ' \"", Color::light_grey()},
{{1 * 8, 1 * 16}, "Lat: \xB0 ' \"", Color::light_grey()}, // 0xB0 is degree ° symbol in our 8x16 font
{{1 * 8, 2 * 16}, "Lon: \xB0 ' \"", Color::light_grey()},
};
NumberField field_altitude{
+1 -1
View File
@@ -48,7 +48,7 @@ class TextEntryView : public View {
void char_add(const char c);
void char_delete();
TextField text_input;
TextEdit text_input;
Button button_ok{
{22 * 8, 33 * 8, 7 * 8, 32},
"OK"};
+57 -12
View File
@@ -22,26 +22,71 @@
#ifndef __CONVERT_H__
#define __CONVERT_H__
#include <charconv>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <string>
#include <string_view>
#include <type_traits>
/* Zero-allocation conversion helper. */
/* Notes:
* - T must be an integer type.
* - For base 16, input _must not_ contain a '0x' or '0X' prefix.
* - For base 8 a leading 0 on the literal is allowed.
* - Leading whitespce will cause the parse to fail.
*/
// TODO: from_chars seems to cause code bloat.
// Look into using strtol and friends instead.
constexpr size_t max_parse_int_length = 64;
/* This undefined type is used to see the size of the
* unhandled type and to create a compilation error. */
template <size_t N>
struct UnhandledSize;
/* Returns true when errno is ERANGE. */
inline bool range_error() {
return errno == ERANGE;
}
/* Assigns 'value' to 'out_val' and returns true if 'value' is
* in bounds for type TOut. */
template <typename TVal, typename TOut>
bool checked_assign(TVal value, TOut& out_val) {
// No chance for overflow, just return.
if constexpr (sizeof(TVal) <= sizeof(TOut)) {
out_val = value;
return true;
}
out_val = static_cast<TOut>(value);
return value >= static_cast<TVal>(std::numeric_limits<TOut>::min()) &&
value <= static_cast<TVal>(std::numeric_limits<TOut>::max());
}
/* Zero-allocation conversion helper. 'str' must be smaller than 'max_parse_int_length'. */
template <typename T>
std::enable_if_t<std::is_integral_v<T>, bool> parse_int(std::string_view str, T& out_val, int base = 10) {
auto result = std::from_chars(str.data(), str.data() + str.length(), out_val, base);
return static_cast<int>(result.ec) == 0;
// Always initialize the output.
out_val = {};
if (str.size() > max_parse_int_length)
return false;
// Copy onto the stack and null terminate.
char zstr[max_parse_int_length + 1];
std::memcpy(zstr, str.data(), str.size());
zstr[str.size()] = '\0';
// A little C++ type magic to select the correct conversion function.
if constexpr (sizeof(T) == sizeof(long long)) {
if constexpr (std::is_unsigned_v<T>)
return checked_assign(strtoull(zstr, nullptr, base), out_val) && !range_error();
else
return checked_assign(strtoll(zstr, nullptr, base), out_val) && !range_error();
} else if constexpr (sizeof(T) <= sizeof(long)) {
if constexpr (std::is_unsigned_v<T>)
return checked_assign(strtoul(zstr, nullptr, base), out_val) && !range_error();
else
return checked_assign(strtol(zstr, nullptr, base), out_val) && !range_error();
} else {
UnhandledSize<sizeof(T) * 8> unhandled_case;
return false;
}
return true;
}
#endif /*__CONVERT_H__*/
@@ -269,7 +269,7 @@ struct data_t {
frequency_rx_correction(0),
frequency_tx_correction(0),
encoder_dial_sensitivity(0),
encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL),
UNUSED_8(0),
headphone_volume_cb(-600),
misc_config(),
@@ -363,6 +363,7 @@ void defaults() {
set_config_backlight_timer(backlight_config_t{});
set_config_splash(true);
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
// Default values for recon app.
set_recon_autosave_freqs(false);
@@ -107,9 +107,11 @@ struct backlight_config_t {
};
enum encoder_dial_sensitivity {
DIAL_SENSITIVITY_MEDIUM = 0,
DIAL_SENSITIVITY_LOW = 1,
DIAL_SENSITIVITY_HIGH = 2,
DIAL_SENSITIVITY_HIGHEST = 0,
DIAL_SENSITIVITY_HIGH = 1,
DIAL_SENSITIVITY_NORMAL = 2,
DIAL_SENSITIVITY_LOW = 3,
DIAL_SENSITIVITY_LOWEST = 4,
NUM_DIAL_SENSITIVITY
};
+17 -2
View File
@@ -24,11 +24,26 @@
namespace ui {
Glyph Font::glyph(const char c) const {
size_t index;
if (c < c_start) {
// Non-display C0 Control characters - map to blank (index 0)
return {w, h, data};
}
const size_t index = c - c_start;
if (index >= c_count) {
// Handle gap in glyphs table for C1 Control characters 0x80-0x9F
if (c < C1_CONTROL_CHARS_START) {
// ASCII chars <0x80:
index = c - c_start;
} else if (c >= C1_CONTROL_CHARS_START + C1_CONTROL_CHARS_COUNT) {
// Latin 1 chars 0xA0-0xFF
index = c - c_start - C1_CONTROL_CHARS_COUNT;
} else {
// C1 Control characters - map to blank
return {w, h, data};
}
if (index >= c_count) { // Latin Extended characters > 0xFF - not supported
return {w, h, data};
} else {
return {w, h, &data[index * data_stride]};
+4
View File
@@ -28,6 +28,10 @@
#include "ui.hpp"
// C1 Control Characters are an unprintable range of glyphs missing from the font files
#define C1_CONTROL_CHARS_START 0x80
#define C1_CONTROL_CHARS_COUNT 32
namespace ui {
class Glyph {
+90 -45
View File
@@ -349,7 +349,7 @@ Text::Text(
Rect parent_rect,
std::string text)
: Widget{parent_rect},
text{text} {
text{std::move(text)} {
}
Text::Text(
@@ -357,14 +357,14 @@ Text::Text(
: Text{parent_rect, {}} {
}
void Text::set(const std::string value) {
text = value;
void Text::set(std::string_view value) {
text = std::string{value};
set_dirty();
}
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
const auto s = style();
auto s = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, s.background);
@@ -1112,11 +1112,13 @@ NewButton::NewButton(
Rect parent_rect,
std::string text,
const Bitmap* bitmap,
Color color)
Color color,
bool vertical_center)
: Widget{parent_rect},
text_{text},
bitmap_{bitmap},
color_{color} {
color_{color},
vertical_center_{vertical_center} {
set_focusable(true);
}
@@ -1143,6 +1145,11 @@ void NewButton::set_color(Color color) {
set_dirty();
}
void NewButton::set_vertical_center(bool value) {
vertical_center_ = value;
set_dirty();
}
ui::Color NewButton::color() {
return color_;
}
@@ -1164,28 +1171,34 @@ void NewButton::paint(Painter& painter) {
const Style paint_style = {style().font, bg, fg};
painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Color::dark_grey());
painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Color::dark_grey());
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
painter.draw_rectangle({r.left() + r.width() - 1, r.top(), 1, r.height()}, Color::dark_grey());
painter.fill_rectangle(
{r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
paint_style.background);
int y = r.location().y();
int y = r.top();
if (bitmap_) {
int offset_y = vertical_center_ ? (r.height() / 2) - (bitmap_->size.height() / 2) : 6;
Point bmp_pos = {r.left() + (r.width() / 2) - (bitmap_->size.width() / 2), r.top() + offset_y};
y += bitmap_->size.height() - offset_y;
painter.draw_bitmap(
{r.location().x() + (r.size().width() / 2) - 8, r.location().y() + 6},
bmp_pos,
*bitmap_,
color_, // Color::green(), //fg,
bg);
y += 10;
}
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.location().x() + (r.size().width() - label_r.width()) / 2, y + (r.size().height() - label_r.height()) / 2},
paint_style,
text_);
if (!text_.empty()) {
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
paint_style,
text_);
}
}
void NewButton::on_focus() {
@@ -1482,11 +1495,11 @@ bool ImageOptionsField::on_touch(const TouchEvent event) {
OptionsField::OptionsField(
Point parent_pos,
int length,
size_t length,
options_t options)
: Widget{{parent_pos, {8 * length, 16}}},
: Widget{{parent_pos, {8 * (int)length, 16}}},
length_{length},
options{std::move(options)} {
options_{std::move(options)} {
set_focusable(true);
}
@@ -1494,16 +1507,20 @@ size_t OptionsField::selected_index() const {
return selected_index_;
}
size_t OptionsField::selected_index_value() const {
return options[selected_index_].second;
const OptionsField::name_t& OptionsField::selected_index_name() const {
return options_[selected_index_].first;
}
const OptionsField::value_t& OptionsField::selected_index_value() const {
return options_[selected_index_].second;
}
void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) {
if (new_index < options.size()) {
if (new_index < options_.size()) {
if (new_index != selected_index() || trigger_change) {
selected_index_ = new_index;
if (on_change) {
on_change(selected_index(), options[selected_index()].second);
on_change(selected_index(), options_[selected_index()].second);
}
set_dirty();
}
@@ -1512,7 +1529,7 @@ void OptionsField::set_selected_index(const size_t new_index, bool trigger_chang
void OptionsField::set_by_value(value_t v) {
size_t new_index = 0;
for (const auto& option : options) {
for (const auto& option : options_) {
if (option.second == v) {
set_selected_index(new_index);
return;
@@ -1529,7 +1546,7 @@ void OptionsField::set_by_nearest_value(value_t v) {
size_t curr_index = 0;
int32_t min_diff = INT32_MAX;
for (const auto& option : options) {
for (const auto& option : options_) {
auto diff = abs(v - option.second);
if (diff < min_diff) {
min_diff = diff;
@@ -1543,7 +1560,7 @@ void OptionsField::set_by_nearest_value(value_t v) {
}
void OptionsField::set_options(options_t new_options) {
options = std::move(new_options);
options_ = std::move(new_options);
// Set an invalid index to force on_change.
selected_index_ = (size_t)-1;
@@ -1556,12 +1573,14 @@ void OptionsField::paint(Painter& painter) {
painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, ui::Color::black());
if (selected_index() < options.size()) {
const auto text = options[selected_index()].first;
if (selected_index() < options_.size()) {
std::string_view temp = selected_index_name();
if (temp.length() > length_)
temp = temp.substr(0, length_);
painter.draw_string(
screen_pos(),
paint_style,
text);
temp);
}
}
@@ -1574,8 +1593,8 @@ void OptionsField::on_focus() {
bool OptionsField::on_encoder(const EncoderEvent delta) {
int32_t new_value = selected_index() + delta;
if (new_value < 0)
new_value = options.size() - 1;
else if ((size_t)new_value >= options.size())
new_value = options_.size() - 1;
else if ((size_t)new_value >= options_.size())
new_value = 0;
set_selected_index(new_value);
@@ -1589,9 +1608,9 @@ bool OptionsField::on_touch(const TouchEvent event) {
return true;
}
/* TextField ***********************************************************/
/* TextEdit ***********************************************************/
TextField::TextField(
TextEdit::TextEdit(
std::string& str,
size_t max_length,
Point position,
@@ -1605,24 +1624,24 @@ TextField::TextField(
set_focusable(true);
}
const std::string& TextField::value() const {
const std::string& TextEdit::value() const {
return text_;
}
void TextField::set_cursor(uint32_t pos) {
void TextEdit::set_cursor(uint32_t pos) {
cursor_pos_ = std::min<size_t>(pos, text_.length());
set_dirty();
}
void TextField::set_insert_mode() {
void TextEdit::set_insert_mode() {
insert_mode_ = true;
}
void TextField::set_overwrite_mode() {
void TextEdit::set_overwrite_mode() {
insert_mode_ = false;
}
void TextField::char_add(char c) {
void TextEdit::char_add(char c) {
// Don't add if inserting and at max_length and
// don't overwrite if past the end of the text.
if ((text_.length() >= max_length_ && insert_mode_) ||
@@ -1638,7 +1657,7 @@ void TextField::char_add(char c) {
set_dirty();
}
void TextField::char_delete() {
void TextEdit::char_delete() {
if (cursor_pos_ == 0)
return;
@@ -1647,7 +1666,7 @@ void TextField::char_delete() {
set_dirty();
}
void TextField::paint(Painter& painter) {
void TextEdit::paint(Painter& painter) {
constexpr int char_width = 8;
auto rect = screen_rect();
@@ -1683,7 +1702,7 @@ void TextField::paint(Painter& painter) {
painter.draw_rectangle(cursor_box, cursor_style.background);
}
bool TextField::on_key(const KeyEvent key) {
bool TextEdit::on_key(const KeyEvent key) {
if (key == KeyEvent::Left && cursor_pos_ > 0)
cursor_pos_--;
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
@@ -1697,7 +1716,7 @@ bool TextField::on_key(const KeyEvent key) {
return true;
}
bool TextField::on_encoder(const EncoderEvent delta) {
bool TextEdit::on_encoder(const EncoderEvent delta) {
int32_t new_pos = cursor_pos_ + delta;
// Let the encoder wrap around the ends of the text.
@@ -1710,7 +1729,7 @@ bool TextField::on_encoder(const EncoderEvent delta) {
return true;
}
bool TextField::on_touch(const TouchEvent event) {
bool TextEdit::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start)
focus();
@@ -1718,6 +1737,32 @@ bool TextField::on_touch(const TouchEvent event) {
return true;
}
/* TextField *************************************************************/
TextField::TextField(Rect parent_rect, std::string text)
: Text(parent_rect, std::move(text)) {
set_focusable(true);
}
const std::string& TextField::get_text() const {
return text;
}
void TextField::set_text(std::string_view value) {
set(value);
if (on_change)
on_change(*this);
}
bool TextField::on_key(KeyEvent key) {
if (key == KeyEvent::Select && on_select) {
on_select(*this);
return true;
}
return false;
}
/* NumberField ***********************************************************/
NumberField::NumberField(
+41 -19
View File
@@ -33,10 +33,11 @@
#include "portapack.hpp"
#include "utility.hpp"
#include <memory>
#include <vector>
#include <string>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace ui {
@@ -202,11 +203,11 @@ class Text : public Widget {
Text(Rect parent_rect, std::string text);
Text(Rect parent_rect);
void set(const std::string value);
void set(std::string_view value);
void paint(Painter& painter) override;
private:
protected:
std::string text;
};
@@ -455,7 +456,7 @@ class NewButton : public Widget {
NewButton(const NewButton&) = delete;
NewButton& operator=(const NewButton&) = delete;
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap);
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color);
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color, bool vertical_center = false);
NewButton()
: NewButton{{}, {}, {}} {
}
@@ -463,6 +464,7 @@ class NewButton : public Widget {
void set_bitmap(const Bitmap* bitmap);
void set_text(const std::string value);
void set_color(Color value);
void set_vertical_center(bool value);
std::string text() const;
const Bitmap* bitmap();
ui::Color color();
@@ -477,6 +479,7 @@ class NewButton : public Widget {
std::string text_;
const Bitmap* bitmap_;
Color color_;
bool vertical_center_{false};
};
class Image : public Widget {
@@ -615,12 +618,15 @@ class OptionsField : public Widget {
std::function<void(size_t, value_t)> on_change{};
std::function<void(void)> on_show_options{};
OptionsField(Point parent_pos, int length, options_t options);
OptionsField(Point parent_pos, size_t length, options_t options);
options_t& options() { return options_; }
const options_t& options() const { return options_; }
void set_options(options_t new_options);
size_t selected_index() const;
size_t selected_index_value() const;
const name_t& selected_index_name() const;
const value_t& selected_index_value() const;
void set_selected_index(const size_t new_index, bool trigger_change = true);
void set_by_value(value_t v);
@@ -633,19 +639,19 @@ class OptionsField : public Widget {
bool on_touch(const TouchEvent event) override;
private:
const int length_;
options_t options;
const size_t length_;
options_t options_;
size_t selected_index_{0};
};
// A TextField is bound to a string reference and allows the string
// A TextEdit is bound to a string reference and allows the string
// to be manipulated. The field itself does not provide the UI for
// setting the value. It provides the UI of rendering the text,
// a cursor, and an API to edit the string content.
class TextField : public Widget {
class TextEdit : public Widget {
public:
TextField(std::string& str, Point position, uint32_t length = 30)
: TextField{str, 64, position, length} {}
TextEdit(std::string& str, Point position, uint32_t length = 30)
: TextEdit{str, 64, position, length} {}
// Str: the string containing the content to edit.
// Max_length: max length the string is allowed to use.
@@ -654,12 +660,12 @@ class TextField : public Widget {
// - Characters are 8 pixels wide.
// - The screen can show 30 characters max.
// - The control is 16 pixels tall.
TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30);
TextEdit(std::string& str, size_t max_length, Point position, uint32_t length = 30);
TextField(const TextField&) = delete;
TextField(TextField&&) = delete;
TextField& operator=(const TextField&) = delete;
TextField& operator=(TextField&&) = delete;
TextEdit(const TextEdit&) = delete;
TextEdit(TextEdit&&) = delete;
TextEdit& operator=(const TextEdit&) = delete;
TextEdit& operator=(TextEdit&&) = delete;
const std::string& value() const;
@@ -684,6 +690,22 @@ class TextField : public Widget {
bool insert_mode_;
};
class TextField : public Text {
public:
std::function<void(TextField&)> on_select{};
std::function<void(TextField&)> on_change{};
TextField(Rect parent_rect, std::string text);
const std::string& get_text() const;
void set_text(std::string_view value);
bool on_key(KeyEvent key) override;
private:
using Text::set;
};
class NumberField : public Widget {
public:
std::function<void(NumberField&)> on_select{};
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

+4 -2
View File
@@ -42,14 +42,15 @@ add_executable(application_test EXCLUDE_FROM_ALL
${PROJECT_SOURCE_DIR}/test_freqman_db.cpp
${PROJECT_SOURCE_DIR}/test_mock_file.cpp
${PROJECT_SOURCE_DIR}/test_optional.cpp
${PROJECT_SOURCE_DIR}/test_string_format.cpp
${PROJECT_SOURCE_DIR}/test_utility.cpp
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
${PROJECT_SOURCE_DIR}/../../application/string_format.cpp
# Dependencies
${PROJECT_SOURCE_DIR}/../../application/file.cpp
${PROJECT_SOURCE_DIR}/../../application/string_format.cpp
${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp
${PROJECT_SOURCE_DIR}/linker_stubs.cpp
)
@@ -70,6 +71,7 @@ target_include_directories(application_test PRIVATE
)
target_compile_options(application_test PRIVATE
-std=c++17
-DLPC43XX
-DLPC43XX_M0
-D__NEWLIB__
+28 -3
View File
@@ -46,10 +46,11 @@ TEST_CASE("It should convert string_views.") {
CHECK_EQ(val, 12345);
}
TEST_CASE("It should return false for invalid input") {
// 'from_chars' supported this, but 'strtol' just returns 0.
/*TEST_CASE("It should return false for invalid input") {
int val = 0;
REQUIRE_FALSE(parse_int("QWERTY", val));
}
}*/
TEST_CASE("It should return false for overflow input") {
uint8_t val = 0;
@@ -58,7 +59,7 @@ TEST_CASE("It should return false for overflow input") {
TEST_CASE("It should convert base 16.") {
int val = 0;
REQUIRE(parse_int("30", val, 16)); // NB: No '0x'
REQUIRE(parse_int("0x30", val, 16));
CHECK_EQ(val, 0x30);
}
@@ -74,4 +75,28 @@ TEST_CASE("It should convert as much of the string as it can.") {
CHECK_EQ(val, 12345);
}
TEST_CASE("It should convert 64-bit.") {
int64_t val = 0;
REQUIRE(parse_int("123456789", val));
CHECK_EQ(val, 123456789);
}
TEST_CASE("It should convert 32-bit.") {
int32_t val = 0;
REQUIRE(parse_int("123456", val));
CHECK_EQ(val, 123456);
}
TEST_CASE("It should convert 16-bit.") {
int16_t val = 0;
REQUIRE(parse_int("12345", val));
CHECK_EQ(val, 12345);
}
TEST_CASE("It should convert 8-bit.") {
int8_t val = 0;
REQUIRE(parse_int("123", val));
CHECK_EQ(val, 123);
}
TEST_SUITE_END();
+34 -4
View File
@@ -166,14 +166,44 @@ TEST_CASE("It can parse tone freq") {
CHECK_EQ(e.tone, 3);
}
#if 0 // New tables for a future PR.
TEST_CASE("It can serialize basic Single entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.description = "Foobar",
.type = freqman_type::Single,
});
CHECK(str == "f=123456000,d=Foobar");
}
TEST_CASE("It can serialize basic Range entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.frequency_b = 423'456'000,
.description = "Foobar",
.type = freqman_type::Range,
});
CHECK(str == "a=123456000,b=423456000,d=Foobar");
}
TEST_CASE("It can serialize basic HamRadio entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.frequency_b = 423'456'000,
.description = "Foobar",
.type = freqman_type::HamRadio,
});
CHECK(str == "r=123456000,t=423456000,d=Foobar");
}
// New tables for a future PR.
/*
TEST_CASE("It can parse modulation") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=AM", e));
CHECK_EQ(e.modulation, freqman_modulation::AM);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=NFM", e));
@@ -201,7 +231,7 @@ TEST_CASE("It can parse frequency step") {
parse_freqman_entry(
"f=123000000,d=This is the description.,s=0.1kHz", e));
CHECK_EQ(e.step, freqman_step::_100Hz);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=50kHz", e));
@@ -212,6 +242,6 @@ TEST_CASE("It can parse frequency step") {
"f=123000000,d=This is the description.,s=FOO", e));
CHECK_EQ(e.step, freqman_step::Unknown);
}
#endif
*/
TEST_SUITE_END();
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "doctest.h"
#include "string_format.hpp"
/* TODO: Tests for all string_format functions. */
TEST_CASE("to_string_dec_uint64 returns correct value.") {
CHECK_EQ(to_string_dec_uint64(0), "0");
CHECK_EQ(to_string_dec_uint64(1), "1");
CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891");
}
TEST_CASE("to_string_freq returns correct value.") {
CHECK_EQ(to_string_freq(0), " 0");
CHECK_EQ(to_string_freq(1), " 1");
CHECK_EQ(to_string_freq(1'000'000), " 1000000");
CHECK_EQ(to_string_freq(1'234'567'890), "1234567890");
CHECK_EQ(to_string_freq(1'234'567'891), "1234567891");
}
TEST_CASE("to_string_short_freq returns correct value.") {
CHECK_EQ(to_string_short_freq(0), " 0.0000");
CHECK_EQ(to_string_short_freq(49), " 0.0000");
CHECK_EQ(to_string_short_freq(50), " 0.0001");
CHECK_EQ(to_string_short_freq(1'000'049), " 1.0000");
CHECK_EQ(to_string_short_freq(1'000'050), " 1.0001");
CHECK_EQ(to_string_short_freq(1'234'567'850), "1234.5679");
CHECK_EQ(to_string_short_freq(1'234'567'849), "1234.5678");
}
TEST_CASE("to_string_rounded_freq returns correct value.") {
CHECK_EQ(to_string_rounded_freq(0, 0), "0");
CHECK_EQ(to_string_rounded_freq(0, 2), "0.00");
CHECK_EQ(to_string_rounded_freq(49, 4), "0.0000");
CHECK_EQ(to_string_rounded_freq(50, 4), "0.0001");
CHECK_EQ(to_string_rounded_freq(23'456, 4), "0.0235");
CHECK_EQ(to_string_rounded_freq(1'000'000, 4), "1.0000");
CHECK_EQ(to_string_rounded_freq(1'000'567'849, 4), "1000.5678");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 0), "1234");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 1), "1234.6");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 2), "1234.57");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 3), "1234.568");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 4), "1234.5679");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 5), "1234.56789");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 6), "1234.567891");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 9), "1234.567891");
}
TEST_CASE("trim removes whitespace.") {
CHECK(trim(" foo\n") == "foo");
}
TEST_CASE("trim returns empty for only whitespace.") {
CHECK(trim(" \n").empty());
}
TEST_CASE("trim empty returns empty.") {
CHECK(trim("").empty());
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
These fonts are for free and can be use for commercial and non-commercial purposes. These aren't professionally made--only beta test. Let me know if there are problems with spaces and kernings.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2017 The Sedgwick Ave Project Authors (https://github.com/googlefonts/sedgwickave)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
,dannybernecker,dannys-mbp.speedport_w_724v_typ_a_05011603_00_007,16.12.2015 04:43,file:///Users/dannybernecker/Library/Application%20Support/LibreOffice/4;
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

@@ -0,0 +1,94 @@
Copyright (c) <2006>, <Punkto Design> (<http://charaktertypen.com | info@charaktertypen.com>),
with Reserved Font Name <„Beatstreet“>.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright (c) <2006>, <Punkto Design, D. Bernecker> (<http://charaktertypen.com | info@charaktertypen.com>), with Reserved Font Name <„Tagster“>.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=shift_jis">
<TITLE>LARABIE FONTS &ldquo;README</TITLE>
<META NAME="GENERATOR" CONTENT="StarOffice 8 (Win32)">
<META NAME="AUTHOR" CONTENT="Rina Larabie">
<META NAME="CREATED" CONTENT="20061130;11580000">
<META NAME="CHANGEDBY" CONTENT="Rina Larabie">
<META NAME="CHANGED" CONTENT="20061207;15120000">
<STYLE>
<!--
@page { size: 8.5in 11in; margin-right: 1.25in; margin-top: 1in; margin-bottom: 1in }
P { margin-bottom: 0.08in; direction: ltr; color: #000000; widows: 2; orphans: 2 }
P.western { font-family: "Times New Roman", serif; font-size: 12pt; so-language: en-US }
P.cjk { font-family: "Times New Roman", serif; font-size: 12pt }
P.ctl { font-family: "Times New Roman", serif; font-size: 12pt; so-language: ar-SA }
H3 { margin-top: 0.19in; margin-bottom: 0.19in; direction: ltr; color: #000000; widows: 2; orphans: 2; page-break-after: auto }
H3.western { font-size: 13pt; so-language: en-US }
H3.cjk { font-family: "Times New Roman", serif; font-size: 13pt }
H3.ctl { font-family: "Times New Roman", serif; font-size: 13pt; so-language: ar-SA }
A:link { color: #0000ff }
-->
</STYLE>
</HEAD>
<BODY LANG="en-US" TEXT="#000000" LINK="#0000ff" DIR="LTR">
<H3 CLASS="western" STYLE="margin-top: 0in"><FONT FACE="Tahoma, sans-serif">LARABIE
FONTS FREEWARE FONTS EULA (End User License Agreement) and SOFTWARE
INCLUSION AGREEMENT</FONT></H3>
<P CLASS="western" STYLE="margin-bottom: 0in"><B><FONT FACE="Tahoma, sans-serif">LARABIE
FONTS FREEWARE FONTS EULA</FONT></B></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Larabie
Fonts Freeware Fonts in TrueType format are free to use for personal
and commercial purposes. No payment is necessary to use Larabie Fonts
Freeware Fonts for personal or commercial use. If you wish to include
Larabie Fonts Freeware Fonts in software see SOFTWARE INCLUSION
AGREEMENT below. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>USAGE</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">You
can offer individual Larabie Fonts Freeware Fonts for download on a
website but do not combine fonts into a single archive or alter them
in any way. We appreciate inclusion of the font name and trademark or
site URL in the credits or documentation but it is not mandatory. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Some
Larabie Fonts Freeware Fonts may have enhanced and/or expanded
families available for sale at </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.typodermic.com/"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.typodermic.com</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>PAYMENT</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Payment
is not required for the use of Larabie Fonts Freeware Fonts unless
they are intended to be <I>included</I> with software. More details
follow.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Donations
to Larabie Fonts are welcome via check or electronic payment via
Paypal. We can accept any amount in US dollars, Canadian dollars, GBP
or euros. If you require an invoice for your donation, please refer
to </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/donation.html"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com/donation.html</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">
for instructions. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Donations
can include CDs, magazines, t-shirts, a sample of your merchandise or
anything featuring Larabie Fonts. Mailing information is available at
the link above.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>SUPPORT</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Font
installation help is available at </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.myfonts.com/support"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.myfonts.com/support</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.
If you experience problems with any Larabie Font, please visit
</FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">
to verify you have the latest version. If you download Larabie Fonts
Freeware Fonts from other websites you may get older versions that
have spacing issues, incomplete character sets or technical problems.
</FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><B><FONT FACE="Tahoma, sans-serif">SOFTWARE
INCLUSION AGREEMENT </FONT></B>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">The
Larabie Fonts SOFTWARE PRODUCT is protected by copyright laws and
International copyright treaties, as well as other intellectual
property laws and treaties. The SOFTWARE PRODUCT is licensed, not
sold.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">1.
GRANT OF LICENSE. This document grants you the following rights:</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>-
Installation and Use. You may install and use an unlimited number of
copies of the SOFTWARE PRODUCT. You may copy and distribute unlimited
copies of the SOFTWARE PRODUCT as you receive them, in any medium,
provided that you publish on each copy an appropriate copyright
notice. Keep intact all the notices that refer to this License and
give any other recipients of the fonts a copy of this License along
with the fonts.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">2.
LIMITED WARRANTY NO WARRANTIES. Larabie Fonts expressly disclaims any
warranty for the SOFTWARE PRODUCT. The SOFTWARE PRODUCT and any
related documentation is provided &quot;as is&quot; without warranty
of any kind, either express or implied, including, without
limitation, the implied warranties or merchantability, fitness for a
particular purpose, or non-infringement. The entire risk arising out
of use or performance of the SOFTWARE PRODUCT remains with you.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">NO
LIABILITY FOR CONSEQUENTIAL DAMAGES. In no event shall Ray Larabie or
Larabie Fonts be liable for any damages whatsoever (including,
without limitation, damages for loss of business profits, business
interruption, loss of business information, or any other pecuniary
loss) arising out of the use of or inability to use this product,
even if Larabie Fonts has been advised of the possibility of such
damages.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>3.
MISCELLANEOUS</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Should
you have any questions concerning this document or you desire to
contact Larabie Fonts for any reason, please email
</FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/email.html"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com/email.html</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">4.
GOVERNING LAW</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>This
agreement is governed by the laws of Canada and the province of
British Columbia.</FONT></FONT></P>
</BODY>
</HTML>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

+456
View File
@@ -0,0 +1,456 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="2320px" height="2320px" viewBox="0 0 2320 2320" enable-background="new 0 0 2320 2320" xml:space="preserve">
<rect x="0" y="0" width="2320" height="2320" fill="rgb(255,255,255)" /><g transform="translate(160,160)"><g transform="translate(640,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,560) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,560) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(240,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(320,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(240,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,0) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(1440,0) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(0,1440) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(160,160) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,160) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1600) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g></g></svg>

After

Width:  |  Height:  |  Size: 30 KiB