From ae39ae816ac42d08e279a72f9a719fa4da89397b Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 2 Oct 2024 17:46:00 -0400 Subject: [PATCH] adjust color palette for TUI; add alpha version for the TUI talkgroup rules file editor; --- CMakeLists.txt | 25 +- src/CMakeLists.txt | 14 +- src/CompilerOptions.cmake | 8 +- src/host/setup/AdjustWndBase.h | 2 +- src/host/setup/HostSetup.cpp | 2 +- src/host/setup/SetupApplication.h | 138 +++++++++ src/monitor/MonitorApplication.h | 138 +++++++++ src/monitor/MonitorMain.cpp | 2 +- src/tged/CMakeLists.txt | 13 + src/tged/CloseWndBase.h | 128 ++++++++ src/tged/Defines.h | 33 +++ src/tged/LogDisplayWnd.h | 144 +++++++++ src/tged/TGEdApplication.h | 214 ++++++++++++++ src/tged/TGEdMain.cpp | 224 ++++++++++++++ src/tged/TGEdMain.h | 63 ++++ src/tged/TGEdMainWnd.h | 183 ++++++++++++ src/tged/TGEditPeerListWnd.h | 301 +++++++++++++++++++ src/tged/TGEditWnd.h | 476 ++++++++++++++++++++++++++++++ src/tged/TGListWnd.h | 297 +++++++++++++++++++ 19 files changed, 2386 insertions(+), 19 deletions(-) create mode 100644 src/tged/CMakeLists.txt create mode 100644 src/tged/CloseWndBase.h create mode 100644 src/tged/Defines.h create mode 100644 src/tged/LogDisplayWnd.h create mode 100644 src/tged/TGEdApplication.h create mode 100644 src/tged/TGEdMain.cpp create mode 100644 src/tged/TGEdMain.h create mode 100644 src/tged/TGEdMainWnd.h create mode 100644 src/tged/TGEditPeerListWnd.h create mode 100644 src/tged/TGEditWnd.h create mode 100644 src/tged/TGListWnd.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bcef6c0..2702609d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,9 +250,9 @@ endif (ENABLE_TESTS) install(TARGETS dvmhost DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) install(TARGETS dvmcmd DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) install(TARGETS dvmfne DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) -if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) +if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) install(TARGETS dvmmon DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) -endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) +endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) install(TARGETS dvmbridge DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) install(FILES configs/config.example.yml configs/fne-config.example.yml configs/monitor-config.example.yml configs/iden_table.dat configs/RSSI.dat configs/rid_acl.example.dat configs/talkgroup_rules.example.yml configs/bridge-config.example.yml DESTINATION ${CMAKE_INSTALL_PREFIX}/etc) install(PROGRAMS tools/start-dvm.sh tools/stop-dvm.sh tools/dvm-watchdog.sh tools/stop-watchdog.sh tools/fne-watchdog.sh tools/start-dvm-fne.sh DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) @@ -267,12 +267,13 @@ install(CODE "execute_process(COMMAND bash \"-c\" \"sed -i 's/file: talkgroup_ru # if (NOT TARGET strip) if (CROSS_COMPILE_ARM) - if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) add_custom_target(strip COMMAND arm-linux-gnueabihf-strip -s dvmhost COMMAND arm-linux-gnueabihf-strip -s dvmfne COMMAND arm-linux-gnueabihf-strip -s dvmcmd COMMAND arm-linux-gnueabihf-strip -s dvmmon + COMMAND arm-linux-gnueabihf-strip -s tged COMMAND arm-linux-gnueabihf-strip -s dvmbridge) else() add_custom_target(strip @@ -280,14 +281,15 @@ if (NOT TARGET strip) COMMAND arm-linux-gnueabihf-strip -s dvmfne COMMAND arm-linux-gnueabihf-strip -s dvmcmd COMMAND arm-linux-gnueabihf-strip -s dvmbridge) - endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) elseif (CROSS_COMPILE_AARCH64) - if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) add_custom_target(strip COMMAND aarch64-linux-gnu-strip -s dvmhost COMMAND aarch64-linux-gnu-strip -s dvmfne COMMAND aarch64-linux-gnu-strip -s dvmcmd COMMAND aarch64-linux-gnu-strip -s dvmmon + COMMAND aarch64-linux-gnu-strip -s tged COMMAND aarch64-linux-gnu-strip -s dvmbridge) else() add_custom_target(strip @@ -295,7 +297,7 @@ if (NOT TARGET strip) COMMAND aarch64-linux-gnu-strip -s dvmfne COMMAND aarch64-linux-gnu-strip -s dvmcmd COMMAND aarch64-linux-gnu-strip -s dvmbridge) - endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) elseif (CROSS_COMPILE_RPI_ARM) if (NOT WITH_RPI_ARM_TOOLS) add_custom_target(strip @@ -311,12 +313,13 @@ if (NOT TARGET strip) COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmbridge) endif () else() - if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) add_custom_target(strip COMMAND strip -s dvmhost COMMAND strip -s dvmfne COMMAND strip -s dvmcmd COMMAND strip -s dvmmon + COMMAND strip -s tged COMMAND strip -s dvmbridge) else() add_custom_target(strip @@ -324,7 +327,7 @@ if (NOT TARGET strip) COMMAND strip -s dvmfne COMMAND strip -s dvmcmd COMMAND strip -s dvmbridge) - endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) endif (CROSS_COMPILE_ARM) endif (NOT TARGET strip) @@ -339,7 +342,7 @@ endif (NOT TARGET strip) # if (NOT TARGET tarball) set(CMAKE_INSTALL_PREFIX_TARBALL "tar_build") - if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) add_custom_target(tarball COMMAND rm -rf ${CMAKE_INSTALL_PREFIX_TARBALL} COMMAND mkdir -p ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin @@ -348,6 +351,7 @@ if (NOT TARGET tarball) COMMAND cp -v dvmhost ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin + COMMAND cp -v tged ${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 ../tools/*.sh ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm @@ -406,7 +410,7 @@ if (NOT TARGET tarball) COMMAND if [ -e DVM-V24-stm32f103.bin ]\; then cp -v DVM-V24-stm32f103.bin ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/fw\; fi COMMAND cd ${CMAKE_INSTALL_PREFIX_TARBALL} && tar czvf ../dvmhost_${CPACK_DEBIAN_PACKAGE_VERSION}_${ARCH}.tar.gz * COMMAND rm -rf ${CMAKE_INSTALL_PREFIX_TARBALL}) - endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) + endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) endif (NOT TARGET tarball) # @@ -425,6 +429,7 @@ add_custom_target(old_install COMMAND install -m 755 dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmcmd ${CMAKE_LEGACY_INSTALL_PREFIX}/bin COMMAND install -m 755 dvmmon ${CMAKE_LEGACY_INSTALL_PREFIX}/bin + COMMAND install -m 755 tged ${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 ../configs/config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b6fee21..d75e0ba4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,12 +82,22 @@ target_include_directories(dvmfne PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host sr # ## dvmmon # -if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) +if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) include(src/monitor/CMakeLists.txt) add_executable(dvmmon ${common_INCLUDE} ${dvmmon_SRC}) target_link_libraries(dvmmon PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) target_include_directories(dvmmon PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/monitor) -endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) +endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + +# +## tged +# +if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) + include(src/tged/CMakeLists.txt) + add_executable(tged ${common_INCLUDE} ${tged_SRC}) + target_link_libraries(tged PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) + target_include_directories(tged PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/tged) +endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) # ## dvmcmd diff --git a/src/CompilerOptions.cmake b/src/CompilerOptions.cmake index 4db620f7..15a2e2b9 100644 --- a/src/CompilerOptions.cmake +++ b/src/CompilerOptions.cmake @@ -21,10 +21,10 @@ if (ENABLE_TCP_SSL) add_definitions(-DENABLE_TCP_SSL) endif (ENABLE_TCP_SSL) -option(DISABLE_MONITOR "Disable dvmmon compilation" off) -if (DISABLE_MONITOR) - message(CHECK_START "Disable dvmmon compilation - enabled") -endif (DISABLE_MONITOR) +option(DISABLE_TUI_APPS "Disable extra TUI applications" off) +if (DISABLE_TUI_APPS) + message(CHECK_START "Disable extra TUI applications - enabled") +endif (DISABLE_TUI_APPS) # Debug compilation features/options (these should not be enabled for production!) option(DEBUG_DMR_PDU_DATA "" off) diff --git a/src/host/setup/AdjustWndBase.h b/src/host/setup/AdjustWndBase.h index 2c6ef8ad..67eef4e1 100644 --- a/src/host/setup/AdjustWndBase.h +++ b/src/host/setup/AdjustWndBase.h @@ -113,7 +113,7 @@ protected: m_connectedLabel.setGeometry(FPoint(36, int(getHeight()) - 3), FSize(20, 3)); if (m_setup->m_isConnected) { m_connectedLabel.setText("Modem Connected"); - m_connectedLabel.setForegroundColor(FColor::Green3); + m_connectedLabel.setForegroundColor(FColor::DarkGreen); } else { m_connectedLabel.setText("Modem Disconnected"); diff --git a/src/host/setup/HostSetup.cpp b/src/host/setup/HostSetup.cpp index a8ffbfda..343d7a82 100644 --- a/src/host/setup/HostSetup.cpp +++ b/src/host/setup/HostSetup.cpp @@ -247,7 +247,7 @@ int HostSetup::run(int argc, char** argv) // show and start the application setupWnd.show(); - finalcut::FApplication::setDarkTheme(); + finalcut::FApplication::setColorTheme(); app.resetColors(); app.redraw(); return app.exec(); diff --git a/src/host/setup/SetupApplication.h b/src/host/setup/SetupApplication.h index 9e8702b2..67b48e0e 100644 --- a/src/host/setup/SetupApplication.h +++ b/src/host/setup/SetupApplication.h @@ -25,6 +25,144 @@ 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::Cyan; + current_element_fg = FColor::LightBlue; + current_element_bg = FColor::Cyan; + 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 setup diff --git a/src/monitor/MonitorApplication.h b/src/monitor/MonitorApplication.h index a721400d..08bd86de 100644 --- a/src/monitor/MonitorApplication.h +++ b/src/monitor/MonitorApplication.h @@ -25,6 +25,144 @@ 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::Cyan; + current_element_fg = FColor::LightBlue; + current_element_bg = FColor::Cyan; + 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 monitor diff --git a/src/monitor/MonitorMain.cpp b/src/monitor/MonitorMain.cpp index a4a4158e..ba6aea8c 100644 --- a/src/monitor/MonitorMain.cpp +++ b/src/monitor/MonitorMain.cpp @@ -234,7 +234,7 @@ int main(int argc, char** argv) // show and start the application wnd.show(); - finalcut::FApplication::setDarkTheme(); + finalcut::FApplication::setColorTheme(); app.resetColors(); app.redraw(); diff --git a/src/tged/CMakeLists.txt b/src/tged/CMakeLists.txt new file mode 100644 index 00000000..36496ac8 --- /dev/null +++ b/src/tged/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only +#/* +# * Digital Voice Modem - Talkgroup 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 tged_SRC + "src/tged/*.h" + "src/tged/*.cpp" +) diff --git a/src/tged/CloseWndBase.h b/src/tged/CloseWndBase.h new file mode 100644 index 00000000..6a44bde5 --- /dev/null +++ b/src/tged/CloseWndBase.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 CloseWndBase.h + * @ingroup tged + */ +#if !defined(__CLOSE_WND_BASE_H__) +#define __CLOSE_WND_BASE_H__ + +#include "common/Thread.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 finalcut::FDialog { +public: + /** + * @brief Initializes a new instance of the CloseWndBase class. + * @param widget + */ + explicit CloseWndBase(FWidget* widget = nullptr) : FDialog{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/tged/Defines.h b/src/tged/Defines.h new file mode 100644 index 00000000..100660ea --- /dev/null +++ b/src/tged/Defines.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 + * + */ +/** + * @defgroup tged Talkgroup Editor (tged) + * @brief Digital Voice Modem - Talkgroup Editor + * @details Helper software that edits talkgroup rules files with a graphical TUI. + * @ingroup tged + * + * @file Defines.h + * @ingroup tged + */ +#if !defined(__DEFINES_H__) +#define __DEFINES_H__ + +#include "common/Defines.h" + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) Talkgroup Rules Editor" +#undef __EXE_NAME__ +#define __EXE_NAME__ "tged" + +#endif // __DEFINES_H__ diff --git a/src/tged/LogDisplayWnd.h b/src/tged/LogDisplayWnd.h new file mode 100644 index 00000000..bea25e78 --- /dev/null +++ b/src/tged/LogDisplayWnd.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 tged + */ +#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 tged + */ +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/tged/TGEdApplication.h b/src/tged/TGEdApplication.h new file mode 100644 index 00000000..d938f58f --- /dev/null +++ b/src/tged/TGEdApplication.h @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGEdApplication.h + * @ingroup tged + */ +#if !defined(__TGED_APPLICATION_H__) +#define __TGED_APPLICATION_H__ + +#include "common/Log.h" +#include "TGEdMain.h" +#include "TGEdMainWnd.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 TGEdApplication final : public finalcut::FApplication { +public: + /** + * @brief Initializes a new instance of the TGEdApplication class. + * @param argc Passed argc. + * @param argv Passed argv. + */ + explicit TGEdApplication(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 // __TGED_APPLICATION_H__ \ No newline at end of file diff --git a/src/tged/TGEdMain.cpp b/src/tged/TGEdMain.cpp new file mode 100644 index 00000000..b7156013 --- /dev/null +++ b/src/tged/TGEdMain.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 "TGEdMain.h" +#include "TGEdApplication.h" +#include "TGEdMainWnd.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::TalkgroupRulesLookup* g_tidLookups = 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-2024 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 talkgroup rules 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 talkgroup rules file to edit"); + g_iniFile = std::string(argv[++i]); + + if (g_iniFile.empty()) + usage("error: %s", "talkgroup rules 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-2024 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-2024 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" \ + ">> Talkgroup Rules 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 + TGEdApplication app{argc, argv}; + + TGEdMainWnd wnd{&app}; + finalcut::FWidget::setMainWidget(&wnd); + + g_logDisplayLevel = 0U; + + g_tidLookups = new TalkgroupRulesLookup(g_iniFile, 0U, false); + g_tidLookups->read(); + + // 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/tged/TGEdMain.h b/src/tged/TGEdMain.h new file mode 100644 index 00000000..7f3c5db0 --- /dev/null +++ b/src/tged/TGEdMain.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGEdMain.h + * @ingroup tged + * @file TGEdMain.cpp + * @ingroup tged + */ +#if !defined(__TGED_MAIN_H__) +#define __TGED_MAIN_H__ + +#include "Defines.h" +#include "common/lookups/TalkgroupRulesLookup.h" +#include "common/yaml/Yaml.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) Talkgroup Rules Editor" +#undef __EXE_NAME__ +#define __EXE_NAME__ "tged" + +// --------------------------------------------------------------------------- +// 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::TalkgroupRulesLookup* g_tidLookups; + +/** + * @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 // __TGED_MAIN_H__ diff --git a/src/tged/TGEdMainWnd.h b/src/tged/TGEdMainWnd.h new file mode 100644 index 00000000..cc70c736 --- /dev/null +++ b/src/tged/TGEdMainWnd.h @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGEdainWnd.h + * @ingroup tged + */ +#if !defined(__TGED_MAIN_WND_H__) +#define __TGED_MAIN_WND_H__ + +#include "common/lookups/AffiliationLookup.h" +#include "common/Log.h" +#include "common/Thread.h" + +using namespace lookups; + +#include +using namespace finalcut; +#undef null + +#include "TGEdMain.h" + +#include "LogDisplayWnd.h" +#include "TGListWnd.h" +#include "TGEditWnd.h" + +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define MINIMUM_SUPPORTED_SIZE_WIDTH 83 +#define MINIMUM_SUPPORTED_SIZE_HEIGHT 30 + +// --------------------------------------------------------------------------- +// Class Prototypes +// --------------------------------------------------------------------------- + +class HOST_SW_API TGEdApplication; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the root window control. + * @ingroup tged + */ +class HOST_SW_API TGEdMainWnd final : public finalcut::FWidget { +public: + /** + * @brief Initializes a new instance of the TGEdMainWnd class. + * @param widget + */ + explicit TGEdMainWnd(FWidget* widget = nullptr) : FWidget{widget} + { + __InternalOutputStream(m_logWnd); + + // file menu + m_fileMenuSeparator1.setSeparator(); + m_saveSettingsItem.addAccelerator(FKey::Meta_s); // Meta/Alt + S + m_saveSettingsItem.addCallback("clicked", this, [&]() { g_tidLookups->commit(); }); + m_keyF2.addCallback("activate", this, [&]() { g_tidLookups->commit(); }); + 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_tidLookups->reload(); m_wnd->loadListView(); }); + + // 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-2024 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 TGEdApplication; + + LogDisplayWnd m_logWnd{this}; + TGListWnd* m_wnd; + + FMenuBar m_menuBar{this}; + + FMenu m_fileMenu{"&File", &m_menuBar}; + FMenuItem m_saveSettingsItem{"&Save", &m_fileMenu}; + FCheckMenuItem m_saveOnCloseToggle{"Save on Close?", &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}; + + /* + ** 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 = TG_LIST_HEIGHT; + if (rootWidget) { + maxHeight = rootWidget->getClientHeight() - 3; + } + + m_wnd = new TGListWnd(this); + if (maxHeight - 21 < TG_LIST_HEIGHT) + maxHeight = TG_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_tidLookups->commit(); + } + + FApplication::closeConfirmationDialog(this, e); + } +}; + +#endif // __TGED_MAIN_WND_H__ \ No newline at end of file diff --git a/src/tged/TGEditPeerListWnd.h b/src/tged/TGEditPeerListWnd.h new file mode 100644 index 00000000..704cb58b --- /dev/null +++ b/src/tged/TGEditPeerListWnd.h @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGEditPeerListWnd.h + * @ingroup tged + */ +#if !defined(__TG_EDIT_PEER_LIST_WND_H__) +#define __TG_EDIT_PEER_LIST_WND_H__ + +#include "common/Log.h" + +#include "tged/CloseWndBase.h" +#include "tged/TGEdMain.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the line edit control for peer IDs. + * @ingroup tged + */ +class HOST_SW_API PeerLineEdit final : public FLineEdit { +public: + /** + * @brief Initializes a new instance of the PeerLineEdit class. + * @param widget + */ + explicit PeerLineEdit(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; + } + + if (key == FKey::Insert) { + emitCallback("insert-pressed"); + e->accept(); + return; + } else if (key == FKey::Return) { + emitCallback("return-pressed"); + e->accept(); + return; + } + + FLineEdit::onKeyPress(e); + } +}; + +/** + * @brief This class implements the talkgroup peer list editor window. + * @ingroup tged + */ +class HOST_SW_API TGEditPeerListWnd final : public CloseWndBase { +public: + /** + * @brief Initializes a new instance of the TGEditWnd class. + * @param rule + * @param peerList + * @param title + * @param widget + */ + explicit TGEditPeerListWnd(lookups::TalkgroupRuleGroupVoice rule, std::vector peerList, + std::string title = "Peer List", FWidget *widget = nullptr) : CloseWndBase{widget} + { + m_rule = rule; + this->peerList = peerList; + m_title = title; + } + + /** + * @brief List of peer IDs. + */ + std::vector peerList; + +private: + bool m_skipSaving; + std::string m_title; + + lookups::TalkgroupRuleGroupVoice m_rule; + + FListBox m_listBox{this}; + + FButton m_add{"&Add", this}; + FButton m_delete{"&Delete", this}; + + FLabel m_entryLabel{"Peer ID: ", this}; + PeerLineEdit m_entry{this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setText(m_title); + FDialog::setSize(FSize{40, 21}); + + m_enableSetButton = false; + CloseWndBase::initLayout(); + loadList(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() override + { + m_closeButton.setText("&OK"); + + m_add.setGeometry(FPoint(2, int(getHeight() - 4)), FSize(9, 1)); + m_add.setBackgroundColor(FColor::DarkGreen); + m_add.setFocusBackgroundColor(FColor::DarkGreen); + m_add.addCallback("clicked", [&]() { addEntry(); }); + + m_delete.setGeometry(FPoint(13, int(getHeight() - 4)), FSize(10, 1)); + m_delete.setBackgroundColor(FColor::DarkRed); + m_delete.setFocusBackgroundColor(FColor::DarkRed); + m_delete.addCallback("clicked", [&]() { deleteEntry(); }); + + m_entryLabel.setGeometry(FPoint(2, int(getHeight() - 6)), FSize(10, 1)); + m_entry.setGeometry(FPoint(12, int(getHeight() - 6)), FSize(11, 1)); + m_entry.setShadow(false); + m_entry.addCallback("up-pressed", [&]() { + uint32_t tgId = ::atoi(m_entry.getText().c_str()); + tgId++; + if (tgId > 999999999U) { + tgId = 999999999U; + } + + m_entry.setText(std::to_string(tgId)); + redraw(); + }); + m_entry.addCallback("down-pressed", [&]() { + uint32_t tgId = ::atoi(m_entry.getText().c_str()); + tgId--; + if (tgId < 1U) { + tgId = 1U; + } + + m_entry.setText(std::to_string(tgId)); + redraw(); + }); + m_entry.addCallback("insert-pressed", [&]() { addEntry(); }); + m_entry.addCallback("return-pressed", [&]() { + size_t curItem = m_listBox.currentItem(); + auto item = m_listBox.getItem(curItem); + LogMessage(LOG_HOST, "Updating %s peer ID %s to %s for TG %s (%u)", m_title.c_str(), item.getText().c_str(), m_entry.getText().c_str(), + m_rule.name().c_str(), m_rule.source().tgId()); + item.setText(m_entry.getText()); + + m_listBox.remove(curItem); + m_listBox.insert(item); + + //setFocusWidget(&m_listBox); + redraw(); + }); + + m_listBox.setGeometry(FPoint{1, 1}, FSize{getWidth() - 1, getHeight() - 7}); + m_listBox.setMultiSelection(false); + m_listBox.addCallback("row-selected", [&]() { + size_t curItem = m_listBox.currentItem(); + auto item = m_listBox.getItem(curItem); + m_entry.setText(item.getText().c_str()); + + setFocusWidget(&m_listBox); + redraw(); + }); + + CloseWndBase::initControls(); + + setFocusWidget(&m_listBox); + redraw(); + } + + /** + * @brief Populates the peer list. + */ + void loadList() + { + m_listBox.clear(); + for (auto entry : peerList) { + m_listBox.insert(std::to_string(entry)); + } + + redraw(); + } + + /** + * @brief + */ + void addEntry() + { + if (m_entry.getText() == "") { + m_listBox.insert(std::to_string(0U)); + } else { + m_listBox.insert(m_entry.getText()); + } + + //setFocusWidget(&m_listBox); + redraw(); + } + + /** + * @brief + */ + void deleteEntry() + { + m_entry.setText(""); + + size_t curItem = m_listBox.currentItem(); + auto item = m_listBox.getItem(curItem); + LogMessage(LOG_HOST, "Removing %s peer ID %s from TG %s (%u)", m_title.c_str(), item.getText().c_str(), + m_rule.name().c_str(), m_rule.source().tgId()); + m_listBox.remove(curItem); + + //setFocusWidget(&m_listBox); + redraw(); + } + + /* + ** 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(); + redraw(); + } else if (key == FKey::Del_char) { + deleteEntry(); + redraw(); + } else if (key == FKey::Enter || key == FKey::Return) { + 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; + } + + peerList.clear(); + for (uint32_t i = 0U; i < m_listBox.getCount(); i++) { + auto item = m_listBox.getItem(i + 1U); + if (item.getText() != "") { + uint32_t peerId = ::atoi(item.getText().c_str()); + LogMessage(LOG_HOST, "%s peer ID %s for TG %s (%u)", m_title.c_str(), item.getText().c_str(), + m_rule.name().c_str(), m_rule.source().tgId()); + peerList.push_back(peerId); + } + } + + CloseWndBase::onClose(e); + } +}; + +#endif // __TG_EDIT_PEER_LIST_WND_H__ \ No newline at end of file diff --git a/src/tged/TGEditWnd.h b/src/tged/TGEditWnd.h new file mode 100644 index 00000000..7223482e --- /dev/null +++ b/src/tged/TGEditWnd.h @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGEditWnd.h + * @ingroup tged + */ +#if !defined(__TG_EDIT_WND_H__) +#define __TG_EDIT_WND_H__ + +#include "common/Log.h" + +#include "tged/CloseWndBase.h" +#include "tged/TGEdMain.h" +#include "tged/TGEditPeerListWnd.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the line edit control for TGIDs. + * @ingroup tged + */ +class HOST_SW_API TGIdLineEdit final : public FLineEdit { +public: + /** + * @brief Initializes a new instance of the TGIdLineEdit class. + * @param widget + */ + explicit TGIdLineEdit(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 talkgroup editor window. + * @ingroup tged + */ +class HOST_SW_API TGEditWnd final : public CloseWndBase { +public: + /** + * @brief Initializes a new instance of the TGEditWnd class. + * + * @param rule + * @param widget + */ + explicit TGEditWnd(lookups::TalkgroupRuleGroupVoice rule, FWidget* widget = nullptr) : CloseWndBase{widget} + { + m_rule = rule; + if (m_rule.isInvalid()) { + m_new = true; + } else { + m_origTgId = m_rule.source().tgId(); + m_origTgSlot = m_rule.source().tgSlot(); + } + } + +private: + bool m_new; + bool m_skipSaving; + lookups::TalkgroupRuleGroupVoice m_rule; + + uint32_t m_origTgId; + uint8_t m_origTgSlot; + + FLabel m_tgNameLabel{"Name: ", this}; + FLineEdit m_tgName{this}; + FLabel m_tgAliasLabel{"Alias: ", this}; + FLineEdit m_tgAlias{this}; + + FCheckBox m_saveCopy{"Save Copy", this}; + FCheckBox m_incOnSave{"Increment On Save", this}; + + FButtonGroup m_sourceGroup{"Source", this}; + FLabel m_tgIdLabel{"TGID: ", &m_sourceGroup}; + TGIdLineEdit m_tgId{&m_sourceGroup}; + FLabel m_tgSlotLabel{"Slot: ", &m_sourceGroup}; + FSpinBox m_tgSlot{&m_sourceGroup}; + + FButtonGroup m_configGroup{"Configuration", this}; + FCheckBox m_activeEnabled{"Active", &m_configGroup}; + FCheckBox m_affiliatedEnabled{"Affiliated", &m_configGroup}; + FCheckBox m_parrotEnabled{"Parrot", &m_configGroup}; + FButton m_inclusionList{"&Inclusions...", this}; + FButton m_exclusionList{"&Exclusions...", this}; + + FButton m_alwaysList{"&Always...", this}; + FButton m_preferredList{"&Preferred...", this}; + + FButton m_rewriteList{"&Rewrites...", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setText("Talkgroup"); + FDialog::setSize(FSize{60, 18}); + + m_enableSetButton = false; + CloseWndBase::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() override + { + m_closeButton.setText("&OK"); + + m_tgNameLabel.setGeometry(FPoint(2, 2), FSize(8, 1)); + m_tgName.setGeometry(FPoint(11, 2), FSize(24, 1)); + if (!m_rule.isInvalid()) { + m_tgName.setText(m_rule.name()); + } + m_tgName.setShadow(false); + m_tgName.addCallback("changed", [&]() { m_rule.name(m_tgName.getText().toString()); }); + + m_tgAliasLabel.setGeometry(FPoint(2, 3), FSize(8, 1)); + m_tgAlias.setGeometry(FPoint(11, 3), FSize(24, 1)); + if (!m_rule.isInvalid()) { + m_tgAlias.setText(m_rule.nameAlias()); + } + m_tgAlias.setShadow(false); + m_tgAlias.addCallback("changed", [&]() { m_rule.nameAlias(m_tgAlias.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_tgIdLabel.setGeometry(FPoint(2, 1), FSize(6, 1)); + m_tgId.setGeometry(FPoint(11, 1), FSize(17, 1)); + m_tgId.setAlignment(finalcut::Align::Right); + if (!m_rule.isInvalid()) { + m_tgId.setText(std::to_string(m_rule.source().tgId())); + } else { + auto source = m_rule.source(); + source.tgId(1U); + m_rule.source(source); + + m_tgId.setText("1"); + } + m_tgId.setShadow(false); + m_tgId.addCallback("up-pressed", [&]() { + uint32_t tgId = ::atoi(m_tgId.getText().c_str()); + tgId++; + if (tgId > 16777215U) { + tgId = 16777215U; + } + + m_tgId.setText(std::to_string(tgId)); + redraw(); + }); + m_tgId.addCallback("down-pressed", [&]() { + uint32_t tgId = ::atoi(m_tgId.getText().c_str()); + tgId--; + if (tgId < 1U) { + tgId = 1U; + } + + m_tgId.setText(std::to_string(tgId)); + redraw(); + }); + m_tgId.addCallback("changed", [&]() { + uint32_t tgId = ::atoi(m_tgId.getText().c_str()); + if (tgId < 1U) { + tgId = 1U; + } + + if (tgId > 16777215U) { + tgId = 16777215U; + } + + m_tgId.setText(std::to_string(tgId)); + + auto source = m_rule.source(); + source.tgId(tgId); + m_rule.source(source); + }); + + m_tgSlotLabel.setGeometry(FPoint(2, 2), FSize(6, 1)); + m_tgSlot.setGeometry(FPoint(11, 2), FSize(17, 1)); + if (m_rule.source().tgSlot() != 0U) + m_tgSlot.setValue(m_rule.source().tgSlot()); + else { + auto source = m_rule.source(); + source.tgSlot(1U); + m_rule.source(source); + + m_tgSlot.setValue(1U); + } + m_tgSlot.setMinValue(1U); + m_tgSlot.setMaxValue(2U); + m_tgSlot.setShadow(false); + m_tgSlot.addCallback("changed", [&]() { + auto source = m_rule.source(); + source.tgSlot(m_tgSlot.getValue()); + m_rule.source(source); + }); + } + + // configuration + { + m_configGroup.setGeometry(FPoint(34, 5), FSize(23, 5)); + + m_activeEnabled.setGeometry(FPoint(2, 1), FSize(10, 1)); + m_activeEnabled.setChecked(m_rule.config().active()); + m_activeEnabled.addCallback("toggled", [&]() { + auto config = m_rule.config(); + config.active(m_activeEnabled.isChecked()); + m_rule.config(config); + }); + + m_affiliatedEnabled.setGeometry(FPoint(2, 2), FSize(10, 1)); + m_affiliatedEnabled.setChecked(m_rule.config().affiliated()); + m_affiliatedEnabled.addCallback("toggled", [&]() { + auto config = m_rule.config(); + config.affiliated(m_affiliatedEnabled.isChecked()); + m_rule.config(config); + }); + + m_parrotEnabled.setGeometry(FPoint(2, 3), FSize(10, 1)); + m_parrotEnabled.setChecked(m_rule.config().parrot()); + m_parrotEnabled.addCallback("toggled", [&]() { + auto config = m_rule.config(); + config.parrot(m_parrotEnabled.isChecked()); + m_rule.config(config); + }); + } + + m_inclusionList.setGeometry(FPoint(2, 10), FSize(16, 1)); + m_inclusionList.addCallback("clicked", [&]() { + TGEditPeerListWnd wnd{m_rule, m_rule.config().inclusion(), "Peer Inclusions", this}; + wnd.show(); + + auto config = m_rule.config(); + config.inclusion(wnd.peerList); + m_rule.config(config); + LogMessage(LOG_HOST, "Updated %s (%u) peer inclusion list", m_rule.name().c_str(), m_rule.source().tgId()); + }); + + m_exclusionList.setGeometry(FPoint(20, 10), FSize(16, 1)); + m_exclusionList.addCallback("clicked", [&]() { + TGEditPeerListWnd wnd{m_rule, m_rule.config().exclusion(), "Peer Exclusions", this}; + wnd.show(); + + auto config = m_rule.config(); + config.exclusion(wnd.peerList); + m_rule.config(config); + LogMessage(LOG_HOST, "Updated %s (%u) peer exclusion list", m_rule.name().c_str(), m_rule.source().tgId()); + }); + + m_alwaysList.setGeometry(FPoint(2, 12), FSize(16, 1)); + m_alwaysList.addCallback("clicked", [&]() { + TGEditPeerListWnd wnd{m_rule, m_rule.config().alwaysSend(), "Peers Always Receiving", this}; + wnd.show(); + + auto config = m_rule.config(); + config.alwaysSend(wnd.peerList); + m_rule.config(config); + LogMessage(LOG_HOST, "Updated %s (%u) peer always receiving list", m_rule.name().c_str(), m_rule.source().tgId()); + }); + + m_preferredList.setGeometry(FPoint(20, 12), FSize(16, 1)); + m_preferredList.addCallback("clicked", [&]() { + TGEditPeerListWnd wnd{m_rule, m_rule.config().preferred(), "Peer Preference", this}; + wnd.show(); + + auto config = m_rule.config(); + config.preferred(wnd.peerList); + m_rule.config(config); + LogMessage(LOG_HOST, "Updated %s (%u) peer preference list", m_rule.name().c_str(), m_rule.source().tgId()); + }); + + m_rewriteList.setGeometry(FPoint(2, 14), FSize(16, 1)); + m_rewriteList.setDisable(); + m_rewriteList.addCallback("clicked", [&]() { + // TODO + }); + + CloseWndBase::initControls(); + } + + /** + * @brief + */ + void logRuleInfo() + { + std::string groupName = m_rule.name(); + uint32_t tgId = m_rule.source().tgId(); + uint8_t tgSlot = m_rule.source().tgSlot(); + bool active = m_rule.config().active(); + bool parrot = m_rule.config().parrot(); + bool affil = m_rule.config().affiliated(); + + uint32_t incCount = m_rule.config().inclusion().size(); + uint32_t excCount = m_rule.config().exclusion().size(); + uint32_t rewrCount = m_rule.config().rewrite().size(); + uint32_t alwyCount = m_rule.config().alwaysSend().size(); + uint32_t prefCount = m_rule.config().preferred().size(); + + if (incCount > 0 && excCount > 0) { + ::LogWarning(LOG_HOST, "Talkgroup (%s) defines both inclusions and exclusions! Inclusion rules take precedence and exclusion rules will be ignored.", groupName.c_str()); + } + + if (alwyCount > 0 && affil) { + ::LogWarning(LOG_HOST, "Talkgroup (%s) is marked as affiliation required and has a defined always send list! Always send peers take rule precedence and defined peers will always receive traffic.", groupName.c_str()); + } + + ::LogInfoEx(LOG_HOST, "Talkgroup NAME: %s SRC_TGID: %u SRC_TS: %u ACTIVE: %u PARROT: %u AFFILIATED: %u INCLUSIONS: %u EXCLUSIONS: %u REWRITES: %u ALWAYS: %u PREFERRED: %u", groupName.c_str(), tgId, tgSlot, active, parrot, affil, incCount, excCount, rewrCount, alwyCount, prefCount); + } + + /* + ** 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.isInvalid()) { + if (m_incOnSave.isChecked()) { + auto source = m_rule.source(); + uint32_t tgId = source.tgId(); + tgId++; + + if (tgId > 16777215U) { + tgId = 16777215U; + } + + source.tgId(tgId); + m_rule.source(source); + + m_tgId.setText(std::to_string(tgId)); + redraw(); + } + + if (m_origTgId != 0U && m_origTgSlot != 0U && !m_saveCopy.isChecked()) { + if (m_rule.name() == "") { + LogError(LOG_HOST, "Not saving talkgroup, TG %s (%u), talkgroup must be named.", m_rule.name().c_str(), m_rule.source().tgId()); + FMessageBox::error(this, "Talkgroup must be named."); + return; + } + + // update TG + auto groupVoice = g_tidLookups->groupVoice(); + auto it = std::find_if(groupVoice.begin(), groupVoice.end(), + [&](lookups::TalkgroupRuleGroupVoice x) + { + return x.source().tgId() == m_origTgId && x.source().tgSlot() == m_origTgSlot; + }); + if (it != groupVoice.end()) { + LogMessage(LOG_HOST, "Updating TG %s (%u) to %s (%u)", it->name().c_str(), it->source().tgId(), m_rule.name().c_str(), m_rule.source().tgId()); + g_tidLookups->eraseEntry(m_origTgId, m_origTgSlot); + g_tidLookups->addEntry(m_rule); + + logRuleInfo(); + } + } else { + if (m_rule.name() == "") { + LogError(LOG_HOST, "Not saving talkgroup, TG %s (%u), talkgroup must be named.", m_rule.name().c_str(), m_rule.source().tgId()); + FMessageBox::error(this, "Talkgroup must be named."); + return; + } + + auto groupVoice = g_tidLookups->groupVoice(); + auto it = std::find_if(groupVoice.begin(), groupVoice.end(), + [&](lookups::TalkgroupRuleGroupVoice x) + { + return x.source().tgId() == m_origTgId && x.source().tgSlot() == m_origTgSlot; + }); + if (it != groupVoice.end()) { + LogError(LOG_HOST, "Not saving duplicate talkgroup, TG %s (%u), talkgroups must be unique.", m_rule.name().c_str(), m_rule.source().tgId()); + FMessageBox::error(this, "Duplicate talkgroup, change TGID. Talkgroups must be unique."); + if (m_saveCopy.isChecked()) + m_saveCopy.setChecked(false); + return; + } + + // add new TG + if (m_saveCopy.isChecked()) { + LogMessage(LOG_HOST, "Copying TG. Adding TG %s (%u)", m_rule.name().c_str(), m_rule.source().tgId()); + } else { + LogMessage(LOG_HOST, "Adding TG %s (%u)", m_rule.name().c_str(), m_rule.source().tgId()); + } + g_tidLookups->addEntry(m_rule); + + logRuleInfo(); + + // don't actually close the modal on a copy save + if (m_saveCopy.isChecked()) { + return; + } + } + } else { + LogError(LOG_HOST, "Not saving talkgroup, TG %s (%u), have a TGID greater than 0.", m_rule.name().c_str(), m_rule.source().tgId()); + FMessageBox::error(this, "Talkgroup must have a TGID greater than 0."); + return; + } + + CloseWndBase::onClose(e); + } +}; + +#endif // __TG_EDIT_WND_H__ \ No newline at end of file diff --git a/src/tged/TGListWnd.h b/src/tged/TGListWnd.h new file mode 100644 index 00000000..d544682b --- /dev/null +++ b/src/tged/TGListWnd.h @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Talkgroup 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 TGListWnd.h + * @ingroup tged + */ +#if !defined(__TG_LIST_WND_H__) +#define __TG_LIST_WND_H__ + +#include "common/Log.h" + +#include "TGEdMainWnd.h" +#include "TGEditWnd.h" + +#include +using namespace finalcut; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define TG_LIST_WIDTH 74 +#define TG_LIST_HEIGHT 15 + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the talkgroup list window. + * @ingroup tged + */ +class HOST_SW_API TGListWnd final : public finalcut::FDialog { +public: + /** + * @brief Initializes a new instance of the TGListWnd class. + * @param widget + */ + explicit TGListWnd(FWidget* widget = nullptr) : FDialog{widget} + { + /* stub */ + } + /** + * @brief Copy constructor. + */ + TGListWnd(const TGListWnd&) = delete; + /** + * @brief Move constructor. + */ + TGListWnd(TGListWnd&&) noexcept = delete; + /** + * @brief Finalizes an instance of the TGListWnd class. + */ + ~TGListWnd() noexcept override = default; + + /** + * @brief Disable copy assignment operator (=). + */ + auto operator= (const TGListWnd&) -> TGListWnd& = delete; + /** + * @brief Disable move assignment operator (=). + */ + auto operator= (TGListWnd&&) noexcept -> TGListWnd& = 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 = TalkgroupRuleGroupVoice(); + m_selectedTgId = 0U; + + m_listView.clear(); + for (auto entry : g_tidLookups->groupVoice()) { + // pad TGs properly + std::ostringstream oss; + oss << std::setw(5) << std::setfill('0') << entry.source().tgId(); + + // build list view entry + const std::array columns = { + entry.name(), entry.nameAlias(), oss.str(), + (entry.config().active()) ? "X" : "", + (entry.config().affiliated()) ? "X" : "", + std::to_string(entry.config().inclusionSize()), + std::to_string(entry.config().exclusionSize()), + std::to_string(entry.config().alwaysSendSize()) + }; + + const finalcut::FStringList line(columns.cbegin(), columns.cend()); + m_listView.insert(line); + } + + setFocusWidget(&m_listView); + redraw(); + } + +private: + lookups::TalkgroupRuleGroupVoice m_selected; + uint32_t m_selectedTgId; + + FListView m_listView{this}; + + FButton m_addTG{"&Add", this}; + FButton m_editTG{"&Edit", this}; + FButton m_deleteTG{"&Delete", this}; + + /** + * @brief Initializes the window layout. + */ + void initLayout() override + { + FDialog::setMinimumSize(FSize{TG_LIST_WIDTH, TG_LIST_HEIGHT}); + + FDialog::setResizeable(false); + FDialog::setMinimizable(false); + FDialog::setTitlebarButtonVisibility(false); + FDialog::setModal(false); + + FDialog::setText("Talkgroup List"); + + initControls(); + loadListView(); + + FDialog::initLayout(); + } + + /** + * @brief Initializes window controls. + */ + void initControls() + { + m_addTG.setGeometry(FPoint(2, int(getHeight() - 4)), FSize(9, 1)); + m_addTG.setBackgroundColor(FColor::DarkGreen); + m_addTG.setFocusBackgroundColor(FColor::DarkGreen); + m_addTG.addCallback("clicked", [&]() { addEntry(); }); + + m_editTG.setGeometry(FPoint(13, int(getHeight() - 4)), FSize(10, 1)); + m_editTG.setDisable(); + m_editTG.addCallback("clicked", [&]() { editEntry(); }); + + m_deleteTG.setGeometry(FPoint(int(getWidth()) - 13, int(getHeight() - 4)), FSize(10, 1)); + m_deleteTG.setDisable(); + m_deleteTG.addCallback("clicked", [&]() { deleteEntry(); }); + + m_listView.setGeometry(FPoint{1, 1}, FSize{getWidth() - 1, getHeight() - 5}); + + // configure list view columns + m_listView.addColumn("Name", 25); + m_listView.addColumn("Alias", 20); + m_listView.addColumn("TGID", 9); + m_listView.addColumn("Active", 5); + m_listView.addColumn("Affiliated", 5); + m_listView.addColumn("Inclusions", 5); + m_listView.addColumn("Exclusions", 5); + m_listView.addColumn("Always", 5); + + // set right alignment for TGID + m_listView.setColumnAlignment(3, finalcut::Align::Right); + m_listView.setColumnAlignment(4, finalcut::Align::Center); + m_listView.setColumnAlignment(5, finalcut::Align::Center); + m_listView.setColumnAlignment(6, finalcut::Align::Right); + m_listView.setColumnAlignment(7, finalcut::Align::Right); + m_listView.setColumnAlignment(8, finalcut::Align::Right); + + // set type of sorting + m_listView.setColumnSortType(1, finalcut::SortType::Name); + m_listView.setColumnSortType(2, finalcut::SortType::Name); + m_listView.setColumnSortType(3, finalcut::SortType::Name); + + // sort by TGID + m_listView.setColumnSort(2, finalcut::SortOrder::Ascending); + m_listView.setColumnSort(3, finalcut::SortOrder::Ascending); + + m_listView.addCallback("clicked", [&]() { editEntry(); }); + m_listView.addCallback("row-changed", [&]() { + FListViewItem* curItem = m_listView.getCurrentItem(); + if (curItem != nullptr) { + FString strTgid = curItem->getText(3); + uint32_t tgid = ::atoi(strTgid.c_str()); + + if (tgid != m_selectedTgId) { + auto entry = g_tidLookups->find(tgid); + if (!entry.isInvalid()) { + m_selected = entry; + if (m_selectedTgId != tgid) + LogMessage(LOG_HOST, "Selected TG %s (%u) for editing", m_selected.name().c_str(), m_selected.source().tgId()); + m_selectedTgId = tgid; + + m_editTG.setEnable(); + m_deleteTG.setEnable(); + m_deleteTG.setBackgroundColor(FColor::DarkRed); + m_deleteTG.setFocusBackgroundColor(FColor::DarkRed); + } else { + m_editTG.setDisable(); + m_deleteTG.setDisable(); + m_deleteTG.resetColors(); + } + + redraw(); + } + } + }); + + setFocusWidget(&m_listView); + redraw(); + } + + /** + * @brief + */ + void addEntry() + { + this->lowerWindow(); + this->deactivateWindow(); + + TGEditWnd wnd{TalkgroupRuleGroupVoice(), this}; + wnd.show(); + + this->raiseWindow(); + this->activateWindow(); + + loadListView(); + } + + /** + * @brief + */ + void editEntry() + { + if (m_selected.isInvalid()) + return; + + this->lowerWindow(); + this->deactivateWindow(); + + TGEditWnd wnd{m_selected, this}; + wnd.show(); + + this->raiseWindow(); + this->activateWindow(); + + loadListView(); + } + + /** + * @brief + */ + void deleteEntry() + { + if (m_selected.isInvalid()) + return; + + LogMessage(LOG_HOST, "Deleting TG %s (%u)", m_selected.name().c_str(), m_selected.source().tgId()); + g_tidLookups->eraseEntry(m_selected.source().tgId(), m_selected.source().tgSlot()); + loadListView(); + } + + /* + ** 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 // __TG_LIST_WND_H__ \ No newline at end of file