diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7936ddd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Story Editor (GDB)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/story-editor/build/story-editor", // Remplacez par le chemin de votre exécutable + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/story-editor", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f729b4a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + + "cmake.sourceDirectory": [ + "${workspaceFolder}/story-editor", + "${workspaceFolder}/story-player", + "${workspaceFolder}/software" + ] + +} \ No newline at end of file diff --git a/hardware/kicad/libs/47219-2001.zip b/hardware/kicad/libs/47219-2001.zip new file mode 100644 index 0000000..3906cf4 Binary files /dev/null and b/hardware/kicad/libs/47219-2001.zip differ diff --git a/hardware/kicad/libs/472192001_sd.pdf b/hardware/kicad/libs/472192001_sd.pdf new file mode 100644 index 0000000..502d28c Binary files /dev/null and b/hardware/kicad/libs/472192001_sd.pdf differ diff --git a/story-editor/CMakeLists.txt b/story-editor/CMakeLists.txt index f278917..236ae91 100644 --- a/story-editor/CMakeLists.txt +++ b/story-editor/CMakeLists.txt @@ -5,33 +5,36 @@ set(STORY_EDITOR_PROJECT story-editor) project(${STORY_EDITOR_PROJECT} LANGUAGES C CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set (CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Version du projet +set(PROJECT_VERSION_MAJOR 1) +set(PROJECT_VERSION_MINOR 0) +set(PROJECT_VERSION_PATCH 0) + # set(CMAKE_VERBOSE_MAKEFILE on) - - if(POLICY CMP0072) cmake_policy(SET CMP0072 NEW) endif() + find_package(OpenGL REQUIRED) set(IMGUI_VERSION 1.90) include(FetchContent) - -#========================================================================================================================= +# ========================================================================================================================= # IMGUI and plugins -#========================================================================================================================= +# ========================================================================================================================= FetchContent_Declare(imgui - URL https://github.com/ocornut/imgui/archive/refs/tags/v${IMGUI_VERSION}-docking.zip + URL https://github.com/ocornut/imgui/archive/refs/tags/v${IMGUI_VERSION}-docking.zip ) - FetchContent_GetProperties(imgui) -if (NOT imgui_POPULATED) + +if(NOT imgui_POPULATED) set(FETCHCONTENT_QUIET NO) FetchContent_Populate(imgui) endif() @@ -42,17 +45,15 @@ add_compile_definitions(CUSTOM_IMGUIFILEDIALOG_CONFIG="${CMAKE_SOURCE_DIR}/src/C add_compile_definitions(IMGUI_INCLUDE="imgui.h") add_subdirectory(libs/ImGuiFileDialog) - -#========================================================================================================================= +# ========================================================================================================================= # SDL -#========================================================================================================================= - +# ========================================================================================================================= Set(FETCHCONTENT_QUIET FALSE) FetchContent_Declare( sdl2 GIT_REPOSITORY https://github.com/libsdl-org/SDL.git - GIT_TAG origin/SDL2 + GIT_TAG origin/SDL2 GIT_SHALLOW TRUE GIT_PROGRESS TRUE ) @@ -60,29 +61,27 @@ FetchContent_Declare( set(BUILD_SHARED_LIBS TRUE) set(SDL_STATIC TRUE) FetchContent_MakeAvailable(sdl2) -#add_subdirectory(libs/SDL) -#include_directories(libs/SDL/include) +# add_subdirectory(libs/SDL) +# include_directories(libs/SDL/include) -#========================================================================================================================= +# ========================================================================================================================= # SDL3-Image -#========================================================================================================================= -#FetchContent_Declare( -# SDL2_image -# GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git -# GIT_TAG origin/main -# GIT_SHALLOW TRUE -# GIT_PROGRESS TRUE -#) - -## START ADDITION -#set(SDL2IMAGE_INSTALL OFF) -#set(BUILD_SHARED_LIBS FALSE) -## END ADDITION - -#FetchContent_MakeAvailable(SDL2_image) +# ========================================================================================================================= +# FetchContent_Declare( +# SDL2_image +# GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git +# GIT_TAG origin/main +# GIT_SHALLOW TRUE +# GIT_PROGRESS TRUE +# ) +# # START ADDITION +# set(SDL2IMAGE_INSTALL OFF) +# set(BUILD_SHARED_LIBS FALSE) +# # END ADDITION +# FetchContent_MakeAvailable(SDL2_image) set(SRCS src/main.cpp @@ -147,7 +146,6 @@ set(SRCS ${imgui_SOURCE_DIR}/imgui_tables.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp - ../software/chip32/chip32_assembler.cpp ../software/chip32/chip32_vm.c ../software/library/audio_player.cpp @@ -169,10 +167,10 @@ if(WIN32) endif() if(WIN32) -add_executable(${STORY_EDITOR_PROJECT} - WIN32 - ${SRCS} -) + add_executable(${STORY_EDITOR_PROJECT} + WIN32 + ${SRCS} + ) else() add_executable(${STORY_EDITOR_PROJECT} ${SRCS} @@ -191,8 +189,6 @@ target_include_directories(${STORY_EDITOR_PROJECT} PUBLIC ../software/chip32/ ) - - add_definitions(-DIMGUI_USE_WCHAR32) add_link_options(-static-libgcc -static-libstdc++) @@ -202,26 +198,48 @@ target_compile_definitions(${STORY_EDITOR_PROJECT} PUBLIC "$<$:DEB target_link_directories(${STORY_EDITOR_PROJECT} PUBLIC ${sdl2_BINARY_DIR}) message(${sdl2_BINARY_DIR}) -if (UNIX) -target_link_libraries(${STORY_EDITOR_PROJECT} - pthread - udev - OpenGL::GL - dl - SDL2 -) +set(SDL2_BIN_DIR ${sdl2_BINARY_DIR}) + +if(UNIX) + target_link_libraries(${STORY_EDITOR_PROJECT} + pthread + udev + OpenGL::GL + dl + SDL2 + ) elseif(WIN32) - -#target_compile_features("-Wl,-subsystem,windows") - -target_link_libraries(${STORY_EDITOR_PROJECT} - OpenGL::GL -# SDL2::SDL2main - SDL2 - ws2_32.lib psapi.lib setupapi.lib cfgmgr32.lib advapi32.lib Dbghelp.lib -) -#set_target_properties(${STORY_EDITOR_PROJECT} PROPERTIES -#LINK_FLAGS /SUBSYSTEM:CONSOLE -#) + target_link_libraries(${STORY_EDITOR_PROJECT} + OpenGL::GL + SDL2 + ws2_32.lib psapi.lib setupapi.lib cfgmgr32.lib advapi32.lib + ) endif() +# ========================================================================================================================= +# CPACK INSTALLER +# ========================================================================================================================= +install(TARGETS ${STORY_EDITOR_PROJECT} RUNTIME DESTINATION ".") + +# Personnaliser les options d'installation +set(CPACK_PACKAGE_NAME "Open-Story-Editor") +set(CPACK_PACKAGE_DESCRIPTION "Open Story Teller - Node based editor") +set(CPACK_PACKAGE_VENDOR "D8S") +set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") +set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") +set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") + +# install(DIRECTORY "${PROJECT_SOURCE_DIR}/assets/" DESTINATION "assets") +install(DIRECTORY "${PROJECT_SOURCE_DIR}/fonts/" DESTINATION "fonts") + +if(WIN32) + install_files("." FILES "${SDL2_BIN_DIR}/SDL2.dll") +endif() + +# Personnaliser l'icône pour les installateurs Windows +if(WIN32) + set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/story-editor-logo.ico") +endif() + +include(CPack) diff --git a/story-editor/Dockerfile b/story-editor/Dockerfile new file mode 100644 index 0000000..7e519a1 --- /dev/null +++ b/story-editor/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:22.04 +LABEL Description="Build environment" + +ENV HOME /root + +SHELL ["/bin/bash", "-c"] + +RUN mkdir /workspace + +RUN apt-get update && apt-get -y --no-install-recommends install \ + build-essential \ + cmake \ + nsis \ + mingw-w64 \ + git \ + wget diff --git a/story-editor/LICENSE b/story-editor/LICENSE new file mode 100644 index 0000000..652bdbb --- /dev/null +++ b/story-editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Anthony Rabine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/story-editor/README.md b/story-editor/README.md new file mode 100644 index 0000000..83df36a --- /dev/null +++ b/story-editor/README.md @@ -0,0 +1,65 @@ +# Story Editor + +## How to generate a Windows executable and setup executable on Ubuntu + +All the commands listed here are invoked from this `story-editor` root directory. +Make sure to have Docker installed: + +``` +sudo apt install docker.io +``` + +Build the Docker image that contains all the necessary development tools: + +``` +docker build -t cpp-dev . +``` + +Run it: + +``` +docker run -it -v $(pwd)/..:/workspace cpp-dev +``` + + +Make sure to use the POSIX version of MinGW: + +``` +update-alternatives --config x86_64-w64-mingw32-g++ + + + Selection Path Priority Status +------------------------------------------------------------ + 0 /usr/bin/x86_64-w64-mingw32-g++-win32 60 auto mode +* 1 /usr/bin/x86_64-w64-mingw32-g++-posix 30 manual mode + 2 /usr/bin/x86_64-w64-mingw32-g++-win32 60 manual mode + + +update-alternatives --config x86_64-w64-mingw32-gcc + + Selection Path Priority Status +------------------------------------------------------------ + 0 /usr/bin/x86_64-w64-mingw32-gcc-win32 60 auto mode +* 1 /usr/bin/x86_64-w64-mingw32-gcc-posix 30 manual mode + 2 /usr/bin/x86_64-w64-mingw32-gcc-win32 60 manual mode + +``` + + +Cross build, first generate the Makefile: + +``` +git config --global http.sslverify false # avoid error during clone +cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/mingw-w64-x86_64.cmake .. +``` + +Then build the executable and then the installer: + + +``` +make -j4 +make package + +``` + + diff --git a/story-editor/cmake/mingw-w64-x86_64.cmake b/story-editor/cmake/mingw-w64-x86_64.cmake new file mode 100644 index 0000000..b1518a4 --- /dev/null +++ b/story-editor/cmake/mingw-w64-x86_64.cmake @@ -0,0 +1,25 @@ +# Sample toolchain file for building for Windows from an Ubuntu Linux system. +# +# Typical usage: +# *) install cross compiler: `sudo apt-get install mingw-w64` +# *) cd build +# *) cmake -DCMAKE_TOOLCHAIN_FILE=~/mingw-w64-x86_64.cmake .. +# This is free and unencumbered software released into the public domain. + +set(CMAKE_SYSTEM_NAME Windows) +set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) + +# cross compilers to use for C, C++ and Fortran +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) +set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) +set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) + +# target environment on the build host system +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +# modify default behavior of FIND_XXX() commands +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + diff --git a/story-editor/src/console_window.h b/story-editor/src/console_window.h index 48a5de8..1563bde 100644 --- a/story-editor/src/console_window.h +++ b/story-editor/src/console_window.h @@ -1,5 +1,4 @@ -#ifndef CONSOLEWINDOW_H -#define CONSOLEWINDOW_H +#pragma once #include "gui.h" #include @@ -8,7 +7,6 @@ #include "window_base.h" - // Demonstrate creating a simple console window, with scrolling, filtering, completion and history. // For the console example, we are using a more C++ like approach of declaring a class to hold both data and functions. struct ConsoleWindow : public WindowBase @@ -18,17 +16,48 @@ public: ~ConsoleWindow(); // Portable helpers - static int Stricmp(const char* s1, const char* s2) { int d; while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; } return d; } - static int Strnicmp(const char* s1, const char* s2, int n) { int d = 0; while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; n--; } return d; } - static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); } - static void Strtrim(char* s) { char* str_end = s + strlen(s); while (str_end > s && str_end[-1] == ' ') str_end--; *str_end = 0; } + static int Stricmp(const char *s1, const char *s2) + { + int d; + while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1) + { + s1++; + s2++; + } + return d; + } + static int Strnicmp(const char *s1, const char *s2, int n) + { + int d = 0; + while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1) + { + s1++; + s2++; + n--; + } + return d; + } + static char *Strdup(const char *s) + { + IM_ASSERT(s); + size_t len = strlen(s) + 1; + void *buf = malloc(len); + IM_ASSERT(buf); + return (char *)memcpy(buf, (const void *)s, len); + } + static void Strtrim(char *s) + { + char *str_end = s + strlen(s); + while (str_end > s && str_end[-1] == ' ') + str_end--; + *str_end = 0; + } - void ClearLog(); + void ClearLog(); void AddLog(const std::string &text, uint32_t type) { - // FIXME-OPT Entry e{text, type}; std::scoped_lock mutex(mLogMutex); @@ -41,22 +70,18 @@ public: virtual void Draw() override; - private: - - struct Entry { + struct Entry + { std::string text; uint32_t type; }; - std::mutex mLogMutex; - char InputBuf[256]; - std::vector Items; - - ImGuiTextFilter Filter; - bool AutoScroll; - bool ScrollToBottom; + std::mutex mLogMutex; + char InputBuf[256]; + std::vector Items; + ImGuiTextFilter Filter; + bool AutoScroll; + bool ScrollToBottom; }; - -#endif // CONSOLEWINDOW_H diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index 334e2a5..c82d4d5 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -334,7 +334,7 @@ void MainWindow::DrawMainMenuBar() std::string home = pf::getUserHome() + "/"; #ifdef DEBUG - home = "/home/anthony/ostproj/ba869e4b-03d6-4249-9202-85b4cec767a7/"; + home = "/mnt/work/git/open-stories/ba869e4b-03d6-4249-9202-85b4cec767a7/"; #endif ImGuiFileDialog::Instance()->OpenDialog("OpenProjectDlgKey", "Choose File", ".json", home, 1, nullptr, ImGuiFileDialogFlags_Modal); diff --git a/story-editor/src/node_editor_window.cpp b/story-editor/src/node_editor_window.cpp index ab60239..75b879a 100644 --- a/story-editor/src/node_editor_window.cpp +++ b/story-editor/src/node_editor_window.cpp @@ -9,6 +9,13 @@ #include "media_node.h" #include "gui.h" +#include // for std::runtime_error +#define JSON_ASSERT(x) \ +if (!(x)) { \ + throw std::runtime_error("Assertion failed: " #x); \ +} +#include "json.hpp" + NodeEditorWindow::NodeEditorWindow(IStoryManager &proj) : WindowBase("Node editor") @@ -35,6 +42,7 @@ void NodeEditorWindow::Initialize() void NodeEditorWindow::Clear() { m_nodes.clear(); + m_ids.clear(); } @@ -57,6 +65,8 @@ void NodeEditorWindow::LoadNode(const nlohmann::json &nodeJson) n->SetPosition(posJson["x"].get(), posJson["y"].get()); n->FromJson(internalDataJson); + m_ids.insert(restoredNodeId); + m_nodes.push_back(n); } else @@ -71,6 +81,18 @@ void NodeEditorWindow::LoadNode(const nlohmann::json &nodeJson) } +int NodeEditorWindow::GenerateNodeId() +{ + int max = 1; + if (m_ids.size() > 0) + { + auto max = *m_ids.rbegin(); + max++; + m_ids.insert(max); + } + return max; +} + ed::PinId NodeEditorWindow::GetInputPin(unsigned long modelNodeId, int pinIndex) { @@ -409,6 +431,41 @@ void NodeEditorWindow::Draw() ed::EndDelete(); // Wrap up deletion action + auto openPopupPosition = ImGui::GetMousePos(); + ed::Suspend(); + + if (ed::ShowBackgroundContextMenu()) + { + ImGui::OpenPopup("Create New Node"); + } + + if (ImGui::BeginPopup("Create New Node")) + { + auto newNodePostion = openPopupPosition; + Node* node = nullptr; + if (ImGui::MenuItem("Media Node")) + { + auto n = createNode("media-node", "", m_story); + if (n) + { + n->SetType("media-node"); // FIXME: set type in createNode factory? + n->SetId(GenerateNodeId()); + n->SetPosition(newNodePostion.x, newNodePostion.y); + m_nodes.push_back(n); + } + } + + // if (node) + // { + // ed::SetNodePosition(node->ID, newNodePostion); + // } + + ImGui::EndPopup(); + } + + ed::Resume(); + + ed::End(); ed::SetCurrentEditor(nullptr); diff --git a/story-editor/src/node_editor_window.h b/story-editor/src/node_editor_window.h index 9a2bdf2..fa8c51a 100644 --- a/story-editor/src/node_editor_window.h +++ b/story-editor/src/node_editor_window.h @@ -1,18 +1,16 @@ #pragma once -#include #include -#include #include - +#include #include #include "base_node.h" #include "window_base.h" #include "i_story_manager.h" -#include "story_project.h" #include "json.hpp" + namespace ed = ax::NodeEditor; @@ -72,6 +70,7 @@ private: std::list> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes. void ToolbarUI(); + std::set m_ids; void BuildNode(Node* node) { @@ -117,5 +116,6 @@ private: ed::PinId GetInputPin(unsigned long modelNodeId, int pinIndex); ed::PinId GetOutputPin(unsigned long modelNodeId, int pinIndex); uint32_t FindFirstNode() const; + int GenerateNodeId(); }; diff --git a/story-editor/src/platform_folders.cpp b/story-editor/src/platform_folders.cpp index 789603d..8c6ef33 100644 --- a/story-editor/src/platform_folders.cpp +++ b/story-editor/src/platform_folders.cpp @@ -43,29 +43,34 @@ SOFTWARE. * Writing to $HOME as root implies security concerns that a multiplatform program cannot be assumed to handle. * @return The home directory. HOME environment is respected for non-root users if it exists. */ -static std::string getHome() { +static std::string getHome() +{ std::string res; int uid = getuid(); - const char* homeEnv = std::getenv("HOME"); - if ( uid != 0 && homeEnv) { - //We only acknowlegde HOME if not root. + const char *homeEnv = std::getenv("HOME"); + if (uid != 0 && homeEnv) + { + // We only acknowlegde HOME if not root. res = homeEnv; return res; } - struct passwd* pw = nullptr; + struct passwd *pw = nullptr; struct passwd pwd; long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); - if (bufsize < 0) { + if (bufsize < 0) + { bufsize = 16384; } std::vector buffer; buffer.resize(bufsize); int error_code = getpwuid_r(uid, &pwd, buffer.data(), buffer.size(), &pw); - if (error_code) { + if (error_code) + { throw std::runtime_error("Unable to get passwd struct."); } - const char* tempRes = pw->pw_dir; - if (!tempRes) { + const char *tempRes = pw->pw_dir; + if (!tempRes) + { throw std::runtime_error("User has no home directory"); } res = tempRes; @@ -85,62 +90,77 @@ static std::string getHome() { #include // For WideCharToMultiByte #include + +#define _WIN32_WINNT 0x0600 // For SHGetFolderPathW and various CSIDL "magic numbers" #include -namespace pf { -namespace internal { +namespace pf +{ + namespace internal + { -std::string win32_utf16_to_utf8(const wchar_t* wstr) { - std::string res; - // If the 6th parameter is 0 then WideCharToMultiByte returns the number of bytes needed to store the result. - int actualSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); - if (actualSize > 0) { - //If the converted UTF-8 string could not be in the initial buffer. Allocate one that can hold it. - std::vector buffer(actualSize); - actualSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &buffer[0], static_cast(buffer.size()), nullptr, nullptr); - res = buffer.data(); - } - if (actualSize == 0) { - // WideCharToMultiByte return 0 for errors. - throw std::runtime_error("UTF16 to UTF8 failed with error code: " + std::to_string(GetLastError())); - } - return res; -} + std::string win32_utf16_to_utf8(const wchar_t *wstr) + { + std::string res; + // If the 6th parameter is 0 then WideCharToMultiByte returns the number of bytes needed to store the result. + int actualSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr); + if (actualSize > 0) + { + // If the converted UTF-8 string could not be in the initial buffer. Allocate one that can hold it. + std::vector buffer(actualSize); + actualSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &buffer[0], static_cast(buffer.size()), nullptr, nullptr); + res = buffer.data(); + } + if (actualSize == 0) + { + // WideCharToMultiByte return 0 for errors. + throw std::runtime_error("UTF16 to UTF8 failed with error code: " + std::to_string(GetLastError())); + } + return res; + } -} // namesapce internal -} // namespace pf + } // namesapce internal +} // namespace pf -class FreeCoTaskMemory { +class FreeCoTaskMemory +{ LPWSTR pointer = NULL; + public: - explicit FreeCoTaskMemory(LPWSTR pointer) : pointer(pointer) {}; - ~FreeCoTaskMemory() { + explicit FreeCoTaskMemory(LPWSTR pointer) : pointer(pointer){}; + ~FreeCoTaskMemory() + { CoTaskMemFree(pointer); } }; -static std::string GetKnownWindowsFolder(REFKNOWNFOLDERID folderId, const char* errorMsg) { +static std::string GetKnownWindowsFolder(REFKNOWNFOLDERID folderId, const char *errorMsg) +{ LPWSTR wszPath = NULL; HRESULT hr; hr = SHGetKnownFolderPath(folderId, KF_FLAG_CREATE, NULL, &wszPath); FreeCoTaskMemory scopeBoundMemory(wszPath); - if (!SUCCEEDED(hr)) { + if (!SUCCEEDED(hr)) + { throw std::runtime_error(errorMsg); } - return pf::internal::win32_utf16_to_utf8(wszPath); + return pf::internal::win32_utf16_to_utf8(wszPath); } -static std::string GetAppData() { +static std::string GetAppData() +{ return GetKnownWindowsFolder(FOLDERID_RoamingAppData, "RoamingAppData could not be found"); } -static std::string GetAppDataCommon() { +static std::string GetAppDataCommon() +{ return GetKnownWindowsFolder(FOLDERID_ProgramData, "ProgramData could not be found"); } -static std::string GetAppDataLocal() { +static std::string GetAppDataLocal() +{ return GetKnownWindowsFolder(FOLDERID_LocalAppData, "LocalAppData could not be found"); } #elif defined(__APPLE__) @@ -151,22 +171,24 @@ static std::string GetAppDataLocal() { // For strlen and strtok #include #include -//Typically Linux. For easy reading the comments will just say Linux but should work with most *nixes +// Typically Linux. For easy reading the comments will just say Linux but should work with most *nixes -static void throwOnRelative(const char* envName, const char* envValue) { - if (envValue[0] != '/') { +static void throwOnRelative(const char *envName, const char *envValue) +{ + if (envValue[0] != '/') + { char buffer[200]; std::snprintf(buffer, sizeof(buffer), "Environment \"%s\" does not start with an '/'. XDG specifies that the value must be absolute. The current value is: \"%s\"", envName, envValue); throw std::runtime_error(buffer); } } - - -static std::string getLinuxFolderDefault(const char* envName, const char* defaultRelativePath) { +static std::string getLinuxFolderDefault(const char *envName, const char *defaultRelativePath) +{ std::string res; - const char* tempRes = std::getenv(envName); - if (tempRes) { + const char *tempRes = std::getenv(envName); + if (tempRes) + { throwOnRelative(envName, tempRes); res = tempRes; return res; @@ -175,294 +197,336 @@ static std::string getLinuxFolderDefault(const char* envName, const char* defaul return res; } -static void appendExtraFolders(const char* envName, const char* defaultValue, std::vector& folders) { - const char* envValue = std::getenv(envName); - if (!envValue) { +static void appendExtraFolders(const char *envName, const char *defaultValue, std::vector &folders) +{ + const char *envValue = std::getenv(envName); + if (!envValue) + { envValue = defaultValue; } - pf::internal::appendExtraFoldersTokenizer(envName, envValue, folders); + pf::internal::appendExtraFoldersTokenizer(envName, envValue, folders); } #endif - -namespace pf { - -#if !defined(_WIN32) && !defined(__APPLE__) -namespace internal { -void appendExtraFoldersTokenizer(const char* envName, const char* envValue, std::vector& folders) { - std::stringstream ss(envValue); - std::string value; - while (std::getline(ss, value, ':')) { - if (value[0] == '/') { - folders.push_back(value); - } - else { - //Unless the system is wrongly configured this should never happen... But of course some systems will be incorectly configured. - //The XDG documentation indicates that the folder should be ignored but that the program should continue. - std::cerr << "Skipping path \"" << value << "\" in \"" << envName << "\" because it does not start with a \"/\"\n"; - } - } -} -} -#endif - -std::string getDataHome() { -#ifdef _WIN32 - return GetAppData(); -#elif defined(__APPLE__) - return getHome()+"/Library/Application Support"; -#else - return getLinuxFolderDefault("XDG_DATA_HOME", ".local/share"); -#endif -} - -std::string getConfigHome() { -#ifdef _WIN32 - return GetAppData(); -#elif defined(__APPLE__) - return getHome()+"/Library/Application Support"; -#else - return getLinuxFolderDefault("XDG_CONFIG_HOME", ".config"); -#endif -} - -std::string getCacheDir() { -#ifdef _WIN32 - return GetAppDataLocal(); -#elif defined(__APPLE__) - return getHome()+"/Library/Caches"; -#else - return getLinuxFolderDefault("XDG_CACHE_HOME", ".cache"); -#endif -} - -std::string getUserHome() +namespace pf { -#ifdef _WIN32 - return std::getenv("USERPROFILE"); -#else - return getHome(); + +#if !defined(_WIN32) && !defined(__APPLE__) + namespace internal + { + void appendExtraFoldersTokenizer(const char *envName, const char *envValue, std::vector &folders) + { + std::stringstream ss(envValue); + std::string value; + while (std::getline(ss, value, ':')) + { + if (value[0] == '/') + { + folders.push_back(value); + } + else + { + // Unless the system is wrongly configured this should never happen... But of course some systems will be incorectly configured. + // The XDG documentation indicates that the folder should be ignored but that the program should continue. + std::cerr << "Skipping path \"" << value << "\" in \"" << envName << "\" because it does not start with a \"/\"\n"; + } + } + } + } #endif -} - -std::string getStateDir() { + std::string getDataHome() + { #ifdef _WIN32 - return GetAppDataLocal(); + return GetAppData(); #elif defined(__APPLE__) - return getHome()+"/Library/Application Support"; + return getHome() + "/Library/Application Support"; #else - return getLinuxFolderDefault("XDG_STATE_HOME", ".local/state"); + return getLinuxFolderDefault("XDG_DATA_HOME", ".local/share"); #endif -} + } -void appendAdditionalDataDirectories(std::vector& homes) { + std::string getConfigHome() + { #ifdef _WIN32 - homes.push_back(GetAppDataCommon()); -#elif !defined(__APPLE__) - appendExtraFolders("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/", homes); + return GetAppData(); +#elif defined(__APPLE__) + return getHome() + "/Library/Application Support"; +#else + return getLinuxFolderDefault("XDG_CONFIG_HOME", ".config"); #endif -} + } -void appendAdditionalConfigDirectories(std::vector& homes) { + std::string getCacheDir() + { #ifdef _WIN32 - homes.push_back(GetAppDataCommon()); -#elif !defined(__APPLE__) - appendExtraFolders("XDG_CONFIG_DIRS", "/etc/xdg", homes); + return GetAppDataLocal(); +#elif defined(__APPLE__) + return getHome() + "/Library/Caches"; +#else + return getLinuxFolderDefault("XDG_CACHE_HOME", ".cache"); #endif -} + } + + std::string getUserHome() + { +#ifdef _WIN32 + return std::getenv("USERPROFILE"); +#else + return getHome(); +#endif + } + + std::string getStateDir() + { +#ifdef _WIN32 + return GetAppDataLocal(); +#elif defined(__APPLE__) + return getHome() + "/Library/Application Support"; +#else + return getLinuxFolderDefault("XDG_STATE_HOME", ".local/state"); +#endif + } + + void appendAdditionalDataDirectories(std::vector &homes) + { +#ifdef _WIN32 + homes.push_back(GetAppDataCommon()); +#elif !defined(__APPLE__) + appendExtraFolders("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/", homes); +#endif + } + + void appendAdditionalConfigDirectories(std::vector &homes) + { +#ifdef _WIN32 + homes.push_back(GetAppDataCommon()); +#elif !defined(__APPLE__) + appendExtraFolders("XDG_CONFIG_DIRS", "/etc/xdg", homes); +#endif + } #if !defined(_WIN32) && !defined(__APPLE__) -struct PlatformFolders::PlatformFoldersData { - std::map folders; -}; + struct PlatformFolders::PlatformFoldersData + { + std::map folders; + }; -static void PlatformFoldersAddFromFile(const std::string& filename, std::map& folders) { - std::ifstream infile(filename.c_str()); - std::string line; - while (std::getline(infile, line)) { - if (line.length() == 0 || line.at(0) == '#' || line.substr(0, 4) != "XDG_" || line.find("_DIR") == std::string::npos) { - continue; - } - try { - std::size_t splitPos = line.find('='); - std::string key = line.substr(0, splitPos); - std::size_t valueStart = line.find('"', splitPos); - std::size_t valueEnd = line.find('"', valueStart+1); - std::string value = line.substr(valueStart+1, valueEnd - valueStart - 1); - folders[key] = value; - } - catch (std::exception& e) { - std::cerr << "WARNING: Failed to process \"" << line << "\" from \"" << filename << "\". Error: "<< e.what() << "\n"; - continue; + static void PlatformFoldersAddFromFile(const std::string &filename, std::map &folders) + { + std::ifstream infile(filename.c_str()); + std::string line; + while (std::getline(infile, line)) + { + if (line.length() == 0 || line.at(0) == '#' || line.substr(0, 4) != "XDG_" || line.find("_DIR") == std::string::npos) + { + continue; + } + try + { + std::size_t splitPos = line.find('='); + std::string key = line.substr(0, splitPos); + std::size_t valueStart = line.find('"', splitPos); + std::size_t valueEnd = line.find('"', valueStart + 1); + std::string value = line.substr(valueStart + 1, valueEnd - valueStart - 1); + folders[key] = value; + } + catch (std::exception &e) + { + std::cerr << "WARNING: Failed to process \"" << line << "\" from \"" << filename << "\". Error: " << e.what() << "\n"; + continue; + } } } -} -static void PlatformFoldersFillData(std::map& folders) { - folders["XDG_DOCUMENTS_DIR"] = "$HOME/Documents"; - folders["XDG_DESKTOP_DIR"] = "$HOME/Desktop"; - folders["XDG_DOWNLOAD_DIR"] = "$HOME/Downloads"; - folders["XDG_MUSIC_DIR"] = "$HOME/Music"; - folders["XDG_PICTURES_DIR"] = "$HOME/Pictures"; - folders["XDG_PUBLICSHARE_DIR"] = "$HOME/Public"; - folders["XDG_TEMPLATES_DIR"] = "$HOME/.Templates"; - folders["XDG_VIDEOS_DIR"] = "$HOME/Videos"; - PlatformFoldersAddFromFile( getConfigHome()+"/user-dirs.dirs", folders); - for (std::map::iterator itr = folders.begin() ; itr != folders.end() ; ++itr ) { - std::string& value = itr->second; - if (value.compare(0, 5, "$HOME") == 0) { - value = getHome() + value.substr(5, std::string::npos); + static void PlatformFoldersFillData(std::map &folders) + { + folders["XDG_DOCUMENTS_DIR"] = "$HOME/Documents"; + folders["XDG_DESKTOP_DIR"] = "$HOME/Desktop"; + folders["XDG_DOWNLOAD_DIR"] = "$HOME/Downloads"; + folders["XDG_MUSIC_DIR"] = "$HOME/Music"; + folders["XDG_PICTURES_DIR"] = "$HOME/Pictures"; + folders["XDG_PUBLICSHARE_DIR"] = "$HOME/Public"; + folders["XDG_TEMPLATES_DIR"] = "$HOME/.Templates"; + folders["XDG_VIDEOS_DIR"] = "$HOME/Videos"; + PlatformFoldersAddFromFile(getConfigHome() + "/user-dirs.dirs", folders); + for (std::map::iterator itr = folders.begin(); itr != folders.end(); ++itr) + { + std::string &value = itr->second; + if (value.compare(0, 5, "$HOME") == 0) + { + value = getHome() + value.substr(5, std::string::npos); + } } } -} #endif -PlatformFolders::PlatformFolders() { + PlatformFolders::PlatformFolders() + { #if !defined(_WIN32) && !defined(__APPLE__) - this->data = new PlatformFolders::PlatformFoldersData(); - try { - PlatformFoldersFillData(data->folders); + this->data = new PlatformFolders::PlatformFoldersData(); + try + { + PlatformFoldersFillData(data->folders); + } + catch (...) + { + delete this->data; + throw; + } +#endif } - catch (...) { + + PlatformFolders::~PlatformFolders() + { +#if !defined(_WIN32) && !defined(__APPLE__) delete this->data; - throw; +#endif } -#endif -} -PlatformFolders::~PlatformFolders() { -#if !defined(_WIN32) && !defined(__APPLE__) - delete this->data; -#endif -} - -std::string PlatformFolders::getDocumentsFolder() const { + std::string PlatformFolders::getDocumentsFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Documents, "Failed to find My Documents folder"); + return GetKnownWindowsFolder(FOLDERID_Documents, "Failed to find My Documents folder"); #elif defined(__APPLE__) - return getHome()+"/Documents"; + return getHome() + "/Documents"; #else - return data->folders["XDG_DOCUMENTS_DIR"]; + return data->folders["XDG_DOCUMENTS_DIR"]; #endif -} + } -std::string PlatformFolders::getDesktopFolder() const { + std::string PlatformFolders::getDesktopFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Desktop, "Failed to find Desktop folder"); + return GetKnownWindowsFolder(FOLDERID_Desktop, "Failed to find Desktop folder"); #elif defined(__APPLE__) - return getHome()+"/Desktop"; + return getHome() + "/Desktop"; #else - return data->folders["XDG_DESKTOP_DIR"]; + return data->folders["XDG_DESKTOP_DIR"]; #endif -} + } -std::string PlatformFolders::getPicturesFolder() const { + std::string PlatformFolders::getPicturesFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Pictures, "Failed to find My Pictures folder"); + return GetKnownWindowsFolder(FOLDERID_Pictures, "Failed to find My Pictures folder"); #elif defined(__APPLE__) - return getHome()+"/Pictures"; + return getHome() + "/Pictures"; #else - return data->folders["XDG_PICTURES_DIR"]; + return data->folders["XDG_PICTURES_DIR"]; #endif -} + } -std::string PlatformFolders::getPublicFolder() const { + std::string PlatformFolders::getPublicFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Public, "Failed to find the Public folder"); + return GetKnownWindowsFolder(FOLDERID_Public, "Failed to find the Public folder"); #elif defined(__APPLE__) - return getHome()+"/Public"; + return getHome() + "/Public"; #else - return data->folders["XDG_PUBLICSHARE_DIR"]; + return data->folders["XDG_PUBLICSHARE_DIR"]; #endif -} + } -std::string PlatformFolders::getDownloadFolder1() const { + std::string PlatformFolders::getDownloadFolder1() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Downloads, "Failed to find My Downloads folder"); + return GetKnownWindowsFolder(FOLDERID_Downloads, "Failed to find My Downloads folder"); #elif defined(__APPLE__) - return getHome()+"/Downloads"; + return getHome() + "/Downloads"; #else - return data->folders["XDG_DOWNLOAD_DIR"]; + return data->folders["XDG_DOWNLOAD_DIR"]; #endif -} + } -std::string PlatformFolders::getMusicFolder() const { + std::string PlatformFolders::getMusicFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Music, "Failed to find My Music folder"); + return GetKnownWindowsFolder(FOLDERID_Music, "Failed to find My Music folder"); #elif defined(__APPLE__) - return getHome()+"/Music"; + return getHome() + "/Music"; #else - return data->folders["XDG_MUSIC_DIR"]; + return data->folders["XDG_MUSIC_DIR"]; #endif -} + } -std::string PlatformFolders::getVideoFolder() const { + std::string PlatformFolders::getVideoFolder() const + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_Videos, "Failed to find My Video folder"); + return GetKnownWindowsFolder(FOLDERID_Videos, "Failed to find My Video folder"); #elif defined(__APPLE__) - return getHome()+"/Movies"; + return getHome() + "/Movies"; #else - return data->folders["XDG_VIDEOS_DIR"]; + return data->folders["XDG_VIDEOS_DIR"]; #endif -} + } -std::string PlatformFolders::getSaveGamesFolder1() const { + std::string PlatformFolders::getSaveGamesFolder1() const + { #ifdef _WIN32 - //A dedicated Save Games folder was not introduced until Vista. For XP and older save games are most often saved in a normal folder named "My Games". - //Data that should not be user accessible should be placed under GetDataHome() instead - return GetKnownWindowsFolder(FOLDERID_Documents, "Failed to find My Documents folder")+"\\My Games"; + // A dedicated Save Games folder was not introduced until Vista. For XP and older save games are most often saved in a normal folder named "My Games". + // Data that should not be user accessible should be placed under GetDataHome() instead + return GetKnownWindowsFolder(FOLDERID_Documents, "Failed to find My Documents folder") + "\\My Games"; #elif defined(__APPLE__) - return getHome()+"/Library/Application Support"; + return getHome() + "/Library/Application Support"; #else - return getDataHome(); + return getDataHome(); #endif -} + } -std::string getDesktopFolder() { - return PlatformFolders().getDesktopFolder(); -} + std::string getDesktopFolder() + { + return PlatformFolders().getDesktopFolder(); + } -std::string getDocumentsFolder() { - return PlatformFolders().getDocumentsFolder(); -} + std::string getDocumentsFolder() + { + return PlatformFolders().getDocumentsFolder(); + } -std::string getDownloadFolder() { - return PlatformFolders().getDownloadFolder1(); -} + std::string getDownloadFolder() + { + return PlatformFolders().getDownloadFolder1(); + } -std::string getDownloadFolder1() { - return getDownloadFolder(); -} + std::string getDownloadFolder1() + { + return getDownloadFolder(); + } -std::string getPicturesFolder() { - return PlatformFolders().getPicturesFolder(); -} + std::string getPicturesFolder() + { + return PlatformFolders().getPicturesFolder(); + } -std::string getPublicFolder() { - return PlatformFolders().getPublicFolder(); -} + std::string getPublicFolder() + { + return PlatformFolders().getPublicFolder(); + } -std::string getMusicFolder() { - return PlatformFolders().getMusicFolder(); -} + std::string getMusicFolder() + { + return PlatformFolders().getMusicFolder(); + } -std::string getVideoFolder() { - return PlatformFolders().getVideoFolder(); -} + std::string getVideoFolder() + { + return PlatformFolders().getVideoFolder(); + } -std::string getSaveGamesFolder1() { - return PlatformFolders().getSaveGamesFolder1(); -} + std::string getSaveGamesFolder1() + { + return PlatformFolders().getSaveGamesFolder1(); + } -std::string getSaveGamesFolder2() { + std::string getSaveGamesFolder2() + { #ifdef _WIN32 - return GetKnownWindowsFolder(FOLDERID_SavedGames, "Failed to find Saved Games folder"); + return GetKnownWindowsFolder(FOLDERID_SavedGames, "Failed to find Saved Games folder"); #else - return PlatformFolders().getSaveGamesFolder1(); + return PlatformFolders().getSaveGamesFolder1(); #endif -} + } - -} //namespace pf +} // namespace pf