├── .clang-format ├── .github ├── dependabot.yml └── workflows │ └── build.yaml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── app.cpp ├── app.h ├── colors.cpp ├── colors.h ├── demo.gif ├── editing.cpp ├── editing.h ├── example.otio ├── examples ├── ElFuente_4096x2160_60fps_10bit_420_remix1.otio ├── Meridian_UHD4k5994_HDR_P3PQ.otio └── timecode_rates.otio ├── fonts ├── LICENSE.txt ├── embedded_font.inc ├── mononoki-Regular Nerd Font Complete.ttf └── readme.md ├── inspector.cpp ├── inspector.h ├── libs ├── CMakeLists.txt └── gl3w │ └── GL │ ├── gl3w.c │ ├── gl3w.h │ └── glcorearb.h ├── main.h ├── main_emscripten.cpp ├── main_glfw.cpp ├── main_macos.mm ├── main_win32.cpp ├── screenshot.png ├── serve.py ├── shell_minimal.html ├── theme.inc ├── timeline.cpp ├── timeline.h ├── widgets.cpp └── widgets.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | IndentWidth: 4 3 | BasedOnStyle: Webkit 4 | TabWidth: 4 5 | BreakBeforeBraces: Attach 6 | AllowShortIfStatementsOnASingleLine: true 7 | AllowShortBlocksOnASingleLine: Always 8 | AllowShortLoopsOnASingleLine: true 9 | ColumnLimit: 0 10 | # PackConstructorInitializers: CurrentLine 11 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 12 | AllowAllConstructorInitializersOnNextLine: false 13 | 14 | --- 15 | 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | 13 | - package-ecosystem: "gitsubmodule" 14 | directory: "/" 15 | schedule: 16 | day: "wednesday" 17 | interval: "monthly" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | Linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 1 16 | submodules: recursive 17 | 18 | - name: Install Dependencies 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -y libglfw3-dev libgtk-3-dev 22 | 23 | - name: Build 24 | run: | 25 | mkdir build 26 | cd build 27 | cmake .. 28 | cmake --build . --parallel 4 29 | 30 | - name: Check Product 31 | run: | 32 | cd build 33 | ls -l 34 | file raven 35 | 36 | - name: Package Product 37 | run: | 38 | cd build 39 | chmod +x raven 40 | zip -r raven-linux.zip raven 41 | 42 | - name: Archive Product 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: raven-linux.zip 46 | path: build/raven-linux.zip 47 | 48 | MacOS: 49 | runs-on: macOS-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 1 54 | submodules: recursive 55 | 56 | - name: Build 57 | run: | 58 | mkdir build 59 | cd build 60 | cmake .. 61 | cmake --build . --parallel 4 62 | 63 | - name: Check Product 64 | run: | 65 | cd build 66 | ls -l 67 | file raven 68 | 69 | - name: Package Product 70 | run: | 71 | cd build 72 | chmod +x raven 73 | zip -r raven-mac.zip raven 74 | 75 | - name: Archive Product 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: raven-mac.zip 79 | path: build/raven-mac.zip 80 | 81 | Emscripten: 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v4 85 | with: 86 | fetch-depth: 1 87 | submodules: recursive 88 | 89 | - name: Install Dependencies 90 | run: | 91 | wget -q https://github.com/emscripten-core/emsdk/archive/master.tar.gz 92 | tar -xvf master.tar.gz 93 | emsdk-master/emsdk update 94 | emsdk-master/emsdk install latest 95 | emsdk-master/emsdk activate latest 96 | 97 | - name: Build 98 | run: | 99 | source emsdk-master/emsdk_env.sh 100 | mkdir build-web 101 | cd build-web 102 | emcmake cmake .. 103 | cmake --build . --parallel 4 104 | 105 | - name: Check Product 106 | run: | 107 | ls -l build-web 108 | 109 | - name: Package Product 110 | run: | 111 | cd build-web 112 | zip -r raven-web.zip raven.html raven.wasm raven.js raven.worker.js 113 | 114 | - name: Archive Product 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: raven-web.zip 118 | path: build-web/raven-web.zip 119 | Windows: 120 | runs-on: windows-latest 121 | steps: 122 | - uses: actions/checkout@v4 123 | with: 124 | fetch-depth: 1 125 | submodules: recursive 126 | 127 | - name: Build 128 | run: | 129 | mkdir build_win64 130 | cd build_win64 131 | cmake .. 132 | cmake --build . --config Debug --parallel 4 133 | cmake --build . --config Release --parallel 4 134 | 135 | - name: Check Product 136 | shell: bash 137 | run: | 138 | cd build_win64 139 | pwd 140 | ls -l 141 | ls -l ./Debug/ 142 | ls -l ./Release/ 143 | file ./Debug/raven.exe 144 | file ./Release/raven.exe 145 | 146 | - name: Archive Product 147 | uses: actions/upload-artifact@v4 148 | with: 149 | name: raven-windows-x64.zip 150 | path: | 151 | build_win64/Release/* 152 | build_win64/Debug/* 153 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | 3 | 4 | build 5 | *.DS_Store 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/opentimelineio"] 2 | path = libs/opentimelineio 3 | url = https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git 4 | [submodule "libs/nativefiledialog"] 5 | path = libs/nativefiledialog 6 | url = https://github.com/mlabbe/nativefiledialog.git 7 | [submodule "libs/implot"] 8 | path = libs/implot 9 | url = https://github.com/epezent/implot 10 | [submodule "libs/glfw"] 11 | path = libs/glfw 12 | url = https://github.com/glfw/glfw 13 | [submodule "libs/imgui"] 14 | path = libs/imgui 15 | url = https://github.com/ocornut/imgui.git 16 | [submodule "libs/ImGuiColorTextEdit"] 17 | path = libs/ImGuiColorTextEdit 18 | url = https://github.com/jminor/ImGuiColorTextEdit.git 19 | [submodule "libs/minizip-ng"] 20 | path = libs/minizip-ng 21 | url = https://github.com/zlib-ng/minizip-ng.git 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | cmake_policy(SET CMP0076 NEW) 3 | 4 | project(raven VERSION 1.0) 5 | 6 | set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") 7 | 8 | add_executable(raven) 9 | set_property(TARGET raven PROPERTY CXX_STANDARD 17) 10 | 11 | target_sources(raven 12 | PUBLIC 13 | app.h 14 | editing.h 15 | inspector.h 16 | timeline.h 17 | colors.h 18 | widgets.h 19 | 20 | app.cpp 21 | editing.cpp 22 | inspector.cpp 23 | timeline.cpp 24 | colors.cpp 25 | widgets.cpp 26 | 27 | fonts/embedded_font.inc 28 | ) 29 | 30 | if(APPLE) 31 | target_sources(raven PUBLIC main_macos.mm) 32 | elseif(WIN32) 33 | target_sources(raven PUBLIC main_win32.cpp) 34 | target_compile_definitions(raven PRIVATE -DUNICODE -D_UNICODE) 35 | elseif(EMSCRIPTEN) 36 | target_sources(raven PUBLIC main_emscripten.cpp) 37 | set(LIBS ${CMAKE_DL_LIBS} SDL2) 38 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s USE_SDL=2 -s DISABLE_EXCEPTION_CATCHING=1") 39 | 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") 40 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DIMGUI_DISABLE_FILE_FUNCTIONS") 41 | set_target_properties(raven PROPERTIES SUFFIX .html) 42 | target_link_libraries(raven PUBLIC ${LIBS}) 43 | else() 44 | target_sources(raven PUBLIC main_glfw.cpp) 45 | endif() 46 | 47 | set(BUILD_TESTING OFF) 48 | add_subdirectory("libs") 49 | 50 | include_directories( 51 | ${PROJECT_SOURCE_DIR}/libs/nativefiledialog/src/include 52 | ) 53 | 54 | set(OTIO_SHARED_LIBS OFF) 55 | add_subdirectory("libs/opentimelineio") 56 | include_directories( 57 | ${PROJECT_SOURCE_DIR}/libs/opentimelineio/src 58 | ${PROJECT_SOURCE_DIR}/libs/opentimelineio/src/deps 59 | ${PROJECT_SOURCE_DIR}/libs/opentimelineio/src/deps/optional-lite/include 60 | ) 61 | 62 | if(NOT EMSCRIPTEN AND NOT WIN32) 63 | set(GLFW_BUILD_EXAMPLES OFF) 64 | set(GLFW_BUILD_TESTS OFF) 65 | set(GLFW_BUILD_DOCS OFF) 66 | set(BUILD_SHARED_LIBS OFF) 67 | add_subdirectory("libs/glfw") 68 | endif() 69 | 70 | # minizip-ng 71 | set(BUILD_SHARED_LIBS OFF) 72 | set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) 73 | set(MZ_FETCH_LIBS ON) 74 | set(MZ_ZLIB ON) 75 | # OTIOZ doesn't need any of these 76 | set(MZ_BZIP2 OFF) 77 | set(MZ_LZMA OFF) 78 | set(MZ_ZSTD OFF) 79 | set(MZ_LIBCOMP OFF) 80 | set(MZ_PKCRYPT OFF) 81 | set(MZ_WZAES OFF) 82 | set(MZ_OPENSSL OFF) 83 | set(MZ_BCRYPT OFF) 84 | set(MZ_LIBBSD OFF) 85 | set(MZ_ICONV OFF) 86 | add_subdirectory("libs/minizip-ng") 87 | 88 | if(NOT EMSCRIPTEN) 89 | add_custom_command( 90 | OUTPUT "${PROJECT_SOURCE_DIR}/fonts/embedded_font.inc" 91 | COMMAND binary_to_compressed_c -base85 "${PROJECT_SOURCE_DIR}/fonts/mononoki-Regular Nerd Font Complete.ttf" MononokiFont > ${PROJECT_SOURCE_DIR}/fonts/embedded_font.inc 92 | COMMENT "Embedding font..." 93 | MAIN_DEPENDENCY "fonts/mononoki-Regular Nerd Font Complete.ttf" 94 | VERBATIM) 95 | endif() 96 | 97 | target_compile_definitions(raven 98 | PRIVATE BUILT_RESOURCE_PATH=${PROJECT_SOURCE_DIR}) 99 | 100 | target_link_libraries(raven PUBLIC 101 | OTIO::opentimelineio 102 | IMGUI 103 | MINIZIP::minizip 104 | ) 105 | 106 | if (APPLE) 107 | find_library(COREFOUNDATION_LIBRARY CoreFoundation) 108 | find_library(METAL_LIBRARY Metal) 109 | find_library(METALKIT_LIBRARY MetalKit) 110 | find_library(QUARTZCORE_LIBRARY QuartzCore) 111 | target_link_libraries(raven PUBLIC 112 | glfw 113 | nativefiledialog 114 | ${COREFOUNDATION_LIBRARY} 115 | ${METAL_LIBRARY} 116 | ${METALKIT_LIBRARY} 117 | ${QUARTZCORE_LIBRARY} 118 | ) 119 | elseif(WIN32) 120 | target_link_libraries(raven PUBLIC 121 | nativefiledialog 122 | d3d11.lib 123 | d3dcompiler.lib 124 | dwmapi.lib 125 | ) 126 | elseif(NOT EMSCRIPTEN) 127 | target_link_libraries(raven PUBLIC 128 | glfw 129 | nativefiledialog 130 | ) 131 | endif() 132 | 133 | install(TARGETS raven 134 | BUNDLE DESTINATION bin 135 | RUNTIME DESTINATION bin) 136 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raven - OTIO Viewer 2 | 3 | An experimental re-write of [OpenTimelineIO](https://opentimeline.io)'s `otioview` timeline viewer application. 4 | 5 | This tool aims to replace [otioview](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/tree/main/src/opentimelineview) but it is missing a few essential features (see "Help Wanted" and "To Do" below). Contributions are welcome! 6 | 7 | [![build](https://github.com/OpenTimelineIO/raven/actions/workflows/build.yaml/badge.svg)](https://github.com/OpenTimelineIO/raven/actions/workflows/build.yaml) 8 | 9 | ![screenshot](screenshot.png) 10 | 11 | ![demo](demo.gif) 12 | 13 | ## Dependencies 14 | 15 | macOS: 16 | - Standard Apple developer toolchain (installed with Xcode) 17 | - A recent version of CMake 18 | - You can get this via `brew install cmake` or by downloading from https://cmake.org/download/ 19 | 20 | Windows: 21 | - Standard Microsoft developer toolchain (installed with Visual Studio) 22 | - A recent version of [CMake](https://cmake.org/download/) 23 | 24 | Linux (Ubuntu, or similar): 25 | - `sudo apt-get install libglfw3-dev libgtk-3-dev` 26 | - A recent version of CMake 27 | - You can get this via `sudo snap install cmake` or by downloading from https://cmake.org/download/ 28 | 29 | __Note__: Before building, please ensure that you clone this project with the `--recursive` flag. 30 | This will also clone and initialize all of the submodules that this project depends on. Downloading a ZIP file from GitHub will not work. 31 | 32 | ## Building (macOS, Windows, Linux) 33 | 34 | Spin up your favourite terminal and follow these steps: 35 | 36 | ```shell 37 | git clone --recursive https://github.com/OpenTimelineIO/raven.git 38 | cd raven 39 | mkdir build 40 | cd build 41 | cmake .. 42 | cmake --build . -j 43 | ./raven ../example.otio 44 | ``` 45 | 46 | ## Building (WASM via Emscripten) 47 | 48 | You will need to install the [Emscripten toolchain](https://emscripten.org) first. 49 | 50 | ```shell 51 | git clone --recursive https://github.com/OpenTimelineIO/raven.git 52 | cd raven 53 | mkdir build-web 54 | cd build-web 55 | emcmake cmake .. 56 | cmake --build . 57 | emrun ./raven.html 58 | ``` 59 | 60 | See also: `serve.py` as an alternative to `emrun`, and as 61 | a reference for which HTTP headers are needed to host the WASM build. 62 | 63 | You can load a file into WASM Raven a few ways: 64 | - Add a JSON string to Module.otioLoadString in the HTML file 65 | - Add a URL to Module.otioLoadURL in the HTML file 66 | - Call Module.LoadString(otio_data) at runtime 67 | - Call Module.LoadURL(otio_url) at runtime 68 | 69 | Note: The WASM build of raven is missing some features - see the Help Wanted section below. 70 | 71 | ## Troubleshooting 72 | 73 | If you have trouble building, these hints might help... 74 | 75 | If you cloned the repo without `--recursive` then you will need to init/update submodules: 76 | ``` 77 | % git submodule update --init --recursive 78 | ``` 79 | 80 | If you downloaded a ZIP archive from GitHub instead of cloning with git, then you are missing the submodules entirely. You need to use git clone instead of downloading a ZIP file. 81 | 82 | You might be missing some dependencies (see above). 83 | 84 | See also [`.github/workflows/build.yaml`](https://github.com/OpenTimelineIO/raven/blob/main/.github/workflows/build.yaml) for a working example of building on each of the platforms listed above. 85 | 86 | ## Example files 87 | 88 | The `examples` folder contains some example `.otio` files for testing. 89 | 90 | The El Fuente and Meridian [examples provided by Netflix](https://opencontent.netflix.com/) (under the 91 | Creative Commons Attribution 4.0 International Public License) were [converted to OTIO, along with several 92 | other examples here](https://github.com/darbyjohnston/otio-oc-examples). 93 | 94 | ## Thanks 95 | 96 | Raven was made possible by these excellent libraries: 97 | - [OpenTimelineIO](https://opentimeline.io) ([Apache 2.0](https://github.com/AcademySoftwareFoundation/OpenTimelineIO/blob/main/LICENSE.txt)) 98 | - [Dear ImGui](https://github.com/ocornut/imgui) ([MIT](https://github.com/ocornut/imgui/blob/master/LICENSE.txt)) 99 | - [ImPlot](https://github.com/epezent/implot) ([MIT](https://github.com/epezent/implot/blob/master/LICENSE)) 100 | - [ImGuiColorTextEdit](https://github.com/santaclose/ImGuiColorTextEdit) ([MIT](https://github.com/santaclose/ImGuiColorTextEdit/blob/master/LICENSE)) 101 | - [glfw](https://github.com/glfw/glfw) ([Zlib](https://github.com/glfw/glfw/blob/master/LICENSE.md)) 102 | - [nativefiledialog](https://github.com/mlabbe/nativefiledialog) ([Zlib](https://github.com/mlabbe/nativefiledialog/blob/master/LICENSE)) 103 | 104 | ## Help Wanted 105 | 106 | - Fully standalone cross-platform build: 107 | - Mac 108 | - [App bundle](https://stackoverflow.com/questions/53560288/how-to-create-a-macos-app-bundle-with-cmake) 109 | - Code signing? 110 | - Linux 111 | - Needs more user testing 112 | - Emscripten 113 | - Where can we host this? (needs specific HTTP headers? See `serve.py`) 114 | - Needs file open/save dialog 115 | - Maybe [this could work](https://stackoverflow.com/questions/69935188/open-a-file-in-emscripten-using-browser-file-selector-dialogue) in app.cpp's `OpenFileDialog()` and `SaveFileDialog()`? 116 | - or this: https://github.com/Armchair-Software/emscripten-browser-file 117 | - See the [web-file-open branch](https://github.com/OpenTimelineIO/raven/tree/web-file-open) 118 | - Avoid continuous rendering 119 | - Is there an equivalent to `ImGui_ImplGlfw_WaitForEvent()` that works with SDL2 + Emscripten? 120 | - Easily downloadable pre-built binaries 121 | - JSON Inspector: 122 | - When loading a very large OTIO, the JSON inspector can double the load time (full feature film ~45 seconds) 123 | - Multiple selection, copy, paste, undo, redo 124 | - Various operations from `otiotool` 125 | 126 | ## To Do 127 | 128 | - Feature parity with `otioview`: 129 | - Show media reference details in the Inspector 130 | - Double-click to expand/collapse nested compositions 131 | - Arrow keys to navigate by selection 132 | - This sort of works already via ImGui's navigation system, but it is too easy to get stuck on a marker, or to walk out of the timeline. 133 | - Can this be rectified by turning off keyboard navigation on the widgets outside the timeline? 134 | - Multiple timelines in separate tabs or windows? 135 | - Look at ImGui document-based demo code for reference. 136 | - Might be fine to just open multiple instances of the app. 137 | - Add support for adapters 138 | - Use embedded Python, or run `otioconvert` via pipe? 139 | - Constraint: We want to ensure this tool stays light weight, and works in the browser. 140 | - Enhancements: 141 | - Double-click a Clip to expand/collapse it's media reference 142 | - Show time-warped ruler inside media reference or nested composition 143 | - Performance optimization: 144 | - Avoid rendering tracks outside the scroll region 145 | - Avoid rendering items smaller than a tiny sliver 146 | - Experiment with drawing the timeline without using a Dear ImGui table 147 | - Results so far: aligning multiple scroll regions causes 1-frame lag which is ugly 148 | - Inspector: 149 | - Show summarized timing information (ala `otiotool --inspect`) 150 | - Range slider could be useful: 151 | - https://github.com/ocornut/imgui/issues/76#issuecomment-288304286 152 | - Per-schema inspector GUI 153 | - Items: 154 | - enable/disable 155 | - Clips: 156 | - show media_reference(s) 157 | - adjust available_range of media reference 158 | - edit target_url 159 | - Transitions: 160 | - nicer GUI for adjusting in/out offsets 161 | - avoid extending beyond range of adjacent Items 162 | - avoid overlap with adjacent Transitions 163 | - Markers: 164 | - color picker 165 | - Compositions: 166 | - show source_range limits in the timeline 167 | - LinearTimeWarp: 168 | - time_scale graph could be nicer 169 | - FreezeFrame: 170 | - UnknownSchema: 171 | - Can we show properties via SerializableObject's introspection? 172 | -------------------------------------------------------------------------------- /app.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "imgui.h" 8 | #include "imgui_internal.h" 9 | 10 | #include 11 | #include 12 | namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; 13 | 14 | enum AppThemeCol_ { 15 | AppThemeCol_Background, 16 | AppThemeCol_Label, 17 | AppThemeCol_TickMajor, 18 | AppThemeCol_TickMinor, 19 | AppThemeCol_GapHovered, 20 | AppThemeCol_GapSelected, 21 | AppThemeCol_Item, 22 | AppThemeCol_ItemHovered, 23 | AppThemeCol_ItemSelected, 24 | AppThemeCol_Transition, 25 | AppThemeCol_TransitionLine, 26 | AppThemeCol_TransitionHovered, 27 | AppThemeCol_TransitionSelected, 28 | AppThemeCol_Effect, 29 | AppThemeCol_EffectHovered, 30 | AppThemeCol_EffectSelected, 31 | AppThemeCol_Playhead, 32 | AppThemeCol_PlayheadLine, 33 | AppThemeCol_PlayheadHovered, 34 | AppThemeCol_PlayheadSelected, 35 | AppThemeCol_MarkerHovered, 36 | AppThemeCol_MarkerSelected, 37 | AppThemeCol_Track, 38 | AppThemeCol_TrackHovered, 39 | AppThemeCol_TrackSelected, 40 | AppThemeCol_COUNT 41 | }; 42 | 43 | extern const char* AppThemeColor_Names[]; 44 | 45 | #ifdef DEFINE_APP_THEME_NAMES 46 | const char* AppThemeColor_Names[] = { 47 | "Background", 48 | "Label", 49 | "Tick Major", 50 | "Tick Minor", 51 | "Gap Hovered", 52 | "Gap Selected", 53 | "Item", 54 | "Item Hovered", 55 | "Item Selected", 56 | "Transition", 57 | "Transition Line", 58 | "Transition Hovered", 59 | "Transition Selected", 60 | "Effect", 61 | "Effect Hovered", 62 | "Effect Selected", 63 | "Playhead", 64 | "Playhead Line", 65 | "Playhead Hovered", 66 | "Playhead Selected", 67 | "Marker Hovered", 68 | "Marker Selected", 69 | "Track", 70 | "Track Hovered", 71 | "Track Selected", 72 | "Invalid" 73 | }; 74 | #endif 75 | 76 | struct AppTheme { 77 | ImU32 colors[AppThemeCol_COUNT]; 78 | }; 79 | 80 | // Struct that holds the application's state 81 | struct AppState { 82 | // What file did we load? 83 | std::string file_path; 84 | 85 | // This holds the main timeline object. 86 | // Pretty much everything drills into this one entry point. 87 | //otio::SerializableObject::Retainer timeline; 88 | otio::SerializableObject::Retainer root; 89 | 90 | // Timeline display settings 91 | float timeline_width = 100.0f; // automatically calculated (pixels) 92 | float scale = 100.0f; // zoom scale, measured in pixels per second 93 | float default_track_height = 30.0f; // (pixels) 94 | float track_height = 30.0f; // current track height (pixels) 95 | otio::RationalTime playhead; 96 | bool scroll_to_playhead = false; // temporary flag, only true until next frame 97 | bool scroll_key = false; // temporary flag, only true until next frame 98 | bool scroll_up_down; // temporary flag, only true until next frame 99 | otio::TimeRange 100 | playhead_limit; // min/max limit for moving the playhead, auto-calculated 101 | float zebra_factor = 0.1; // opacity of the per-frame zebra stripes 102 | 103 | bool snap_to_frames = true; // user preference to snap the playhead, times, 104 | // ranges, etc. to frames 105 | bool display_timecode = true; 106 | bool display_frames = false; 107 | bool display_seconds = false; 108 | bool display_rate = false; 109 | opentime::IsDropFrameRate drop_frame_mode = opentime::InferFromRate; 110 | 111 | // Selection. 112 | otio::SerializableObject* selected_object; // maybe NULL 113 | otio::SerializableObject* 114 | selected_context; // often NULL, parent to the selected object for OTIO 115 | // objects which don't track their parent 116 | std::string selected_text; // displayed in the JSON inspector 117 | char message[1024]; // single-line message displayed in main window 118 | bool message_is_error = false; 119 | 120 | // Toggles for Dear ImGui windows 121 | bool show_main_window = true; 122 | bool show_style_editor = false; 123 | bool show_demo_window = false; 124 | bool show_metrics = false; 125 | bool show_implot_demo_window = false; 126 | }; 127 | 128 | extern AppState appState; 129 | extern AppTheme appTheme; 130 | extern ImFont* gFont; 131 | 132 | void Log(const char* format, ...); 133 | void Message(const char* format, ...); 134 | void ErrorMessage(const char* format, ...); 135 | std::string Format(const char* format, ...); 136 | 137 | void LoadString(std::string json); 138 | bool LoadRoot(otio::SerializableObjectWithMetadata* root); 139 | 140 | std::string otio_error_string(otio::ErrorStatus const& error_status); 141 | 142 | void SelectObject( 143 | otio::SerializableObject* object, 144 | otio::SerializableObject* context = NULL); 145 | void SeekPlayhead(double seconds); 146 | void SnapPlayhead(); 147 | void DetectPlayheadLimits(); 148 | void FitZoomWholeTimeline(); 149 | float CalculateDynamicHeight(); 150 | std::string FormattedStringFromTime(otio::RationalTime time, bool allow_rate = true); 151 | std::string TimecodeStringFromTime(otio::RationalTime); 152 | std::string FramesStringFromTime(otio::RationalTime); 153 | std::string SecondsStringFromTime(otio::RationalTime); 154 | void UpdateJSONInspector(); 155 | -------------------------------------------------------------------------------- /colors.cpp: -------------------------------------------------------------------------------- 1 | #include "colors.h" 2 | 3 | #include "imgui_internal.h" 4 | 5 | #include 6 | namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; 7 | 8 | ImU32 LerpColors(ImU32 col_a, ImU32 col_b, float t) { 9 | int r = ImLerp( 10 | (int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, 11 | (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, 12 | t); 13 | int g = ImLerp( 14 | (int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, 15 | (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, 16 | t); 17 | int b = ImLerp( 18 | (int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, 19 | (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, 20 | t); 21 | int a = ImLerp( 22 | (int)(col_a >> IM_COL32_A_SHIFT) & 0xFF, 23 | (int)(col_b >> IM_COL32_A_SHIFT) & 0xFF, 24 | t); 25 | return IM_COL32(r, g, b, a); 26 | } 27 | 28 | ImU32 UIColorFromName(std::string color) { 29 | if (color == otio::Marker::Color::pink) 30 | return IM_COL32(0xff, 0x70, 0x70, 0xff); 31 | if (color == otio::Marker::Color::red) 32 | return IM_COL32(0xff, 0x00, 0x00, 0xff); 33 | if (color == otio::Marker::Color::orange) 34 | return IM_COL32(0xff, 0xa0, 0x00, 0xff); 35 | if (color == otio::Marker::Color::yellow) 36 | return IM_COL32(0xff, 0xff, 0x00, 0xff); 37 | if (color == otio::Marker::Color::green) 38 | return IM_COL32(0x00, 0xff, 0x00, 0xff); 39 | if (color == otio::Marker::Color::cyan) 40 | return IM_COL32(0x00, 0xff, 0xff, 0xff); 41 | if (color == otio::Marker::Color::blue) 42 | return IM_COL32(0x00, 0x00, 0xff, 0xff); 43 | if (color == otio::Marker::Color::purple) 44 | return IM_COL32(0xa0, 0x00, 0xd0, 0xff); 45 | if (color == otio::Marker::Color::magenta) 46 | return IM_COL32(0xff, 0x00, 0xff, 0xff); 47 | if (color == otio::Marker::Color::black) 48 | return IM_COL32(0x00, 0x00, 0x00, 0xff); 49 | if (color == otio::Marker::Color::white) 50 | return IM_COL32(0xff, 0xff, 0xff, 0xff); 51 | return IM_COL32(0x88, 0x88, 0x88, 0xff); 52 | } 53 | 54 | bool ColorIsBright(ImU32 color) 55 | { 56 | int r = (int)(color >> IM_COL32_R_SHIFT) & 0xFF; 57 | int g = (int)(color >> IM_COL32_G_SHIFT) & 0xFF; 58 | int b = (int)(color >> IM_COL32_B_SHIFT) & 0xFF; 59 | // What color space is this in? Does it really matter here? 60 | // https://en.wikipedia.org/wiki/Relative_luminance 61 | return (0.2126*r + 0.7152*g + 0.0722*b) > 127.0; 62 | } 63 | 64 | ImU32 ColorInvert(ImU32 color) 65 | { 66 | int r = (int)(color >> IM_COL32_R_SHIFT) & 0xFF; 67 | int g = (int)(color >> IM_COL32_G_SHIFT) & 0xFF; 68 | int b = (int)(color >> IM_COL32_B_SHIFT) & 0xFF; 69 | int a = (int)(color >> IM_COL32_A_SHIFT) & 0xFF; 70 | return IM_COL32(255-r, 255-g, 255-b, a); 71 | } 72 | 73 | ImU32 TintedColorForUI(ImU32 color) 74 | { 75 | return LerpColors(color, IM_COL32(150,150,150,255), 0.3); 76 | } 77 | 78 | -------------------------------------------------------------------------------- /colors.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "imgui.h" 3 | 4 | ImU32 UIColorFromName(std::string color); 5 | ImU32 LerpColors(ImU32 col_a, ImU32 col_b, float t); 6 | bool ColorIsBright(ImU32 color); 7 | ImU32 ColorInvert(ImU32 color); 8 | ImU32 TintedColorForUI(ImU32 color); 9 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/raven/6d21529b6b0560e35870bc52ffd49ac530b7b81d/demo.gif -------------------------------------------------------------------------------- /editing.cpp: -------------------------------------------------------------------------------- 1 | #include "editing.h" 2 | #include "app.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | void DeleteSelectedObject() { 11 | if (appState.selected_object == appState.root) { 12 | appState.root = NULL; 13 | SelectObject(NULL); 14 | return; 15 | } 16 | 17 | if (const auto& selected_composable = dynamic_cast(appState.selected_object)) { 18 | if (const auto& parent = selected_composable->parent()) { 19 | auto& children = parent->children(); 20 | auto it = std::find( 21 | children.begin(), 22 | children.end(), 23 | selected_composable); 24 | if (it != children.end()) { 25 | int index = (int)std::distance(children.begin(), it); 26 | parent->remove_child(index); 27 | } 28 | } 29 | SelectObject(NULL); 30 | return; 31 | } 32 | 33 | if (const auto& selected_marker = dynamic_cast(appState.selected_object)) { 34 | if (const auto& item = dynamic_cast(appState.selected_context)) { 35 | auto& markers = item->markers(); 36 | auto it = std::find(markers.begin(), markers.end(), selected_marker); 37 | if (it != markers.end()) { 38 | markers.erase(it); 39 | } 40 | } 41 | SelectObject(NULL); 42 | return; 43 | } 44 | 45 | if (const auto& selected_effect = dynamic_cast(appState.selected_object)) { 46 | if (const auto& item = dynamic_cast(appState.selected_context)) { 47 | auto& effects = item->effects(); 48 | auto it = std::find(effects.begin(), effects.end(), selected_effect); 49 | if (it != effects.end()) { 50 | effects.erase(it); 51 | } 52 | } 53 | SelectObject(NULL); 54 | return; 55 | } 56 | } 57 | 58 | bool ReplaceObject(otio::SerializableObject* old_object, otio::SerializableObject* new_object) { 59 | if (old_object == appState.root) { 60 | appState.root = dynamic_cast(new_object); 61 | return true; 62 | } 63 | 64 | if (const auto& old_composable = dynamic_cast(old_object)) { 65 | auto new_composable = dynamic_cast(new_object); 66 | if (new_composable == nullptr) { 67 | ErrorMessage("Cannot replace %s with %s (must be Composable)", 68 | old_composable->schema_name().c_str(), 69 | new_object->schema_name().c_str()); 70 | return false; 71 | } 72 | 73 | const auto& parent = old_composable->parent(); 74 | if (parent == nullptr) { 75 | ErrorMessage("Cannot replace Composable with nil parent."); 76 | return false; 77 | } 78 | 79 | auto& children = parent->children(); 80 | auto it = std::find(children.begin(), children.end(), old_composable); 81 | if (it != children.end()) { 82 | int index = (int)std::distance(children.begin(), it); 83 | parent->remove_child(index); 84 | parent->insert_child(index, new_composable); 85 | } 86 | return true; 87 | } 88 | 89 | if (const auto& old_marker = dynamic_cast(old_object)) { 90 | auto new_marker = dynamic_cast(new_object); 91 | if (new_marker == nullptr) { 92 | ErrorMessage("Cannot replace %s with %s", 93 | old_marker->schema_name().c_str(), 94 | new_object->schema_name().c_str()); 95 | return false; 96 | } 97 | 98 | const auto& item = dynamic_cast(appState.selected_context); 99 | if (item == nullptr) { 100 | ErrorMessage("Cannot replace Marker without parent item."); 101 | return false; 102 | } 103 | 104 | auto& markers = item->markers(); 105 | auto it = std::find(markers.begin(), markers.end(), old_marker); 106 | if (it != markers.end()) { 107 | *it = dynamic_cast(new_object); 108 | } 109 | return true; 110 | } 111 | 112 | if (const auto& old_effect = dynamic_cast(old_object)) { 113 | auto new_effect = dynamic_cast(new_object); 114 | if (new_effect == nullptr) { 115 | ErrorMessage("Cannot replace %s with %s", 116 | old_effect->schema_name().c_str(), 117 | new_object->schema_name().c_str()); 118 | return false; 119 | } 120 | const auto& item = dynamic_cast(appState.selected_context); 121 | if (item == nullptr) { 122 | ErrorMessage("Cannot replace Effect without parent item."); 123 | return false; 124 | } 125 | 126 | auto& effects = item->effects(); 127 | auto it = std::find(effects.begin(), effects.end(), old_effect); 128 | if (it != effects.end()) { 129 | *it = new_effect; 130 | } 131 | return true; 132 | } 133 | 134 | ErrorMessage("Cannot replace %s.", old_object->schema_name().c_str()); 135 | return false; 136 | } 137 | 138 | void AddMarkerAtPlayhead(otio::Item* item, std::string name, std::string color) { 139 | auto playhead = appState.playhead; 140 | 141 | if (!appState.root){ 142 | return; 143 | } 144 | 145 | const auto& timeline = dynamic_cast(appState.root.value); 146 | if (!timeline){ 147 | return; 148 | } 149 | 150 | // Default to the selected item, or the top-level timeline. 151 | if (item == NULL) { 152 | if (const auto& selected_item = dynamic_cast(appState.selected_object)) { 153 | item = selected_item; 154 | } else { 155 | // item = appState.selected_object_parent; 156 | } 157 | if (item == NULL) { 158 | item = timeline->tracks(); 159 | } 160 | } 161 | 162 | otio::ErrorStatus error_status; 163 | auto global_start = timeline->global_start_time().value_or(otio::RationalTime()); 164 | auto time = timeline->tracks()->transformed_time(playhead - global_start, item, &error_status); 165 | if (otio::is_error(error_status)) { 166 | ErrorMessage( 167 | "Error transforming time: %s", 168 | otio_error_string(error_status).c_str()); 169 | return; 170 | } 171 | 172 | const auto marked_range = otio::TimeRange(time); // default 0 duration 173 | otio::SerializableObject::Retainer marker = new otio::Marker(name, marked_range, color); 174 | 175 | item->markers().push_back(marker); 176 | } 177 | 178 | void AddTrack(std::string kind) { 179 | if (!appState.root){ 180 | return; 181 | } 182 | 183 | const auto& timeline = dynamic_cast(appState.root.value); 184 | if (!timeline) { 185 | return; 186 | } 187 | 188 | // Fall back to the top level stack. 189 | int insertion_index = -1; 190 | otio::Stack* stack = timeline->tracks(); 191 | 192 | // Start with the selected object, if it is a Composable. 193 | auto child = dynamic_cast(appState.selected_object); 194 | if (child) { 195 | // Walk up from the selected object until we find a Stack 196 | otio::Composition* search = child->parent(); 197 | while (search) { 198 | if (const auto& found = dynamic_cast(search)) { 199 | stack = found; 200 | 201 | auto& children = stack->children(); 202 | auto it = std::find(children.begin(), children.end(), child); 203 | if (it != children.end()) { 204 | insertion_index = (int)std::distance(children.begin(), it) + 1; 205 | } 206 | 207 | if (kind == "") { 208 | if (const auto& peer = dynamic_cast(child)) { 209 | kind = peer->kind(); 210 | } 211 | } 212 | 213 | break; 214 | } 215 | child = search; 216 | search = search->parent(); 217 | } 218 | } 219 | 220 | if (kind == "") { 221 | kind = otio::Track::Kind::video; 222 | } 223 | 224 | if (stack) { 225 | otio::SerializableObject::Retainer new_track = new otio::Track("", std::nullopt, kind); 226 | 227 | otio::ErrorStatus error_status; 228 | if (insertion_index == -1) { 229 | stack->append_child(new_track, &error_status); 230 | } else { 231 | stack->insert_child(insertion_index, new_track, &error_status); 232 | } 233 | if (otio::is_error(error_status)) { 234 | ErrorMessage( 235 | "Error inserting track: %s", 236 | otio_error_string(error_status).c_str()); 237 | return; 238 | } 239 | } 240 | } 241 | 242 | void FlattenTrackDown() { 243 | if (!appState.root) { 244 | return; 245 | } 246 | 247 | const auto& timeline = dynamic_cast(appState.root.value); 248 | if (!timeline) { 249 | ErrorMessage("Cannot flatten: No timeline."); 250 | return; 251 | } 252 | 253 | if (appState.selected_object == NULL) { 254 | ErrorMessage("Cannot flatten: No Track is selected."); 255 | return; 256 | } 257 | 258 | auto selected_track = dynamic_cast(appState.selected_object); 259 | if (selected_track == NULL) { 260 | ErrorMessage("Cannot flatten: Selected object is not a Track."); 261 | return; 262 | } 263 | 264 | otio::Stack* stack = dynamic_cast(selected_track->parent()); 265 | if (stack == NULL) { 266 | ErrorMessage("Cannot flatten: Parent of selected Track is not a Stack."); 267 | return; 268 | } 269 | 270 | int selected_index = -1; 271 | auto& children = stack->children(); 272 | auto it = std::find(children.begin(), children.end(), selected_track); 273 | if (it != children.end()) { 274 | selected_index = (int)std::distance(children.begin(), it); 275 | } 276 | if (selected_index < 0) { 277 | ErrorMessage("Cannot flatten: Cannot find selected Track in Stack."); 278 | return; 279 | } 280 | if (selected_index == 0) { 281 | ErrorMessage("Cannot flatten: Selected Track has nothing below it."); 282 | return; 283 | } 284 | auto track_below = dynamic_cast(children[selected_index - 1].value); 285 | if (track_below == NULL) { 286 | ErrorMessage( 287 | "Cannot flatten: Item below selected Track is not a Track itself."); 288 | return; 289 | } 290 | 291 | otio::ErrorStatus error_status; 292 | std::vector tracks; 293 | tracks.push_back(track_below); 294 | tracks.push_back(selected_track); 295 | auto flat_track = otio::flatten_stack(tracks, &error_status); 296 | if (!flat_track || is_error(error_status)) { 297 | ErrorMessage("Cannot flatten: %s.", otio_error_string(error_status).c_str()); 298 | return; 299 | } 300 | int insertion_index = selected_index + 1; 301 | 302 | stack->insert_child(insertion_index, flat_track, &error_status); 303 | 304 | if (otio::is_error(error_status)) { 305 | ErrorMessage( 306 | "Error inserting track: %s", 307 | otio_error_string(error_status).c_str()); 308 | return; 309 | } 310 | 311 | // stack->remove_child(selected_index - 1); 312 | // stack->remove_child(selected_index); 313 | SelectObject(flat_track); 314 | 315 | // Success! 316 | } 317 | 318 | std::string GetItemColor(otio::Item* item) 319 | { 320 | std::string item_color = ""; 321 | 322 | if (item->metadata().has_key("raven") && 323 | item->metadata()["raven"].type() == typeid(otio::AnyDictionary)) 324 | { 325 | auto raven_md = std::any_cast(item->metadata()["raven"]); 326 | 327 | if (raven_md.has_key("color") && 328 | raven_md["color"].type() == typeid(std::string)) 329 | { 330 | item_color = std::any_cast(raven_md["color"]); 331 | } 332 | } 333 | 334 | return item_color; 335 | } 336 | 337 | void SetItemColor(otio::Item* item, std::string color_name) 338 | { 339 | otio::AnyDictionary raven_md; 340 | if (item->metadata().has_key("raven") && 341 | item->metadata()["raven"].type() == typeid(otio::AnyDictionary)) 342 | { 343 | raven_md = std::any_cast(item->metadata()["raven"]); 344 | } 345 | raven_md["color"] = color_name; 346 | item->metadata()["raven"] = raven_md; 347 | } 348 | -------------------------------------------------------------------------------- /editing.h: -------------------------------------------------------------------------------- 1 | // Editing operations 2 | 3 | #include 4 | #include 5 | namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; 6 | 7 | bool ReplaceObject(otio::SerializableObject* old_object, otio::SerializableObject* new_object); 8 | void DeleteSelectedObject(); 9 | void AddMarkerAtPlayhead( 10 | otio::Item* item = NULL, 11 | std::string name = "", 12 | std::string color = "RED"); 13 | void AddTrack(std::string kind = ""); 14 | void FlattenTrackDown(); 15 | 16 | std::string GetItemColor(otio::Item* item); 17 | void SetItemColor(otio::Item* item, std::string color_name); 18 | -------------------------------------------------------------------------------- /example.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "metadata": {}, 4 | "name": "OTIO TEST - multitrack.Exported.01", 5 | "tracks": { 6 | "OTIO_SCHEMA": "Stack.1", 7 | "children": [ 8 | { 9 | "OTIO_SCHEMA": "Track.1", 10 | "children": [ 11 | { 12 | "OTIO_SCHEMA": "Clip.1", 13 | "effects": [], 14 | "markers": [], 15 | "enabled": true, 16 | "media_reference": { 17 | "OTIO_SCHEMA": "MissingReference.1", 18 | "available_range": null, 19 | "metadata": {}, 20 | "name": null 21 | }, 22 | "metadata": {}, 23 | "name": "tech.fux (loop)-HD.mp4", 24 | "source_range": { 25 | "OTIO_SCHEMA": "TimeRange.1", 26 | "duration": { 27 | "OTIO_SCHEMA": "RationalTime.1", 28 | "rate": 24, 29 | "value": 720 30 | }, 31 | "start_time": { 32 | "OTIO_SCHEMA": "RationalTime.1", 33 | "rate": 24, 34 | "value": 0 35 | } 36 | } 37 | }, 38 | { 39 | "OTIO_SCHEMA": "Gap.1", 40 | "effects": [], 41 | "markers": [], 42 | "enabled": true, 43 | "metadata": {}, 44 | "name": "Filler", 45 | "source_range": { 46 | "OTIO_SCHEMA": "TimeRange.1", 47 | "duration": { 48 | "OTIO_SCHEMA": "RationalTime.1", 49 | "rate": 24, 50 | "value": 83 51 | }, 52 | "start_time": { 53 | "OTIO_SCHEMA": "RationalTime.1", 54 | "rate": 24, 55 | "value": 0 56 | } 57 | } 58 | }, 59 | { 60 | "OTIO_SCHEMA": "Clip.1", 61 | "effects": [], 62 | "markers": [], 63 | "enabled": true, 64 | "media_reference": { 65 | "OTIO_SCHEMA": "MissingReference.1", 66 | "available_range": null, 67 | "metadata": {}, 68 | "name": null 69 | }, 70 | "metadata": {}, 71 | "name": "out-b (loop)-HD.mp4", 72 | "source_range": { 73 | "OTIO_SCHEMA": "TimeRange.1", 74 | "duration": { 75 | "OTIO_SCHEMA": "RationalTime.1", 76 | "rate": 24, 77 | "value": 722 78 | }, 79 | "start_time": { 80 | "OTIO_SCHEMA": "RationalTime.1", 81 | "rate": 24, 82 | "value": 0 83 | } 84 | } 85 | }, 86 | { 87 | "OTIO_SCHEMA": "Gap.1", 88 | "effects": [], 89 | "markers": [], 90 | "enabled": true, 91 | "metadata": {}, 92 | "name": "Filler 2", 93 | "source_range": { 94 | "OTIO_SCHEMA": "TimeRange.1", 95 | "duration": { 96 | "OTIO_SCHEMA": "RationalTime.1", 97 | "rate": 24, 98 | "value": 432 99 | }, 100 | "start_time": { 101 | "OTIO_SCHEMA": "RationalTime.1", 102 | "rate": 24, 103 | "value": 0 104 | } 105 | } 106 | }, 107 | { 108 | "OTIO_SCHEMA": "Clip.1", 109 | "effects": [], 110 | "markers": [], 111 | "enabled": true, 112 | "media_reference": { 113 | "OTIO_SCHEMA": "MissingReference.1", 114 | "available_range": null, 115 | "metadata": {}, 116 | "name": null 117 | }, 118 | "metadata": {}, 119 | "name": "brokchrd (loop)-HD.mp4", 120 | "source_range": { 121 | "OTIO_SCHEMA": "TimeRange.1", 122 | "duration": { 123 | "OTIO_SCHEMA": "RationalTime.1", 124 | "rate": 24, 125 | "value": 720 126 | }, 127 | "start_time": { 128 | "OTIO_SCHEMA": "RationalTime.1", 129 | "rate": 24, 130 | "value": 0 131 | } 132 | } 133 | }, 134 | { 135 | "OTIO_SCHEMA": "Gap.1", 136 | "effects": [], 137 | "markers": [], 138 | "enabled": true, 139 | "metadata": {}, 140 | "name": "Filler 3", 141 | "source_range": { 142 | "OTIO_SCHEMA": "TimeRange.1", 143 | "duration": { 144 | "OTIO_SCHEMA": "RationalTime.1", 145 | "rate": 24, 146 | "value": 605 147 | }, 148 | "start_time": { 149 | "OTIO_SCHEMA": "RationalTime.1", 150 | "rate": 24, 151 | "value": 0 152 | } 153 | } 154 | } 155 | ], 156 | "effects": [], 157 | "kind": "Video", 158 | "markers": [], 159 | "enabled": true, 160 | "metadata": {}, 161 | "name": "Sequence", 162 | "source_range": null 163 | }, 164 | { 165 | "OTIO_SCHEMA": "Track.1", 166 | "children": [ 167 | { 168 | "OTIO_SCHEMA": "Gap.1", 169 | "effects": [], 170 | "markers": [], 171 | "enabled": true, 172 | "metadata": {}, 173 | "name": "ScopeReference", 174 | "source_range": { 175 | "OTIO_SCHEMA": "TimeRange.1", 176 | "duration": { 177 | "OTIO_SCHEMA": "RationalTime.1", 178 | "rate": 24, 179 | "value": 482 180 | }, 181 | "start_time": { 182 | "OTIO_SCHEMA": "RationalTime.1", 183 | "rate": 24, 184 | "value": 0 185 | } 186 | } 187 | }, 188 | { 189 | "OTIO_SCHEMA": "Clip.1", 190 | "effects": [], 191 | "markers": [], 192 | "enabled": true, 193 | "media_reference": { 194 | "OTIO_SCHEMA": "MissingReference.1", 195 | "available_range": null, 196 | "metadata": {}, 197 | "name": null 198 | }, 199 | "metadata": {}, 200 | "name": "t-hawk (loop)-HD.mp4", 201 | "source_range": { 202 | "OTIO_SCHEMA": "TimeRange.1", 203 | "duration": { 204 | "OTIO_SCHEMA": "RationalTime.1", 205 | "rate": 24, 206 | "value": 480 207 | }, 208 | "start_time": { 209 | "OTIO_SCHEMA": "RationalTime.1", 210 | "rate": 24, 211 | "value": 0 212 | } 213 | } 214 | }, 215 | { 216 | "OTIO_SCHEMA": "Gap.1", 217 | "effects": [], 218 | "markers": [], 219 | "enabled": true, 220 | "metadata": {}, 221 | "name": "ScopeReference 2", 222 | "source_range": { 223 | "OTIO_SCHEMA": "TimeRange.1", 224 | "duration": { 225 | "OTIO_SCHEMA": "RationalTime.1", 226 | "rate": 24, 227 | "value": 2320 228 | }, 229 | "start_time": { 230 | "OTIO_SCHEMA": "RationalTime.1", 231 | "rate": 24, 232 | "value": 0 233 | } 234 | } 235 | } 236 | ], 237 | "effects": [], 238 | "kind": "Video", 239 | "markers": [], 240 | "enabled": true, 241 | "metadata": {}, 242 | "name": "Sequence 2", 243 | "source_range": null 244 | }, 245 | { 246 | "OTIO_SCHEMA": "Track.1", 247 | "children": [ 248 | { 249 | "OTIO_SCHEMA": "Gap.1", 250 | "effects": [], 251 | "markers": [], 252 | "enabled": true, 253 | "metadata": {}, 254 | "name": "ScopeReference 3", 255 | "source_range": { 256 | "OTIO_SCHEMA": "TimeRange.1", 257 | "duration": { 258 | "OTIO_SCHEMA": "RationalTime.1", 259 | "rate": 24, 260 | "value": 1198 261 | }, 262 | "start_time": { 263 | "OTIO_SCHEMA": "RationalTime.1", 264 | "rate": 24, 265 | "value": 0 266 | } 267 | } 268 | }, 269 | { 270 | "OTIO_SCHEMA": "Clip.1", 271 | "effects": [], 272 | "markers": [], 273 | "enabled": true, 274 | "media_reference": { 275 | "OTIO_SCHEMA": "MissingReference.1", 276 | "available_range": null, 277 | "metadata": {}, 278 | "name": null 279 | }, 280 | "metadata": {}, 281 | "name": "KOLL-HD.mp4", 282 | "source_range": { 283 | "OTIO_SCHEMA": "TimeRange.1", 284 | "duration": { 285 | "OTIO_SCHEMA": "RationalTime.1", 286 | "rate": 24, 287 | "value": 640 288 | }, 289 | "start_time": { 290 | "OTIO_SCHEMA": "RationalTime.1", 291 | "rate": 24, 292 | "value": 0 293 | } 294 | } 295 | }, 296 | { 297 | "OTIO_SCHEMA": "Gap.1", 298 | "effects": [], 299 | "markers": [], 300 | "enabled": true, 301 | "metadata": {}, 302 | "name": "ScopeReference 4", 303 | "source_range": { 304 | "OTIO_SCHEMA": "TimeRange.1", 305 | "duration": { 306 | "OTIO_SCHEMA": "RationalTime.1", 307 | "rate": 24, 308 | "value": 1444 309 | }, 310 | "start_time": { 311 | "OTIO_SCHEMA": "RationalTime.1", 312 | "rate": 24, 313 | "value": 0 314 | } 315 | } 316 | } 317 | ], 318 | "effects": [], 319 | "kind": "Video", 320 | "markers": [], 321 | "enabled": true, 322 | "metadata": {}, 323 | "name": "Sequence 3", 324 | "source_range": null 325 | } 326 | ], 327 | "effects": [], 328 | "markers": [], 329 | "enabled": true, 330 | "metadata": {}, 331 | "name": "NestedScope", 332 | "source_range": null 333 | } 334 | } -------------------------------------------------------------------------------- /examples/ElFuente_4096x2160_60fps_10bit_420_remix1.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "metadata": {}, 4 | "name": "test", 5 | "tracks": { 6 | "OTIO_SCHEMA": "Stack.1", 7 | "children": [ 8 | { 9 | "OTIO_SCHEMA": "Track.1", 10 | "children": [ 11 | { 12 | "OTIO_SCHEMA": "Clip.1", 13 | "media_reference": { 14 | "OTIO_SCHEMA": "ExternalReference.1", 15 | "available_range": { 16 | "OTIO_SCHEMA": "TimeRange.1", 17 | "duration": { 18 | "OTIO_SCHEMA": "RationalTime.1", 19 | "rate": 60, 20 | "value": 254 21 | }, 22 | "start_time": { 23 | "OTIO_SCHEMA": "RationalTime.1", 24 | "rate": 60, 25 | "value": 0 26 | } 27 | }, 28 | "target_url": "Netflix_BoxingPractice_4096x2160_60fps_10bit_420.y4m" 29 | }, 30 | "source_range": { 31 | "OTIO_SCHEMA": "TimeRange.1", 32 | "duration": { 33 | "OTIO_SCHEMA": "RationalTime.1", 34 | "rate": 60, 35 | "value": 204 36 | }, 37 | "start_time": { 38 | "OTIO_SCHEMA": "RationalTime.1", 39 | "rate": 60, 40 | "value": 0 41 | } 42 | }, 43 | "name": "BoxingPractice" 44 | }, 45 | { 46 | "OTIO_SCHEMA": "Transition.1", 47 | "name": "Dissolve", 48 | "transition_type": "SMPTE_Dissolve", 49 | "in_offset": { 50 | "OTIO_SCHEMA": "RationalTime.1", 51 | "rate": 60, 52 | "value": 50 53 | }, 54 | "out_offset": { 55 | "OTIO_SCHEMA": "RationalTime.1", 56 | "rate": 60, 57 | "value": 50 58 | } 59 | }, 60 | { 61 | "OTIO_SCHEMA": "Clip.1", 62 | "media_reference": { 63 | "OTIO_SCHEMA": "ExternalReference.1", 64 | "available_range": { 65 | "OTIO_SCHEMA": "TimeRange.1", 66 | "duration": { 67 | "OTIO_SCHEMA": "RationalTime.1", 68 | "rate": 60, 69 | "value": 300 70 | }, 71 | "start_time": { 72 | "OTIO_SCHEMA": "RationalTime.1", 73 | "rate": 60, 74 | "value": 0 75 | } 76 | }, 77 | "target_url": "Netflix_FoodMarket2_4096x2160_60fps_10bit_420.y4m" 78 | }, 79 | "source_range": { 80 | "OTIO_SCHEMA": "TimeRange.1", 81 | "duration": { 82 | "OTIO_SCHEMA": "RationalTime.1", 83 | "rate": 60, 84 | "value": 200 85 | }, 86 | "start_time": { 87 | "OTIO_SCHEMA": "RationalTime.1", 88 | "rate": 60, 89 | "value": 50 90 | } 91 | }, 92 | "name": "FoodMarket2" 93 | }, 94 | { 95 | "OTIO_SCHEMA": "Transition.1", 96 | "name": "Dissolve", 97 | "transition_type": "SMPTE_Dissolve", 98 | "in_offset": { 99 | "OTIO_SCHEMA": "RationalTime.1", 100 | "rate": 60, 101 | "value": 50 102 | }, 103 | "out_offset": { 104 | "OTIO_SCHEMA": "RationalTime.1", 105 | "rate": 60, 106 | "value": 50 107 | } 108 | }, 109 | { 110 | "OTIO_SCHEMA": "Clip.1", 111 | "media_reference": { 112 | "OTIO_SCHEMA": "ExternalReference.1", 113 | "available_range": { 114 | "OTIO_SCHEMA": "TimeRange.1", 115 | "duration": { 116 | "OTIO_SCHEMA": "RationalTime.1", 117 | "rate": 60, 118 | "value": 300 119 | }, 120 | "start_time": { 121 | "OTIO_SCHEMA": "RationalTime.1", 122 | "rate": 60, 123 | "value": 0 124 | } 125 | }, 126 | "target_url": "Netflix_Narrator_4096x2160_60fps_10bit_420.y4m" 127 | }, 128 | "source_range": { 129 | "OTIO_SCHEMA": "TimeRange.1", 130 | "duration": { 131 | "OTIO_SCHEMA": "RationalTime.1", 132 | "rate": 60, 133 | "value": 200 134 | }, 135 | "start_time": { 136 | "OTIO_SCHEMA": "RationalTime.1", 137 | "rate": 60, 138 | "value": 50 139 | } 140 | }, 141 | "name": "Narrator" 142 | }, 143 | { 144 | "OTIO_SCHEMA": "Transition.1", 145 | "name": "Dissolve", 146 | "transition_type": "SMPTE_Dissolve", 147 | "in_offset": { 148 | "OTIO_SCHEMA": "RationalTime.1", 149 | "rate": 60, 150 | "value": 50 151 | }, 152 | "out_offset": { 153 | "OTIO_SCHEMA": "RationalTime.1", 154 | "rate": 60, 155 | "value": 50 156 | } 157 | }, 158 | { 159 | "OTIO_SCHEMA": "Clip.1", 160 | "media_reference": { 161 | "OTIO_SCHEMA": "ExternalReference.1", 162 | "available_range": { 163 | "OTIO_SCHEMA": "TimeRange.1", 164 | "duration": { 165 | "OTIO_SCHEMA": "RationalTime.1", 166 | "rate": 60, 167 | "value": 282 168 | }, 169 | "start_time": { 170 | "OTIO_SCHEMA": "RationalTime.1", 171 | "rate": 60, 172 | "value": 0 173 | } 174 | }, 175 | "target_url": "Netflix_RitualDanceShot_4096x2160_60fps_10bit_420.y4m" 176 | }, 177 | "source_range": { 178 | "OTIO_SCHEMA": "TimeRange.1", 179 | "duration": { 180 | "OTIO_SCHEMA": "RationalTime.1", 181 | "rate": 60, 182 | "value": 182 183 | }, 184 | "start_time": { 185 | "OTIO_SCHEMA": "RationalTime.1", 186 | "rate": 60, 187 | "value": 50 188 | } 189 | }, 190 | "name": "RitualDanceShot" 191 | }, 192 | { 193 | "OTIO_SCHEMA": "Transition.1", 194 | "name": "Dissolve", 195 | "transition_type": "SMPTE_Dissolve", 196 | "in_offset": { 197 | "OTIO_SCHEMA": "RationalTime.1", 198 | "rate": 60, 199 | "value": 50 200 | }, 201 | "out_offset": { 202 | "OTIO_SCHEMA": "RationalTime.1", 203 | "rate": 60, 204 | "value": 50 205 | } 206 | }, 207 | { 208 | "OTIO_SCHEMA": "Clip.1", 209 | "media_reference": { 210 | "OTIO_SCHEMA": "ExternalReference.1", 211 | "available_range": { 212 | "OTIO_SCHEMA": "TimeRange.1", 213 | "duration": { 214 | "OTIO_SCHEMA": "RationalTime.1", 215 | "rate": 60, 216 | "value": 294 217 | }, 218 | "start_time": { 219 | "OTIO_SCHEMA": "RationalTime.1", 220 | "rate": 60, 221 | "value": 0 222 | } 223 | }, 224 | "target_url": "Netflix_Tango_4096x2160_60fps_10bit_420.y4m" 225 | }, 226 | "source_range": { 227 | "OTIO_SCHEMA": "TimeRange.1", 228 | "duration": { 229 | "OTIO_SCHEMA": "RationalTime.1", 230 | "rate": 60, 231 | "value": 244 232 | }, 233 | "start_time": { 234 | "OTIO_SCHEMA": "RationalTime.1", 235 | "rate": 60, 236 | "value": 50 237 | } 238 | }, 239 | "name": "Tango" 240 | } 241 | ], 242 | "kind": "Video", 243 | "name": "Sequence" 244 | }, 245 | { 246 | "OTIO_SCHEMA": "Track.1", 247 | "children": [ 248 | { 249 | "OTIO_SCHEMA": "Clip.1", 250 | "media_reference": { 251 | "OTIO_SCHEMA": "ExternalReference.1", 252 | "available_range": { 253 | "OTIO_SCHEMA": "TimeRange.1", 254 | "duration": { 255 | "OTIO_SCHEMA": "RationalTime.1", 256 | "rate": 60, 257 | "value": 1 258 | }, 259 | "start_time": { 260 | "OTIO_SCHEMA": "RationalTime.1", 261 | "rate": 60, 262 | "value": 0 263 | } 264 | }, 265 | "target_url": "images/ElFuente_Overlay_BoxingPractice.png" 266 | }, 267 | "source_range": { 268 | "OTIO_SCHEMA": "TimeRange.1", 269 | "duration": { 270 | "OTIO_SCHEMA": "RationalTime.1", 271 | "rate": 60, 272 | "value": 204 273 | }, 274 | "start_time": { 275 | "OTIO_SCHEMA": "RationalTime.1", 276 | "rate": 60, 277 | "value": 0 278 | } 279 | }, 280 | "name": "Overlay" 281 | }, 282 | { 283 | "OTIO_SCHEMA": "Clip.1", 284 | "media_reference": { 285 | "OTIO_SCHEMA": "ExternalReference.1", 286 | "available_range": { 287 | "OTIO_SCHEMA": "TimeRange.1", 288 | "duration": { 289 | "OTIO_SCHEMA": "RationalTime.1", 290 | "rate": 60, 291 | "value": 1 292 | }, 293 | "start_time": { 294 | "OTIO_SCHEMA": "RationalTime.1", 295 | "rate": 60, 296 | "value": 0 297 | } 298 | }, 299 | "target_url": "images/ElFuente_Overlay_FoodMarket.png" 300 | }, 301 | "source_range": { 302 | "OTIO_SCHEMA": "TimeRange.1", 303 | "duration": { 304 | "OTIO_SCHEMA": "RationalTime.1", 305 | "rate": 60, 306 | "value": 200 307 | }, 308 | "start_time": { 309 | "OTIO_SCHEMA": "RationalTime.1", 310 | "rate": 60, 311 | "value": 0 312 | } 313 | }, 314 | "name": "Overlay" 315 | }, 316 | { 317 | "OTIO_SCHEMA": "Clip.1", 318 | "media_reference": { 319 | "OTIO_SCHEMA": "ExternalReference.1", 320 | "available_range": { 321 | "OTIO_SCHEMA": "TimeRange.1", 322 | "duration": { 323 | "OTIO_SCHEMA": "RationalTime.1", 324 | "rate": 60, 325 | "value": 1 326 | }, 327 | "start_time": { 328 | "OTIO_SCHEMA": "RationalTime.1", 329 | "rate": 60, 330 | "value": 0 331 | } 332 | }, 333 | "target_url": "images/ElFuente_Overlay_Narrator.png" 334 | }, 335 | "source_range": { 336 | "OTIO_SCHEMA": "TimeRange.1", 337 | "duration": { 338 | "OTIO_SCHEMA": "RationalTime.1", 339 | "rate": 60, 340 | "value": 200 341 | }, 342 | "start_time": { 343 | "OTIO_SCHEMA": "RationalTime.1", 344 | "rate": 60, 345 | "value": 0 346 | } 347 | }, 348 | "name": "Overlay" 349 | }, 350 | { 351 | "OTIO_SCHEMA": "Clip.1", 352 | "media_reference": { 353 | "OTIO_SCHEMA": "ExternalReference.1", 354 | "available_range": { 355 | "OTIO_SCHEMA": "TimeRange.1", 356 | "duration": { 357 | "OTIO_SCHEMA": "RationalTime.1", 358 | "rate": 60, 359 | "value": 1 360 | }, 361 | "start_time": { 362 | "OTIO_SCHEMA": "RationalTime.1", 363 | "rate": 60, 364 | "value": 0 365 | } 366 | }, 367 | "target_url": "images/ElFuente_Overlay_RitualDance.png" 368 | }, 369 | "source_range": { 370 | "OTIO_SCHEMA": "TimeRange.1", 371 | "duration": { 372 | "OTIO_SCHEMA": "RationalTime.1", 373 | "rate": 60, 374 | "value": 182 375 | }, 376 | "start_time": { 377 | "OTIO_SCHEMA": "RationalTime.1", 378 | "rate": 60, 379 | "value": 0 380 | } 381 | }, 382 | "name": "Overlay" 383 | }, 384 | { 385 | "OTIO_SCHEMA": "Clip.1", 386 | "media_reference": { 387 | "OTIO_SCHEMA": "ExternalReference.1", 388 | "available_range": { 389 | "OTIO_SCHEMA": "TimeRange.1", 390 | "duration": { 391 | "OTIO_SCHEMA": "RationalTime.1", 392 | "rate": 60, 393 | "value": 1 394 | }, 395 | "start_time": { 396 | "OTIO_SCHEMA": "RationalTime.1", 397 | "rate": 60, 398 | "value": 0 399 | } 400 | }, 401 | "target_url": "images/ElFuente_Overlay_Tango.png" 402 | }, 403 | "source_range": { 404 | "OTIO_SCHEMA": "TimeRange.1", 405 | "duration": { 406 | "OTIO_SCHEMA": "RationalTime.1", 407 | "rate": 60, 408 | "value": 244 409 | }, 410 | "start_time": { 411 | "OTIO_SCHEMA": "RationalTime.1", 412 | "rate": 60, 413 | "value": 0 414 | } 415 | }, 416 | "name": "Overlay" 417 | } 418 | ], 419 | "kind": "Video", 420 | "name": "Sequence" 421 | } 422 | ], 423 | "name": "NestedScope" 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /examples/timecode_rates.otio: -------------------------------------------------------------------------------- 1 | { 2 | "OTIO_SCHEMA": "Timeline.1", 3 | "metadata": {}, 4 | "name": "Timecode Rates", 5 | "global_start_time": { 6 | "OTIO_SCHEMA": "RationalTime.1", 7 | "rate": 24.0, 8 | "value": 0.0 9 | }, 10 | "tracks": { 11 | "OTIO_SCHEMA": "Stack.1", 12 | "metadata": {}, 13 | "name": "tracks", 14 | "source_range": null, 15 | "effects": [], 16 | "markers": [], 17 | "enabled": true, 18 | "children": [ 19 | { 20 | "OTIO_SCHEMA": "Track.1", 21 | "metadata": {}, 22 | "name": "24000/1001 fps", 23 | "source_range": null, 24 | "effects": [], 25 | "markers": [], 26 | "enabled": true, 27 | "children": [ 28 | { 29 | "OTIO_SCHEMA": "Clip.2", 30 | "metadata": {}, 31 | "name": "24000/1001 fps", 32 | "source_range": { 33 | "OTIO_SCHEMA": "TimeRange.1", 34 | "duration": { 35 | "OTIO_SCHEMA": "RationalTime.1", 36 | "rate": 23.976023976, 37 | "value": 24.0 38 | }, 39 | "start_time": { 40 | "OTIO_SCHEMA": "RationalTime.1", 41 | "rate": 23.976023976, 42 | "value": 0.0 43 | } 44 | }, 45 | "effects": [], 46 | "markers": [], 47 | "enabled": true, 48 | "media_references": { 49 | "DEFAULT_MEDIA": { 50 | "OTIO_SCHEMA": "ExternalReference.1", 51 | "metadata": {}, 52 | "name": "", 53 | "available_range": null, 54 | "available_image_bounds": null, 55 | "target_url": "" 56 | } 57 | }, 58 | "active_media_reference_key": "DEFAULT_MEDIA" 59 | } 60 | ], 61 | "kind": "Video" 62 | }, 63 | { 64 | "OTIO_SCHEMA": "Track.1", 65 | "metadata": {}, 66 | "name": "24 fps", 67 | "source_range": null, 68 | "effects": [], 69 | "markers": [], 70 | "enabled": true, 71 | "children": [ 72 | { 73 | "OTIO_SCHEMA": "Clip.2", 74 | "metadata": {}, 75 | "name": "24 fps", 76 | "source_range": { 77 | "OTIO_SCHEMA": "TimeRange.1", 78 | "duration": { 79 | "OTIO_SCHEMA": "RationalTime.1", 80 | "rate": 24.0, 81 | "value": 24.0 82 | }, 83 | "start_time": { 84 | "OTIO_SCHEMA": "RationalTime.1", 85 | "rate": 24.0, 86 | "value": 0.0 87 | } 88 | }, 89 | "effects": [], 90 | "markers": [], 91 | "enabled": true, 92 | "media_references": { 93 | "DEFAULT_MEDIA": { 94 | "OTIO_SCHEMA": "ExternalReference.1", 95 | "metadata": {}, 96 | "name": "", 97 | "available_range": null, 98 | "available_image_bounds": null, 99 | "target_url": "" 100 | } 101 | }, 102 | "active_media_reference_key": "DEFAULT_MEDIA" 103 | } 104 | ], 105 | "kind": "Video" 106 | }, 107 | { 108 | "OTIO_SCHEMA": "Track.1", 109 | "metadata": {}, 110 | "name": "30000/1001 fps", 111 | "source_range": null, 112 | "effects": [], 113 | "markers": [], 114 | "enabled": true, 115 | "children": [ 116 | { 117 | "OTIO_SCHEMA": "Clip.2", 118 | "metadata": {}, 119 | "name": "30000/1001 fps", 120 | "source_range": { 121 | "OTIO_SCHEMA": "TimeRange.1", 122 | "duration": { 123 | "OTIO_SCHEMA": "RationalTime.1", 124 | "rate": 29.97002997, 125 | "value": 30.0 126 | }, 127 | "start_time": { 128 | "OTIO_SCHEMA": "RationalTime.1", 129 | "rate": 29.97002997, 130 | "value": 0.0 131 | } 132 | }, 133 | "effects": [], 134 | "markers": [], 135 | "enabled": true, 136 | "media_references": { 137 | "DEFAULT_MEDIA": { 138 | "OTIO_SCHEMA": "ExternalReference.1", 139 | "metadata": {}, 140 | "name": "", 141 | "available_range": null, 142 | "available_image_bounds": null, 143 | "target_url": "" 144 | } 145 | }, 146 | "active_media_reference_key": "DEFAULT_MEDIA" 147 | } 148 | ], 149 | "kind": "Video" 150 | }, 151 | { 152 | "OTIO_SCHEMA": "Track.1", 153 | "metadata": {}, 154 | "name": "30 fps", 155 | "source_range": null, 156 | "effects": [], 157 | "markers": [], 158 | "enabled": true, 159 | "children": [ 160 | { 161 | "OTIO_SCHEMA": "Clip.2", 162 | "metadata": {}, 163 | "name": "30 fps", 164 | "source_range": { 165 | "OTIO_SCHEMA": "TimeRange.1", 166 | "duration": { 167 | "OTIO_SCHEMA": "RationalTime.1", 168 | "rate": 30.0, 169 | "value": 30.0 170 | }, 171 | "start_time": { 172 | "OTIO_SCHEMA": "RationalTime.1", 173 | "rate": 30.0, 174 | "value": 0.0 175 | } 176 | }, 177 | "effects": [], 178 | "markers": [], 179 | "enabled": true, 180 | "media_references": { 181 | "DEFAULT_MEDIA": { 182 | "OTIO_SCHEMA": "ExternalReference.1", 183 | "metadata": {}, 184 | "name": "", 185 | "available_range": null, 186 | "available_image_bounds": null, 187 | "target_url": "" 188 | } 189 | }, 190 | "active_media_reference_key": "DEFAULT_MEDIA" 191 | } 192 | ], 193 | "kind": "Video" 194 | }, 195 | { 196 | "OTIO_SCHEMA": "Track.1", 197 | "metadata": {}, 198 | "name": "60000/1001 fps", 199 | "source_range": null, 200 | "effects": [], 201 | "markers": [], 202 | "enabled": true, 203 | "children": [ 204 | { 205 | "OTIO_SCHEMA": "Clip.2", 206 | "metadata": {}, 207 | "name": "60000/1001 fps", 208 | "source_range": { 209 | "OTIO_SCHEMA": "TimeRange.1", 210 | "duration": { 211 | "OTIO_SCHEMA": "RationalTime.1", 212 | "rate": 59.9400599401, 213 | "value": 60.0 214 | }, 215 | "start_time": { 216 | "OTIO_SCHEMA": "RationalTime.1", 217 | "rate": 59.9400599401, 218 | "value": 0.0 219 | } 220 | }, 221 | "effects": [], 222 | "markers": [], 223 | "enabled": true, 224 | "media_references": { 225 | "DEFAULT_MEDIA": { 226 | "OTIO_SCHEMA": "ExternalReference.1", 227 | "metadata": {}, 228 | "name": "", 229 | "available_range": null, 230 | "available_image_bounds": null, 231 | "target_url": "" 232 | } 233 | }, 234 | "active_media_reference_key": "DEFAULT_MEDIA" 235 | } 236 | ], 237 | "kind": "Video" 238 | }, 239 | { 240 | "OTIO_SCHEMA": "Track.1", 241 | "metadata": {}, 242 | "name": "60 fps", 243 | "source_range": null, 244 | "effects": [], 245 | "markers": [], 246 | "enabled": true, 247 | "children": [ 248 | { 249 | "OTIO_SCHEMA": "Clip.2", 250 | "metadata": {}, 251 | "name": "60 fps", 252 | "source_range": { 253 | "OTIO_SCHEMA": "TimeRange.1", 254 | "duration": { 255 | "OTIO_SCHEMA": "RationalTime.1", 256 | "rate": 60.0, 257 | "value": 60.0 258 | }, 259 | "start_time": { 260 | "OTIO_SCHEMA": "RationalTime.1", 261 | "rate": 60.0, 262 | "value": 0.0 263 | } 264 | }, 265 | "effects": [], 266 | "markers": [], 267 | "enabled": true, 268 | "media_references": { 269 | "DEFAULT_MEDIA": { 270 | "OTIO_SCHEMA": "ExternalReference.1", 271 | "metadata": {}, 272 | "name": "", 273 | "available_range": null, 274 | "available_image_bounds": null, 275 | "target_url": "" 276 | } 277 | }, 278 | "active_media_reference_key": "DEFAULT_MEDIA" 279 | } 280 | ], 281 | "kind": "Video" 282 | } 283 | ] 284 | } 285 | } -------------------------------------------------------------------------------- /fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Matthias Tellen matthias.tellen@googlemail.com, 2 | with Reserved Font Name monoOne. 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | -------------------------------------------------------------------------------- /fonts/mononoki-Regular Nerd Font Complete.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/raven/6d21529b6b0560e35870bc52ffd49ac530b7b81d/fonts/mononoki-Regular Nerd Font Complete.ttf -------------------------------------------------------------------------------- /fonts/readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Nerd Fonts 3 | 4 | This is an archived font from a Nerd Fonts release. 5 | 6 | For more information see: 7 | * https://github.com/ryanoasis/nerd-fonts/ 8 | * https://github.com/ryanoasis/nerd-fonts/releases/latest/ 9 | -------------------------------------------------------------------------------- /inspector.cpp: -------------------------------------------------------------------------------- 1 | // Inspector 2 | 3 | #include "inspector.h" 4 | #include "app.h" 5 | #include "libs/imgui/imgui.h" 6 | #include "widgets.h" 7 | #include "editing.h" 8 | #include "colors.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | static const char* marker_color_names[] = { 24 | "PINK", "RED", "ORANGE", "YELLOW", 25 | "GREEN", "CYAN", "BLUE", "PURPLE", 26 | "MAGENTA", "BLACK", "WHITE" 27 | }; 28 | 29 | const TextEditor::LanguageDefinition& OTIOLanguageDef() 30 | { 31 | static bool inited = false; 32 | static TextEditor::LanguageDefinition langDef; 33 | if (!inited) 34 | { 35 | static const char* const keywords[] = { 36 | "true", "false", "null" 37 | }; 38 | 39 | for (auto& k : keywords) 40 | langDef.mKeywords.insert(k); 41 | 42 | static const char* const identifiers[] = { 43 | "\"OTIO_SCHEMA\"" 44 | }; 45 | for (auto& k : identifiers) 46 | { 47 | TextEditor::Identifier id; 48 | id.mDeclaration = "OpenTimelineIO Schema"; 49 | langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); 50 | } 51 | 52 | langDef.mTokenRegexStrings.push_back(std::make_pair("\\\"OTIO_SCHEMA\\\"", TextEditor::PaletteIndex::Identifier)); 53 | langDef.mTokenRegexStrings.push_back(std::make_pair("\\\"(\\\\.|[^\\\"])*\\\"", TextEditor::PaletteIndex::String)); 54 | langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?", TextEditor::PaletteIndex::Number)); 55 | langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", TextEditor::PaletteIndex::Identifier)); 56 | langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\,\\:]", TextEditor::PaletteIndex::Punctuation)); 57 | 58 | langDef.mCommentStart = "/*"; 59 | langDef.mCommentEnd = "*/"; 60 | langDef.mSingleLineComment = "//"; 61 | 62 | langDef.mCaseSensitive = true; 63 | langDef.mAutoIndentation = true; 64 | 65 | langDef.mName = "JSON"; 66 | 67 | inited = true; 68 | } 69 | return langDef; 70 | } 71 | 72 | TextEditor jsonEditor; 73 | TextEditor::LanguageDefinition otioLangDef = OTIOLanguageDef(); 74 | bool json_rendered = false; 75 | bool json_edited = false; 76 | std::string json_error_message; 77 | int json_error_line = -1; 78 | 79 | void UpdateJSONInspector() { 80 | jsonEditor.SetReadOnly(false); 81 | jsonEditor.SetLanguageDefinition(otioLangDef); 82 | jsonEditor.SetText(appState.selected_text); 83 | jsonEditor.SetErrorMarkers({}); 84 | json_rendered = false; 85 | json_edited = false; 86 | json_error_message = ""; 87 | json_error_line = -1; 88 | } 89 | 90 | void SetJSONErrorMessage(std::string message) { 91 | // Look for a pattern like "(line 123, column 45)" or 92 | // "near line 54" in the error message and extract the line number. 93 | std::regex line_number_regex("[ (]line ([0-9]+)"); 94 | std::smatch match; 95 | if (std::regex_search(message, match, line_number_regex)) { 96 | json_error_line = std::stoi(match[1]); 97 | } else { 98 | json_error_line = -1; 99 | } 100 | 101 | // Messages can be quite long, so let's line wrap 102 | // the message at 80 characters, preserving words. 103 | std::string wrapped_message; 104 | int line_length = 0; 105 | for (char c : message) { 106 | if (c == '\n') { 107 | line_length = 0; 108 | } else { 109 | line_length++; 110 | } 111 | if (line_length > 80 && c == ' ') { 112 | wrapped_message += '\n'; 113 | line_length = 0; 114 | } 115 | wrapped_message += c; 116 | } 117 | 118 | if (json_error_line >= 0) { 119 | jsonEditor.SetErrorMarkers({ { json_error_line, wrapped_message } }); 120 | } else { 121 | jsonEditor.SetErrorMarkers({}); 122 | } 123 | 124 | json_error_message = wrapped_message; 125 | 126 | ErrorMessage("%s", json_error_message.c_str()); 127 | } 128 | 129 | void DrawJSONApplyEditButtons() { 130 | if (ImGui::Button("Apply")) { 131 | otio::ErrorStatus error_status; 132 | auto replacement_json = jsonEditor.GetText(); 133 | auto replacement_object = otio::SerializableObject::from_json_string(replacement_json, &error_status); 134 | if (is_error(error_status)) { 135 | auto message = otio_error_string(error_status); 136 | SetJSONErrorMessage(message); 137 | } else 138 | if (replacement_object == nullptr) { 139 | SetJSONErrorMessage("Error parsing JSON: Nil object result."); 140 | } else { 141 | auto success = ReplaceObject(appState.selected_object, replacement_object); 142 | if (success) { 143 | SelectObject(replacement_object); 144 | UpdateJSONInspector(); 145 | Message("Edits applied."); 146 | } 147 | } 148 | } 149 | 150 | ImGui::SameLine(); 151 | 152 | if (ImGui::Button("Revert")) { 153 | UpdateJSONInspector(); 154 | Message("Edits reverted."); 155 | } 156 | } 157 | 158 | void DrawJSONInspector() { 159 | // Check if the text was edited this frame. 160 | // Note that IsTextChanged() is true only until Render is called. 161 | // We have to also check if Render was called since the text 162 | // was last set via SetText() inside UpdateJSONInspector(). 163 | if (json_rendered && jsonEditor.IsTextChanged()) { 164 | json_edited = true; 165 | } 166 | 167 | auto available_size = ImGui::GetContentRegionAvail(); 168 | available_size.y -= ImGui::GetFrameHeightWithSpacing(); 169 | jsonEditor.Render("JSON",false, available_size); 170 | json_rendered = true; 171 | 172 | if (json_edited) { 173 | DrawJSONApplyEditButtons(); 174 | } else { 175 | ImGui::BeginDisabled(); 176 | DrawJSONApplyEditButtons(); 177 | ImGui::EndDisabled(); 178 | } 179 | 180 | } 181 | 182 | void DrawNonEditableTextField(const char* label, const char* format, ...) { 183 | char tmp_str[1000]; 184 | 185 | va_list args; 186 | va_start(args, format); 187 | vsnprintf(tmp_str, sizeof(tmp_str), format, args); 188 | va_end(args); 189 | 190 | // Adjust style so the user can see that it is not editable 191 | ImGui::PushStyleColor( 192 | ImGuiCol_FrameBg, 193 | ImGui::GetStyleColorVec4(ImGuiCol_TableHeaderBg)); 194 | ImGui::InputText( 195 | label, 196 | tmp_str, 197 | sizeof(tmp_str), 198 | ImGuiInputTextFlags_ReadOnly); 199 | ImGui::PopStyleColor(); 200 | } 201 | 202 | std::string DrawColorChooser(std::string current_color_name) 203 | { 204 | const char** color_choices = marker_color_names; 205 | int num_color_choices = IM_ARRAYSIZE(marker_color_names); 206 | 207 | int current_index = -1; 208 | for (int i = 0; i < num_color_choices; i++) { 209 | if (current_color_name == color_choices[i]) { 210 | current_index = i; 211 | break; 212 | } 213 | } 214 | if (ImGui::Combo("Color", ¤t_index, color_choices, num_color_choices)) { 215 | if (current_index >= 0 && current_index < num_color_choices) { 216 | return color_choices[current_index]; 217 | } 218 | } 219 | return ""; 220 | } 221 | 222 | bool DrawRationalTime( 223 | const char* label, 224 | otio::RationalTime* time, 225 | bool allow_negative = false) { 226 | if (time == NULL) 227 | return false; 228 | auto formatted = FormattedStringFromTime(*time); 229 | if (appState.snap_to_frames) { 230 | int val = floor(time->value()); // snap with floor() 231 | if (ImGui::DragInt( 232 | label, 233 | &val, 234 | 0.1, 235 | allow_negative ? INT_MIN : 0, 236 | INT_MAX, 237 | formatted.c_str())) { 238 | *time = otio::RationalTime(val, time->rate()); 239 | return true; 240 | } 241 | } else { 242 | float val = time->value(); 243 | if (ImGui::DragFloat( 244 | label, 245 | &val, 246 | 0.01, 247 | allow_negative ? -FLT_MAX : 0, 248 | FLT_MAX, 249 | formatted.c_str())) { 250 | *time = otio::RationalTime(val, time->rate()); 251 | return true; 252 | } 253 | } 254 | return false; 255 | /* 256 | float vals[2]; 257 | vals[0] = time->value(); 258 | vals[1] = time->rate(); 259 | if (ImGui::DragFloat2( 260 | label, 261 | vals, 262 | 0.01, 263 | allow_negative ? -FLT_MAX : 0, FLT_MAX, 264 | FormattedStringFromTime(*time, false).c_str())) 265 | { 266 | *time = otio::RationalTime(vals[0], vals[1]); 267 | return true; 268 | } 269 | return false; 270 | */ 271 | } 272 | 273 | bool DrawTimeRange( 274 | const char* label, 275 | otio::TimeRange* range, 276 | bool allow_negative = false) { 277 | if (range == NULL) 278 | return false; 279 | 280 | otio::RationalTime start = range->start_time(); 281 | otio::RationalTime duration = range->duration(); 282 | 283 | bool changed = false; 284 | 285 | ImGui::TextUnformatted(label); 286 | ImGui::Indent(); 287 | 288 | char buf[100]; 289 | snprintf(buf, sizeof(buf), "Start##%s", label); 290 | if (DrawRationalTime(buf, &start, allow_negative)) { 291 | changed = true; 292 | } 293 | snprintf(buf, sizeof(buf), "Duration##%s", label); 294 | if (DrawRationalTime(buf, &duration, false)) { // never negative 295 | changed = true; 296 | } 297 | snprintf(buf, sizeof(buf), "End##%s", label); 298 | DrawNonEditableTextField( 299 | buf, 300 | "%s", 301 | FormattedStringFromTime(range->end_time_inclusive()).c_str()); 302 | 303 | ImGui::Unindent(); 304 | 305 | if (changed) { 306 | *range = otio::TimeRange(start, duration); 307 | } 308 | return changed; 309 | } 310 | 311 | void DrawMetadataSubtree(std::string key, otio::AnyDictionary& metadata); 312 | 313 | void DrawMetadataArray(std::string key, otio::AnyVector& vector); 314 | 315 | void DrawMetadataRow(std::string key, std::any& value) { 316 | std::type_info const& type = value.type(); 317 | 318 | if (type == typeid(otio::AnyDictionary)) { 319 | auto dict = std::any_cast(value); 320 | DrawMetadataSubtree(key, dict); 321 | return; 322 | } 323 | 324 | if (type == typeid(otio::AnyVector)) { 325 | auto vector = std::any_cast(value); 326 | DrawMetadataArray(key, vector); 327 | return; 328 | } 329 | 330 | std::string type_name = ""; 331 | std::string string_val = ""; 332 | 333 | otio::SerializableObject *selectable = nullptr; 334 | 335 | if (type == typeid(std::string)) { 336 | type_name = "string"; 337 | string_val = std::any_cast(value); 338 | } 339 | else if (type == typeid(bool)) { 340 | type_name = "bool"; 341 | string_val = std::any_cast(value) ? "true" : "false"; 342 | } 343 | else if (type == typeid(int64_t)) { 344 | type_name = "int64_t"; 345 | string_val = std::to_string(std::any_cast(value)); 346 | } 347 | else if (type == typeid(double)) { 348 | type_name = "double"; 349 | string_val = std::to_string(std::any_cast(value)); 350 | } 351 | else if (type == typeid(otio::RationalTime)) { 352 | type_name = "otio::RationalTime"; 353 | auto time = std::any_cast(value); 354 | string_val = FormattedStringFromTime(time); 355 | } 356 | else if (type == typeid(otio::SerializableObject::Retainer)) { 357 | auto obj = std::any_cast>(value); 358 | type_name = Format("%s.%d", obj->schema_name().c_str(), obj->schema_version()); 359 | string_val = obj->name(); 360 | selectable = &*obj; 361 | } 362 | else if (type == typeid(otio::SerializableObject::Retainer)) { 363 | auto obj = std::any_cast>(value); 364 | type_name = Format("%s.%d", obj->schema_name().c_str(), obj->schema_version()); 365 | selectable = &*obj; 366 | } 367 | 368 | // TODO: Handle these types also? 369 | // TimeRange 370 | // TimeTransform 371 | // void 372 | // char const * 373 | // Imath::V2d 374 | // Imath::Box2d 375 | // SerializableObject::Retainer 376 | 377 | ImGui::TableNextRow(); 378 | ImGui::TableNextColumn(); 379 | ImGui::TreeNodeEx( 380 | key.c_str(), 381 | ImGuiTreeNodeFlags_Leaf | 382 | //ImGuiTreeNodeFlags_Bullet | 383 | ImGuiTreeNodeFlags_NoTreePushOnOpen 384 | | ImGuiTreeNodeFlags_SpanFullWidth | 0); 385 | 386 | ImGui::TableNextColumn(); 387 | if (selectable != nullptr) { 388 | ImGui::PushID(selectable); 389 | if (ImGui::Button(string_val.c_str())) { 390 | SelectObject(selectable); 391 | } 392 | ImGui::PopID(); 393 | }else{ 394 | ImGui::TextUnformatted(string_val.c_str()); 395 | } 396 | 397 | ImGui::TableNextColumn(); 398 | ImGui::TextUnformatted(type_name.c_str()); 399 | } 400 | 401 | void DrawMetadataRows(otio::AnyDictionary& metadata) { 402 | for (auto& pair : metadata) { 403 | const auto& key = pair.first; 404 | auto& val = pair.second; 405 | 406 | DrawMetadataRow(key, val); 407 | } 408 | } 409 | 410 | void DrawMetadataSubtree(std::string key, otio::AnyDictionary& metadata) { 411 | ImGui::TableNextRow(); 412 | ImGui::TableNextColumn(); 413 | bool open = ImGui::TreeNodeEx( 414 | key.c_str(), 415 | ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanFullWidth); 416 | 417 | ImGui::TableNextColumn(); 418 | ImGui::TextDisabled(""); 419 | 420 | ImGui::TableNextColumn(); 421 | ImGui::TextDisabled(""); 422 | 423 | if (open) { 424 | DrawMetadataRows(metadata); 425 | 426 | ImGui::TreePop(); 427 | } 428 | } 429 | 430 | void DrawMetadataArray(std::string key, otio::AnyVector& vector) { 431 | ImGui::TableNextRow(); 432 | ImGui::TableNextColumn(); 433 | bool open = ImGui::TreeNodeEx( 434 | key.c_str(), 435 | ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanFullWidth); 436 | 437 | ImGui::TableNextColumn(); 438 | ImGui::TextDisabled(""); 439 | 440 | ImGui::TableNextColumn(); 441 | ImGui::TextDisabled(""); 442 | 443 | if (open) { 444 | for (int i = 0; i < vector.size(); i++) { 445 | DrawMetadataRow(Format("[%d]", i), vector[i]); 446 | } 447 | 448 | ImGui::TreePop(); 449 | } 450 | } 451 | void DrawMetadataTable(otio::AnyDictionary& metadata) { 452 | static ImGuiTableFlags flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH 453 | | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg 454 | | ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_Hideable; 455 | 456 | if (ImGui::BeginTable("Metadata", 3, flags)) { 457 | ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_NoHide); 458 | ImGui::TableSetupColumn("Value"); 459 | ImGui::TableSetupColumn("Type"); 460 | ImGui::TableHeadersRow(); 461 | 462 | DrawMetadataRows(metadata); 463 | 464 | ImGui::EndTable(); 465 | } 466 | } 467 | 468 | void DrawLinearTimeWarp(otio::LinearTimeWarp* timewarp, otio::Item* item) { 469 | auto item_range = item->trimmed_range(); 470 | auto time_scalar = timewarp->time_scalar(); 471 | auto media_start = item_range.start_time(); 472 | auto media_duration = otio::RationalTime( 473 | item_range.duration().value() * time_scalar, 474 | item_range.duration().rate()); 475 | auto media_range = otio::TimeRange(media_start, media_duration); 476 | 477 | // Special case for reverse timewarp 478 | // TODO: Test to see if this works the way real NLEs handle reverse 479 | // TODO: Specifically at -0.25 do you get the first 25% of media or the last? 480 | if (time_scalar < 0.0f) { 481 | // Reminder: media_duration is already negative 482 | media_range = otio::TimeRange(media_start - media_duration, media_duration); 483 | } 484 | 485 | // x = output in clip/item time 486 | // y = input in media time 487 | 488 | ImPlotPoint control_points[2] { 489 | ImPlotPoint(0, media_range.start_time().to_seconds()), 490 | ImPlotPoint( 491 | item_range.duration().to_seconds(), 492 | media_range.end_time_exclusive().to_seconds()) 493 | }; 494 | ImPlotPoint* start = &control_points[0]; 495 | ImPlotPoint* end = &control_points[1]; 496 | 497 | // Helpful when debugging this code... 498 | // ImGui::Text("Start: %f, %f", start->x, start->y); 499 | // ImGui::Text("End: %f, %f", end->x, end->y); 500 | // ImGui::Text("Duration: %f", media_range.duration().to_seconds()); 501 | 502 | const float line_width = 2; 503 | const float knot_radius = 4; 504 | const ImColor line_color = appTheme.colors[AppThemeCol_Item]; 505 | const ImColor knot_color = appTheme.colors[AppThemeCol_ItemSelected]; 506 | 507 | ImPlotFlags plot_flags = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoInputs 508 | | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoChild 509 | | ImPlotFlags_NoFrame | ImPlotFlags_Equal | ImPlotFlags_None; 510 | ImPlotDragToolFlags drag_flags = ImPlotDragToolFlags_NoInputs 511 | | ImPlotDragToolFlags_None; 512 | ImPlotAxisFlags ax_flags = ImPlotAxisFlags_Lock | ImPlotAxisFlags_None; 513 | if (ImPlot::BeginPlot("##LinearTimeWarp", ImVec2(-1, 0), plot_flags)) { 514 | ImPlot::SetupAxes("Output", "Media", ax_flags, ax_flags); 515 | ImPlot::SetupAxesLimits( 516 | fmin(start->x, end->x), 517 | fmax(start->x, end->x), 518 | fmin(start->y, end->y), 519 | fmax(start->y, end->y), 520 | ImGuiCond_Always); 521 | 522 | ImPlot::SetNextLineStyle(line_color, line_width); 523 | ImPlot::PlotLine( 524 | "##Line", 525 | &start->x, 526 | &start->y, 527 | 2, 528 | 0, 529 | 0, 530 | sizeof(ImPlotPoint)); 531 | 532 | // start handle 533 | ImPlot::SetNextLineStyle(knot_color); 534 | if (ImPlot::DragPoint( 535 | 0, 536 | &start->x, 537 | &start->y, 538 | ImVec4(0, 0.9f, 0, 1), 539 | knot_radius, 540 | drag_flags)) { 541 | ; 542 | } 543 | 544 | // end handle 545 | ImPlot::SetNextLineStyle(knot_color); 546 | if (ImPlot::DragPoint( 547 | 3, 548 | &end->x, 549 | &end->y, 550 | ImVec4(0, 0.9f, 0, 1), 551 | knot_radius, 552 | drag_flags)) { 553 | ; 554 | } 555 | 556 | ImPlot::EndPlot(); 557 | } 558 | } 559 | 560 | void DrawInspector() { 561 | auto selected_object = appState.selected_object; 562 | auto selected_context = appState.selected_context; 563 | 564 | auto playhead = appState.playhead; 565 | 566 | if (!selected_object) { 567 | ImGui::Text("Nothing selected."); 568 | return; 569 | } 570 | 571 | // This temporary variable is used only for a moment to convert 572 | // between the datatypes that OTIO uses vs the one that ImGui widget uses. 573 | char tmp_str[1000]; 574 | 575 | // If the selected Item has effects, lets show them in-line 576 | // so the user doesn't have to click on each one separately 577 | std::vector> effects; 578 | 579 | // SerializableObjectWithMetadata 580 | if (const auto& obj = dynamic_cast( 581 | selected_object)) { 582 | snprintf(tmp_str, sizeof(tmp_str), "%s", obj->name().c_str()); 583 | if (ImGui::InputText("Name", tmp_str, sizeof(tmp_str))) { 584 | obj->set_name(tmp_str); 585 | } 586 | } 587 | 588 | // SerializableObject (everything) 589 | DrawNonEditableTextField( 590 | "Schema", 591 | "%s.%d", 592 | selected_object->schema_name().c_str(), 593 | selected_object->schema_version()); 594 | 595 | // Timeline 596 | if (const auto& timeline = dynamic_cast(selected_object)) { 597 | // Since global_start_time is optional, default to 0 598 | // but take care not to *set* the value unless the user changes it. 599 | auto rate = timeline->global_start_time().value_or(playhead).rate(); 600 | auto global_start_time = timeline->global_start_time().value_or(otio::RationalTime(0, rate)); 601 | // don't allow negative duration - but 0 is okay 602 | if (DrawRationalTime("Global Start", &global_start_time, true)) { 603 | timeline->set_global_start_time(global_start_time); 604 | DetectPlayheadLimits(); 605 | } 606 | } 607 | 608 | // Item 609 | if (const auto& item = dynamic_cast(selected_object)) { 610 | bool is_gap = dynamic_cast(selected_object); 611 | 612 | if (!is_gap) { 613 | auto item_color = GetItemColor(item); 614 | item_color = DrawColorChooser(item_color); 615 | if (item_color != "") { 616 | SetItemColor(item, item_color); 617 | } 618 | } 619 | 620 | auto trimmed_range = item->trimmed_range(); 621 | if (DrawTimeRange("Trimmed Range", &trimmed_range, true)) { 622 | item->set_source_range(trimmed_range); 623 | } 624 | // Grab the effects list so we can display it later 625 | effects = item->effects(); 626 | } 627 | 628 | // Composition 629 | if (const auto& comp = dynamic_cast(selected_object)) { 630 | DrawNonEditableTextField("Children", "%ld", comp->children().size()); 631 | } 632 | 633 | // Transition 634 | if (const auto& transition = dynamic_cast(selected_object)) { 635 | auto in_offset = transition->in_offset(); 636 | if (DrawRationalTime("In Offset", &in_offset, false)) { 637 | transition->set_in_offset(in_offset); 638 | } 639 | 640 | auto out_offset = transition->out_offset(); 641 | if (DrawRationalTime("Out Offset", &out_offset, false)) { 642 | transition->set_out_offset(out_offset); 643 | } 644 | 645 | DrawNonEditableTextField( 646 | "Duration", 647 | "%s", 648 | FormattedStringFromTime(transition->duration()).c_str()); 649 | } 650 | 651 | // Effects - either 1 selected, or a list of effects 652 | auto effect_context = selected_context; 653 | if (const auto& effect = dynamic_cast(selected_object)) { 654 | // Just one 655 | effects.push_back(effect); 656 | } else { 657 | // A list of effects inside this object 658 | effect_context = selected_object; 659 | } 660 | for (const auto effect : effects) { 661 | ImGui::Text("Effect Name: %s", effect->effect_name().c_str()); 662 | if (const auto& timewarp = dynamic_cast(effect.value)) { 663 | float val = timewarp->time_scalar(); 664 | if (ImGui::DragFloat("Time Scale", &val, 0.01, -FLT_MAX, FLT_MAX)) { 665 | timewarp->set_time_scalar(val); 666 | } 667 | if (const auto& item = dynamic_cast(effect_context)) { 668 | DrawLinearTimeWarp(timewarp, item); 669 | } 670 | } 671 | } 672 | 673 | // Marker 674 | if (const auto& marker = dynamic_cast(selected_object)) { 675 | auto rate = marker->marked_range().start_time().rate(); 676 | 677 | auto color_name = DrawColorChooser(marker->color()); 678 | if (color_name != "") { 679 | marker->set_color(color_name); 680 | } 681 | 682 | ImGui::SameLine(); 683 | ImGui::PushStyleColor(ImGuiCol_Text, UIColorFromName(marker->color())); 684 | ImGui::TextUnformatted("\xef\x80\xab"); 685 | ImGui::PopStyleColor(); 686 | 687 | auto marked_range = marker->marked_range(); 688 | if (DrawTimeRange("Marked Range", &marked_range, false)) { 689 | marker->set_marked_range(marked_range); 690 | } 691 | } 692 | 693 | // Track 694 | if (const auto& track = dynamic_cast(selected_object)) { 695 | DrawNonEditableTextField("Kind", "%s", track->kind().c_str()); 696 | } 697 | 698 | // SerializableObjectWithMetadata 699 | if (const auto& obj = dynamic_cast( 700 | selected_object)) { 701 | auto& metadata = obj->metadata(); 702 | 703 | ImGui::TextUnformatted("Metadata:"); 704 | 705 | DrawMetadataTable(metadata); 706 | } 707 | } 708 | 709 | void DrawMarkersInspector() { 710 | // This temporary variable is used only for a moment to convert 711 | // between the datatypes that OTIO uses vs the one that ImGui widget uses. 712 | char tmp_str[1000]; 713 | 714 | typedef std::pair, otio::SerializableObject::Retainer> marker_parent_pair; 715 | std::vector pairs; 716 | 717 | auto root = new otio::Stack(); 718 | auto global_start = otio::RationalTime(0.0); 719 | 720 | if (const auto& timeline = dynamic_cast(appState.root.value)) { 721 | root = timeline->tracks(); 722 | global_start = timeline->global_start_time().value_or(otio::RationalTime()); 723 | 724 | for (const auto& marker : root->markers()) { 725 | pairs.push_back(marker_parent_pair(marker, root)); 726 | } 727 | 728 | for (const auto& child : 729 | timeline->tracks()->find_children()) 730 | { 731 | if (const auto& item = dynamic_cast(&*child)) 732 | { 733 | for (const auto& marker : item->markers()) { 734 | pairs.push_back(marker_parent_pair(marker, item)); 735 | } 736 | } 737 | } 738 | } 739 | 740 | auto selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap; 741 | 742 | if (ImGui::BeginTable("Markers", 743 | 5, 744 | // ImGuiTableFlags_Sortable | 745 | ImGuiTableFlags_NoSavedSettings | 746 | ImGuiTableFlags_Resizable | 747 | ImGuiTableFlags_Reorderable | 748 | ImGuiTableFlags_Hideable | 749 | ImGuiTableFlags_RowBg | 750 | ImGuiTableFlags_ScrollY)) 751 | { 752 | ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible 753 | ImGui::TableSetupColumn("Local Time", ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_WidthFixed); 754 | ImGui::TableSetupColumn("Global Time", ImGuiTableColumnFlags_WidthFixed); 755 | ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_WidthFixed); 756 | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); 757 | ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); 758 | 759 | ImGui::TableHeadersRow(); 760 | 761 | ImGuiListClipper marker_clipper; 762 | 763 | marker_clipper.Begin(pairs.size()); 764 | 765 | while(marker_clipper.Step()) 766 | { 767 | for (int row = marker_clipper.DisplayStart; row < marker_clipper.DisplayEnd; row++) 768 | { 769 | auto pair = pairs.at(row); 770 | auto marker = pair.first; 771 | auto parent = pair.second; 772 | 773 | ImGui::PushID(marker.value); 774 | ImGui::TableNextRow(); 775 | 776 | // Local Time 777 | ImGui::TableNextColumn(); 778 | 779 | auto range = marker->marked_range(); 780 | ImGui::TextUnformatted(TimecodeStringFromTime(range.start_time()).c_str()); 781 | 782 | // Global Time 783 | ImGui::TableNextColumn(); 784 | 785 | auto global_time = parent->transformed_time(range.start_time(), root) + global_start; 786 | 787 | // Make this row selectable & jump the playhead when clicked 788 | auto is_selected = 789 | (appState.selected_object == marker) || 790 | (appState.selected_object == parent); 791 | if (ImGui::Selectable(TimecodeStringFromTime(global_time).c_str(), 792 | is_selected, 793 | selectable_flags)) { 794 | appState.playhead = global_time; 795 | SelectObject(marker, parent); 796 | appState.scroll_to_playhead = true; 797 | } 798 | 799 | // Duration 800 | ImGui::TableNextColumn(); 801 | 802 | auto duration = range.duration(); 803 | ImGui::TextUnformatted(TimecodeStringFromTime(duration).c_str()); 804 | 805 | // Color + Name 806 | ImGui::TableNextColumn(); 807 | 808 | ImGui::PushStyleColor(ImGuiCol_Text, UIColorFromName(marker->color())); 809 | ImGui::TextUnformatted("\xef\x80\xab"); 810 | ImGui::PopStyleColor(); 811 | ImGui::SameLine(); 812 | 813 | ImGui::TextUnformatted(marker->name().c_str()); 814 | 815 | // Item 816 | ImGui::TableNextColumn(); 817 | 818 | ImGui::TextUnformatted(parent->name().c_str()); 819 | 820 | ImGui::PopID(); 821 | } 822 | } 823 | } 824 | ImGui::EndTable(); 825 | } 826 | 827 | void DrawEffectsInspector() { 828 | typedef std::pair, otio::SerializableObject::Retainer> effect_parent_pair; 829 | std::vector pairs; 830 | 831 | auto root = new otio::Stack(); 832 | auto global_start = otio::RationalTime(0.0); 833 | 834 | if (const auto& timeline = dynamic_cast(appState.root.value)) { 835 | root = timeline->tracks(); 836 | global_start = timeline->global_start_time().value_or(otio::RationalTime()); 837 | 838 | for (const auto& effect : root->effects()) { 839 | pairs.push_back(effect_parent_pair(effect, root)); 840 | } 841 | 842 | for (const auto& child : 843 | timeline->tracks()->find_children()) 844 | { 845 | if (const auto& item = dynamic_cast(&*child)) 846 | { 847 | for (const auto& effect : item->effects()) { 848 | pairs.push_back(effect_parent_pair(effect, item)); 849 | } 850 | } 851 | } 852 | } 853 | 854 | auto selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap; 855 | 856 | if (ImGui::BeginTable("Effects", 857 | 4, 858 | // ImGuiTableFlags_Sortable | 859 | ImGuiTableFlags_NoSavedSettings | 860 | ImGuiTableFlags_Resizable | 861 | ImGuiTableFlags_Reorderable | 862 | ImGuiTableFlags_Hideable | 863 | ImGuiTableFlags_RowBg | 864 | ImGuiTableFlags_ScrollY)) 865 | { 866 | ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible 867 | ImGui::TableSetupColumn("Global Time", ImGuiTableColumnFlags_WidthFixed); 868 | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); 869 | ImGui::TableSetupColumn("Effect", ImGuiTableColumnFlags_WidthStretch); 870 | ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch); 871 | 872 | ImGui::TableHeadersRow(); 873 | 874 | ImGuiListClipper effects_clipper; 875 | 876 | effects_clipper.Begin(pairs.size()); 877 | 878 | while (effects_clipper.Step()) 879 | { 880 | for (int row = effects_clipper.DisplayStart; row < effects_clipper.DisplayEnd; row++) 881 | { 882 | auto pair = pairs.at(row); 883 | auto effect = pair.first; 884 | auto parent = pair.second; 885 | 886 | ImGui::PushID(effect.value); 887 | 888 | ImGui::TableNextRow(); 889 | 890 | // Global Time 891 | ImGui::TableNextColumn(); 892 | 893 | auto range = parent->trimmed_range(); 894 | auto global_time = parent->transformed_time(range.start_time(), root) + global_start; 895 | 896 | // Make this row selectable & jump the playhead when clicked 897 | auto is_selected = 898 | (appState.selected_object == effect) || 899 | (appState.selected_object == parent); 900 | if (ImGui::Selectable(TimecodeStringFromTime(global_time).c_str(), 901 | is_selected, 902 | selectable_flags)) { 903 | //printf("DEBUG: clicked %s\n", TimecodeStringFromTime(global_time).c_str()); 904 | appState.playhead = global_time; 905 | SelectObject(effect, parent); 906 | appState.scroll_to_playhead = true; 907 | } 908 | 909 | // Name 910 | ImGui::TableNextColumn(); 911 | ImGui::TextUnformatted(effect->name().c_str()); 912 | 913 | // Effect 914 | ImGui::TableNextColumn(); 915 | ImGui::TextUnformatted(effect->effect_name().c_str()); 916 | 917 | // Item 918 | ImGui::TableNextColumn(); 919 | ImGui::TextUnformatted(parent->name().c_str()); 920 | 921 | ImGui::PopID(); 922 | } 923 | } 924 | } 925 | ImGui::EndTable(); 926 | } 927 | 928 | void DrawTreeInspector() { 929 | enum FilterOptions { 930 | All, 931 | Clips, 932 | Transitions, 933 | Gaps 934 | }; 935 | 936 | otio::Composition* tree_root = nullptr; 937 | otio::RationalTime global_start = otio::RationalTime(); 938 | 939 | if (auto timeline = dynamic_cast(appState.root.value)) { 940 | tree_root = timeline->tracks(); 941 | auto global_start = timeline->global_start_time().value_or(otio::RationalTime()); 942 | }else if (auto composition = dynamic_cast(appState.root.value)) { 943 | tree_root = composition; 944 | }else{ 945 | ImGui::Text("Root is not a Timeline or Composition"); 946 | return; 947 | } 948 | 949 | static const char* filter_options[] = { "All", "Clips", "Transitions", "Gaps" }; 950 | static FilterOptions current_filter = All; 951 | 952 | ImGui::Combo("Filter", reinterpret_cast(¤t_filter), filter_options, IM_ARRAYSIZE(filter_options)); 953 | 954 | std::function draw_composable; 955 | draw_composable = [&](otio::Composable* composable, otio::Composition* parent) { 956 | bool is_leaf = dynamic_cast(composable) == nullptr; 957 | 958 | // Apply filter 959 | if (is_leaf) { 960 | if (current_filter == Clips && !dynamic_cast(composable)) return; 961 | if (current_filter == Transitions && !dynamic_cast(composable)) return; 962 | if (current_filter == Gaps && !dynamic_cast(composable)) return; 963 | } 964 | 965 | ImGui::TableNextRow(); 966 | ImGui::PushID(composable); 967 | 968 | ImGui::TableNextColumn(); 969 | 970 | bool open = false; 971 | if (auto composition = dynamic_cast(composable)) { 972 | open = ImGui::TreeNodeEx( 973 | composable, 974 | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_DefaultOpen, 975 | "%s", composable->name().c_str()); 976 | } else { 977 | ImGui::TreeNodeEx( 978 | composable, 979 | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth, 980 | "%s", composable->name().c_str()); 981 | } 982 | 983 | otio::RationalTime global_time; 984 | if (parent == nullptr) { 985 | global_time = global_start; 986 | } else { 987 | auto t = parent->trimmed_range_of_child(composable)->start_time(); 988 | global_time = parent->transformed_time(t, tree_root) + global_start; 989 | } 990 | 991 | // Highlight the row if the object is selected, or if it is the context object 992 | // For example, if the selected object is a marker, the context object is the item containing that marker. 993 | bool is_selected = (appState.selected_object == composable || appState.selected_context == composable); 994 | if (is_selected) { 995 | ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_Header)); 996 | } 997 | 998 | // The next column is the schema name 999 | // We use that column to make the entire row selectable (via SpanAllColumns) 1000 | // instead of only the 1st column with the tree node. 1001 | ImGui::TableNextColumn(); 1002 | bool just_clicked = ImGui::IsItemClicked(); 1003 | bool just_selected = ImGui::Selectable(composable->schema_name().c_str(), is_selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap); 1004 | if (just_clicked || just_selected) { 1005 | SelectObject(composable); 1006 | appState.playhead = global_time; 1007 | appState.scroll_to_playhead = true; 1008 | } 1009 | 1010 | ImGui::TableNextColumn(); 1011 | if (auto item = dynamic_cast(composable)) { 1012 | auto start_time = item->trimmed_range().start_time(); 1013 | ImGui::TextUnformatted(TimecodeStringFromTime(start_time).c_str()); 1014 | } else { 1015 | ImGui::TextUnformatted("-"); 1016 | } 1017 | 1018 | ImGui::TableNextColumn(); 1019 | ImGui::TextUnformatted(TimecodeStringFromTime(global_time).c_str()); 1020 | 1021 | ImGui::TableNextColumn(); 1022 | ImGui::TextUnformatted(TimecodeStringFromTime(composable->duration()).c_str()); 1023 | 1024 | if (open) { 1025 | if (auto composition = dynamic_cast(composable)) { 1026 | for (const auto& child : composition->children()) { 1027 | draw_composable(child, composition); 1028 | } 1029 | } 1030 | ImGui::TreePop(); 1031 | } 1032 | ImGui::PopID(); 1033 | }; 1034 | 1035 | if (ImGui::BeginTable("Tree", 1036 | 5, 1037 | ImGuiTableFlags_NoSavedSettings | 1038 | ImGuiTableFlags_Resizable | 1039 | ImGuiTableFlags_Reorderable | 1040 | ImGuiTableFlags_Hideable)) { 1041 | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); 1042 | ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed); 1043 | ImGui::TableSetupColumn("Start Time", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_DefaultHide); 1044 | ImGui::TableSetupColumn("Global Start Time", ImGuiTableColumnFlags_WidthFixed); 1045 | ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthFixed); 1046 | 1047 | ImGui::TableHeadersRow(); 1048 | 1049 | draw_composable(tree_root, nullptr); 1050 | 1051 | ImGui::EndTable(); 1052 | } 1053 | } 1054 | -------------------------------------------------------------------------------- /inspector.h: -------------------------------------------------------------------------------- 1 | // Inspector 2 | 3 | void DrawInspector(); 4 | void DrawJSONInspector(); 5 | void DrawTreeInspector(); 6 | void DrawMarkersInspector(); 7 | void DrawEffectsInspector(); 8 | -------------------------------------------------------------------------------- /libs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_policy(SET CMP0076 NEW) 2 | cmake_policy(SET CMP0072 NEW) 3 | 4 | if(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) 5 | find_package(PkgConfig REQUIRED) 6 | pkg_check_modules(GTK3 REQUIRED gtk+-3.0) 7 | endif() 8 | 9 | #################################### 10 | 11 | if(NOT EMSCRIPTEN) 12 | add_library(nativefiledialog STATIC) 13 | 14 | set_property(TARGET nativefiledialog PROPERTY CXX_STANDARD 11) 15 | 16 | set(SOURCES nativefiledialog/src/nfd_common.c) 17 | set(INCLUDE_DIRS nativefiledialog/src nativefiledialog/src/include) 18 | set(LIBRARIES) 19 | if(APPLE) 20 | list(APPEND SOURCES nativefiledialog/src/nfd_cocoa.m) 21 | elseif(WIN32) 22 | list(APPEND SOURCES nativefiledialog/src/nfd_win.cpp) 23 | elseif(UNIX) 24 | list(APPEND SOURCES nativefiledialog/src/nfd_gtk.c) 25 | list(APPEND INCLUDE_DIRS ${GTK3_INCLUDE_DIRS}) 26 | list(APPEND LIBRARIES ${GTK3_LIBRARIES}) 27 | endif() 28 | 29 | target_sources(nativefiledialog PRIVATE ${SOURCES}) 30 | target_include_directories(nativefiledialog PRIVATE ${INCLUDE_DIRS}) 31 | target_link_libraries(nativefiledialog PRIVATE ${LIBRARIES}) 32 | endif() 33 | 34 | #################################### 35 | 36 | add_library(IMGUI STATIC) 37 | 38 | set_property(TARGET IMGUI PROPERTY CXX_STANDARD 14) 39 | 40 | set(IMGUI_DIR imgui) 41 | set(IMPLOT_DIR implot) 42 | set(TEXTEDIT_DIR ImGuiColorTextEdit) 43 | 44 | target_sources(IMGUI 45 | PRIVATE 46 | # core 47 | ${IMGUI_DIR}/imgui_demo.cpp 48 | ${IMGUI_DIR}/imgui_draw.cpp 49 | ${IMGUI_DIR}/imgui_tables.cpp 50 | ${IMGUI_DIR}/imgui_widgets.cpp 51 | ${IMGUI_DIR}/imgui.cpp 52 | 53 | # add-ons 54 | ${IMPLOT_DIR}/implot.cpp 55 | ${IMPLOT_DIR}/implot_demo.cpp 56 | ${IMPLOT_DIR}/implot_items.cpp 57 | ${TEXTEDIT_DIR}/TextEditor.cpp 58 | 59 | PUBLIC 60 | ${IMGUI_DIR}/imgui.h 61 | ${IMPLOT_DIR}/implot.h 62 | ${TEXTEDIT_DIR}/TextEditor.h 63 | ) 64 | 65 | if(APPLE) 66 | target_sources(IMGUI 67 | PRIVATE 68 | # backends 69 | ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp 70 | ${IMGUI_DIR}/backends/imgui_impl_metal.mm 71 | #gl3w/GL/gl3w.c 72 | ) 73 | elseif(WIN32) 74 | target_sources(IMGUI 75 | PRIVATE 76 | # backends 77 | ${IMGUI_DIR}/backends/imgui_impl_win32.cpp 78 | ${IMGUI_DIR}/backends/imgui_impl_dx11.cpp 79 | ) 80 | elseif(EMSCRIPTEN) 81 | target_sources(IMGUI 82 | PRIVATE 83 | # backends 84 | ${IMGUI_DIR}/backends/imgui_impl_sdl2.cpp 85 | ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp 86 | #gl3w/GL/gl3w.c 87 | ) 88 | else() 89 | target_sources(IMGUI 90 | PRIVATE 91 | # backends 92 | ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp 93 | ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp 94 | gl3w/GL/gl3w.c 95 | ) 96 | find_package(OpenGL REQUIRED) 97 | target_link_libraries(IMGUI OpenGL::GL ${CMAKE_DL_LIBS}) 98 | target_compile_definitions(IMGUI PUBLIC -DIMGUI_IMPL_OPENGL_LOADER_GL3W) 99 | endif() 100 | 101 | target_include_directories(IMGUI 102 | SYSTEM 103 | PUBLIC ${IMGUI_DIR} 104 | PUBLIC ${IMGUI_DIR}/backends 105 | PUBLIC gl3w 106 | PUBLIC glfw/include 107 | PUBLIC ${IMPLOT_DIR} 108 | PUBLIC ${TEXTEDIT_DIR} 109 | ) 110 | 111 | #################################### 112 | 113 | if(NOT EMSCRIPTEN) 114 | # TODO: Figure out how to tell CMake to build binary_to_compressed_c 115 | # for the native platform, not cross-compile to WASM/JS with emscripten... 116 | add_executable(binary_to_compressed_c 117 | ${IMGUI_DIR}/misc/fonts/binary_to_compressed_c.cpp 118 | ) 119 | set_property(TARGET binary_to_compressed_c PROPERTY CXX_STANDARD 14) 120 | endif() 121 | -------------------------------------------------------------------------------- /main.h: -------------------------------------------------------------------------------- 1 | #include "implot.h" 2 | void MainInit(int argc, char** argv, int initial_width, int initial_height); 3 | void MainGui(); 4 | void MainCleanup(); 5 | void FileDropCallback(int count, const char** paths); 6 | 7 | #ifdef EMSCRIPTEN 8 | extern "C" { 9 | void js_LoadUrl(char* url); 10 | void js_LoadString(char* json); 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /main_emscripten.cpp: -------------------------------------------------------------------------------- 1 | // Dear ImGui: standalone example application for Emscripten, using SDL2 + OpenGL3 2 | // (Emscripten is a C++-to-javascript compiler, used to publish executables for the web. See https://emscripten.org/) 3 | // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. 4 | // Read online: https://github.com/ocornut/imgui/tree/master/docs 5 | 6 | // This is mostly the same code as the SDL2 + OpenGL3 example, simply with the modifications needed to run on Emscripten. 7 | // It is possible to combine both code into a single source file that will compile properly on Desktop and using Emscripten. 8 | // See https://github.com/ocornut/imgui/pull/2492 as an example on how to do just that. 9 | 10 | #include "imgui.h" 11 | #include "imgui_impl_opengl3.h" 12 | #include "imgui_impl_sdl2.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "app.h" 21 | #include "main.h" 22 | 23 | // Emscripten requires to have full control over the main loop. We're going to store our SDL book-keeping variables globally. 24 | // Having a single function that acts as a loop prevents us to store state in the stack of said function. So we need some location for this. 25 | SDL_Window* g_Window = NULL; 26 | SDL_GLContext g_GLContext = NULL; 27 | 28 | // For clarity, our main loop code is declared at the end. 29 | static void main_loop(void*); 30 | 31 | int main(int argc, char** argv) 32 | { 33 | // Setup SDL 34 | if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) 35 | { 36 | printf("Error: %s\n", SDL_GetError()); 37 | return -1; 38 | } 39 | 40 | // For the browser using Emscripten, we are going to use WebGL1 with GL ES2. See the Makefile. for requirement details. 41 | // It is very likely the generated file won't work in many browsers. Firefox is the only sure bet, but I have successfully 42 | // run this code on Chrome for Android for example. 43 | const char* glsl_version = "#version 100"; 44 | //const char* glsl_version = "#version 300 es"; 45 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); 46 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); 47 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); 48 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); 49 | 50 | // Create window with graphics context 51 | SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); 52 | SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); 53 | SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); 54 | SDL_DisplayMode current; 55 | SDL_GetCurrentDisplayMode(0, ¤t); 56 | SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); 57 | int initial_width = 1280; 58 | int initial_height = 768; 59 | g_Window = SDL_CreateWindow( 60 | "Raven", 61 | SDL_WINDOWPOS_CENTERED, 62 | SDL_WINDOWPOS_CENTERED, 63 | initial_width, 64 | initial_height, 65 | window_flags); 66 | g_GLContext = SDL_GL_CreateContext(g_Window); 67 | if (!g_GLContext) 68 | { 69 | fprintf(stderr, "Failed to initialize WebGL context!\n"); 70 | return 1; 71 | } 72 | SDL_GL_SetSwapInterval(1); // Enable vsync 73 | 74 | // Setup Dear ImGui context 75 | IMGUI_CHECKVERSION(); 76 | ImGui::CreateContext(); 77 | ImGuiIO& io = ImGui::GetIO(); (void)io; 78 | io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 79 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 80 | io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking 81 | 82 | // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file. 83 | // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage. 84 | io.IniFilename = NULL; 85 | 86 | // Setup Dear ImGui style 87 | ImGui::StyleColorsDark(); 88 | //ImGui::StyleColorsLight(); 89 | 90 | // Setup Platform/Renderer backends 91 | ImGui_ImplSDL2_InitForOpenGL(g_Window, g_GLContext); 92 | ImGui_ImplOpenGL3_Init(glsl_version); 93 | 94 | MainInit(argc, argv, initial_width, initial_height); 95 | 96 | // Load Fonts 97 | // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. 98 | // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. 99 | // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). 100 | // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. 101 | // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering. 102 | // - Read 'docs/FONTS.md' for more instructions and details. 103 | // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! 104 | // - Emscripten allows preloading a file or folder to be accessible at runtime. See Makefile for details. 105 | //io.Fonts->AddFontDefault(); 106 | #ifndef IMGUI_DISABLE_FILE_FUNCTIONS 107 | //io.Fonts->AddFontFromFileTTF("fonts/segoeui.ttf", 18.0f); 108 | //io.Fonts->AddFontFromFileTTF("fonts/DroidSans.ttf", 16.0f); 109 | //io.Fonts->AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0f); 110 | //io.Fonts->AddFontFromFileTTF("fonts/Cousine-Regular.ttf", 15.0f); 111 | //io.Fonts->AddFontFromFileTTF("fonts/ProggyTiny.ttf", 10.0f); 112 | //ImFont* font = io.Fonts->AddFontFromFileTTF("fonts/ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); 113 | //IM_ASSERT(font != NULL); 114 | #endif 115 | 116 | // This function call won't return, and will engage in an infinite loop, processing events from the browser, and dispatching them. 117 | emscripten_set_main_loop_arg(main_loop, NULL, 0, true); 118 | } 119 | 120 | static void main_loop(void* arg) 121 | { 122 | ImGuiIO& io = ImGui::GetIO(); 123 | IM_UNUSED(arg); // We can pass this argument as the second parameter of emscripten_set_main_loop_arg(), but we don't use that. 124 | 125 | static ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); 126 | 127 | // Poll and handle events (inputs, window resize, etc.) 128 | // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. 129 | // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. 130 | // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. 131 | // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. 132 | SDL_Event event; 133 | while (SDL_PollEvent(&event)) 134 | { 135 | ImGui_ImplSDL2_ProcessEvent(&event); 136 | // Capture events here, based on io.WantCaptureMouse and io.WantCaptureKeyboard 137 | } 138 | 139 | // Start the Dear ImGui frame 140 | ImGui_ImplOpenGL3_NewFrame(); 141 | ImGui_ImplSDL2_NewFrame(); 142 | ImGui::NewFrame(); 143 | 144 | MainGui(); 145 | 146 | // Rendering 147 | ImGui::Render(); 148 | SDL_GL_MakeCurrent(g_Window, g_GLContext); 149 | glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); 150 | glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); 151 | glClear(GL_COLOR_BUFFER_BIT); 152 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 153 | SDL_GL_SwapWindow(g_Window); 154 | } 155 | 156 | void LoadUrlSuccess(emscripten_fetch_t* fetch) { 157 | printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url); 158 | 159 | std::string otio_string = std::string(fetch->data, fetch->numBytes); 160 | emscripten_fetch_close(fetch); 161 | 162 | LoadString(otio_string); 163 | 164 | appState.file_path = fetch->url; 165 | } 166 | 167 | void LoadUrlFailure(emscripten_fetch_t* fetch) { 168 | printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status); 169 | emscripten_fetch_close(fetch); 170 | } 171 | 172 | void LoadUrl(std::string url) { 173 | printf("Downloading %s...\n", url.c_str()); 174 | emscripten_fetch_attr_t attr; 175 | emscripten_fetch_attr_init(&attr); 176 | strcpy(attr.requestMethod, "GET"); 177 | attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; 178 | // This is async, so we can provide callbacks to handle the result 179 | attr.onsuccess = LoadUrlSuccess; 180 | attr.onerror = LoadUrlFailure; 181 | emscripten_fetch(&attr, url.c_str()); 182 | } 183 | 184 | extern "C" { 185 | EMSCRIPTEN_KEEPALIVE 186 | void js_LoadUrl(char* url) { 187 | LoadUrl(std::string(url)); 188 | } 189 | EMSCRIPTEN_KEEPALIVE 190 | void js_LoadString(char* json) { 191 | LoadString(std::string(json)); 192 | } 193 | } -------------------------------------------------------------------------------- /main_glfw.cpp: -------------------------------------------------------------------------------- 1 | // Dear ImGui: standalone example application for GLFW + OpenGL 3, using programmable pipeline 2 | // (GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.) 3 | // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. 4 | // Read online: https://github.com/ocornut/imgui/tree/master/docs 5 | 6 | #include "imgui.h" 7 | #include "imgui_impl_glfw.h" 8 | #include "imgui_impl_opengl3.h" 9 | #include 10 | 11 | #include "main.h" 12 | 13 | #if defined(IMGUI_IMPL_OPENGL_ES2) 14 | #include 15 | // About Desktop OpenGL function loaders: 16 | // Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers. 17 | // Helper libraries are often used for this purpose! Here we are supporting a few common ones (gl3w, glew, glad). 18 | // You may use another loader/header of your choice (glext, glLoadGen, etc.), or chose to manually implement your own. 19 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GL3W) 20 | #include // Initialize with gl3wInit() 21 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW) 22 | #include // Initialize with glewInit() 23 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD) 24 | #include // Initialize with gladLoadGL() 25 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD2) 26 | #include // Initialize with gladLoadGL(...) or gladLoaderLoadGL() 27 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2) 28 | #define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors. 29 | #include // Initialize with glbinding::Binding::initialize() 30 | #include 31 | using namespace gl; 32 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3) 33 | #define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors. 34 | #include // Initialize with glbinding::initialize() 35 | #include 36 | using namespace gl; 37 | #else 38 | #include IMGUI_IMPL_OPENGL_LOADER_CUSTOM 39 | #endif 40 | 41 | // Include glfw3.h after our OpenGL definitions 42 | #include 43 | 44 | // [Win32] Our example includes a copy of glfw3.lib pre-compiled with VS2010 to maximize ease of testing and compatibility with old VS compilers. 45 | // To link with VS2010-era libraries, VS2015+ requires linking with legacy_stdio_definitions.lib, which we do using this pragma. 46 | // Your own project should not be affected, as you are likely to link with a newer binary of GLFW that is adequate for your version of Visual Studio. 47 | #if defined(_MSC_VER) && (_MSC_VER >= 1900) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) 48 | #pragma comment(lib, "legacy_stdio_definitions") 49 | #endif 50 | 51 | static void glfw_error_callback(int error, const char* description) 52 | { 53 | fprintf(stderr, "Glfw Error %d: %s\n", error, description); 54 | } 55 | 56 | int main(int argc, char** argv) 57 | { 58 | // Setup window 59 | glfwSetErrorCallback(glfw_error_callback); 60 | if (!glfwInit()) 61 | return 1; 62 | 63 | // Decide GL+GLSL versions 64 | #if defined(IMGUI_IMPL_OPENGL_ES2) 65 | // GL ES 2.0 + GLSL 100 66 | const char* glsl_version = "#version 100"; 67 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); 68 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 69 | glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); 70 | #elif defined(__APPLE__) 71 | // GL 3.2 + GLSL 150 72 | const char* glsl_version = "#version 150"; 73 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 74 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); 75 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only 76 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac 77 | #else 78 | // GL 3.0 + GLSL 130 79 | const char* glsl_version = "#version 130"; 80 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 81 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 82 | //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only 83 | //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only 84 | #endif 85 | 86 | // Create window with graphics context 87 | int initial_width = 1280; 88 | int initial_height = 768; 89 | GLFWwindow* window = glfwCreateWindow(initial_width, initial_height, "Raven", NULL, NULL); 90 | if (window == NULL) 91 | return 1; 92 | glfwMakeContextCurrent(window); 93 | glfwSwapInterval(1); // Enable vsync 94 | 95 | // Initialize OpenGL loader 96 | #if defined(IMGUI_IMPL_OPENGL_LOADER_GL3W) 97 | bool err = gl3wInit() != 0; 98 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW) 99 | bool err = glewInit() != GLEW_OK; 100 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD) 101 | bool err = gladLoadGL() == 0; 102 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD2) 103 | bool err = gladLoadGL(glfwGetProcAddress) == 0; // glad2 recommend using the windowing library loader instead of the (optionally) bundled one. 104 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2) 105 | bool err = false; 106 | glbinding::Binding::initialize(); 107 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3) 108 | bool err = false; 109 | glbinding::initialize([](const char* name) { return (glbinding::ProcAddress)glfwGetProcAddress(name); }); 110 | #else 111 | bool err = false; // If you use IMGUI_IMPL_OPENGL_LOADER_CUSTOM, your loader is likely to requires some form of initialization. 112 | #endif 113 | if (err) 114 | { 115 | fprintf(stderr, "Failed to initialize OpenGL loader!\n"); 116 | return 1; 117 | } 118 | 119 | // Setup Dear ImGui context 120 | IMGUI_CHECKVERSION(); 121 | ImGui::CreateContext(); 122 | ImPlot::CreateContext(); 123 | ImGuiIO& io = ImGui::GetIO(); (void)io; 124 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 125 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 126 | io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking 127 | io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows 128 | io.ConfigViewportsNoAutoMerge = true; 129 | //io.ConfigViewportsNoTaskBarIcon = true; 130 | 131 | // Setup Dear ImGui style 132 | ImGui::StyleColorsDark(); 133 | //ImGui::StyleColorsClassic(); 134 | 135 | // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. 136 | ImGuiStyle& style = ImGui::GetStyle(); 137 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 138 | { 139 | style.WindowRounding = 0.0f; 140 | style.Colors[ImGuiCol_WindowBg].w = 1.0f; 141 | } 142 | 143 | // Setup Platform/Renderer backends 144 | ImGui_ImplGlfw_InitForOpenGL(window, true); 145 | ImGui_ImplOpenGL3_Init(glsl_version); 146 | 147 | MainInit(argc, argv, initial_width, initial_height); 148 | 149 | // Load Fonts 150 | // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. 151 | // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. 152 | // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). 153 | // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. 154 | // - Read 'docs/FONTS.md' for more instructions and details. 155 | // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! 156 | //io.Fonts->AddFontDefault(); 157 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); 158 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); 159 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); 160 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/ProggyTiny.ttf", 10.0f); 161 | //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); 162 | //IM_ASSERT(font != NULL); 163 | 164 | ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); 165 | 166 | int cooldown_counter = 0; 167 | 168 | // Main loop 169 | while (!glfwWindowShouldClose(window)) 170 | { 171 | // This application doesn't do any animation, so instead 172 | // of rendering all the time, we block waiting for events. 173 | // However, ImGui sometimes needs to render several frames 174 | // in a row to fully handle an input event and it's follow-on 175 | // effects (e.g. responding to a click which scrolls the timeline) 176 | // so we use cooldown_counter to ensure at least 5 frames are 177 | // rendered before we block again. 178 | if (cooldown_counter <= 0) { 179 | glfwWaitEvents(); 180 | cooldown_counter = 5; 181 | }else{ 182 | // Poll and handle events (inputs, window resize, etc.) 183 | glfwPollEvents(); 184 | cooldown_counter--; 185 | } 186 | 187 | // Start the Dear ImGui frame 188 | ImGui_ImplOpenGL3_NewFrame(); 189 | ImGui_ImplGlfw_NewFrame(); 190 | ImGui::NewFrame(); 191 | 192 | MainGui(); 193 | 194 | // Rendering 195 | ImGui::Render(); 196 | int display_w, display_h; 197 | glfwGetFramebufferSize(window, &display_w, &display_h); 198 | glViewport(0, 0, display_w, display_h); 199 | glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); 200 | glClear(GL_COLOR_BUFFER_BIT); 201 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 202 | 203 | // Update and Render additional Platform Windows 204 | // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere. 205 | // For this specific demo app we could also call glfwMakeContextCurrent(window) directly) 206 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 207 | { 208 | GLFWwindow* backup_current_context = glfwGetCurrentContext(); 209 | ImGui::UpdatePlatformWindows(); 210 | ImGui::RenderPlatformWindowsDefault(); 211 | glfwMakeContextCurrent(backup_current_context); 212 | } 213 | 214 | glfwSwapBuffers(window); 215 | } 216 | 217 | MainCleanup(); 218 | 219 | // Cleanup 220 | ImGui_ImplOpenGL3_Shutdown(); 221 | ImGui_ImplGlfw_Shutdown(); 222 | ImPlot::DestroyContext(); 223 | ImGui::DestroyContext(); 224 | 225 | glfwDestroyWindow(window); 226 | glfwTerminate(); 227 | 228 | return 0; 229 | } 230 | -------------------------------------------------------------------------------- /main_macos.mm: -------------------------------------------------------------------------------- 1 | // Dear ImGui: standalone example application for GLFW + Metal, using programmable pipeline 2 | // (GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.) 3 | // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. 4 | // Read online: https://github.com/ocornut/imgui/tree/master/docs 5 | 6 | #include "imgui.h" 7 | #include "imgui_impl_glfw.h" 8 | #include "imgui_impl_metal.h" 9 | #include 10 | 11 | #include "main.h" 12 | 13 | #define GLFW_INCLUDE_NONE 14 | #define GLFW_EXPOSE_NATIVE_COCOA 15 | #include 16 | #include 17 | 18 | #import 19 | #import 20 | 21 | // Accept and open a file path 22 | void file_drop_callback(GLFWwindow* window, int count, const char** paths) { 23 | FileDropCallback(count, paths); 24 | } 25 | 26 | static void glfw_error_callback(int error, const char* description) 27 | { 28 | fprintf(stderr, "Glfw Error %d: %s\n", error, description); 29 | } 30 | 31 | int main(int argc, char** argv) 32 | { 33 | // Setup Dear ImGui context 34 | IMGUI_CHECKVERSION(); 35 | ImGui::CreateContext(); 36 | ImPlot::CreateContext(); 37 | ImGuiIO& io = ImGui::GetIO(); (void)io; 38 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 39 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 40 | io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking 41 | io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows 42 | 43 | // Setup style 44 | ImGui::StyleColorsDark(); 45 | //ImGui::StyleColorsLight(); 46 | 47 | // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. 48 | ImGuiStyle& style = ImGui::GetStyle(); 49 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 50 | { 51 | style.WindowRounding = 0.0f; 52 | style.Colors[ImGuiCol_WindowBg].w = 1.0f; 53 | } 54 | 55 | // Load Fonts 56 | // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. 57 | // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. 58 | // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). 59 | // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. 60 | // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering. 61 | // - Read 'docs/FONTS.md' for more instructions and details. 62 | // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! 63 | //io.Fonts->AddFontDefault(); 64 | //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f); 65 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); 66 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); 67 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); 68 | //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); 69 | //IM_ASSERT(font != NULL); 70 | 71 | // Setup window 72 | glfwSetErrorCallback(glfw_error_callback); 73 | if (!glfwInit()) 74 | return 1; 75 | 76 | // Create window with graphics context 77 | int initial_width = 1280; 78 | int initial_height = 768; 79 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 80 | GLFWwindow* window = glfwCreateWindow(initial_width, initial_height, "Raven", NULL, NULL); 81 | if (window == NULL) 82 | return 1; 83 | 84 | id device = MTLCreateSystemDefaultDevice(); 85 | id commandQueue = [device newCommandQueue]; 86 | 87 | // Setup Platform/Renderer backends 88 | ImGui_ImplGlfw_InitForOther(window, true); 89 | ImGui_ImplMetal_Init(device); 90 | 91 | MainInit(argc, argv, initial_width, initial_height); 92 | 93 | NSWindow *nswin = glfwGetCocoaWindow(window); 94 | CAMetalLayer *layer = [CAMetalLayer layer]; 95 | layer.device = device; 96 | layer.pixelFormat = MTLPixelFormatBGRA8Unorm; 97 | nswin.contentView.layer = layer; 98 | nswin.contentView.wantsLayer = YES; 99 | 100 | MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new]; 101 | 102 | // Our state 103 | float clear_color[4] = {0.45f, 0.55f, 0.60f, 1.00f}; 104 | 105 | // Set the drop callback 106 | glfwSetDropCallback(window, file_drop_callback); 107 | 108 | int cooldown_counter = 0; 109 | 110 | // Main loop 111 | while (!glfwWindowShouldClose(window)) 112 | { 113 | @autoreleasepool 114 | { 115 | // This application doesn't do any animation, so instead 116 | // of rendering all the time, we block waiting for events. 117 | // However, ImGui sometimes needs to render several frames 118 | // in a row to fully handle an input event and it's follow-on 119 | // effects (e.g. responding to a click which scrolls the timeline) 120 | // so we use cooldown_counter to ensure at least 5 frames are 121 | // rendered before we block again. 122 | if (cooldown_counter <= 0) { 123 | glfwWaitEvents(); 124 | cooldown_counter = 5; 125 | }else{ 126 | // Poll and handle events (inputs, window resize, etc.) 127 | glfwPollEvents(); 128 | cooldown_counter--; 129 | } 130 | 131 | int width, height; 132 | glfwGetFramebufferSize(window, &width, &height); 133 | layer.drawableSize = CGSizeMake(width, height); 134 | id drawable = [layer nextDrawable]; 135 | 136 | id commandBuffer = [commandQueue commandBuffer]; 137 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color[0] * clear_color[3], clear_color[1] * clear_color[3], clear_color[2] * clear_color[3], clear_color[3]); 138 | renderPassDescriptor.colorAttachments[0].texture = drawable.texture; 139 | renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; 140 | renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; 141 | id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; 142 | [renderEncoder pushDebugGroup:@"ImGui"]; 143 | 144 | // Start the Dear ImGui frame 145 | ImGui_ImplMetal_NewFrame(renderPassDescriptor); 146 | ImGui_ImplGlfw_NewFrame(); 147 | ImGui::NewFrame(); 148 | 149 | MainGui(); 150 | 151 | // Rendering 152 | ImGui::Render(); 153 | ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), commandBuffer, renderEncoder); 154 | 155 | // Update and Render additional Platform Windows 156 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 157 | { 158 | ImGui::UpdatePlatformWindows(); 159 | ImGui::RenderPlatformWindowsDefault(); 160 | } 161 | 162 | [renderEncoder popDebugGroup]; 163 | [renderEncoder endEncoding]; 164 | 165 | [commandBuffer presentDrawable:drawable]; 166 | [commandBuffer commit]; 167 | } 168 | } 169 | 170 | MainCleanup(); 171 | 172 | // Cleanup 173 | ImGui_ImplMetal_Shutdown(); 174 | ImGui_ImplGlfw_Shutdown(); 175 | ImPlot::DestroyContext(); 176 | ImGui::DestroyContext(); 177 | 178 | glfwDestroyWindow(window); 179 | glfwTerminate(); 180 | 181 | return 0; 182 | } 183 | -------------------------------------------------------------------------------- /main_win32.cpp: -------------------------------------------------------------------------------- 1 | // Dear ImGui: standalone example application for DirectX 11 2 | // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. 3 | // Read online: https://github.com/ocornut/imgui/tree/master/docs 4 | 5 | #include "imgui.h" 6 | #include "imgui_impl_win32.h" 7 | #include "imgui_impl_dx11.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "main.h" 14 | 15 | // Data 16 | static ID3D11Device* g_pd3dDevice = NULL; 17 | static ID3D11DeviceContext* g_pd3dDeviceContext = NULL; 18 | static IDXGISwapChain* g_pSwapChain = NULL; 19 | static ID3D11RenderTargetView* g_mainRenderTargetView = NULL; 20 | static LPVOID g_messageFiber = NULL; 21 | static LPVOID g_mainFiber = NULL; 22 | static bool g_done = false; 23 | 24 | // Forward declarations of helper functions 25 | bool CreateDeviceD3D(HWND hWnd); 26 | void CleanupDeviceD3D(); 27 | void CreateRenderTarget(); 28 | void CleanupRenderTarget(); 29 | LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); 30 | 31 | void CALLBACK MessageFiberProc(LPVOID lpFiberParameter) 32 | { 33 | for (;;) 34 | { 35 | MSG msg; 36 | while (::PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) 37 | { 38 | ::TranslateMessage(&msg); 39 | ::DispatchMessage(&msg); 40 | if (msg.message == WM_QUIT) 41 | g_done = true; 42 | } 43 | SwitchToFiber(g_mainFiber); 44 | } 45 | } 46 | 47 | // Allows sleep at fine granularity with no stuttering, and not pegging CPU to 100% 48 | void PowerConserve( float Seconds ) { 49 | // Determine time to wait. 50 | static HANDLE Timer = CreateWaitableTimer( NULL, FALSE, NULL ); 51 | LARGE_INTEGER WaitTime; 52 | WaitTime.QuadPart = (LONGLONG)(Seconds * -10000000); 53 | 54 | if ( WaitTime.QuadPart >= 0 ) 55 | return; // Give up the rest of the frame. 56 | 57 | if ( !SetWaitableTimer( Timer, &WaitTime, 0, NULL, NULL, FALSE ) ) 58 | return; 59 | 60 | DWORD Result = MsgWaitForMultipleObjects ( 1, &Timer, FALSE, INFINITE, QS_ALLINPUT ); 61 | } 62 | 63 | // Main code 64 | int main(int argc, char** argv) 65 | { 66 | int initial_width = 1280; 67 | int initial_height = 800; 68 | // Create application window 69 | //ImGui_ImplWin32_EnableDpiAwareness(); 70 | 71 | // Handle message processing in a fiber, aka coroutine. 72 | // This allows pausing execution of the message loop during a resize/move. 73 | // This is done by using timer which fires after 1msec on window resize/move event. 74 | // After timer fires, the control returns to main code. After frame 75 | // is drawn, control is then returned back to message loop. 76 | // This based on a technique proposed by mmozeiko for glfw 77 | // https://github.com/glfw/glfw/pull/1426 78 | g_mainFiber = ConvertThreadToFiber(NULL); 79 | if (!g_mainFiber) 80 | return 1; 81 | 82 | g_messageFiber = CreateFiber(0, &MessageFiberProc, NULL); 83 | if (!g_messageFiber) 84 | return 1; 85 | 86 | WNDCLASSEXW wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(NULL), NULL, NULL, NULL, NULL, L"Raven", NULL }; 87 | ::RegisterClassExW(&wc); 88 | HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"Raven", WS_OVERLAPPEDWINDOW, 100, 100, initial_width, initial_height, NULL, NULL, wc.hInstance, NULL); 89 | 90 | // Initialize Direct3D 91 | if (!CreateDeviceD3D(hwnd)) 92 | { 93 | CleanupDeviceD3D(); 94 | ::UnregisterClassW(wc.lpszClassName, wc.hInstance); 95 | return 1; 96 | } 97 | 98 | // Show the window 99 | ::ShowWindow(hwnd, SW_SHOWDEFAULT); 100 | ::UpdateWindow(hwnd); 101 | 102 | // Setup Dear ImGui context 103 | IMGUI_CHECKVERSION(); 104 | ImGui::CreateContext(); 105 | ImPlot::CreateContext(); 106 | ImGuiIO& io = ImGui::GetIO(); (void)io; 107 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 108 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 109 | io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking 110 | io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows 111 | //io.ConfigViewportsNoAutoMerge = true; 112 | //io.ConfigViewportsNoTaskBarIcon = true; 113 | //io.ConfigViewportsNoDefaultParent = true; 114 | //io.ConfigDockingAlwaysTabBar = true; 115 | //io.ConfigDockingTransparentPayload = true; 116 | //io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleFonts; // FIXME-DPI: Experimental. THIS CURRENTLY DOESN'T WORK AS EXPECTED. DON'T USE IN USER APP! 117 | //io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleViewports; // FIXME-DPI: Experimental. 118 | 119 | // Setup Dear ImGui style 120 | ImGui::StyleColorsDark(); 121 | //ImGui::StyleColorsLight(); 122 | 123 | // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. 124 | ImGuiStyle& style = ImGui::GetStyle(); 125 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 126 | { 127 | style.WindowRounding = 0.0f; 128 | style.Colors[ImGuiCol_WindowBg].w = 1.0f; 129 | } 130 | 131 | // Get display settings for drawing frame rate 132 | DEVMODE lpDevMode; 133 | memset(&lpDevMode, 0, sizeof(DEVMODE)); 134 | lpDevMode.dmSize = sizeof(DEVMODE); 135 | lpDevMode.dmDriverExtra = 0; 136 | unsigned int refresh_rate; 137 | 138 | // NULL value indicates the display device used by the calling thread. 139 | if(EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &lpDevMode) == 0){ 140 | // default value if we can't access the settings 141 | refresh_rate = 60; 142 | } else { 143 | refresh_rate = lpDevMode.dmDisplayFrequency; 144 | } 145 | 146 | // Setup Platform/Renderer backends 147 | ImGui_ImplWin32_Init(hwnd); 148 | ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); 149 | 150 | MainInit(argc, argv, initial_width, initial_height); 151 | 152 | DragAcceptFiles(hwnd, TRUE); 153 | 154 | // Load Fonts 155 | // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. 156 | // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. 157 | // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). 158 | // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. 159 | // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering. 160 | // - Read 'docs/FONTS.md' for more instructions and details. 161 | // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! 162 | //io.Fonts->AddFontDefault(); 163 | //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f); 164 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); 165 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); 166 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); 167 | //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); 168 | //IM_ASSERT(font != NULL); 169 | 170 | // Our state 171 | ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); 172 | int cooldown_counter = 0; 173 | while (!g_done) 174 | { 175 | // Poll and handle messages (inputs, window resize, etc.) 176 | // See the WndProc() function below for our to dispatch events to the Win32 backend. 177 | SwitchToFiber(g_messageFiber); 178 | 179 | // This application doesn't do any animation, so instead 180 | // of rendering all the time, we block waiting for events. 181 | // However, ImGui sometimes needs to render several frames 182 | // in a row to fully handle an input event and it's follow-on 183 | // effects (e.g. responding to a click which scrolls the timeline) 184 | // so we use cooldown_counter to ensure at least 5 frames are 185 | // rendered before we block again. 186 | 187 | PowerConserve(((float)1.0/refresh_rate)); 188 | if (cooldown_counter <= 0){ 189 | cooldown_counter = 5; 190 | WaitMessage(); 191 | }else{ 192 | cooldown_counter--; 193 | } 194 | 195 | if (g_done) 196 | break; 197 | 198 | // Start the Dear ImGui frame 199 | ImGui_ImplDX11_NewFrame(); 200 | ImGui_ImplWin32_NewFrame(); 201 | ImGui::NewFrame(); 202 | 203 | MainGui(); 204 | 205 | // Rendering 206 | ImGui::Render(); 207 | const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w }; 208 | g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, NULL); 209 | g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha); 210 | ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); 211 | 212 | // Update and Render additional Platform Windows 213 | if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) 214 | { 215 | ImGui::UpdatePlatformWindows(); 216 | ImGui::RenderPlatformWindowsDefault(); 217 | } 218 | 219 | g_pSwapChain->Present(1, 0); // Present with vsync 220 | //g_pSwapChain->Present(0, 0); // Present without vsync 221 | } 222 | 223 | // Cleanup 224 | MainCleanup(); 225 | ImGui_ImplDX11_Shutdown(); 226 | ImGui_ImplWin32_Shutdown(); 227 | ImGui::DestroyContext(); 228 | 229 | CleanupDeviceD3D(); 230 | ::DestroyWindow(hwnd); 231 | ::UnregisterClassW(wc.lpszClassName, wc.hInstance); 232 | 233 | DeleteFiber(g_messageFiber); 234 | ConvertFiberToThread(); 235 | 236 | DragAcceptFiles(hwnd, FALSE); 237 | 238 | return 0; 239 | } 240 | 241 | // Helper functions 242 | 243 | bool CreateDeviceD3D(HWND hWnd) 244 | { 245 | // Setup swap chain 246 | DXGI_SWAP_CHAIN_DESC sd; 247 | ZeroMemory(&sd, sizeof(sd)); 248 | sd.BufferCount = 2; 249 | sd.BufferDesc.Width = 0; 250 | sd.BufferDesc.Height = 0; 251 | sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; 252 | sd.BufferDesc.RefreshRate.Numerator = 60; 253 | sd.BufferDesc.RefreshRate.Denominator = 1; 254 | sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; 255 | sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; 256 | sd.OutputWindow = hWnd; 257 | sd.SampleDesc.Count = 1; 258 | sd.SampleDesc.Quality = 0; 259 | sd.Windowed = TRUE; 260 | sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; 261 | 262 | UINT createDeviceFlags = 0; 263 | //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; 264 | D3D_FEATURE_LEVEL featureLevel; 265 | const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, }; 266 | if (D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext) != S_OK) 267 | return false; 268 | 269 | CreateRenderTarget(); 270 | return true; 271 | } 272 | 273 | void CleanupDeviceD3D() 274 | { 275 | CleanupRenderTarget(); 276 | if (g_pSwapChain) { g_pSwapChain->Release(); g_pSwapChain = NULL; } 277 | if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = NULL; } 278 | if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = NULL; } 279 | } 280 | 281 | void CreateRenderTarget() 282 | { 283 | ID3D11Texture2D* pBackBuffer; 284 | g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); 285 | g_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, &g_mainRenderTargetView); 286 | pBackBuffer->Release(); 287 | } 288 | 289 | void CleanupRenderTarget() 290 | { 291 | if (g_mainRenderTargetView) { g_mainRenderTargetView->Release(); g_mainRenderTargetView = NULL; } 292 | } 293 | 294 | #ifndef WM_DPICHANGED 295 | #define WM_DPICHANGED 0x02E0 // From Windows SDK 8.1+ headers 296 | #endif 297 | 298 | // Forward declare message handler from imgui_impl_win32.cpp 299 | extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); 300 | 301 | // Win32 message handler 302 | // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. 303 | // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. 304 | // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. 305 | // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. 306 | LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) 307 | { 308 | if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) 309 | return true; 310 | 311 | switch (msg) 312 | { 313 | case WM_SIZE: 314 | if (g_pd3dDevice != NULL && wParam != SIZE_MINIMIZED) 315 | { 316 | CleanupRenderTarget(); 317 | g_pSwapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam), DXGI_FORMAT_UNKNOWN, 0); 318 | CreateRenderTarget(); 319 | } 320 | return 0; 321 | case WM_SYSCOMMAND: 322 | if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu 323 | return 0; 324 | break; 325 | case WM_DESTROY: 326 | ::PostQuitMessage(0); 327 | return 0; 328 | case WM_DPICHANGED: 329 | if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) 330 | { 331 | //const int dpi = HIWORD(wParam); 332 | //printf("WM_DPICHANGED to %d (%.0f%%)\n", dpi, (float)dpi / 96.0f * 100.0f); 333 | const RECT* suggested_rect = (RECT*)lParam; 334 | ::SetWindowPos(hWnd, NULL, suggested_rect->left, suggested_rect->top, suggested_rect->right - suggested_rect->left, suggested_rect->bottom - suggested_rect->top, SWP_NOZORDER | SWP_NOACTIVATE); 335 | } 336 | break; 337 | case WM_ENTERSIZEMOVE: 338 | case WM_ENTERMENULOOP: 339 | SetTimer(hWnd, 1, 1, NULL); 340 | break; 341 | case WM_EXITSIZEMOVE: 342 | case WM_EXITMENULOOP: 343 | KillTimer(hWnd, 1); 344 | break; 345 | case WM_TIMER: 346 | // During a window resize/move PeekMessage is essentially blocked, but 347 | // event timers still trigger. Using a event timer we can switch 348 | // back to the main fiber allowing gui to draw a frame. After a frame is drawn 349 | // execution is yielded back to the message fiber. 350 | SwitchToFiber(g_mainFiber); 351 | break; 352 | case WM_DROPFILES: 353 | HDROP hDrop = reinterpret_cast(wParam); 354 | UINT count = DragQueryFileW(hDrop, -1, NULL, 0); 355 | 356 | char** file_list; 357 | file_list = (char**)malloc(count * sizeof(char*)); 358 | wchar_t temp_filename[MAX_PATH]; 359 | 360 | for(UINT i = 0; i < count; ++i) 361 | { 362 | if (DragQueryFileW(hDrop, i, temp_filename, MAX_PATH)) 363 | { 364 | // Determine the required buffer size for the multi-byte string 365 | int buffer_size = WideCharToMultiByte(CP_UTF8, 0, temp_filename, -1, NULL, 0, NULL, NULL); 366 | file_list[i] = (char*)malloc(buffer_size * sizeof(char)); 367 | 368 | // Convert wide character filename to UTF-8 369 | WideCharToMultiByte(CP_UTF8, 0, temp_filename, -1, file_list[i], buffer_size, NULL, NULL); 370 | } 371 | } 372 | 373 | DragFinish(hDrop); 374 | 375 | FileDropCallback(count, (const char**)file_list); 376 | 377 | for (UINT i = 0; i < count; ++i){ 378 | free(file_list[i]); 379 | } 380 | free(file_list); 381 | 382 | break; 383 | } 384 | return ::DefWindowProc(hWnd, msg, wParam, lParam); 385 | } 386 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenTimelineIO/raven/6d21529b6b0560e35870bc52ffd49ac530b7b81d/screenshot.png -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from http.server import HTTPServer, SimpleHTTPRequestHandler, test 3 | import sys 4 | 5 | class CORSRequestHandler (SimpleHTTPRequestHandler): 6 | def end_headers (self): 7 | self.send_header('Access-Control-Allow-Origin', '*') 8 | self.send_header('Cross-Origin-Opener-Policy', 'same-origin') 9 | self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') 10 | # self.send_header('Cross-Origin-Resource-Policy', 'cross-origin') 11 | 12 | SimpleHTTPRequestHandler.end_headers(self) 13 | 14 | if __name__ == '__main__': 15 | test(CORSRequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000) 16 | 17 | 18 | -------------------------------------------------------------------------------- /shell_minimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Raven 7 | 29 | 30 | 31 | 32 | 95 | {{{ SCRIPT }}} 96 | 97 | 98 | -------------------------------------------------------------------------------- /theme.inc: -------------------------------------------------------------------------------- 1 | appTheme.colors[AppThemeCol_Background] = 0xFF141414; 2 | appTheme.colors[AppThemeCol_Label] = 0xFFFFFFFF; 3 | appTheme.colors[AppThemeCol_TickMajor] = 0xFF8C8C8C; 4 | appTheme.colors[AppThemeCol_TickMinor] = 0xFF363636; 5 | appTheme.colors[AppThemeCol_GapHovered] = 0xFF282828; 6 | appTheme.colors[AppThemeCol_GapSelected] = 0xFFFFFFFF; 7 | appTheme.colors[AppThemeCol_Item] = 0xFF507C35; 8 | appTheme.colors[AppThemeCol_ItemHovered] = 0xFF57A451; 9 | appTheme.colors[AppThemeCol_ItemSelected] = 0xFF57A451; 10 | appTheme.colors[AppThemeCol_Transition] = 0x8830DEE6; 11 | appTheme.colors[AppThemeCol_TransitionLine] = 0xFFA7E9F0; 12 | appTheme.colors[AppThemeCol_TransitionHovered] = 0xC830DEE6; 13 | appTheme.colors[AppThemeCol_TransitionSelected] = 0xFFFFFFFF; 14 | appTheme.colors[AppThemeCol_Effect] = 0x9A8A205A; 15 | appTheme.colors[AppThemeCol_EffectHovered] = 0xCC903567; 16 | appTheme.colors[AppThemeCol_EffectSelected] = 0xFFFFFFFF; 17 | appTheme.colors[AppThemeCol_Playhead] = 0xB445A7E6; 18 | appTheme.colors[AppThemeCol_PlayheadLine] = 0xFF9FCAE6; 19 | appTheme.colors[AppThemeCol_PlayheadHovered] = 0xCE73B2DA; 20 | appTheme.colors[AppThemeCol_PlayheadSelected] = 0xB4E6E145; 21 | appTheme.colors[AppThemeCol_MarkerHovered] = 0xFFFFFFFF; 22 | appTheme.colors[AppThemeCol_MarkerSelected] = 0xFFFFFFFF; 23 | appTheme.colors[AppThemeCol_Track] = 0xFF3F3F3A; 24 | appTheme.colors[AppThemeCol_TrackHovered] = 0xFF8DA282; 25 | appTheme.colors[AppThemeCol_TrackSelected] = 0xFFFFFFFF; 26 | -------------------------------------------------------------------------------- /timeline.h: -------------------------------------------------------------------------------- 1 | // Timeline widget 2 | 3 | #include 4 | namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; 5 | 6 | void DrawTimeline(otio::Timeline* timeline); 7 | bool DrawTransportControls(otio::Timeline* timeline); 8 | void DrawTimecodeRuler( 9 | const void* ptr_id, 10 | otio::RationalTime start, 11 | otio::RationalTime end, 12 | float frame_rate, 13 | float time_scalar, 14 | float scale, 15 | float width, 16 | float height); 17 | -------------------------------------------------------------------------------- /widgets.cpp: -------------------------------------------------------------------------------- 1 | // widgets.cpp 2 | 3 | #define IMGUI_DEFINE_MATH_OPERATORS 4 | #include "imgui.h" 5 | #include "imgui_internal.h" 6 | 7 | #include "widgets.h" 8 | 9 | bool Splitter( 10 | const char* str_id, 11 | bool split_vertically, 12 | float thickness, 13 | float* size1, 14 | float* size2, 15 | float min_size1, 16 | float min_size2, 17 | float splitter_long_axis_size, 18 | float hover_extend) { 19 | using namespace ImGui; 20 | ImGuiContext& g = *GImGui; 21 | ImGuiWindow* window = g.CurrentWindow; 22 | ImGuiID id = window->GetID(str_id); 23 | ImRect bb; 24 | bb.Min = window->DC.CursorPos 25 | + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); 26 | bb.Max = bb.Min 27 | + CalcItemSize( 28 | split_vertically ? ImVec2(thickness, splitter_long_axis_size) 29 | : ImVec2(splitter_long_axis_size, thickness), 30 | 0.0f, 31 | 0.0f); 32 | return ImGui::SplitterBehavior( 33 | bb, 34 | id, 35 | split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, 36 | size1, 37 | size2, 38 | min_size1, 39 | min_size2, 40 | hover_extend, 41 | 0.0f // hover_visibility_delay 42 | ); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /widgets.h: -------------------------------------------------------------------------------- 1 | // widgets.h 2 | 3 | bool Splitter( 4 | const char* str_id, 5 | bool split_vertically, 6 | float thickness, 7 | float* size1, 8 | float* size2, 9 | float min_size1, 10 | float min_size2, 11 | float splitter_long_axis_size = -1.0f, 12 | float hover_extend = 0.0f); 13 | --------------------------------------------------------------------------------