SDRPlusPlus/frequency_manager/src/main.cpp

588 wiersze
21 KiB
C++

#include <imgui.h>
#include <spdlog/spdlog.h>
#include <module.h>
#include <gui/gui.h>
#include <gui/style.h>
#include <core.h>
#include <thread>
#include <options.h>
#include <radio_interface.h>
#include <signal_path/signal_path.h>
#include <vector>
#include <gui/tuner.h>
#include <gui/file_dialogs.h>
SDRPP_MOD_INFO {
/* Name: */ "frequency_manager",
/* Description: */ "Frequency manager module for SDR++",
/* Author: */ "Ryzerth;zimm",
/* Version: */ 0, 3, 0,
/* Max instances */ 1
};
struct FrequencyBookmark {
double frequency;
double bandwidth;
int mode;
bool selected;
};
ConfigManager config;
const char* demodModeList[] = {
"NFM",
"WFM",
"AM",
"DSB",
"USB",
"CW",
"LSB",
"RAW"
};
const char* demodModeListTxt = "NFM\0WFM\0AM\0DSB\0USB\0CW\0LSB\0RAW\0";
class FrequencyManagerModule : public ModuleManager::Instance {
public:
FrequencyManagerModule(std::string name) {
this->name = name;
config.aquire();
std::string selList = config.conf["selectedList"];
config.release();
refreshLists();
loadByName(selList);
gui::menu.registerEntry(name, menuHandler, this, NULL);
}
~FrequencyManagerModule() {
gui::menu.removeEntry(name);
}
void enable() {
enabled = true;
}
void disable() {
enabled = false;
}
bool isEnabled() {
return enabled;
}
private:
static std::string freqToStr(double freq) {
char str[128];
if (freq >= 1000000.0) {
sprintf(str, "%.06lf", freq / 1000000.0);
int len = strlen(str) - 1;
while ((str[len] == '0' || str[len] == '.') && len > 0) { len--; }
return std::string(str).substr(0, len + 1) + "MHz";
}
else if (freq >= 1000.0) {
sprintf(str, "%.06lf", freq / 1000.0);
int len = strlen(str) - 1;
while ((str[len] == '0' || str[len] == '.') && len > 0) { len--; }
return std::string(str).substr(0, len + 1) + "KHz";
}
else {
sprintf(str, "%.06lf", freq);
int len = strlen(str) - 1;
while ((str[len] == '0' || str[len] == '.') && len > 0) { len--; }
return std::string(str).substr(0, len + 1) + "Hz";
}
}
static void applyBookmark(FrequencyBookmark bm, std::string vfoName) {
if (vfoName == "") {
// TODO: Replace with proper tune call
gui::waterfall.setCenterFrequency(bm.frequency);
gui::waterfall.centerFreqMoved = true;
}
else {
tuner::tune(tuner::TUNER_MODE_NORMAL, vfoName, bm.frequency);
if (core::modComManager.interfaceExists(vfoName)) {
if (core::modComManager.getModuleName(vfoName) == "radio") {
int mode = bm.mode;
core::modComManager.callInterface(vfoName, RADIO_IFACE_CMD_SET_MODE, &mode, NULL);
// TODO: Set bandwidth as well
}
}
}
}
bool bookmarkEditDialog() {
bool open = true;
gui::mainWindow.lockWaterfallControls = true;
std::string id = "Edit##freq_manager_edit_popup_" + name;
ImGui::OpenPopup(id.c_str());
char nameBuf[1024];
strcpy(nameBuf, editedBookmarkName.c_str());
if (ImGui::BeginPopup(id.c_str(), ImGuiWindowFlags_NoResize)) {
ImGui::BeginTable(("freq_manager_edit_table"+name).c_str(), 2);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Name");
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(200);
if (ImGui::InputText(("##freq_manager_edit_name"+name).c_str(), nameBuf, 1023)) {
editedBookmarkName = nameBuf;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Frequency");
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(200);
ImGui::InputDouble(("##freq_manager_edit_freq"+name).c_str(), &editedBookmark.frequency);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Bandwidth");
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(200);
ImGui::InputDouble(("##freq_manager_edit_bw"+name).c_str(), &editedBookmark.bandwidth);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Mode");
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(200);
ImGui::Combo(("##freq_manager_edit_mode"+name).c_str(), &editedBookmark.mode, demodModeListTxt);
ImGui::EndTable();
if (strlen(nameBuf) == 0) { style::beginDisabled(); }
if (ImGui::Button("Apply")) {
open = false;
// If editing, delete the original one
if (editOpen) {
bookmarks.erase(firstEeditedBookmarkName);
}
bookmarks[nameBuf] = editedBookmark;
saveByName(selectedListName);
}
if (strlen(nameBuf) == 0) { style::endDisabled(); }
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
open = false;
}
ImGui::EndPopup();
}
return open;
}
bool newListDialog() {
bool open = true;
gui::mainWindow.lockWaterfallControls = true;
float menuWidth = ImGui::GetContentRegionAvailWidth();
std::string id = "New##freq_manager_new_popup_" + name;
ImGui::OpenPopup(id.c_str());
char nameBuf[1024];
strcpy(nameBuf, editedListName.c_str());
if (ImGui::BeginPopup(id.c_str(), ImGuiWindowFlags_NoResize)) {
ImGui::Text("Name");
ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX());
if (ImGui::InputText(("##freq_manager_edit_name"+name).c_str(), nameBuf, 1023)) {
editedListName = nameBuf;
}
if (strlen(nameBuf) == 0) { style::beginDisabled(); }
if (ImGui::Button("Apply")) {
open = false;
config.aquire();
if (renameListOpen) {
config.conf["lists"][editedListName] = config.conf["lists"][firstEditedListName];
config.conf["lists"].erase(firstEditedListName);
}
else {
config.conf["lists"][editedListName] = json::object();
}
config.release(true);
refreshLists();
loadByName(editedListName);
}
if (strlen(nameBuf) == 0) { style::endDisabled(); }
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
open = false;
}
ImGui::EndPopup();
}
return open;
}
void refreshLists() {
listNames.clear();
listNamesTxt = "";
config.aquire();
for (auto [_name, list] : config.conf["lists"].items()) {
listNames.push_back(_name);
listNamesTxt += _name;
listNamesTxt += '\0';
}
config.release();
}
void loadFirst() {
if (listNames.size() > 0) {
loadByName(listNames[0]);
return;
}
selectedListName = "";
selectedListId = 0;
}
void loadByName(std::string listName) {
bookmarks.clear();
if (std::find(listNames.begin(), listNames.end(), listName) == listNames.end()) {
selectedListName = "";
selectedListId = 0;
loadFirst();
return;
}
selectedListId = std::distance(listNames.begin(), std::find(listNames.begin(), listNames.end(), listName));
selectedListName = listName;
config.aquire();
for (auto [bmName, bm] : config.conf["lists"][listName].items()) {
FrequencyBookmark fbm;
fbm.frequency = bm["frequency"];
fbm.bandwidth = bm["bandwidth"];
fbm.mode = bm["mode"];
fbm.selected = false;
bookmarks[bmName] = fbm;
}
config.release();
}
void saveByName(std::string listName) {
config.aquire();
config.conf["lists"][listName] = json::object();
for (auto [bmName, bm] : bookmarks) {
config.conf["lists"][listName][bmName]["frequency"] = bm.frequency;
config.conf["lists"][listName][bmName]["bandwidth"] = bm.bandwidth;
config.conf["lists"][listName][bmName]["mode"] = bm.mode;
}
config.release(true);
}
static void menuHandler(void* ctx) {
FrequencyManagerModule* _this = (FrequencyManagerModule*)ctx;
float menuWidth = ImGui::GetContentRegionAvailWidth();
// TODO: Replace with something that won't iterate every frame
std::vector<std::string> selectedNames;
for (auto& [name, bm] : _this->bookmarks) { if (bm.selected) { selectedNames.push_back(name); } }
float lineHeight = ImGui::GetTextLineHeightWithSpacing();
float btnSize = ImGui::CalcTextSize("Rename").x + 8;
ImGui::SetNextItemWidth(menuWidth - 24 - (2*lineHeight) - btnSize);
if (ImGui::Combo(("##freq_manager_list_sel"+_this->name).c_str(), &_this->selectedListId, _this->listNamesTxt.c_str())) {
_this->loadByName(_this->listNames[_this->selectedListId]);
}
ImGui::SameLine();
if (_this->listNames.size() == 0) { style::beginDisabled(); }
if (ImGui::Button(("Rename##_freq_mgr_ren_lst_" + _this->name).c_str(), ImVec2(btnSize, 0))) {
_this->firstEditedListName = _this->listNames[_this->selectedListId];
_this->editedListName = _this->firstEditedListName;
_this->renameListOpen = true;
}
if (_this->listNames.size() == 0) { style::endDisabled(); }
ImGui::SameLine();
if (ImGui::Button(("+##_freq_mgr_add_lst_" + _this->name).c_str(), ImVec2(lineHeight, 0))) {
// Find new unique default name
if (std::find(_this->listNames.begin(), _this->listNames.end(), "New List") == _this->listNames.end()) {
_this->editedListName = "New List";
}
else {
char buf[64];
for (int i = 1; i < 1000; i++) {
sprintf(buf, "New List (%d)", i);
if (std::find(_this->listNames.begin(), _this->listNames.end(), buf) == _this->listNames.end()) { break; }
}
_this->editedListName = buf;
}
_this->newListOpen = true;
}
ImGui::SameLine();
if (_this->selectedListName == "") { style::beginDisabled(); }
if (ImGui::Button(("-##_freq_mgr_del_lst_" + _this->name).c_str(), ImVec2(lineHeight, 0))) {
if (_this->selectedListName == "") { style::endDisabled(); }
config.aquire();
config.conf["lists"].erase(_this->selectedListName);
config.release(true);
_this->refreshLists();
_this->selectedListId = std::clamp<int>(_this->selectedListId, 0, _this->listNames.size());
if (_this->listNames.size() > 0) {
_this->loadByName(_this->listNames[_this->selectedListId]);
}
else {
_this->selectedListName = "";
}
}
else {
if (_this->selectedListName == "") { style::endDisabled(); }
}
if (_this->selectedListName == "") { style::beginDisabled(); }
//Draw buttons on top of the list
ImGui::BeginTable(("freq_manager_btn_table"+_this->name).c_str(), 3);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::Button(("Add##_freq_mgr_add_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvailWidth(), 0))) {
// If there's no VFO selected, just save the center freq
if (gui::waterfall.selectedVFO == "") {
_this->editedBookmark.frequency = gui::waterfall.getCenterFrequency();
_this->editedBookmark.bandwidth = 0;
_this->editedBookmark.mode = 7;
}
else {
_this->editedBookmark.frequency = gui::waterfall.getCenterFrequency() + sigpath::vfoManager.getOffset(gui::waterfall.selectedVFO);
_this->editedBookmark.bandwidth = sigpath::vfoManager.getBandwidth(gui::waterfall.selectedVFO);
_this->editedBookmark.mode = 7;
if (core::modComManager.getModuleName(gui::waterfall.selectedVFO) == "radio") {
int mode;
core::modComManager.callInterface(gui::waterfall.selectedVFO, RADIO_IFACE_CMD_GET_MODE, NULL, &mode);
_this->editedBookmark.mode = mode;
}
}
_this->editedBookmark.selected = false;
_this->editOpen = true;
// Find new unique default name
if (_this->bookmarks.find("New Bookmark") == _this->bookmarks.end()) {
_this->editedBookmarkName = "New Bookmark";
}
else {
char buf[64];
for (int i = 1; i < 1000; i++) {
sprintf(buf, "New Bookmark (%d)", i);
if (_this->bookmarks.find(buf) == _this->bookmarks.end()) { break; }
}
_this->editedBookmarkName = buf;
}
}
ImGui::TableSetColumnIndex(1);
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::beginDisabled(); }
if (ImGui::Button(("Remove##_freq_mgr_rem_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvailWidth(), 0))) {
for (auto& _name : selectedNames) { _this->bookmarks.erase(_name); }
_this->saveByName(_this->selectedListName);
}
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::endDisabled(); }
ImGui::TableSetColumnIndex(2);
if (selectedNames.size() != 1 && _this->selectedListName != "") { style::beginDisabled(); }
if (ImGui::Button(("Edit##_freq_mgr_edt_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvailWidth(), 0))) {
_this->editOpen = true;
_this->editedBookmark = _this->bookmarks[selectedNames[0]];
_this->editedBookmarkName = selectedNames[0];
_this->firstEeditedBookmarkName = selectedNames[0];
}
if (selectedNames.size() != 1 && _this->selectedListName != "") { style::endDisabled(); }
ImGui::EndTable();
// Bookmark list
ImGui::BeginTable(("freq_manager_bkm_table"+_this->name).c_str(), 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImVec2(0, 200));
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Bookmark", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
for (auto& [name, bm] : _this->bookmarks) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImVec2 min = ImGui::GetCursorPos();
ImGui::Selectable((name + "##_freq_mgr_bkm_name_" + _this->name).c_str(), &bm.selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnClick);
if (ImGui::TableGetHoveredColumn() >= 0 && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
applyBookmark(bm, gui::waterfall.selectedVFO);
}
ImGui::TableSetColumnIndex(1);
ImGui::Text("%s %s", freqToStr(bm.frequency).c_str(), demodModeList[bm.mode]);
ImVec2 max = ImGui::GetCursorPos();
}
ImGui::EndTable();
if (selectedNames.size() != 1 && _this->selectedListName != "") { style::beginDisabled(); }
if (ImGui::Button(("Apply##_freq_mgr_apply_" + _this->name).c_str(), ImVec2(menuWidth, 0))) {
FrequencyBookmark& bm = _this->bookmarks[selectedNames[0]];
applyBookmark(bm, gui::waterfall.selectedVFO);
bm.selected = false;
}
if (selectedNames.size() != 1 && _this->selectedListName != "") { style::endDisabled(); }
//Draw import and export buttons
ImGui::BeginTable(("freq_manager_bottom_btn_table"+_this->name).c_str(), 2);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::Button(("Import##_freq_mgr_imp_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvailWidth(), 0)) && !_this->importOpen) {
_this->importOpen = true;
_this->importDialog = new pfd::open_file("Import bookmarks", "", {"JSON Files (*.json)", "*.json", "All Files", "*"}, true);
}
ImGui::TableSetColumnIndex(1);
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::beginDisabled(); }
if (ImGui::Button(("Export##_freq_mgr_exp_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvailWidth(), 0)) && !_this->exportOpen) {
_this->exportedBookmarks = json::object();
config.aquire();
for (auto& _name : selectedNames) {
_this->exportedBookmarks["bookmarks"][_name] = config.conf["lists"][_this->selectedListName][_name];
}
config.release();
_this->exportOpen = true;
_this->exportDialog = new pfd::save_file("Export bookmarks", "", {"JSON Files (*.json)", "*.json", "All Files", "*"}, true);
}
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::endDisabled(); }
ImGui::EndTable();
if (_this->selectedListName == "") { style::endDisabled(); }
if (_this->createOpen) {
_this->createOpen = _this->bookmarkEditDialog();
}
if (_this->editOpen) {
_this->editOpen = _this->bookmarkEditDialog();
}
if (_this->newListOpen) {
_this->newListOpen = _this->newListDialog();
}
if (_this->renameListOpen) {
_this->renameListOpen = _this->newListDialog();
}
// Handle import and export
if (_this->importOpen && _this->importDialog->ready()) {
_this->importOpen = false;
std::vector<std::string> paths = _this->importDialog->result();
if (paths.size() > 0 && _this->listNames.size() > 0) {
_this->importBookmarks(paths[0]);
}
delete _this->importDialog;
}
if (_this->exportOpen && _this->exportDialog->ready()) {
_this->exportOpen = false;
std::string path = _this->exportDialog->result();
if (path != "") {
_this->exportBookmarks(path);
}
delete _this->exportDialog;
}
}
json exportedBookmarks;
bool importOpen = false;
bool exportOpen = false;
pfd::open_file* importDialog;
pfd::save_file* exportDialog;
void importBookmarks(std::string path) {
std::ifstream fs(path);
json importBookmarks;
fs >> importBookmarks;
if (!importBookmarks.contains("bookmarks")) {
spdlog::error("File does not contains any bookmarks");
return;
}
if (!importBookmarks["bookmarks"].is_object()) {
spdlog::error("Bookmark attribute is invalid");
return;
}
// Load every bookmark
for (auto const [_name, bm] : importBookmarks["bookmarks"].items()) {
if (bookmarks.find(_name) != bookmarks.end()) {
spdlog::warn("Bookmark with the name '{0}' already exists in list, skipping", _name);
continue;
}
FrequencyBookmark fbm;
fbm.frequency = bm["frequency"];
fbm.bandwidth = bm["bandwidth"];
fbm.mode = bm["mode"];
fbm.selected = false;
bookmarks[_name] = fbm;
}
saveByName(selectedListName);
fs.close();
}
void exportBookmarks(std::string path) {
std::ofstream fs(path);
exportedBookmarks >> fs;
fs.close();
}
std::string name;
bool enabled = true;
bool createOpen = false;
bool editOpen = false;
bool newListOpen = false;
bool renameListOpen = false;
std::map<std::string, FrequencyBookmark> bookmarks;
std::string editedBookmarkName = "";
std::string firstEeditedBookmarkName = "";
FrequencyBookmark editedBookmark;
std::vector<std::string> listNames;
std::string listNamesTxt = "";
std::string selectedListName = "";
int selectedListId = 0;
std::string editedListName;
std::string firstEditedListName;
int testN = 0;
};
MOD_EXPORT void _INIT_() {
json def = json({});
def["selectedList"] = "General";
def["lists"]["General"] = json::array();
config.setPath(options::opts.root + "/frequency_manager_config.json");
config.load(def);
config.enableAutoSave();
}
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
return new FrequencyManagerModule(name);
}
MOD_EXPORT void _DELETE_INSTANCE_(void* instance) {
delete (FrequencyManagerModule*)instance;
}
MOD_EXPORT void _END_() {
// Nothing here
}