diff --git a/.gitignore b/.gitignore index f22e2f5..fd46092 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ build -.vscode \ No newline at end of file + +.vscode +*.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index d284534..efcebde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ elseif(EMSCRIPTEN) target_sources(raven PUBLIC main_emscripten.cpp) set(LIBS ${CMAKE_DL_LIBS} SDL2) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s USE_SDL=2 -s DISABLE_EXCEPTION_CATCHING=1") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s WASM=1 -s NO_EXIT_RUNTIME=0 -s ASSERTIONS=1 -s NO_FILESYSTEM=1 -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 -Wl,--shared-memory,--no-check-features --shell-file ${CMAKE_CURRENT_LIST_DIR}/shell_minimal.html") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s WASM=1 -s NO_EXIT_RUNTIME=0 -s ASSERTIONS=1 -s FETCH -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_RUNTIME_METHODS=ccall,cwrap -Wl,--shared-memory,--no-check-features --shell-file ${CMAKE_CURRENT_LIST_DIR}/shell_minimal.html") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DIMGUI_DISABLE_FILE_FUNCTIONS") set_target_properties(raven PROPERTIES SUFFIX .html) target_link_libraries(raven PUBLIC ${LIBS}) diff --git a/README.md b/README.md index 64de337..53ef4ed 100644 --- a/README.md +++ b/README.md @@ -26,27 +26,44 @@ Linux (Ubuntu, or similar): - A recent version of CMake - You can get this via `sudo snap install cmake` or by downloading from https://cmake.org/download/ +__Note__: Before building, please ensure that you clone this project with the `--recursive` flag. +This will also clone and initialize all of the submodules that this project depends on. + ## Building (macOS, Windows, Linux) - % mkdir build - % cd build - % cmake .. - % cmake --build . -j - % ./raven ../example.otio +Spin up your favourite terminal and follow these steps: + +```shell + git submodule update --init --recursive + mkdir build + cd build + cmake .. + cmake --build . -j + ./raven ../example.otio +``` ## Building (WASM via Emscripten) You will need to install the [Emscripten toolchain](https://emscripten.org) first. - % mkdir build-web - % cd build-web - % emcmake cmake .. - % cmake --build . - % emrun ./raven.html +```shell + git submodule update --init --recursive + mkdir build-web + cd build-web + emcmake cmake .. + cmake --build . + emrun ./raven.html +``` See also: `serve.py` as an alternative to `emrun`, and as a reference for which HTTP headers are needed to host the WASM build. +You can load a file into WASM Raven a few ways: +- Add a JSON string to Module.otioLoadString in the HTML file +- Add a URL to Module.otioLoadURL in the HTML file +- Call Module.LoadString(otio_data) at runtime +- Call Module.LoadURL(otio_url) at runtime + Note: The WASM build of raven is missing some features - see the Help Wanted section below. ## Troubleshooting diff --git a/app.cpp b/app.cpp index 5e2dad8..e26e4ee 100644 --- a/app.cpp +++ b/app.cpp @@ -22,6 +22,7 @@ void DrawMenu(); void DrawToolbar(ImVec2 buttonSize); +void DrawDroppedFilesPrompt(); #define DEFINE_APP_THEME_NAMES #include "app.h" @@ -37,6 +38,9 @@ AppTheme appTheme; ImFont* gFont = nullptr; +// Variable to store dropped file to load +std::string prompt_dropped_file = ""; + // Log a message to the terminal void Log(const char* format, ...) { va_list args; @@ -241,6 +245,32 @@ void LoadTimeline(otio::Timeline* timeline) { SelectObject(timeline); } +void LoadString(std::string json) { + auto start = std::chrono::high_resolution_clock::now(); + + otio::ErrorStatus error_status; + auto timeline = dynamic_cast( + otio::Timeline::from_json_string(json, &error_status)); + if (!timeline || otio::is_error(error_status)) { + Message( + "Error loading JSON: %s", + otio_error_string(error_status).c_str()); + return; + } + + LoadTimeline(timeline); + + appState.file_path = timeline->name().c_str(); + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = (end - start); + double elapsed_seconds = elapsed.count(); + Message( + "Loaded \"%s\" in %.3f seconds", + timeline->name().c_str(), + elapsed_seconds); +} + void LoadFile(std::string path) { auto start = std::chrono::high_resolution_clock::now(); @@ -315,6 +345,43 @@ void MainInit(int argc, char** argv, int initial_width, int initial_height) { void MainCleanup() { } +// Validate that a file has the .otio extension +bool is_valid_file(const std::string& filepath) { + size_t last_dot = filepath.find_last_of('.'); + + // If no dot is found, it's not a valid file + if (last_dot == std::string::npos) { + return false; + } + + // Get and check the extension + std::string extension = filepath.substr(last_dot + 1); + return extension == "otio"; +} + +// Accept and open a file path +void FileDropCallback(int count, const char** filepaths) { + if (count > 1){ + Message("Cannot open multiple files."); + return; + } + + else if (count == 0) { + return; + } + + std::string file_path = filepaths[0]; + + if (!is_valid_file(file_path)){ + Message("Invalid file: %s", file_path.c_str()); + return; + } + + // Loading is done in DrawDroppedFilesPrompt() + prompt_dropped_file = file_path; + +} + // Make a button using the fancy icon font bool IconButton(const char* label, const ImVec2 size = ImVec2(0, 0)) { bool result = ImGui::Button(label, size); @@ -383,6 +450,7 @@ void MainGui() { exit(0); } + DrawDroppedFilesPrompt(); DrawMenu(); // ImGui::SameLine(ImGui::GetContentRegionAvailWidth() - button_size.x + @@ -785,6 +853,32 @@ void DrawToolbar(ImVec2 button_size) { #endif } +// Prompt the user to confirm file loading +void DrawDroppedFilesPrompt() { + if (prompt_dropped_file == "") { + return; + } + + ImGui::OpenPopup("Open File?"); + // Modal window for confirmation + if (ImGui::BeginPopupModal("Open File?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Open file \n%s?", prompt_dropped_file.c_str()); + + if (ImGui::Button("Yes")) { + LoadFile(prompt_dropped_file); + prompt_dropped_file = ""; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("No")) { + Message(""); // Reset last message + prompt_dropped_file = ""; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + void SelectObject( otio::SerializableObject* object, otio::SerializableObject* context) { @@ -829,7 +923,27 @@ void DetectPlayheadLimits() { void FitZoomWholeTimeline() { appState.scale = appState.timeline_width / appState.timeline->duration().to_seconds(); } - +// GUI utility to add dynamic height to GUI elements + +float CalculateDynamicHeight() { + // Get the current font size + float fontSize = ImGui::GetFontSize(); + // Get the vertical spacing from the ImGui style + float verticalSpacing = ImGui::GetStyle().ItemSpacing.y; + + // Determine how many elements are selected + int visibleElementCount = 0; + if (appState.display_timecode) visibleElementCount++; + if (appState.display_frames) visibleElementCount++; + if (appState.display_seconds) visibleElementCount++; + if (appState.display_rate) visibleElementCount++; + + // Set height based on selected elements + // Use fontSize as base height and verticalSpacing for additional height + float calculatedHeight = fontSize + (visibleElementCount - 1) * (fontSize + verticalSpacing); + // Return the maximum of calculatedHeight and the minimum height (10) + return std::max(calculatedHeight, 10.0f); +} std::string FormattedStringFromTime(otio::RationalTime time, bool allow_rate) { std::string result; if (appState.display_timecode) { diff --git a/app.h b/app.h index 30557f7..3c60bd0 100644 --- a/app.h +++ b/app.h @@ -131,6 +131,8 @@ void Log(const char* format, ...); void Message(const char* format, ...); std::string Format(const char* format, ...); +void LoadString(std::string json); + std::string otio_error_string(otio::ErrorStatus const& error_status); void SelectObject( @@ -140,6 +142,7 @@ void SeekPlayhead(double seconds); void SnapPlayhead(); void DetectPlayheadLimits(); void FitZoomWholeTimeline(); +float CalculateDynamicHeight(); std::string FormattedStringFromTime(otio::RationalTime time, bool allow_rate = true); std::string TimecodeStringFromTime(otio::RationalTime); std::string FramesStringFromTime(otio::RationalTime); diff --git a/libs/glfw b/libs/glfw index dc557ec..b35641f 160000 --- a/libs/glfw +++ b/libs/glfw @@ -1 +1 @@ -Subproject commit dc557ecf38a42b0b93898a7aef69f6dc48bf0e57 +Subproject commit b35641f4a3c62aa86a0b3c983d163bc0fe36026d diff --git a/libs/opentimelineio b/libs/opentimelineio index 4b3b673..5184c36 160000 --- a/libs/opentimelineio +++ b/libs/opentimelineio @@ -1 +1 @@ -Subproject commit 4b3b6736c18bd28d2936acdede779424a2e73f45 +Subproject commit 5184c36403e6ab94caae20acf6ced709f0dc9c0e diff --git a/main.h b/main.h index 269598b..b57a742 100644 --- a/main.h +++ b/main.h @@ -2,4 +2,11 @@ void MainInit(int argc, char** argv, int initial_width, int initial_height); void MainGui(); void MainCleanup(); +void FileDropCallback(int count, const char** paths); +#ifdef EMSCRIPTEN +extern "C" { +void js_LoadUrl(char* url); +void js_LoadString(char* json); +} +#endif diff --git a/main_emscripten.cpp b/main_emscripten.cpp index 589b838..f4c5760 100644 --- a/main_emscripten.cpp +++ b/main_emscripten.cpp @@ -8,13 +8,16 @@ // See https://github.com/ocornut/imgui/pull/2492 as an example on how to do just that. #include "imgui.h" -#include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" -#include -#include +#include "imgui_impl_sdl2.h" #include #include +#include +#include +#include +#include +#include "app.h" #include "main.h" // Emscripten requires to have full control over the main loop. We're going to store our SDL book-keeping variables globally. @@ -149,3 +152,42 @@ static void main_loop(void* arg) ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); SDL_GL_SwapWindow(g_Window); } + +void LoadUrlSuccess(emscripten_fetch_t* fetch) { + printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url); + + std::string otio_string = std::string(fetch->data, fetch->numBytes); + emscripten_fetch_close(fetch); + + LoadString(otio_string); + + appState.file_path = fetch->url; +} + +void LoadUrlFailure(emscripten_fetch_t* fetch) { + printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status); + emscripten_fetch_close(fetch); +} + +void LoadUrl(std::string url) { + printf("Downloading %s...\n", url.c_str()); + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + strcpy(attr.requestMethod, "GET"); + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; + // This is async, so we can provide callbacks to handle the result + attr.onsuccess = LoadUrlSuccess; + attr.onerror = LoadUrlFailure; + emscripten_fetch(&attr, url.c_str()); +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE +void js_LoadUrl(char* url) { + LoadUrl(std::string(url)); +} +EMSCRIPTEN_KEEPALIVE +void js_LoadString(char* json) { + LoadString(std::string(json)); +} +} \ No newline at end of file diff --git a/main_macos.mm b/main_macos.mm index 8af61c1..7536b9f 100644 --- a/main_macos.mm +++ b/main_macos.mm @@ -18,6 +18,11 @@ #import #import +// Accept and open a file path +void file_drop_callback(GLFWwindow* window, int count, const char** paths) { + FileDropCallback(count, paths); +} + static void glfw_error_callback(int error, const char* description) { fprintf(stderr, "Glfw Error %d: %s\n", error, description); @@ -100,6 +105,9 @@ int main(int argc, char** argv) // Our state float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; + // Set the drop callback + glfwSetDropCallback(window, file_drop_callback); + // Main loop while (!glfwWindowShouldClose(window)) { diff --git a/shell_minimal.html b/shell_minimal.html index f2bd0c7..d45b812 100644 --- a/shell_minimal.html +++ b/shell_minimal.html @@ -30,9 +30,42 @@