From 5a96f109068512f9c48c6f613a956249469e883a Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Tue, 18 Mar 2025 23:44:11 -0400 Subject: [PATCH] add peer ID editor; implement support in the peer lookup and FNE to validate whether or not a peer can perform a encryption key request; --- CMakeLists.txt | 6 + configs/peer_list.example.dat | 12 +- src/CMakeLists.txt | 10 + src/common/lookups/PeerListLookup.cpp | 29 +- src/common/lookups/PeerListLookup.h | 21 +- src/fne/network/FNENetwork.cpp | 13 + src/peered/CMakeLists.txt | 13 + src/peered/CloseWndBase.h | 130 +++++++++ src/peered/Defines.h | 33 +++ src/peered/FDblDialog.h | 87 ++++++ src/peered/LogDisplayWnd.h | 144 ++++++++++ src/peered/PeerEdApplication.h | 214 ++++++++++++++ src/peered/PeerEdMain.cpp | 225 +++++++++++++++ src/peered/PeerEdMain.h | 101 +++++++ src/peered/PeerEdMainWnd.h | 218 ++++++++++++++ src/peered/PeerEditWnd.h | 367 ++++++++++++++++++++++++ src/peered/PeerListWnd.h | 391 ++++++++++++++++++++++++++ 17 files changed, 1997 insertions(+), 17 deletions(-) create mode 100644 src/peered/CMakeLists.txt create mode 100644 src/peered/CloseWndBase.h create mode 100644 src/peered/Defines.h create mode 100644 src/peered/FDblDialog.h create mode 100644 src/peered/LogDisplayWnd.h create mode 100644 src/peered/PeerEdApplication.h create mode 100644 src/peered/PeerEdMain.cpp create mode 100644 src/peered/PeerEdMain.h create mode 100644 src/peered/PeerEdMainWnd.h create mode 100644 src/peered/PeerEditWnd.h create mode 100644 src/peered/PeerListWnd.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e823d82..ce8dae05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -298,6 +298,7 @@ if (NOT TARGET strip) COMMAND arm-linux-gnueabihf-strip -s dvmmon COMMAND arm-linux-gnueabihf-strip -s sysview COMMAND arm-linux-gnueabihf-strip -s tged + COMMAND arm-linux-gnueabihf-strip -s peered COMMAND arm-linux-gnueabihf-strip -s dvmbridge) else() add_custom_target(strip @@ -315,6 +316,7 @@ if (NOT TARGET strip) COMMAND aarch64-linux-gnu-strip -s dvmmon COMMAND aarch64-linux-gnu-strip -s sysview COMMAND aarch64-linux-gnu-strip -s tged + COMMAND aarch64-linux-gnu-strip -s peered COMMAND aarch64-linux-gnu-strip -s dvmbridge) else() add_custom_target(strip @@ -346,6 +348,7 @@ if (NOT TARGET strip) COMMAND strip -s dvmmon COMMAND strip -s sysview COMMAND strip -s tged + COMMAND strip -s peered COMMAND strip -s dvmbridge) else() add_custom_target(strip @@ -379,6 +382,7 @@ if (NOT TARGET tarball) COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v sysview ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v tged ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin + COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp ${CMAKE_SOURCE_DIR}/tools/*.sh ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm @@ -457,6 +461,7 @@ if (NOT TARGET tarball_notools) COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v sysview ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v tged ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin + COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm @@ -532,6 +537,7 @@ add_custom_target(old_install COMMAND install -m 755 dvmmon ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 sysview ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 tged ${CMAKE_LEGACY_INSTALL_PREFIX}/bin + COMMAND install -m 755 peered ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmfne ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmbridge ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml diff --git a/configs/peer_list.example.dat b/configs/peer_list.example.dat index 50a77385..bfed0ea9 100644 --- a/configs/peer_list.example.dat +++ b/configs/peer_list.example.dat @@ -1,9 +1,9 @@ # # This file sets the valid peer IDs allowed on a FNE. # -# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional)," -#1234,,0, -#5678,MYSECUREPASSWORD,0, -#9876,MYSECUREPASSWORD,1, -#5432,MYSECUREPASSWORD,,Peer Alias 1, -#1012,MYSECUREPASSWORD,1,Peer Alias 2, +# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled)," +#1234,,0,,1, +#5678,MYSECUREPASSWORD,0,,0, +#9876,MYSECUREPASSWORD,1,,0, +#5432,MYSECUREPASSWORD,,Peer Alias 1,0, +#1012,MYSECUREPASSWORD,1,Peer Alias 2,1, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d32bc131..82dae26b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -113,6 +113,16 @@ if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) target_include_directories(tged PRIVATE ${OPENSSL_INCLUDE_DIR} websocketpp src src/host src/tged) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) +# +## peered +# +if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + include(src/peered/CMakeLists.txt) + add_executable(peered ${common_INCLUDE} ${peered_SRC}) + target_link_libraries(peered PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) + target_include_directories(peered PRIVATE ${OPENSSL_INCLUDE_DIR} websocketpp src src/host src/peered) +endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + # ## dvmcmd # diff --git a/src/common/lookups/PeerListLookup.cpp b/src/common/lookups/PeerListLookup.cpp index f4112499..b41f1f15 100644 --- a/src/common/lookups/PeerListLookup.cpp +++ b/src/common/lookups/PeerListLookup.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2022,2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2022,2024,2025 Bryan Biedenkapp, N2PLL * Copyright (c) 2024 Patrick McDonnell, W3AXL * Copyright (c) 2024 Caleb, KO4UYJ * @@ -46,16 +46,16 @@ void PeerListLookup::clear() /* Adds a new entry to the list. */ -void PeerListLookup::addEntry(uint32_t id, const std::string& alias, const std::string& password, bool peerLink) +void PeerListLookup::addEntry(uint32_t id, const std::string& alias, const std::string& password, bool peerLink, bool canRequestKeys) { - PeerId entry = PeerId(id, alias, password, peerLink, false); + PeerId entry = PeerId(id, alias, password, peerLink, canRequestKeys, false); std::lock_guard lock(m_mutex); try { PeerId _entry = m_table.at(id); // if either the alias or the enabled flag doesn't match, update the entry if (_entry.peerId() == id) { - _entry = PeerId(id, alias, password, peerLink, false); + _entry = PeerId(id, alias, password, peerLink, canRequestKeys, false); m_table[id] = _entry; } } catch (...) { @@ -87,7 +87,7 @@ PeerId PeerListLookup::find(uint32_t id) try { entry = m_table.at(id); } catch (...) { - entry = PeerId(0U, "", "", false, true); + entry = PeerId(0U, "", "", false, false, true); } return entry; @@ -226,19 +226,25 @@ bool PeerListLookup::load() if (parsed.size() >= 3) peerLink = ::atoi(parsed[2].c_str()) == 1; + // Parse can request keys flag + bool canRequestKeys = false; + if (parsed.size() >= 5) + canRequestKeys = ::atoi(parsed[4].c_str()) == 1; + // Parse optional password std::string password = ""; if (parsed.size() >= 2) password = parsed[1].c_str(); // Load into table - m_table[id] = PeerId(id, alias, password, peerLink, false); + m_table[id] = PeerId(id, alias, password, peerLink, canRequestKeys, false); // Log depending on what was loaded LogMessage(LOG_HOST, "Loaded peer ID %u%s into peer ID lookup table, %s%s", id, (!alias.empty() ? (" (" + alias + ")").c_str() : ""), (!password.empty() ? "using unique peer password" : "using master password"), - (peerLink) ? ", Peer-Link Enabled" : ""); + (peerLink) ? ", Peer-Link Enabled" : "", + (canRequestKeys) ? ", Can Request Keys" : ""); } } @@ -299,6 +305,15 @@ bool PeerListLookup::save() if (alias.length() > 0) { line += alias; line += ","; + } else { + line += ","; + } + // Add canRequestKeys flag + bool canRequestKeys = entry.second.canRequestKeys(); + if (canRequestKeys) { + line += "1,"; + } else { + line += "0,"; } // Add the newline line += "\n"; diff --git a/src/common/lookups/PeerListLookup.h b/src/common/lookups/PeerListLookup.h index 494ec13f..fb8af519 100644 --- a/src/common/lookups/PeerListLookup.h +++ b/src/common/lookups/PeerListLookup.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2022,2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2022,2024,2025 Bryan Biedenkapp, N2PLL * Copyright (c) 2024 Patrick McDonnell, W3AXL * Copyright (c) 2024 Caleb, KO4UYJ * @@ -50,6 +50,7 @@ namespace lookups m_peerAlias(), m_peerPassword(), m_peerLink(false), + m_canRequestKeys(false), m_peerDefault(false) { /* stub */ @@ -60,13 +61,16 @@ namespace lookups * @param peerAlias Peer alias * @param peerPassword Per Peer Password. * @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration. + * @param peerLink lag indicating if the peer participates in peer link and should be sent configuration. + * @param canRequestKeys Flag indicating if the peer can request encryption keys. * @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer. */ - PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool peerDefault) : + PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault) : m_peerId(peerId), m_peerAlias(peerAlias), m_peerPassword(peerPassword), m_peerLink(peerLink), + m_canRequestKeys(canRequestKeys), m_peerDefault(peerDefault) { /* stub */ @@ -83,6 +87,7 @@ namespace lookups m_peerAlias = data.m_peerAlias; m_peerPassword = data.m_peerPassword; m_peerLink = data.m_peerLink; + m_canRequestKeys = data.m_canRequestKeys; m_peerDefault = data.m_peerDefault; } @@ -95,14 +100,17 @@ namespace lookups * @param peerAlias Peer Alias * @param peerPassword Per Peer Password. * @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration. + * @param peerLink lag indicating if the peer participates in peer link and should be sent configuration. + * @param canRequestKeys Flag indicating if the peer can request encryption keys. * @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer. */ - void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool peerDefault) + void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault) { m_peerId = peerId; m_peerAlias = peerAlias; m_peerPassword = peerPassword; m_peerLink = peerLink; + m_canRequestKeys = canRequestKeys; m_peerDefault = peerDefault; } @@ -123,6 +131,10 @@ namespace lookups * @brief Flag indicating if the peer participates in peer link and should be sent configuration. */ __PROPERTY_PLAIN(bool, peerLink); + /** + * @brief Flag indicating if the peer can request encryption keys. + */ + __PROPERTY_PLAIN(bool, canRequestKeys); /** * @brief Flag indicating if the peer is default. */ @@ -166,8 +178,9 @@ namespace lookups * @param peerId Unique peer ID to add. * @param password Per Peer Password. * @param peerLink Flag indicating this peer will participate in peer link and should be sent configuration. + * @param canRequestKeys Flag indicating if the peer can request encryption keys. */ - void addEntry(uint32_t id, const std::string& alias = "", const std::string& password = "", bool peerLink = false); + void addEntry(uint32_t id, const std::string& alias = "", const std::string& password = "", bool peerLink = false, bool canRequestKeys = false); /** * @brief Removes an existing entry from the list. * @param peerId Unique peer ID to remove. diff --git a/src/fne/network/FNENetwork.cpp b/src/fne/network/FNENetwork.cpp index 45f13706..7d1ff244 100644 --- a/src/fne/network/FNENetwork.cpp +++ b/src/fne/network/FNENetwork.cpp @@ -1102,6 +1102,19 @@ void* FNENetwork::threadedNetworkRx(void* arg) // validate peer (simple validation really) if (connection->connected() && connection->address() == ip) { + // is this peer allowed to request keys? + if (network->m_peerListLookup->getACL()) { + if (network->m_peerListLookup->getMode() == lookups::PeerListLookup::WHITELIST) { + lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId); + if (peerEntry.peerDefault()) { + break; + } else { + if (!peerEntry.canRequestKeys()) + break; + } + } + } + std::unique_ptr frame = KMMFactory::create(req->buffer + 11U); if (frame == nullptr) { LogWarning(LOG_NET, "PEER %u (%s), undecodable KMM frame from peer", peerId, connection->identity().c_str()); diff --git a/src/peered/CMakeLists.txt b/src/peered/CMakeLists.txt new file mode 100644 index 00000000..96641d58 --- /dev/null +++ b/src/peered/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only +#/* +# * Digital Voice Modem - Peer ID Editor +# * GPLv2 Open Source. Use is subject to license terms. +# * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# * +# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * +# */ +file(GLOB peered_SRC + "src/peered/*.h" + "src/peered/*.cpp" +) diff --git a/src/peered/CloseWndBase.h b/src/peered/CloseWndBase.h new file mode 100644 index 00000000..646a792f --- /dev/null +++ b/src/peered/CloseWndBase.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file CloseWndBase.h + * @ingroup peered + */ +#if !defined(__CLOSE_WND_BASE_H__) +#define __CLOSE_WND_BASE_H__ + +#include "common/Thread.h" + +#include "FDblDialog.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the base class for windows with close buttons. + * @ingroup tged + */ +class HOST_SW_API CloseWndBase : public FDblDialog { +public: + /** + * @brief Initializes a new instance of the CloseWndBase class. + * @param widget + */ + explicit CloseWndBase(FWidget* widget = nullptr) : FDblDialog{widget} + { + /* stub */ + } + +protected: + bool m_enableSetButton = false; + FButton m_setButton{"Set", this}; + bool m_enableCloseButton = true; + FButton m_closeButton{"&Close", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setMinimizable(true); + FDialog::setShadow(); + + std::size_t maxWidth, maxHeight; + const auto& rootWidget = getRootWidget(); + + if (rootWidget) { + maxWidth = rootWidget->getClientWidth(); + maxHeight = rootWidget->getClientHeight(); + } + else { + // fallback to xterm default size + maxWidth = 80; + maxHeight = 24; + } + + const int x = 1 + int((maxWidth - getWidth()) / 2); + const int y = 1 + int((maxHeight - getHeight()) / 3); + FWindow::setPos(FPoint{x, y}, false); + FDialog::adjustSize(); + + FDialog::setModal(); + + initControls(); + + FDialog::initLayout(); + + rootWidget->redraw(); // bryanb: wtf? + redraw(); + } + + /** + * @brief Initializes window controls. + */ + virtual void initControls() + { + m_closeButton.setGeometry(FPoint(int(getWidth()) - 12, int(getHeight()) - 6), FSize(9, 3)); + m_closeButton.addCallback("clicked", [&]() { close(); }); + if (!m_enableCloseButton) { + m_closeButton.setDisable(); + m_closeButton.setVisible(false); + } + + m_setButton.setDisable(); + m_setButton.setVisible(false); + if (m_enableSetButton) { + m_setButton.setEnable(); + m_setButton.setVisible(true); + m_setButton.setGeometry(FPoint(int(getWidth()) - 24, int(getHeight()) - 6), FSize(9, 3)); + } + + focusFirstChild(); + } + + /** + * @brief Adjusts window size. + */ + void adjustSize() override + { + FDialog::adjustSize(); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs when the window is closed. + * @param e Close event. + */ + void onClose(FCloseEvent* e) override + { + hide(); + } +}; + +#endif // __CLOSE_WND_BASE_H__ \ No newline at end of file diff --git a/src/peered/Defines.h b/src/peered/Defines.h new file mode 100644 index 00000000..7ec07b5e --- /dev/null +++ b/src/peered/Defines.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup peered Peer ID Editor (peered) + * @brief Digital Voice Modem - Peer ID Editor + * @details Helper software that edits peer ID files with a graphical TUI. + * @ingroup peered + * + * @file Defines.h + * @ingroup peered + */ +#if !defined(__DEFINES_H__) +#define __DEFINES_H__ + +#include "common/Defines.h" + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) Peer ID Editor" +#undef __EXE_NAME__ +#define __EXE_NAME__ "peered" + +#endif // __DEFINES_H__ diff --git a/src/peered/FDblDialog.h b/src/peered/FDblDialog.h new file mode 100644 index 00000000..18072bb8 --- /dev/null +++ b/src/peered/FDblDialog.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file FDblDialog.h + * @ingroup peered + */ +#if !defined(__F_DBL_DIALOG_H__) +#define __F_DBL_DIALOG_H__ + +#include "common/Defines.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the double-border dialog. + * @ingroup peered + */ +class HOST_SW_API FDblDialog : public finalcut::FDialog { +public: + /** + * @brief Initializes a new instance of the FDblDialog class. + * @param widget + */ + explicit FDblDialog(FWidget* widget = nullptr) : finalcut::FDialog{widget} + { + /* stub */ + } + +protected: + /** + * @brief + */ + void drawBorder() override + { + if (!hasBorder()) + return; + + setColor(); + + FRect box{{1, 2}, getSize()}; + box.scaleBy(0, -1); + + FRect rect = box; + if (rect.x1_ref() > rect.x2_ref()) + std::swap(rect.x1_ref(), rect.x2_ref()); + + if (rect.y1_ref() > rect.y2_ref()) + std::swap(rect.y1_ref(), rect.y2_ref()); + + rect.x1_ref() = std::max(rect.x1_ref(), 1); + rect.y1_ref() = std::max(rect.y1_ref(), 1); + rect.x2_ref() = std::min(rect.x2_ref(), rect.x1_ref() + int(getWidth()) - 1); + rect.y2_ref() = std::min(rect.y2_ref(), rect.y1_ref() + int(getHeight()) - 1); + + if (box.getWidth() < 3) + return; + + // Use box-drawing characters to draw a border + constexpr std::array box_char + {{ + static_cast(0x2554), // ╔ + static_cast(0x2550), // ═ + static_cast(0x2557), // ╗ + static_cast(0x2551), // ║ + static_cast(0x2551), // ║ + static_cast(0x255A), // ╚ + static_cast(0x2550), // ═ + static_cast(0x255D) // ╝ + }}; + + drawGenericBox(this, box, box_char); + } +}; + +#endif // __F_DBL_DIALOG_H__ diff --git a/src/peered/LogDisplayWnd.h b/src/peered/LogDisplayWnd.h new file mode 100644 index 00000000..829a614b --- /dev/null +++ b/src/peered/LogDisplayWnd.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file LogDisplayWnd.h + * @ingroup peered + */ +#if !defined(__LOG_DISPLAY_WND_H__) +#define __LOG_DISPLAY_WND_H__ + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the log display window. + * @ingroup peered + */ +class HOST_SW_API LogDisplayWnd final : public finalcut::FDialog, public std::ostringstream { +public: + /** + * @brief Initializes a new instance of the LogDisplayWnd class. + * @param widget + */ + explicit LogDisplayWnd(FWidget* widget = nullptr) : FDialog{widget} + { + m_scrollText.ignorePadding(); + + m_timerId = addTimer(250); // starts the timer every 250 milliseconds + } + /** + * @brief Copy constructor. + */ + LogDisplayWnd(const LogDisplayWnd&) = delete; + /** + * @brief Move constructor. + */ + LogDisplayWnd(LogDisplayWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the LogDisplayWnd class. + */ + ~LogDisplayWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const LogDisplayWnd&) -> LogDisplayWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (LogDisplayWnd&&) noexcept -> LogDisplayWnd& = delete; + +private: + FTextView m_scrollText{this}; + int m_timerId; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + using namespace std::string_literals; + auto lightning = "\u26a1"; + FDialog::setText("System Log"s + lightning); + + std::size_t maxWidth; + const auto& rootWidget = getRootWidget(); + + if (rootWidget) { + maxWidth = rootWidget->getClientWidth() - 3; + } + else { + // fallback to xterm default size + maxWidth = 77; + } + + FDialog::setGeometry(FPoint{2, (int)(rootWidget->getClientHeight() - 20)}, FSize{maxWidth, 20}); + FDialog::setMinimumSize(FSize{80, 20}); + FDialog::setResizeable(true); + FDialog::setMinimizable(true); + FDialog::setTitlebarButtonVisibility(true); + FDialog::setShadow(); + + minimizeWindow(); + + m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1}); + + FDialog::initLayout(); + } + + /** + * @brief Adjusts window size. + */ + void adjustSize() override + { + FDialog::adjustSize(); + + m_scrollText.setGeometry(FPoint{1, 2}, FSize{getWidth(), getHeight() - 1}); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs when the window is closed. + * @param e Close Event + */ + void onClose(FCloseEvent* e) override + { + minimizeWindow(); + } + + /** + * @brief Event that occurs on interval by timer. + * @param timer Timer Event + */ + void onTimer(FTimerEvent* timer) override + { + if (timer != nullptr) { + if (timer->getTimerId() == m_timerId) { + if (str().empty()) { + return; + } + + m_scrollText.append(str()); + str(""); + m_scrollText.scrollToEnd(); + redraw(); + } + } + } +}; + +#endif // __LOG_DISPLAY_WND_H__ \ No newline at end of file diff --git a/src/peered/PeerEdApplication.h b/src/peered/PeerEdApplication.h new file mode 100644 index 00000000..755c887c --- /dev/null +++ b/src/peered/PeerEdApplication.h @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerEdApplication.h + * @ingroup peered + */ +#if !defined(__PEERED_APPLICATION_H__) +#define __PEERED_APPLICATION_H__ + +#include "common/Log.h" +#include "PeerEdMain.h" +#include "PeerEdMainWnd.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements a color theme for a finalcut application. + * @ingroup setup + */ +class HOST_SW_API dvmColorTheme final : public FWidgetColors +{ +public: + /** + * @brief Initializes a new instance of the dvmColorTheme class. + */ + dvmColorTheme() + { + dvmColorTheme::setColorTheme(); + } + + /** + * @brief Finalizes a instance of the dvmColorTheme class. + */ + ~dvmColorTheme() noexcept override = default; + + /** + * @brief Get the Class Name object + * @return FString + */ + auto getClassName() const -> FString override { return "dvmColorTheme"; } + /** + * @brief Set the Color Theme object + */ + void setColorTheme() override + { + term_fg = FColor::Cyan; + term_bg = FColor::Blue; + + list_fg = FColor::Black; + list_bg = FColor::LightGray; + selected_list_fg = FColor::Red; + selected_list_bg = FColor::LightGray; + + dialog_fg = FColor::Black; + dialog_resize_fg = FColor::LightBlue; + dialog_emphasis_fg = FColor::Blue; + dialog_bg = FColor::LightGray; + + error_box_fg = FColor::LightRed; + error_box_emphasis_fg = FColor::Yellow; + error_box_bg = FColor::Black; + + tooltip_fg = FColor::White; + tooltip_bg = FColor::Black; + + shadow_fg = FColor::Black; + shadow_bg = FColor::LightGray; // only for transparent shadow + + current_element_focus_fg = FColor::White; + current_element_focus_bg = FColor::Blue; + current_element_fg = FColor::LightGray; + current_element_bg = FColor::DarkGray; + + current_inc_search_element_fg = FColor::LightRed; + + selected_current_element_focus_fg = FColor::LightRed; + selected_current_element_focus_bg = FColor::Cyan; + selected_current_element_fg = FColor::Red; + selected_current_element_bg = FColor::Cyan; + + label_fg = FColor::Black; + label_bg = FColor::LightGray; + label_inactive_fg = FColor::DarkGray; + label_inactive_bg = FColor::LightGray; + label_hotkey_fg = FColor::Red; + label_hotkey_bg = FColor::LightGray; + label_emphasis_fg = FColor::Blue; + label_ellipsis_fg = FColor::DarkGray; + + inputfield_active_focus_fg = FColor::Yellow; + inputfield_active_focus_bg = FColor::Blue; + inputfield_active_fg = FColor::LightGray; + inputfield_active_bg = FColor::Blue; + inputfield_inactive_fg = FColor::Black; + inputfield_inactive_bg = FColor::DarkGray; + + toggle_button_active_focus_fg = FColor::Yellow; + toggle_button_active_focus_bg = FColor::Blue; + toggle_button_active_fg = FColor::LightGray; + toggle_button_active_bg = FColor::Blue; + toggle_button_inactive_fg = FColor::Black; + toggle_button_inactive_bg = FColor::DarkGray; + + button_active_focus_fg = FColor::Yellow; + button_active_focus_bg = FColor::Blue; + button_active_fg = FColor::White; + button_active_bg = FColor::Blue; + button_inactive_fg = FColor::Black; + button_inactive_bg = FColor::DarkGray; + button_hotkey_fg = FColor::Yellow; + + titlebar_active_fg = FColor::Blue; + titlebar_active_bg = FColor::White; + titlebar_inactive_fg = FColor::Blue; + titlebar_inactive_bg = FColor::LightGray; + titlebar_button_fg = FColor::Yellow; + titlebar_button_bg = FColor::LightBlue; + titlebar_button_focus_fg = FColor::LightGray; + titlebar_button_focus_bg = FColor::Black; + + menu_active_focus_fg = FColor::Black; + menu_active_focus_bg = FColor::White; + menu_active_fg = FColor::Black; + menu_active_bg = FColor::LightGray; + menu_inactive_fg = FColor::DarkGray; + menu_inactive_bg = FColor::LightGray; + menu_hotkey_fg = FColor::Blue; + menu_hotkey_bg = FColor::LightGray; + + statusbar_fg = FColor::Black; + statusbar_bg = FColor::LightGray; + statusbar_hotkey_fg = FColor::Blue; + statusbar_hotkey_bg = FColor::LightGray; + statusbar_separator_fg = FColor::Black; + statusbar_active_fg = FColor::Black; + statusbar_active_bg = FColor::White; + statusbar_active_hotkey_fg = FColor::Blue; + statusbar_active_hotkey_bg = FColor::White; + + scrollbar_fg = FColor::Cyan; + scrollbar_bg = FColor::DarkGray; + scrollbar_button_fg = FColor::Yellow; + scrollbar_button_bg = FColor::DarkGray; + scrollbar_button_inactive_fg = FColor::LightGray; + scrollbar_button_inactive_bg = FColor::Black; + + progressbar_fg = FColor::Yellow; + progressbar_bg = FColor::Blue; + } +}; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the finalcut application. + * @ingroup tged + */ +class HOST_SW_API PeerEdApplication final : public finalcut::FApplication { +public: + /** + * @brief Initializes a new instance of the PeerEdApplication class. + * @param argc Passed argc. + * @param argv Passed argv. + */ + explicit PeerEdApplication(const int& argc, char** argv) : FApplication{argc, argv} + { + m_statusRefreshTimer = addTimer(1000); + } + +protected: + /** + * @brief Process external user events. + */ + void processExternalUserEvent() override + { + /* stub */ + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on interval by timer. + * @param timer Timer Event + */ + void onTimer(FTimerEvent* timer) override + { + if (timer != nullptr) { + if (timer->getTimerId() == m_statusRefreshTimer) { + /* stub */ + } + } + } + +private: + int m_statusRefreshTimer; +}; + +#endif // __PEERED_APPLICATION_H__ \ No newline at end of file diff --git a/src/peered/PeerEdMain.cpp b/src/peered/PeerEdMain.cpp new file mode 100644 index 00000000..a6d68ae1 --- /dev/null +++ b/src/peered/PeerEdMain.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/yaml/Yaml.h" +#include "common/Log.h" +#include "PeerEdMain.h" +#include "PeerEdApplication.h" +#include "PeerEdMainWnd.h" + +using namespace lookups; + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Macros +// --------------------------------------------------------------------------- + +#define IS(s) (::strcmp(argv[i], s) == 0) + +// --------------------------------------------------------------------------- +// Global Variables +// --------------------------------------------------------------------------- + +std::string g_progExe = std::string(__EXE_NAME__); +std::string g_iniFile = std::string(DEFAULT_CONF_FILE); +yaml::Node g_conf; +bool g_debug = false; + +bool g_hideLoggingWnd = false; + +lookups::PeerListLookup* g_pidLookups = nullptr; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/* Helper to print a fatal error message and exit. */ + +void fatal(const char* msg, ...) +{ + char buffer[400U]; + ::memset(buffer, 0x20U, 400U); + + va_list vl; + va_start(vl, msg); + + ::vsprintf(buffer, msg, vl); + + va_end(vl); + + ::fprintf(stderr, "%s: FATAL PANIC; %s\n", g_progExe.c_str(), buffer); + exit(EXIT_FAILURE); +} + +/* Helper to pring usage the command line arguments. (And optionally an error.) */ + +void usage(const char* message, const char* arg) +{ + ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); + ::fprintf(stdout, "Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n"); + ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n"); + if (message != nullptr) { + ::fprintf(stderr, "%s: ", g_progExe.c_str()); + ::fprintf(stderr, message, arg); + ::fprintf(stderr, "\n\n"); + } + + ::fprintf(stdout, + "usage: %s [-dvh]" + "[--hide-log]" + "[-c ]" + "\n\n" + " -d enable debug\n" + " -v show version information\n" + " -h show this screen\n" + "\n" + " --hide-log hide interactive logging window on startup\n" + "\n" + " -c specifies the peer ID file to edit\n" + "\n" + " -- stop handling options\n", + g_progExe.c_str()); + + exit(EXIT_FAILURE); +} + +/* Helper to validate the command line arguments. */ + +int checkArgs(int argc, char* argv[]) +{ + int i, p = 0; + + // iterate through arguments + for (i = 1; i <= argc; i++) + { + if (argv[i] == nullptr) { + break; + } + + if (*argv[i] != '-') { + continue; + } + else if (IS("--")) { + ++p; + break; + } + else if (IS("-c")) { + if (argc-- <= 0) + usage("error: %s", "must specify the peer ID file to edit"); + g_iniFile = std::string(argv[++i]); + + if (g_iniFile.empty()) + usage("error: %s", "peer ID file cannot be blank!"); + + p += 2; + } + else if (IS("--hide-log")) { + ++p; + g_hideLoggingWnd = true; + } + else if (IS("-d")) { + ++p; + g_debug = true; + } + else if (IS("-v")) { + ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); + ::fprintf(stdout, "Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n"); + ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n"); + if (argc == 2) + exit(EXIT_SUCCESS); + } + else if (IS("-h")) { + usage(nullptr, nullptr); + if (argc == 2) + exit(EXIT_SUCCESS); + } + else { + usage("unrecognized option `%s'", argv[i]); + } + } + + if (p < 0 || p > argc) { + p = 0; + } + + return ++p; +} + +// --------------------------------------------------------------------------- +// Program Entry Point +// --------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + if (argv[0] != nullptr && *argv[0] != 0) + g_progExe = std::string(argv[0]); + + if (argc > 1) { + // check arguments + int i = checkArgs(argc, argv); + if (i < argc) { + argc -= i; + argv += i; + } + else { + argc--; + argv++; + } + } + + // initialize system logging + bool ret = ::LogInitialise("", "", 0U, 1U); + if (!ret) { + ::fprintf(stderr, "unable to open the log file\n"); + return 1; + } + + ::LogInfo(__PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ + "Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \ + "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \ + ">> Peer ID Editor\r\n"); + + try { + ret = yaml::Parse(g_conf, g_iniFile.c_str()); + if (!ret) { + ::fatal("cannot read the configuration file, %s\n", g_iniFile.c_str()); + } + } + catch (yaml::OperationException const& e) { + ::fatal("cannot read the configuration file - %s (%s)", g_iniFile.c_str(), e.message()); + } + + // setup the finalcut tui + PeerEdApplication app{argc, argv}; + + PeerEdMainWnd wnd{&app}; + finalcut::FWidget::setMainWidget(&wnd); + + g_logDisplayLevel = 0U; + + g_pidLookups = new PeerListLookup(g_iniFile, PeerListLookup::WHITELIST, 0U, false); + g_pidLookups->read(); + LogMessage(LOG_HOST, "Loaded peer ID file: %s", g_iniFile.c_str()); + + // show and start the application + wnd.show(); + + finalcut::FApplication::setColorTheme(); + app.resetColors(); + app.redraw(); + + int _errno = app.exec(); + ::LogFinalise(); + return _errno; +} diff --git a/src/peered/PeerEdMain.h b/src/peered/PeerEdMain.h new file mode 100644 index 00000000..4150e036 --- /dev/null +++ b/src/peered/PeerEdMain.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2023 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerEdMain.h + * @ingroup peered + * @file PeerEdMain.cpp + * @ingroup peered + */ +#if !defined(__PEERED_MAIN_H__) +#define __PEERED_MAIN_H__ + +#include "Defines.h" +#include "common/lookups/PeerListLookup.h" +#include "common/yaml/Yaml.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) Peer ID Editor" +#undef __EXE_NAME__ +#define __EXE_NAME__ "peered" + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * bryanb: This is some low-down, dirty, C++ hack-o-ramma. + */ + +/** + * @brief Implements RTTI type defining. + * @typedef Tag + */ +template +struct RTTIResult { + typedef typename Tag::type type; + static type ptr; +}; + +template +typename RTTIResult::type RTTIResult::ptr; + +/** + * @brief Implements nasty hack to access private members of a class. + * @typedef Tag + * @typedef TypePtr + */ +template +struct HackTheGibson : RTTIResult { + /* fill it ... */ + struct filler { + filler() { RTTIResult::ptr = TypePtr; } + }; + static filler fillerObj; +}; + +template +typename HackTheGibson::filler HackTheGibson::fillerObj; + +// --------------------------------------------------------------------------- +// Externs +// --------------------------------------------------------------------------- + +/** @brief */ +extern std::string g_progExe; +/** @brief */ +extern std::string g_iniFile; +/** @brief */ +extern yaml::Node g_conf; +/** @brief */ +extern bool g_debug; + +/** @brief */ +extern bool g_hideLoggingWnd; + +/** @brief */ +extern lookups::PeerListLookup* g_pidLookups; + +/** + * @brief Helper to trigger a fatal error message. This will cause the program to terminate + * immediately with an error message. + * + * @param msg String format. + * + * This is a variable argument function. + */ +extern HOST_SW_API void fatal(const char* msg, ...); + +#endif // __PEERED_MAIN_H__ diff --git a/src/peered/PeerEdMainWnd.h b/src/peered/PeerEdMainWnd.h new file mode 100644 index 00000000..90b49e73 --- /dev/null +++ b/src/peered/PeerEdMainWnd.h @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerEdMainWnd.h + * @ingroup peered + */ +#if !defined(__PEERED_MAIN_WND_H__) +#define __PEERED_MAIN_WND_H__ + +#include "common/Log.h" +#include "common/Thread.h" + +using namespace lookups; + +#include +using namespace finalcut; +#undef null + +#include "PeerEdMain.h" + +#include "LogDisplayWnd.h" +#include "PeerListWnd.h" +#include "PeerEditWnd.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define MINIMUM_SUPPORTED_SIZE_WIDTH 83 +#define MINIMUM_SUPPORTED_SIZE_HEIGHT 30 + +// --------------------------------------------------------------------------- +// Class Prototypes +// --------------------------------------------------------------------------- + +class HOST_SW_API PeerEdApplication; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the root window control. + * @ingroup peered + */ +class HOST_SW_API PeerEdMainWnd final : public finalcut::FWidget { +public: + /** + * @brief Initializes a new instance of the PeerEdMainWnd class. + * @param widget + */ + explicit PeerEdMainWnd(FWidget* widget = nullptr) : FWidget{widget} + { + __InternalOutputStream(m_logWnd); + + // file menu + m_fileMenuSeparator1.setSeparator(); + m_fileMenuSeparator2.setSeparator(); + m_saveSettingsItem.addAccelerator(FKey::Meta_s); // Meta/Alt + S + m_saveSettingsItem.addCallback("clicked", this, [&]() { save(); }); + m_reloadSettingsItem.addAccelerator(FKey::Meta_r); // Meta/Alt + R + m_reloadSettingsItem.addCallback("clicked", this, [&]() { g_pidLookups->reload(); m_wnd->loadListView(); }); + m_keyF2.addCallback("activate", this, [&]() { save(); }); + m_quitItem.addAccelerator(FKey::Meta_x); // Meta/Alt + X + m_quitItem.addCallback("clicked", getFApplication(), &FApplication::cb_exitApp, this); + m_keyF3.addCallback("activate", getFApplication(), &FApplication::cb_exitApp, this); + m_keyF5.addCallback("activate", this, [&]() { g_pidLookups->reload(); m_wnd->loadListView(); LogMessage(LOG_HOST, "Loaded peer ID file: %s", g_iniFile.c_str()); }); + + m_backupOnSave.setChecked(); + + // help menu + m_aboutItem.addCallback("clicked", this, [&]() { + const FString line(2, UniChar::BoxDrawingsHorizontal); + FMessageBox info("About", line + __PROG_NAME__ + line + L"\n\n" + L"" + __BANNER__ + L"\n" + L"Version " + __VER__ + L"\n\n" + L"Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors." + L"\n" + L"Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others", + FMessageBox::ButtonType::Ok, FMessageBox::ButtonType::Reject, FMessageBox::ButtonType::Reject, this); + info.setCenterText(); + info.show(); + }); + } + +private: + friend class PeerEdApplication; + + LogDisplayWnd m_logWnd{this}; + PeerListWnd* m_wnd; + + FMenuBar m_menuBar{this}; + + FMenu m_fileMenu{"&File", &m_menuBar}; + FMenuItem m_reloadSettingsItem{"&Reload", &m_fileMenu}; + FMenuItem m_saveSettingsItem{"&Save", &m_fileMenu}; + FMenuItem m_fileMenuSeparator2{&m_fileMenu}; + FCheckMenuItem m_saveOnCloseToggle{"Save on Close?", &m_fileMenu}; + FCheckMenuItem m_backupOnSave{"Backup Rules File?", &m_fileMenu}; + FMenuItem m_fileMenuSeparator1{&m_fileMenu}; + FMenuItem m_quitItem{"&Quit", &m_fileMenu}; + + FMenu m_helpMenu{"&Help", &m_menuBar}; + FMenuItem m_aboutItem{"&About", &m_helpMenu}; + + FStatusBar m_statusBar{this}; + FStatusKey m_keyF2{FKey::F2, "Save", &m_statusBar}; + FStatusKey m_keyF3{FKey::F3, "Quit", &m_statusBar}; + FStatusKey m_keyF5{FKey::F5, "Reload", &m_statusBar}; + + /** + * @brief + */ + void save() + { + if (m_backupOnSave.isChecked()) { + std::string bakFile = g_iniFile + ".bak"; + LogMessage(LOG_HOST, "Backing up existing file %s to %s", g_iniFile.c_str(), bakFile.c_str()); + copyFile(g_iniFile.c_str(), bakFile.c_str()); + } + + g_pidLookups->commit(); + } + + /** + * @brief Helper to copy one file to another. + * @param src Source file. + * @param dest Destination File. + * @returns bool True, if file copied, otherwise false. + */ + bool copyFile(const char *srcFilePath, const char* destFilePath) + { + std::ifstream src(srcFilePath, std::ios::binary); + std::ofstream dest(destFilePath, std::ios::binary); + dest << src.rdbuf(); + return src && dest; + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs when the window is shown. + * @param e Show Event + */ + void onShow(FShowEvent* e) override + { + const auto& rootWidget = getRootWidget(); + + int fullWidth = rootWidget->getWidth(); + if (fullWidth < MINIMUM_SUPPORTED_SIZE_WIDTH) { + clearArea(); + ::fatal("screen resolution too small must be wider then %u characters, console width = %u", MINIMUM_SUPPORTED_SIZE_WIDTH, fullWidth); + } + + int fullHeight = rootWidget->getHeight(); + if (fullHeight < MINIMUM_SUPPORTED_SIZE_HEIGHT) { + clearArea(); + ::fatal("screen resolution too small must be taller then %u characters, console height = %u", MINIMUM_SUPPORTED_SIZE_HEIGHT, fullHeight); + } + + int maxWidth = 77; + if (rootWidget) { + maxWidth = rootWidget->getClientWidth() - 3; + } + + int maxHeight = PEER_LIST_HEIGHT; + if (rootWidget) { + maxHeight = rootWidget->getClientHeight() - 3; + } + + m_wnd = new PeerListWnd(this); + if (maxHeight - 21 < PEER_LIST_HEIGHT) + maxHeight = PEER_LIST_HEIGHT + 21; + + m_wnd->setGeometry(FPoint{2, 2}, FSize{(size_t)(maxWidth - 1), (size_t)(maxHeight - 21)}); + + m_wnd->setModal(false); + m_wnd->show(); + + m_wnd->raiseWindow(); + m_wnd->activateWindow(); + + redraw(); + + if (g_hideLoggingWnd) { + const auto& rootWidget = getRootWidget(); + m_logWnd.setGeometry(FPoint{(int)(rootWidget->getClientWidth() - 81), (int)(rootWidget->getClientHeight() - 1)}, FSize{80, 20}); + + m_logWnd.minimizeWindow(); + } + } + + /** + * @brief Event that occurs when the window is closed. + * @param e Close Event + */ + void onClose(FCloseEvent* e) override + { + // if we are saving on close -- fire off the file save event + if (m_saveOnCloseToggle.isChecked()) { + g_pidLookups->commit(); + } + + FApplication::closeConfirmationDialog(this, e); + } +}; + +#endif // __PEERED_MAIN_WND_H__ \ No newline at end of file diff --git a/src/peered/PeerEditWnd.h b/src/peered/PeerEditWnd.h new file mode 100644 index 00000000..5c6444dd --- /dev/null +++ b/src/peered/PeerEditWnd.h @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerEditWnd.h + * @ingroup peered + */ +#if !defined(__PEER_EDIT_WND_H__) +#define __PEER_EDIT_WND_H__ + +#include "common/Log.h" + +#include "peered/CloseWndBase.h" +#include "peered/PeerEdMain.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the line edit control for peer IDs. + * @ingroup peered + */ +class HOST_SW_API PeerIdLineEdit final : public FLineEdit { +public: + /** + * @brief Initializes a new instance of the PeerIdLineEdit class. + * @param widget + */ + explicit PeerIdLineEdit(FWidget* widget = nullptr) : FLineEdit{widget} + { + setInputFilter("[[:digit:]]"); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on keyboard key press. + * @param e Keyboard Event. + */ + void onKeyPress(finalcut::FKeyEvent* e) override + { + const auto key = e->key(); + if (key == FKey::Up) { + emitCallback("up-pressed"); + e->accept(); + return; + } else if (key == FKey::Down) { + emitCallback("down-pressed"); + e->accept(); + return; + } + + FLineEdit::onKeyPress(e); + } +}; + +/** + * @brief This class implements the peer ID editor window. + * @ingroup peered + */ +class HOST_SW_API PeerEditWnd final : public CloseWndBase { +public: + /** + * @brief Initializes a new instance of the PeerEditWnd class. + * + * @param rule + * @param widget + */ + explicit PeerEditWnd(lookups::PeerId rule, FWidget* widget = nullptr) : CloseWndBase{widget} + { + m_rule = rule; + if (m_rule.peerDefault()) { + m_new = true; + } else { + m_origPeerId = m_rule.peerId(); + } + } + +private: + bool m_new; + bool m_skipSaving; + lookups::PeerId m_rule; + + uint32_t m_origPeerId; + + FLabel m_peerAliasLabel{"Alias: ", this}; + FLineEdit m_peerAlias{this}; + + FCheckBox m_saveCopy{"Save Copy", this}; + FCheckBox m_incOnSave{"Increment On Save", this}; + + FButtonGroup m_sourceGroup{"Peer ID", this}; + FLabel m_peerIdLabel{"Peer ID: ", &m_sourceGroup}; + PeerIdLineEdit m_peerId{&m_sourceGroup}; + FLabel m_peerPasswordLabel{"Password: ", &m_sourceGroup}; + FLineEdit m_peerPassword{&m_sourceGroup}; + + FButtonGroup m_configGroup{"Configuration", this}; + FCheckBox m_peerLinkEnabled{"Peer Link", &m_configGroup}; + FCheckBox m_canReqKeysEnabled{"Request Keys", &m_configGroup}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setText("Peer ID"); + FDialog::setSize(FSize{60, 18}); + + m_enableSetButton = false; + CloseWndBase::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() override + { + m_closeButton.setText("&OK"); + + m_peerAliasLabel.setGeometry(FPoint(2, 2), FSize(8, 1)); + m_peerAlias.setGeometry(FPoint(11, 2), FSize(24, 1)); + if (!m_rule.peerDefault()) { + m_peerAlias.setText(m_rule.peerAlias()); + } + m_peerAlias.setShadow(false); + m_peerAlias.addCallback("changed", [&]() { m_rule.peerAlias(m_peerAlias.getText().toString()); }); + + m_saveCopy.setGeometry(FPoint(36, 2), FSize(18, 1)); + m_saveCopy.addCallback("toggled", [&]() { + if (m_saveCopy.isChecked()) { + m_incOnSave.setEnable(); + } else { + m_incOnSave.setChecked(false); + m_incOnSave.setDisable(); + } + + redraw(); + }); + m_incOnSave.setGeometry(FPoint(36, 3), FSize(18, 1)); + m_incOnSave.setDisable(); + + // talkgroup source + { + m_sourceGroup.setGeometry(FPoint(2, 5), FSize(30, 5)); + m_peerIdLabel.setGeometry(FPoint(2, 1), FSize(10, 1)); + m_peerId.setGeometry(FPoint(11, 1), FSize(17, 1)); + m_peerId.setAlignment(finalcut::Align::Right); + if (!m_rule.peerDefault()) { + m_peerId.setText(std::to_string(m_rule.peerId())); + } else { + m_rule.peerId(1U); + m_peerId.setText("1"); + } + m_peerId.setShadow(false); + m_peerId.addCallback("up-pressed", [&]() { + uint32_t peerId = ::atoi(m_peerId.getText().c_str()); + peerId++; + if (peerId > 999999999U) { + peerId = 999999999U; + } + + m_peerId.setText(std::to_string(peerId)); + + m_rule.peerId(peerId); + redraw(); + }); + m_peerId.addCallback("down-pressed", [&]() { + uint32_t peerId = ::atoi(m_peerId.getText().c_str()); + peerId--; + if (peerId < 1U) { + peerId = 1U; + } + + m_peerId.setText(std::to_string(peerId)); + + m_rule.peerId(peerId); + redraw(); + }); + m_peerId.addCallback("changed", [&]() { + if (m_peerId.getText().getLength() == 0) { + m_rule.peerId(1U); + return; + } + + uint32_t peerId = ::atoi(m_peerId.getText().c_str()); + if (peerId < 1U) { + peerId = 1U; + } + + if (peerId > 999999999U) { + peerId = 999999999U; + } + + m_peerId.setText(std::to_string(peerId)); + + m_rule.peerId(peerId); + }); + + m_peerPasswordLabel.setGeometry(FPoint(2, 2), FSize(10, 1)); + m_peerPassword.setGeometry(FPoint(11, 2), FSize(17, 1)); + if (!m_rule.peerDefault()) { + m_peerPassword.setText(m_rule.peerPassword()); + } + m_peerPassword.setShadow(false); + m_peerPassword.addCallback("changed", [&]() { m_rule.peerPassword(m_peerPassword.getText().toString()); }); + } + + // configuration + { + m_configGroup.setGeometry(FPoint(34, 5), FSize(23, 5)); + + m_peerLinkEnabled.setGeometry(FPoint(2, 1), FSize(10, 1)); + m_peerLinkEnabled.setChecked(m_rule.peerLink()); + m_peerLinkEnabled.addCallback("toggled", [&]() { + m_rule.peerLink(m_peerLinkEnabled.isChecked()); + }); + + m_canReqKeysEnabled.setGeometry(FPoint(2, 2), FSize(10, 1)); + m_canReqKeysEnabled.setChecked(m_rule.peerLink()); + m_canReqKeysEnabled.addCallback("toggled", [&]() { + m_rule.canRequestKeys(m_canReqKeysEnabled.isChecked()); + }); + } + + CloseWndBase::initControls(); + } + + /** + * @brief + */ + void logRuleInfo() + { + std::string peerAlias = m_rule.peerAlias(); + uint32_t peerId = m_rule.peerId(); + bool peerLink = m_rule.peerLink(); + bool canRequestKeys = m_rule.canRequestKeys(); + + ::LogInfoEx(LOG_HOST, "Peer ALIAS: %s PEERID: %u PEER LINK: %u CAN REQUEST KEYS: %u", peerAlias.c_str(), peerId, peerLink, canRequestKeys); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on keyboard key press. + * @param e Keyboard Event. + */ + void onKeyPress(finalcut::FKeyEvent* e) override + { + const auto key = e->key(); + if (key == FKey::Enter) { + this->close(); + } else if (key == FKey::Escape) { + m_skipSaving = true; + this->close(); + } + } + + /** + * @brief Event that occurs when the window is closed. + * @param e Close event. + */ + void onClose(FCloseEvent* e) override + { + if (m_skipSaving) { + m_skipSaving = false; + CloseWndBase::onClose(e); + return; + } + + if (!m_rule.peerDefault()) { + if (m_incOnSave.isChecked()) { + uint32_t peerId = m_rule.peerId(); + peerId++; + + if (peerId > 999999999U) { + peerId = 999999999U; + } + + m_rule.peerId(peerId); + + m_peerId.setText(std::to_string(peerId)); + redraw(); + } + + if (m_origPeerId != 0U && !m_saveCopy.isChecked()) { + if (m_rule.peerId() == 0U) { + LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId()); + FMessageBox::error(this, "Peer ID must be valid."); + return; + } + + // update peer + auto peers = g_pidLookups->tableAsList(); + auto it = std::find_if(peers.begin(), peers.end(), + [&](lookups::PeerId x) + { + return x.peerId() == m_origPeerId; + }); + if (it != peers.end()) { + LogMessage(LOG_HOST, "Updating peer %s (%u) to %s (%u)", it->peerAlias().c_str(), it->peerId(), m_rule.peerAlias().c_str(), m_rule.peerId()); + g_pidLookups->eraseEntry(m_origPeerId); + g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink()); + + logRuleInfo(); + } + } else { + if (m_rule.peerId() == 0U) { + LogError(LOG_HOST, "Not saving peer, peer %s (%u), peer ID must be greater then 0.", m_rule.peerAlias().c_str(), m_rule.peerId()); + FMessageBox::error(this, "Peer ID must be valid."); + return; + } + + auto peers = g_pidLookups->tableAsList(); + auto it = std::find_if(peers.begin(), peers.end(), + [&](lookups::PeerId x) + { + return x.peerId() == m_rule.peerId(); + }); + if (it != peers.end()) { + LogError(LOG_HOST, "Not saving duplicate peer, peer %s (%u), peers must be unique.", m_rule.peerAlias().c_str(), m_rule.peerId()); + FMessageBox::error(this, "Duplicate peer, change peer ID. Peers must be unique."); + if (m_saveCopy.isChecked()) + m_saveCopy.setChecked(false); + return; + } + + // add new peer + if (m_saveCopy.isChecked()) { + LogMessage(LOG_HOST, "Copying Peer. Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId()); + } else { + LogMessage(LOG_HOST, "Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId()); + } + g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink()); + + logRuleInfo(); + + // don't actually close the modal on a copy save + if (m_saveCopy.isChecked()) { + return; + } + } + } else { + LogError(LOG_HOST, "Not saving peer, peer %s (%u), have a peer ID greater than 0.", m_rule.peerAlias().c_str(), m_rule.peerId()); + FMessageBox::error(this, "Talkgroup must have a peer ID greater than 0."); + return; + } + + CloseWndBase::onClose(e); + } +}; + +#endif // __PEER_EDIT_WND_H__ \ No newline at end of file diff --git a/src/peered/PeerListWnd.h b/src/peered/PeerListWnd.h new file mode 100644 index 00000000..609fce20 --- /dev/null +++ b/src/peered/PeerListWnd.h @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Peer ID Editor + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file PeerListWnd.h + * @ingroup peered + */ +#if !defined(__PEER_LIST_WND_H__) +#define __PEER_LIST_WND_H__ + +#include "common/Log.h" + +#include "FDblDialog.h" +#include "PeerEdMainWnd.h" +#include "PeerEditWnd.h" + +#include +using namespace finalcut; + +struct PrivateFListViewScrollToY { typedef void(FListView::*type)(int); }; +template class HackTheGibson; +struct PrivateFListViewIteratorFirst { typedef FListViewIterator FListView::*type; }; +template class HackTheGibson; +struct PrivateFListViewVBarPtr { typedef FScrollbarPtr FListView::*type; }; +template class HackTheGibson; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define PEER_LIST_WIDTH 74 +#define PEER_LIST_HEIGHT 15 + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the peer list window. + * @ingroup peered + */ +class HOST_SW_API PeerListWnd final : public FDblDialog { +public: + /** + * @brief Initializes a new instance of the PeerListWnd class. + * @param widget + */ + explicit PeerListWnd(FWidget* widget = nullptr) : FDblDialog{widget} + { + /* stub */ + } + /** + * @brief Copy constructor. + */ + PeerListWnd(const PeerListWnd&) = delete; + /** + * @brief Move constructor. + */ + PeerListWnd(PeerListWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the PeerListWnd class. + */ + ~PeerListWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const PeerListWnd&) -> PeerListWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (PeerListWnd&&) noexcept -> PeerListWnd& = delete; + + /** + * @brief Disable set X coordinate. + */ + void setX(int, bool = true) override { } + /** + * @brief Disable set Y coordinate. + */ + void setY(int, bool = true) override { } + /** + * @brief Disable set position. + */ + void setPos(const FPoint&, bool = true) override { } + + /** + * @brief Populates the talkgroup listview. + */ + void loadListView() + { + m_selected = PeerId(); + m_selectedPeerId = 0U; + + auto entry = g_pidLookups->tableAsList()[0U]; + m_selected = entry; + + // bryanb: HACK -- use HackTheGibson to access the private current listview iterator to get the scroll position + /* + * This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence. + */ + int firstScrollLinePos = 0; + if (m_listView.getCount() > 0) { + firstScrollLinePos = (m_listView.*RTTIResult::ptr).getPosition(); + } + + m_listView.clear(); + for (auto entry : g_pidLookups->tableAsList()) { + // pad peer ID properly + std::ostringstream oss; + oss << std::setw(7) << std::setfill('0') << entry.peerId(); + + bool masterPassword = (entry.peerPassword().size() == 0U); + + // build list view entry + const std::array columns = { + oss.str(), + (masterPassword) ? "X" : "", + (entry.peerLink()) ? "X" : "", + (entry.canRequestKeys()) ? "X" : "", + entry.peerAlias() + }; + + const finalcut::FStringList line(columns.cbegin(), columns.cend()); + m_listView.insert(line); + } + + // bryanb: HACK -- use HackTheGibson to access the private set scroll Y to set the scroll position + /* + * This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence. + */ + if ((size_t)firstScrollLinePos > m_listView.getCount()) + firstScrollLinePos = 0; + if (firstScrollLinePos > 0 && m_listView.getCount() > 0) { + (m_listView.*RTTIResult::ptr)(firstScrollLinePos); + (m_listView.*RTTIResult::ptr)->setValue(firstScrollLinePos); + } + + // generate dialog title + uint32_t len = g_pidLookups->tableAsList().size(); + std::stringstream ss; + ss << "Peer ID List (" << len << " Peers)"; + FDialog::setText(ss.str()); + + setFocusWidget(&m_listView); + redraw(); + } + +private: + lookups::PeerId m_selected; + uint32_t m_selectedPeerId; + + FListView m_listView{this}; + + FButton m_addPeer{"&Add", this}; + FButton m_editPeer{"&Edit", this}; + FLabel m_fileName{"/path/to/peer.dat", this}; + FButton m_deletePeer{"&Delete", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setMinimumSize(FSize{PEER_LIST_WIDTH, PEER_LIST_HEIGHT}); + + FDialog::setResizeable(false); + FDialog::setMinimizable(false); + FDialog::setTitlebarButtonVisibility(false); + FDialog::setModal(false); + + FDialog::setText("Peer ID List"); + + initControls(); + loadListView(); + + FDialog::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() + { + m_addPeer.setGeometry(FPoint(2, int(getHeight() - 4)), FSize(9, 1)); + m_addPeer.setBackgroundColor(FColor::DarkGreen); + m_addPeer.setFocusBackgroundColor(FColor::DarkGreen); + m_addPeer.addCallback("clicked", [&]() { addEntry(); }); + + m_editPeer.setGeometry(FPoint(13, int(getHeight() - 4)), FSize(10, 1)); + m_editPeer.setDisable(); + m_editPeer.addCallback("clicked", [&]() { editEntry(); }); + + m_fileName.setGeometry(FPoint(27, int(getHeight() - 4)), FSize(42, 1)); + m_fileName.setText(g_iniFile); + + m_deletePeer.setGeometry(FPoint(int(getWidth()) - 13, int(getHeight() - 4)), FSize(10, 1)); + m_deletePeer.setDisable(); + m_deletePeer.addCallback("clicked", [&]() { deleteEntry(); }); + + m_listView.setGeometry(FPoint{1, 1}, FSize{getWidth() - 1, getHeight() - 5}); + + // configure list view columns + m_listView.addColumn("Peer ID", 10); + m_listView.addColumn("Master Password", 16); + m_listView.addColumn("Peer Link", 12); + m_listView.addColumn("Can Request Keys", 12); + m_listView.addColumn("Alias", 40); + + // set right alignment for peer ID + m_listView.setColumnAlignment(2, finalcut::Align::Center); + m_listView.setColumnAlignment(3, finalcut::Align::Center); + m_listView.setColumnAlignment(4, finalcut::Align::Center); + m_listView.setColumnAlignment(5, finalcut::Align::Left); + + // set type of sorting + m_listView.setColumnSortType(1, finalcut::SortType::Name); + + // sort by peer ID + m_listView.setColumnSort(1, finalcut::SortOrder::Ascending); + + m_listView.addCallback("clicked", [&]() { editEntry(); }); + m_listView.addCallback("row-changed", [&]() { + FListViewItem* curItem = m_listView.getCurrentItem(); + if (curItem != nullptr) { + FString strPeerId = curItem->getText(1); + uint32_t peerId = ::atoi(strPeerId.c_str()); + + if (peerId != m_selectedPeerId) { + auto entry = g_pidLookups->find(peerId); + if (!entry.peerDefault()) { + m_selected = entry; + + m_selectedPeerId = peerId; + + m_editPeer.setEnable(); + m_deletePeer.setEnable(); + m_deletePeer.setBackgroundColor(FColor::DarkRed); + m_deletePeer.setFocusBackgroundColor(FColor::DarkRed); + } else { + m_editPeer.setDisable(); + m_deletePeer.setDisable(); + m_deletePeer.resetColors(); + } + + redraw(); + } + } + }); + + setFocusWidget(&m_listView); + redraw(); + } + + /** + * @brief + */ + void addEntry() + { + this->lowerWindow(); + this->deactivateWindow(); + + PeerEditWnd wnd{PeerId(), this}; + wnd.show(); + + this->raiseWindow(); + this->activateWindow(); + + loadListView(); + } + + /** + * @brief + */ + void editEntry() + { + if (m_selected.peerDefault()) + return; + + this->lowerWindow(); + this->deactivateWindow(); + + PeerEditWnd wnd{m_selected, this}; + wnd.show(); + + this->raiseWindow(); + this->activateWindow(); + + loadListView(); + } + + /** + * @brief + */ + void deleteEntry() + { + if (m_selected.peerDefault()) + return; + + LogMessage(LOG_HOST, "Deleting peer ID %s (%u)", m_selected.peerAlias().c_str(), m_selected.peerId()); + g_pidLookups->eraseEntry(m_selected.peerId()); + + // bryanb: HACK -- use HackTheGibson to access the private current listview iterator to get the scroll position + /* + * This uses the RTTI hack to access private members on FListView; and this code *could* break as a consequence. + */ + int firstScrollLinePos = 0; + if (m_listView.getCount() > 0) { + firstScrollLinePos = (m_listView.*RTTIResult::ptr).getPosition(); + } + if ((size_t)firstScrollLinePos > m_listView.getCount()) + firstScrollLinePos = 0; + if (firstScrollLinePos > 0 && m_listView.getCount() > 0) { + --firstScrollLinePos; + (m_listView.*RTTIResult::ptr)(firstScrollLinePos); + (m_listView.*RTTIResult::ptr)->setValue(firstScrollLinePos); + } + + loadListView(); + } + + /** + * @brief + */ + void drawBorder() override + { + if (!hasBorder()) + return; + + setColor(); + + FRect box{{1, 2}, getSize()}; + box.scaleBy(0, -1); + + FRect rect = box; + if (rect.x1_ref() > rect.x2_ref()) + std::swap(rect.x1_ref(), rect.x2_ref()); + + if (rect.y1_ref() > rect.y2_ref()) + std::swap(rect.y1_ref(), rect.y2_ref()); + + rect.x1_ref() = std::max(rect.x1_ref(), 1); + rect.y1_ref() = std::max(rect.y1_ref(), 1); + rect.x2_ref() = std::min(rect.x2_ref(), rect.x1_ref() + int(getWidth()) - 1); + rect.y2_ref() = std::min(rect.y2_ref(), rect.y1_ref() + int(getHeight()) - 1); + + if (box.getWidth() < 3) + return; + + // Use box-drawing characters to draw a border + constexpr std::array box_char + {{ + static_cast(0x2554), // ╔ + static_cast(0x2550), // ═ + static_cast(0x2557), // ╗ + static_cast(0x2551), // ║ + static_cast(0x2551), // ║ + static_cast(0x255A), // ╚ + static_cast(0x2550), // ═ + static_cast(0x255D) // ╝ + }}; + + drawGenericBox(this, box, box_char); + } + + /* + ** Event Handlers + */ + + /** + * @brief Event that occurs on keyboard key press. + * @param e Keyboard Event. + */ + void onKeyPress(finalcut::FKeyEvent* e) override + { + const auto key = e->key(); + if (key == FKey::Insert) { + addEntry(); + } else if (key == FKey::Enter || key == FKey::Return) { + editEntry(); + } + } +}; + +#endif // __PEER_LIST_WND_H__ \ No newline at end of file