#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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; }; struct WaterfallBookmark { std::string listName; std::string bookmarkName; FrequencyBookmark bookmark; }; 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"; enum { BOOKMARK_DISP_MODE_OFF, BOOKMARK_DISP_MODE_TOP, BOOKMARK_DISP_MODE_BOTTOM, _BOOKMARK_DISP_MODE_COUNT }; const char* bookmarkDisplayModesTxt = "Off\0Top\0Bottom\0"; class FrequencyManagerModule : public ModuleManager::Instance { public: FrequencyManagerModule(std::string name) { this->name = name; config.acquire(); std::string selList = config.conf["selectedList"]; bookmarkDisplayMode = config.conf["bookmarkDisplayMode"]; config.release(); refreshLists(); loadByName(selList); refreshWaterfallBookmarks(); fftRedrawHandler.ctx = this; fftRedrawHandler.handler = fftRedraw; inputHandler.ctx = this; inputHandler.handler = fftInput; gui::menu.registerEntry(name, menuHandler, this, NULL); gui::waterfall.onFFTRedraw.bindHandler(&fftRedrawHandler); gui::waterfall.onInputProcess.bindHandler(&inputHandler); } ~FrequencyManagerModule() { gui::menu.removeEntry(name); gui::waterfall.onFFTRedraw.unbindHandler(&fftRedrawHandler); gui::waterfall.onInputProcess.unbindHandler(&inputHandler); } void postInit() {} void enable() { enabled = true; } void disable() { enabled = false; } bool isEnabled() { return enabled; } private: 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 { if (core::modComManager.interfaceExists(vfoName)) { if (core::modComManager.getModuleName(vfoName) == "radio") { int mode = bm.mode; float bandwidth = bm.bandwidth; core::modComManager.callInterface(vfoName, RADIO_IFACE_CMD_SET_MODE, &mode, NULL); core::modComManager.callInterface(vfoName, RADIO_IFACE_CMD_SET_BANDWIDTH, &bandwidth, NULL); } } tuner::tune(tuner::TUNER_MODE_NORMAL, vfoName, bm.frequency); } } 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::LeftLabel("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::LeftLabel("Frequency"); ImGui::TableSetColumnIndex(1); ImGui::SetNextItemWidth(200); ImGui::InputDouble(("##freq_manager_edit_freq" + name).c_str(), &editedBookmark.frequency); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::LeftLabel("Bandwidth"); ImGui::TableSetColumnIndex(1); ImGui::SetNextItemWidth(200); ImGui::InputDouble(("##freq_manager_edit_bw" + name).c_str(), &editedBookmark.bandwidth); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::LeftLabel("Mode"); ImGui::TableSetColumnIndex(1); ImGui::SetNextItemWidth(200); ImGui::Combo(("##freq_manager_edit_mode" + name).c_str(), &editedBookmark.mode, demodModeListTxt); ImGui::EndTable(); bool applyDisabled = (strlen(nameBuf) == 0) || (bookmarks.find(editedBookmarkName) != bookmarks.end() && editedBookmarkName != firstEditedBookmarkName); if (applyDisabled) { style::beginDisabled(); } if (ImGui::Button("Apply")) { open = false; // If editing, delete the original one if (editOpen) { bookmarks.erase(firstEditedBookmarkName); } bookmarks[editedBookmarkName] = editedBookmark; saveByName(selectedListName); } if (applyDisabled) { 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::GetContentRegionAvail().x; 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::LeftLabel("Name"); ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); if (ImGui::InputText(("##freq_manager_edit_name" + name).c_str(), nameBuf, 1023)) { editedListName = nameBuf; } bool alreadyExists = (std::find(listNames.begin(), listNames.end(), editedListName) != listNames.end()); if (strlen(nameBuf) == 0 || alreadyExists) { style::beginDisabled(); } if (ImGui::Button("Apply")) { open = false; config.acquire(); if (renameListOpen) { config.conf["lists"][editedListName] = config.conf["lists"][firstEditedListName]; config.conf["lists"].erase(firstEditedListName); } else { config.conf["lists"][editedListName]["showOnWaterfall"] = true; config.conf["lists"][editedListName]["bookmarks"] = json::object(); } refreshWaterfallBookmarks(false); config.release(true); refreshLists(); loadByName(editedListName); } if (strlen(nameBuf) == 0 || alreadyExists) { style::endDisabled(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { open = false; } ImGui::EndPopup(); } return open; } bool selectListsDialog() { gui::mainWindow.lockWaterfallControls = true; float menuWidth = ImGui::GetContentRegionAvail().x; std::string id = "Select lists##freq_manager_sel_popup_" + name; ImGui::OpenPopup(id.c_str()); bool open = true; if (ImGui::BeginPopup(id.c_str(), ImGuiWindowFlags_NoResize)) { // No need to lock config since we're not modifying anything and there's only one instance for (auto [listName, list] : config.conf["lists"].items()) { bool shown = list["showOnWaterfall"]; if (ImGui::Checkbox((listName + "##freq_manager_sel_list_").c_str(), &shown)) { config.acquire(); config.conf["lists"][listName]["showOnWaterfall"] = shown; refreshWaterfallBookmarks(false); config.release(true); } } if (ImGui::Button("Ok")) { open = false; } ImGui::EndPopup(); } return open; } void refreshLists() { listNames.clear(); listNamesTxt = ""; config.acquire(); for (auto [_name, list] : config.conf["lists"].items()) { listNames.push_back(_name); listNamesTxt += _name; listNamesTxt += '\0'; } config.release(); } void refreshWaterfallBookmarks(bool lockConfig = true) { if (lockConfig) { config.acquire(); } waterfallBookmarks.clear(); for (auto [listName, list] : config.conf["lists"].items()) { if (!((bool)list["showOnWaterfall"])) { continue; } WaterfallBookmark wbm; wbm.listName = listName; for (auto [bookmarkName, bm] : config.conf["lists"][listName]["bookmarks"].items()) { wbm.bookmarkName = bookmarkName; wbm.bookmark.frequency = config.conf["lists"][listName]["bookmarks"][bookmarkName]["frequency"]; wbm.bookmark.bandwidth = config.conf["lists"][listName]["bookmarks"][bookmarkName]["bandwidth"]; wbm.bookmark.mode = config.conf["lists"][listName]["bookmarks"][bookmarkName]["mode"]; wbm.bookmark.selected = false; waterfallBookmarks.push_back(wbm); } } if (lockConfig) { 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.acquire(); for (auto [bmName, bm] : config.conf["lists"][listName]["bookmarks"].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.acquire(); config.conf["lists"][listName]["bookmarks"] = json::object(); for (auto [bmName, bm] : bookmarks) { config.conf["lists"][listName]["bookmarks"][bmName]["frequency"] = bm.frequency; config.conf["lists"][listName]["bookmarks"][bmName]["bandwidth"] = bm.bandwidth; config.conf["lists"][listName]["bookmarks"][bmName]["mode"] = bm.mode; } refreshWaterfallBookmarks(false); config.release(true); } static void menuHandler(void* ctx) { FrequencyManagerModule* _this = (FrequencyManagerModule*)ctx; float menuWidth = ImGui::GetContentRegionAvail().x; // TODO: Replace with something that won't iterate every frame std::vector 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]); config.acquire(); config.conf["selectedList"] = _this->selectedListName; config.release(true); } 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))) { _this->deleteListOpen = true; } if (_this->selectedListName == "") { style::endDisabled(); } // List delete confirmation if (ImGui::GenericDialog(("freq_manager_del_list_confirm" + _this->name).c_str(), _this->deleteListOpen, GENERIC_DIALOG_BUTTONS_YES_NO, [_this]() { ImGui::Text("Deleting list named \"%s\". Are you sure?", _this->selectedListName.c_str()); }) == GENERIC_DIALOG_BUTTON_YES) { config.acquire(); config.conf["lists"].erase(_this->selectedListName); _this->refreshWaterfallBookmarks(false); config.release(true); _this->refreshLists(); _this->selectedListId = std::clamp(_this->selectedListId, 0, _this->listNames.size()); if (_this->listNames.size() > 0) { _this->loadByName(_this->listNames[_this->selectedListId]); } else { _this->selectedListName = ""; } } 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::GetContentRegionAvail().x, 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->createOpen = 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::GetContentRegionAvail().x, 0))) { _this->deleteBookmarksOpen = true; } 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::GetContentRegionAvail().x, 0))) { _this->editOpen = true; _this->editedBookmark = _this->bookmarks[selectedNames[0]]; _this->editedBookmarkName = selectedNames[0]; _this->firstEditedBookmarkName = selectedNames[0]; } if (selectedNames.size() != 1 && _this->selectedListName != "") { style::endDisabled(); } ImGui::EndTable(); // Bookmark delete confirm dialog // List delete confirmation if (ImGui::GenericDialog(("freq_manager_del_list_confirm" + _this->name).c_str(), _this->deleteBookmarksOpen, GENERIC_DIALOG_BUTTONS_YES_NO, [_this]() { ImGui::TextUnformatted("Deleting selected bookmaks. Are you sure?"); }) == GENERIC_DIALOG_BUTTON_YES) { for (auto& _name : selectedNames) { _this->bookmarks.erase(_name); } _this->saveByName(_this->selectedListName); } // Bookmark list if (ImGui::BeginTable(("freq_manager_bkm_table" + _this->name).c_str(), 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, 200))) { ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("Bookmark"); ImGui::TableSetupScrollFreeze(2, 1); ImGui::TableHeadersRow(); for (auto& [name, bm] : _this->bookmarks) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImVec2 min = ImGui::GetCursorPos(); if (ImGui::Selectable((name + "##_freq_mgr_bkm_name_" + _this->name).c_str(), &bm.selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnClick)) { // if shift or control isn't pressed, deselect all others if (!ImGui::GetIO().KeyShift && !ImGui::GetIO().KeyCtrl) { for (auto& [_name, _bm] : _this->bookmarks) { if (name == _name) { continue; } _bm.selected = false; } } } if (ImGui::TableGetHoveredColumn() >= 0 && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { applyBookmark(bm, gui::waterfall.selectedVFO); } ImGui::TableSetColumnIndex(1); ImGui::Text("%s %s", utils::formatFreq(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::GetContentRegionAvail().x, 0)) && !_this->importOpen) { _this->importOpen = true; _this->importDialog = new pfd::open_file("Import bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" }, pfd::opt::multiselect); } ImGui::TableSetColumnIndex(1); if (selectedNames.size() == 0 && _this->selectedListName != "") { style::beginDisabled(); } if (ImGui::Button(("Export##_freq_mgr_exp_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)) && !_this->exportOpen) { _this->exportedBookmarks = json::object(); config.acquire(); for (auto& _name : selectedNames) { _this->exportedBookmarks["bookmarks"][_name] = config.conf["lists"][_this->selectedListName]["bookmarks"][_name]; } config.release(); _this->exportOpen = true; _this->exportDialog = new pfd::save_file("Export bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" }); } if (selectedNames.size() == 0 && _this->selectedListName != "") { style::endDisabled(); } ImGui::EndTable(); if (ImGui::Button(("Select displayed lists##_freq_mgr_exp_" + _this->name).c_str(), ImVec2(menuWidth, 0))) { _this->selectListsOpen = true; } ImGui::LeftLabel("Bookmark display mode"); ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); if (ImGui::Combo(("##_freq_mgr_dms_" + _this->name).c_str(), &_this->bookmarkDisplayMode, bookmarkDisplayModesTxt)) { config.acquire(); config.conf["bookmarkDisplayMode"] = _this->bookmarkDisplayMode; config.release(true); } 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(); } if (_this->selectListsOpen) { _this->selectListsOpen = _this->selectListsDialog(); } // Handle import and export if (_this->importOpen && _this->importDialog->ready()) { _this->importOpen = false; std::vector 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; } } static void fftRedraw(ImGui::WaterFall::FFTRedrawArgs args, void* ctx) { FrequencyManagerModule* _this = (FrequencyManagerModule*)ctx; if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_OFF) { return; } if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_TOP) { for (auto const bm : _this->waterfallBookmarks) { double centerXpos = args.min.x + std::round((bm.bookmark.frequency - args.lowFreq) * args.freqToPixelRatio); if (bm.bookmark.frequency >= args.lowFreq && bm.bookmark.frequency <= args.highFreq) { args.window->DrawList->AddLine(ImVec2(centerXpos, args.min.y), ImVec2(centerXpos, args.max.y), IM_COL32(255, 255, 0, 255)); } ImVec2 nameSize = ImGui::CalcTextSize(bm.bookmarkName.c_str()); ImVec2 rectMin = ImVec2(centerXpos - (nameSize.x / 2) - 5, args.min.y); ImVec2 rectMax = ImVec2(centerXpos + (nameSize.x / 2) + 5, args.min.y + nameSize.y); ImVec2 clampedRectMin = ImVec2(std::clamp(rectMin.x, args.min.x, args.max.x), rectMin.y); ImVec2 clampedRectMax = ImVec2(std::clamp(rectMax.x, args.min.x, args.max.x), rectMax.y); if (clampedRectMax.x - clampedRectMin.x > 0) { args.window->DrawList->AddRectFilled(clampedRectMin, clampedRectMax, IM_COL32(255, 255, 0, 255)); } if (rectMin.x >= args.min.x && rectMax.x <= args.max.x) { args.window->DrawList->AddText(ImVec2(centerXpos - (nameSize.x / 2), args.min.y), IM_COL32(0, 0, 0, 255), bm.bookmarkName.c_str()); } } } else if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_BOTTOM) { for (auto const bm : _this->waterfallBookmarks) { double centerXpos = args.min.x + std::round((bm.bookmark.frequency - args.lowFreq) * args.freqToPixelRatio); if (bm.bookmark.frequency >= args.lowFreq && bm.bookmark.frequency <= args.highFreq) { args.window->DrawList->AddLine(ImVec2(centerXpos, args.min.y), ImVec2(centerXpos, args.max.y), IM_COL32(255, 255, 0, 255)); } ImVec2 nameSize = ImGui::CalcTextSize(bm.bookmarkName.c_str()); ImVec2 rectMin = ImVec2(centerXpos - (nameSize.x / 2) - 5, args.max.y - nameSize.y); ImVec2 rectMax = ImVec2(centerXpos + (nameSize.x / 2) + 5, args.max.y); ImVec2 clampedRectMin = ImVec2(std::clamp(rectMin.x, args.min.x, args.max.x), rectMin.y); ImVec2 clampedRectMax = ImVec2(std::clamp(rectMax.x, args.min.x, args.max.x), rectMax.y); if (clampedRectMax.x - clampedRectMin.x > 0) { args.window->DrawList->AddRectFilled(clampedRectMin, clampedRectMax, IM_COL32(255, 255, 0, 255)); } if (rectMin.x >= args.min.x && rectMax.x <= args.max.x) { args.window->DrawList->AddText(ImVec2(centerXpos - (nameSize.x / 2), args.max.y - nameSize.y), IM_COL32(0, 0, 0, 255), bm.bookmarkName.c_str()); } } } } bool mouseAlreadyDown = false; bool mouseClickedInLabel = false; static void fftInput(ImGui::WaterFall::InputHandlerArgs args, void* ctx) { FrequencyManagerModule* _this = (FrequencyManagerModule*)ctx; if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_OFF) { return; } if (_this->mouseClickedInLabel) { if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { _this->mouseClickedInLabel = false; } gui::waterfall.inputHandled = true; return; } // First check that the mouse clicked outside of any label. Also get the bookmark that's hovered bool inALabel = false; WaterfallBookmark hoveredBookmark; std::string hoveredBookmarkName; if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_TOP) { int count = _this->waterfallBookmarks.size(); for (int i = count - 1; i >= 0; i--) { auto& bm = _this->waterfallBookmarks[i]; double centerXpos = args.fftRectMin.x + std::round((bm.bookmark.frequency - args.lowFreq) * args.freqToPixelRatio); ImVec2 nameSize = ImGui::CalcTextSize(bm.bookmarkName.c_str()); ImVec2 rectMin = ImVec2(centerXpos - (nameSize.x / 2) - 5, args.fftRectMin.y); ImVec2 rectMax = ImVec2(centerXpos + (nameSize.x / 2) + 5, args.fftRectMin.y + nameSize.y); ImVec2 clampedRectMin = ImVec2(std::clamp(rectMin.x, args.fftRectMin.x, args.fftRectMax.x), rectMin.y); ImVec2 clampedRectMax = ImVec2(std::clamp(rectMax.x, args.fftRectMin.x, args.fftRectMax.x), rectMax.y); if (ImGui::IsMouseHoveringRect(clampedRectMin, clampedRectMax)) { inALabel = true; hoveredBookmark = bm; hoveredBookmarkName = bm.bookmarkName; break; } } } else if (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_BOTTOM) { int count = _this->waterfallBookmarks.size(); for (int i = count - 1; i >= 0; i--) { auto& bm = _this->waterfallBookmarks[i]; double centerXpos = args.fftRectMin.x + std::round((bm.bookmark.frequency - args.lowFreq) * args.freqToPixelRatio); ImVec2 nameSize = ImGui::CalcTextSize(bm.bookmarkName.c_str()); ImVec2 rectMin = ImVec2(centerXpos - (nameSize.x / 2) - 5, args.fftRectMax.y - nameSize.y); ImVec2 rectMax = ImVec2(centerXpos + (nameSize.x / 2) + 5, args.fftRectMax.y); ImVec2 clampedRectMin = ImVec2(std::clamp(rectMin.x, args.fftRectMin.x, args.fftRectMax.x), rectMin.y); ImVec2 clampedRectMax = ImVec2(std::clamp(rectMax.x, args.fftRectMin.x, args.fftRectMax.x), rectMax.y); if (ImGui::IsMouseHoveringRect(clampedRectMin, clampedRectMax)) { inALabel = true; hoveredBookmark = bm; hoveredBookmarkName = bm.bookmarkName; break; } } } // Check if mouse was already down if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !inALabel) { _this->mouseAlreadyDown = true; } if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { _this->mouseAlreadyDown = false; _this->mouseClickedInLabel = false; } // If yes, cancel if (_this->mouseAlreadyDown || !inALabel) { return; } gui::waterfall.inputHandled = true; double centerXpos = args.fftRectMin.x + std::round((hoveredBookmark.bookmark.frequency - args.lowFreq) * args.freqToPixelRatio); ImVec2 nameSize = ImGui::CalcTextSize(hoveredBookmarkName.c_str()); ImVec2 rectMin = ImVec2(centerXpos - (nameSize.x / 2) - 5, (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_BOTTOM) ? (args.fftRectMax.y - nameSize.y) : args.fftRectMin.y); ImVec2 rectMax = ImVec2(centerXpos + (nameSize.x / 2) + 5, (_this->bookmarkDisplayMode == BOOKMARK_DISP_MODE_BOTTOM) ? args.fftRectMax.y : args.fftRectMin.y + nameSize.y); ImVec2 clampedRectMin = ImVec2(std::clamp(rectMin.x, args.fftRectMin.x, args.fftRectMax.x), rectMin.y); ImVec2 clampedRectMax = ImVec2(std::clamp(rectMax.x, args.fftRectMin.x, args.fftRectMax.x), rectMax.y); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { _this->mouseClickedInLabel = true; applyBookmark(hoveredBookmark.bookmark, gui::waterfall.selectedVFO); } ImGui::BeginTooltip(); ImGui::TextUnformatted(hoveredBookmarkName.c_str()); ImGui::Separator(); ImGui::Text("List: %s", hoveredBookmark.listName.c_str()); ImGui::Text("Frequency: %s", utils::formatFreq(hoveredBookmark.bookmark.frequency).c_str()); ImGui::Text("Bandwidth: %s", utils::formatFreq(hoveredBookmark.bookmark.bandwidth).c_str()); ImGui::Text("Mode: %s", demodModeList[hoveredBookmark.bookmark.mode]); ImGui::EndTooltip(); } 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")) { flog::error("File does not contains any bookmarks"); return; } if (!importBookmarks["bookmarks"].is_object()) { flog::error("Bookmark attribute is invalid"); return; } // Load every bookmark for (auto const [_name, bm] : importBookmarks["bookmarks"].items()) { if (bookmarks.find(_name) != bookmarks.end()) { flog::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); fs << exportedBookmarks; fs.close(); } std::string name; bool enabled = true; bool createOpen = false; bool editOpen = false; bool newListOpen = false; bool renameListOpen = false; bool selectListsOpen = false; bool deleteListOpen = false; bool deleteBookmarksOpen = false; EventHandler fftRedrawHandler; EventHandler inputHandler; std::map bookmarks; std::string editedBookmarkName = ""; std::string firstEditedBookmarkName = ""; FrequencyBookmark editedBookmark; std::vector listNames; std::string listNamesTxt = ""; std::string selectedListName = ""; int selectedListId = 0; std::string editedListName; std::string firstEditedListName; std::vector waterfallBookmarks; int bookmarkDisplayMode = 0; }; MOD_EXPORT void _INIT_() { json def = json({}); def["selectedList"] = "General"; def["bookmarkDisplayMode"] = BOOKMARK_DISP_MODE_TOP; def["lists"]["General"]["showOnWaterfall"] = true; def["lists"]["General"]["bookmarks"] = json::object(); config.setPath(core::args["root"].s() + "/frequency_manager_config.json"); config.load(def); config.enableAutoSave(); // Check if of list and convert if they're the old type config.acquire(); if (!config.conf.contains("bookmarkDisplayMode")) { config.conf["bookmarkDisplayMode"] = BOOKMARK_DISP_MODE_TOP; } for (auto [listName, list] : config.conf["lists"].items()) { if (list.contains("bookmarks") && list.contains("showOnWaterfall") && list["showOnWaterfall"].is_boolean()) { continue; } json newList; newList = json::object(); newList["showOnWaterfall"] = true; newList["bookmarks"] = list; config.conf["lists"][listName] = newList; } config.release(true); } 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_() { config.disableAutoSave(); config.save(); }