diff --git a/src/FlashCom/App.cpp b/src/FlashCom/App.cpp index 3069aee..f52afc4 100644 --- a/src/FlashCom/App.cpp +++ b/src/FlashCom/App.cpp @@ -8,16 +8,6 @@ namespace { const std::unordered_set c_hotkeyCombo{ VK_LWIN, VK_SPACE }; - - std::unique_ptr CreateDataModel( - FlashCom::Settings::SettingsManager& settingsManager) - { - - auto rootNode{ settingsManager.GetTree() }; - - return std::make_unique( - std::move(rootNode)); - } } namespace FlashCom @@ -25,7 +15,24 @@ namespace FlashCom #pragma region Public std::unique_ptr App::CreateApp(const HINSTANCE& hInstance) { - return std::unique_ptr(new App(hInstance)); + std::unique_ptr app{ new App(hInstance) }; + app->LoadDataModel(); + return app; + } + + void App::LoadDataModel() + { + auto result{ m_settingsManager.LoadSettings() }; + m_dataModel->RootNode = m_settingsManager.GetCommandTreeRoot(); + m_dataModel->CurrentNode = m_dataModel->RootNode.get(); + if (!result.has_value()) + { + m_dataModel->LoadErrorMessage = result.error(); + } + else + { + m_dataModel->LoadErrorMessage = ""; + } } int App::RunMessageLoop() @@ -121,10 +128,11 @@ namespace FlashCom #pragma endregion Public #pragma region Private App::App(const HINSTANCE& hInstance) : - m_dataModel{ std::move(CreateDataModel(m_settingsManager)) }, + m_dataModel{ std::make_unique() }, m_hostWindow{ hInstance, L"FlashCom", std::bind(&App::HandleFocusLost, this) }, m_trayIcon{ hInstance, { std::make_pair(std::wstring{ L"Settings" }, std::bind(&App::OnSettingsCommand, this)), + std::make_pair(std::wstring{ L"Reload" }, std::bind(&App::OnReloadCommand, this)), std::make_pair(std::wstring{ L"Exit" }, std::bind(&App::OnExitCommand, this)) } }, m_ui{ m_hostWindow, m_dataModel.get() } @@ -189,6 +197,13 @@ namespace FlashCom nullptr, nullptr, SW_SHOWNORMAL); } + void App::OnReloadCommand() + { + SPDLOG_INFO("App::OnReloadCommand"); + LoadDataModel(); + m_ui.Update(); + } + void App::OnExitCommand() { SPDLOG_INFO("App::OnExitCommand"); diff --git a/src/FlashCom/App.h b/src/FlashCom/App.h index 1ba1d60..d9a38af 100644 --- a/src/FlashCom/App.h +++ b/src/FlashCom/App.h @@ -15,6 +15,7 @@ namespace FlashCom struct App { static std::unique_ptr CreateApp(const HINSTANCE& hInstance); + void LoadDataModel(); int RunMessageLoop(); FlashCom::Input::LowLevelCallbackReturnKind HandleLowLevelKeyboardInput( WPARAM wParam, KBDLLHOOKSTRUCT* kb); @@ -36,6 +37,7 @@ namespace FlashCom void Show(); void Hide(); void OnSettingsCommand(); + void OnReloadCommand(); void OnExitCommand(); }; } \ No newline at end of file diff --git a/src/FlashCom/Models/DataModel.cpp b/src/FlashCom/Models/DataModel.cpp index 1d39833..a97f46a 100644 --- a/src/FlashCom/Models/DataModel.cpp +++ b/src/FlashCom/Models/DataModel.cpp @@ -2,9 +2,4 @@ #include "DataModel.h" namespace FlashCom::Models -{ - DataModel::DataModel(std::unique_ptr&& rootNode) : - RootNode{ std::move(rootNode) }, - CurrentNode{ RootNode.get() } - { } -} \ No newline at end of file +{ } \ No newline at end of file diff --git a/src/FlashCom/Models/DataModel.h b/src/FlashCom/Models/DataModel.h index e24e4e4..e56b8e7 100644 --- a/src/FlashCom/Models/DataModel.h +++ b/src/FlashCom/Models/DataModel.h @@ -5,10 +5,8 @@ namespace FlashCom::Models { struct DataModel { - DataModel(std::unique_ptr&& rootNode); - DataModel(DataModel&& other) = default; - - const std::unique_ptr RootNode; + std::string LoadErrorMessage; + std::shared_ptr RootNode; TreeNode* CurrentNode{ nullptr }; }; } diff --git a/src/FlashCom/Settings/SettingsManager.cpp b/src/FlashCom/Settings/SettingsManager.cpp index f72572a..28fdab5 100644 --- a/src/FlashCom/Settings/SettingsManager.cpp +++ b/src/FlashCom/Settings/SettingsManager.cpp @@ -10,8 +10,23 @@ namespace { constexpr std::string_view c_settingsFileName{ "settings.json" }; + // JSON property names + constexpr std::string_view c_commandsJsonProperty{ "commands" }; + constexpr std::string_view c_commandNameJsonProperty{ "name" }; + constexpr std::string_view c_commandKeyJsonProperty{ "key" }; + constexpr std::string_view c_commandChildrenJsonProperty{ "children" }; + constexpr std::string_view c_commandTypeJsonProperty{ "type" }; + constexpr std::string_view c_commandExecuteFileJsonProperty{ "executeFile" }; + constexpr std::string_view c_commandExecuteParametersJsonProperty{ "executeParameters" }; + constexpr std::string_view c_commandUriJsonProperty{ "uri" }; + constexpr std::string_view c_commandAumidJsonProperty{ "aumid" }; + // JSON command node 'type' values + constexpr std::string_view c_commandTypeValueShellExecute{ "shellExecute" }; + constexpr std::string_view c_commandTypeValueUri{ "uri" }; + constexpr std::string_view c_commandTypeValueAumid{ "aumid" }; + // Default settings.json contents constexpr std::string_view c_defaultSettingsFileContents{ R"({ - "commandTree": [ + "commands": [ { "name": "Tools", "key": "T", @@ -60,65 +75,192 @@ namespace .LocalFolder().Path().c_str() } / c_settingsFileName; } - std::unique_ptr ParseToTreeNode(const nlohmann::json& json) + bool IsValidKeyString(const std::string& key) + { + // Valid keys map to single vkey codes as defined in winuser.h and are not + // special characters or modifier keys. + // https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes + if (key.size() != 1) + { + return false; + } + + auto keyCode{ static_cast(std::toupper(key.at(0))) }; + if ((keyCode < 0x30) || + (keyCode > 0x39 && keyCode < 0x41) || + (keyCode > 0x5A)) + { + return false; + } + + return true; + } + + std::expected, std::string> + ParseToShellExecuteTreeNode(uint8_t keyCode, const std::string& name, + const nlohmann::json& json) + { + std::string executeFile; + std::string executeParameters; + if (!json.contains(c_commandExecuteFileJsonProperty) || + !json.at(c_commandExecuteFileJsonProperty).is_string()) + { + SPDLOG_ERROR("::ParseToShellExecuteTreeNode - Command '{}' type '{}' " + "has no valid '{}' string property", name, c_commandTypeValueShellExecute, + c_commandExecuteFileJsonProperty); + return std::unexpected(std::format( + "Command '{}' type '{}' has no valid '{}' string property", + name, c_commandTypeValueShellExecute, c_commandExecuteFileJsonProperty)); + } + executeFile = json.at(c_commandExecuteFileJsonProperty).get(); + + // executeParameters is optional + if (json.contains(c_commandExecuteParametersJsonProperty) && + json.at(c_commandExecuteParametersJsonProperty).is_string()) + { + executeParameters = json.at(c_commandExecuteParametersJsonProperty).get(); + } + + SPDLOG_INFO("::ParseToShellExecuteTreeNode - Creating ShellExecute node {}:{}, " + "file: '{}', parameters: '{}'", static_cast(keyCode), name, + executeFile, executeParameters); + return std::make_unique(keyCode, + name, executeFile, executeParameters); + } + + std::expected, std::string> + ParseToLaunchUriTreeNode(uint8_t keyCode, const std::string& name, + const nlohmann::json& json) + { + if (!json.contains(c_commandUriJsonProperty) || + !json.at(c_commandUriJsonProperty).is_string()) + { + SPDLOG_ERROR("::ParseToLaunchUriTreeNode - Command '{}' type '{}' " + "has no valid '{}' string property", name, c_commandTypeValueUri, + c_commandUriJsonProperty); + return std::unexpected(std::format( + "Command '{}' type '{}' has no valid '{}' string property", + name, c_commandTypeValueUri, c_commandUriJsonProperty)); + } + auto uri{ json.at(c_commandUriJsonProperty).get() }; + SPDLOG_INFO("::ParseToLaunchUriTreeNode - Creating LaunchUri node {}:{}, uri: '{}'", + static_cast(keyCode), name, uri); + return std::make_unique(keyCode, name, uri); + } + + std::expected, std::string> + ParseToActivateAumidTreeNode(uint8_t keyCode, const std::string& name, + const nlohmann::json& json) + { + if (!json.contains(c_commandAumidJsonProperty) || + !json.at(c_commandAumidJsonProperty).is_string()) + { + SPDLOG_ERROR("::ParseToActivateAumidTreeNode - Command '{}' type '{}' " + "has no valid '{}' string property", name, c_commandTypeValueAumid, + c_commandAumidJsonProperty); + return std::unexpected(std::format( + "Command '{}' type '{}' has no valid '{}' string property", + name, c_commandTypeValueAumid, c_commandAumidJsonProperty)); + } + auto aumid{ json.at(c_commandAumidJsonProperty).get() }; + SPDLOG_INFO( + "::ParseToActivateAumidTreeNode - Creating ActivateAumid node {}:{}, aumid: '{}'", + static_cast(keyCode), name, aumid); + return std::make_unique(keyCode, name, aumid); + } + + std::expected, std::string> ParseToTreeNode( + const nlohmann::json& json) { std::vector> nodeChildren; - // Basic validation - if (!json.contains("name") || !json.at("name").is_string() || - !json.contains("key") || !json.at("key").is_string()) + if (!json.contains(c_commandNameJsonProperty) || + !json.at(c_commandNameJsonProperty).is_string() || + !json.contains(c_commandKeyJsonProperty) || + !json.at(c_commandKeyJsonProperty).is_string()) + { + SPDLOG_ERROR("::ParseToTreeNode - no valid '{}' or '{}' string properties found", + c_commandNameJsonProperty, c_commandKeyJsonProperty); + return std::unexpected(std::format( + "Command object has no '{}' or '{}' string properties", + c_commandNameJsonProperty, c_commandKeyJsonProperty)); + } + + auto nodeName{ json.at(c_commandNameJsonProperty).get() }; + auto jsonKeyCodeString{ json.at(c_commandKeyJsonProperty).get() }; + if (!IsValidKeyString(jsonKeyCodeString)) { - // TODO: Log warning - return nullptr; + SPDLOG_ERROR("::ParseToTreeNode - invalid '{}' value '{}', " + "must be single alphanumeric keyboard key", c_commandKeyJsonProperty, + jsonKeyCodeString); + return std::unexpected(std::format( + "Command object has invalid '{}' value '{}', " + "must be single alphanumeric keyboard key", c_commandKeyJsonProperty, + jsonKeyCodeString)); } + auto nodeKeyCode{ static_cast(std::toupper(jsonKeyCodeString.at(0))) }; - auto nodeName{ json.at("name").get() }; - auto jsonKeyCodeString{ json.at("key").get() }; - uint32_t nodeKeyCode{ static_cast(std::toupper(jsonKeyCodeString.at(0))) }; - - if (json.contains("children") && json.at("children").is_array()) + if (json.contains(c_commandChildrenJsonProperty)) { - for (auto& childJson : json.at("children")) + if (!json.at(c_commandChildrenJsonProperty).is_array()) + { + SPDLOG_ERROR("::ParseToTreeNode - '{}' must be an array of child commands", + c_commandChildrenJsonProperty); + return std::unexpected(std::format( + "Command object has invalid '{}' type, must be an array of child commands.", + c_commandChildrenJsonProperty)); + } + + for (auto& childJson : json.at(c_commandChildrenJsonProperty)) { - if (auto childNode{ ParseToTreeNode(childJson) }) + auto childNode{ ParseToTreeNode(childJson) }; + if (childNode.has_value()) { - nodeChildren.push_back(std::move(childNode)); + nodeChildren.push_back(std::move(childNode.value())); + } + else + { + SPDLOG_WARN("::ParseToTreeNode - Skipping child node of '{}': {}", + nodeName, childNode.error()); } } } - if (json.contains("type") && json.at("type").is_string()) + if (json.contains(c_commandTypeJsonProperty) && + json.at(c_commandTypeJsonProperty).is_string()) { - auto nodeType{ json.at("type").get() }; - if (nodeType == "shellExecute") + auto nodeType{ json.at(c_commandTypeJsonProperty).get() }; + if (json.contains(c_commandChildrenJsonProperty)) { - std::string executeParameters; - auto executeFile{ json.at("executeFile").get() }; - if (json.contains("executeParameters") && json.at("executeParameters").is_string()) - { - executeParameters = json.at("executeParameters").get(); - } - return std::make_unique(nodeKeyCode, - nodeName, executeFile, executeParameters); + SPDLOG_WARN("::ParseToTreeNode - Command '{}' contains both '{}' and '{}' " + "properties. Skipping '{}'...", nodeName, c_commandChildrenJsonProperty, + c_commandTypeJsonProperty, c_commandTypeJsonProperty); } - else if (nodeType == "uri") + else if (nodeType == c_commandTypeValueShellExecute) { - auto uri{ json.at("uri").get() }; - return std::make_unique(nodeKeyCode, - nodeName, uri); + return ParseToShellExecuteTreeNode(nodeKeyCode, nodeName, json); } - else if (nodeType == "aumid") + else if (nodeType == c_commandTypeValueUri) { - auto aumid{ json.at("aumid").get() }; - return std::make_unique(nodeKeyCode, - nodeName, aumid); + return ParseToLaunchUriTreeNode(nodeKeyCode, nodeName, json); + } + else if (nodeType == c_commandTypeValueAumid) + { + return ParseToActivateAumidTreeNode(nodeKeyCode, nodeName, json); } else { - // TODO: LOG ERROR + SPDLOG_ERROR( + "::ParseToTreeNode - Command '{}' contains an unknown '{}' value '{}'", + nodeName, c_commandTypeJsonProperty, nodeType); + return std::unexpected(std::format( + "Command '{}' contains an unknown '{}' value '{}'", + nodeName, c_commandTypeJsonProperty, nodeType)); } } + SPDLOG_INFO("::ParseToTreeNode - Creating node {}:{} with {} children", + static_cast(nodeKeyCode), nodeName, nodeChildren.size()); return std::make_unique(nodeKeyCode, nodeName, std::move(nodeChildren)); } @@ -138,31 +280,77 @@ namespace FlashCom::Settings } } + std::expected SettingsManager::LoadSettings() + { + SPDLOG_INFO("SettingsManager::LoadSettings - Loading settings from {}...", + m_settingsFilePath.string()); + std::unique_lock settingsLock{ m_settingsAccessMutex }; + std::ifstream settingsFileStream{}; + nlohmann::json settingsJson{}; + try + { + settingsFileStream = std::ifstream{ m_settingsFilePath }; + settingsJson = nlohmann::json::parse(settingsFileStream); + } + catch (const nlohmann::json::exception& e) + { + SPDLOG_ERROR("SettingsManager::LoadSettings - Could not parse settings.json file: {}", + e.what()); + return std::unexpected(std::format("Could not parse settings.json file: {}", + e.what())); + } + + PopulateCommandTree(settingsLock, settingsJson); + return {}; // Spurious warning C4715 + } + std::filesystem::path SettingsManager::GetSettingsFilePath() { + std::shared_lock settingsLock{ m_settingsAccessMutex }; return m_settingsFilePath; } - std::unique_ptr SettingsManager::GetTree() + std::shared_ptr SettingsManager::GetCommandTreeRoot() + { + std::shared_lock settingsLock{ m_settingsAccessMutex }; + return m_commandTreeRoot; + } + + std::expected SettingsManager::PopulateCommandTree( + const std::unique_lock& /*accessLock*/, + const nlohmann::json& settingsJson) { + SPDLOG_INFO("SettingsManager::PopulateCommandTree"); + m_commandTreeRoot = nullptr; + + if (!settingsJson.contains(c_commandsJsonProperty) || + !settingsJson.at(c_commandsJsonProperty).is_array()) + { + SPDLOG_ERROR("SettingsManager::PopulateCommandTree - No '{}' array property found", + c_commandsJsonProperty); + return std::unexpected(std::format("No '{}' array property found.", + c_commandsJsonProperty)); + } + std::vector> rootChildren; - std::ifstream settingsFileStream{ m_settingsFilePath }; - const auto settingsJson{ nlohmann::json::parse(settingsFileStream) }; - if (settingsJson.contains("commandTree")) + const auto& commandTreeJson{ settingsJson.at(c_commandsJsonProperty) }; + for (const auto& commandTreeObject : commandTreeJson) { - const auto& commandTreeJson{ settingsJson.at("commandTree") }; - commandTreeJson.is_array(); - for (const auto& commandTreeObject : commandTreeJson) + auto rootChild{ ParseToTreeNode(commandTreeObject) }; + if (rootChild.has_value()) { - auto treeNode{ ParseToTreeNode(commandTreeObject) }; - rootChildren.push_back(std::move(treeNode)); + rootChildren.push_back(std::move(rootChild.value())); + } + else + { + SPDLOG_WARN("SettingsManager::PopulateCommandTree - Skipping root child: {}", + rootChild.error()); } - } - else - { - // Log error } - return std::make_unique(0, "Root", std::move(rootChildren)); + SPDLOG_INFO("SettingsManager::PopulateCommandTree - Creating root node with {} children", + rootChildren.size()); + m_commandTreeRoot = std::make_shared(0, "Root", std::move(rootChildren)); + return {}; // Spurious warning C4715 } } diff --git a/src/FlashCom/Settings/SettingsManager.h b/src/FlashCom/Settings/SettingsManager.h index 13e3a99..5d92f12 100644 --- a/src/FlashCom/Settings/SettingsManager.h +++ b/src/FlashCom/Settings/SettingsManager.h @@ -1,5 +1,7 @@ #pragma once +#include #include +#include #include "../Models/TreeNode.h" namespace FlashCom::Settings @@ -7,10 +9,17 @@ namespace FlashCom::Settings struct SettingsManager { SettingsManager(); + std::expected LoadSettings(); std::filesystem::path GetSettingsFilePath(); - std::unique_ptr GetTree(); + std::shared_ptr GetCommandTreeRoot(); private: std::filesystem::path const m_settingsFilePath; + std::shared_mutex m_settingsAccessMutex; + std::shared_ptr m_commandTreeRoot; + + std::expected PopulateCommandTree( + const std::unique_lock& accessLock, + const nlohmann::json& settingsJson); }; } \ No newline at end of file