diff --git a/.gitmodules b/.gitmodules index e69de29b..643c39bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "viewer"] + path = viewer + url = https://github.com/calband/calchart-viewer.git diff --git a/DeveloperDocs/ViewerIntegration.md b/DeveloperDocs/ViewerIntegration.md new file mode 100644 index 00000000..29411610 --- /dev/null +++ b/DeveloperDocs/ViewerIntegration.md @@ -0,0 +1,274 @@ +# CalChart Viewer Integration + +The CalChart viewer is integrated as a Git submodule at `viewer/`. + +## Center View Modes + +CalChart supports three center view modes that can be cycled through with **Cmd+Return** (Ctrl+Return on Windows/Linux): + +1. **Field View** (`mCanvas`) - The main field editor where you create and edit stuntsheets +2. **Animation View** (`mShadowAnimationPanel`) - Shows the animated preview of the show +3. **Viewer View** (`mViewerPanel`) - Shows the web-based CalChart viewer (experimental) + +All three views share the same center pane position, with only one visible at a time. The architecture uses `CenterViewMode` enum to track which view is currently active. + +### Experimental Feature Gate + +The Viewer is an **experimental feature** that is disabled by default. To enable viewer access via Cmd+Return cycling: + +1. Open **CalChart > Preferences > General Setup** +2. Check **"Enable Viewer (experimental)"** +3. Click **OK** + +When disabled (default), Cmd+Return cycles: Field → Animation → Field (skipping Viewer). +When enabled, Cmd+Return cycles: Field → Animation → Viewer → Field. + +The viewer can also be accessed directly via **View > Preview in Viewer (experimental)...** regardless of the preference setting. + +### Implementation + +The center view system is implemented in [CalChartFrame.h](../src/CalChartFrame.h) and [CalChartFrame.cpp](../src/CalChartFrame.cpp): + +- **`CenterViewMode` enum** - Tracks Field, Animation, or Viewer +- **`ShowCenterView(mode)`** - Shows the requested center pane and hides others +- **`SetCenterViewMode(mode)`** - Sets the mode and updates UI labels +- **`CycleToNextCenterView()`** - Cycles views, checks `Get_AllowViewer()` config to determine if viewer should be included +- **`OnSwapAnimation()`** - Keyboard handler (Cmd+Return) that cycles views +- **`OnToggleViewerPanel()`** - Menu handler that directly toggles viewer visibility +- **`AllowViewer` config flag** - Stored in [CalChartConfiguration.h](../core/CalChartConfiguration.h), default `false` + +## Architecture + +### HTTP Server (ViewerServer) + +CalChart runs an embedded HTTP server ([ViewerServer.cpp](../src/ViewerServer.cpp)) on port 8888 that: + +- Serves the viewer HTML/CSS/JS to the embedded wxWebView browser +- Provides a REST API at `/api/show` that returns the current show as JSON +- Provides a health check endpoint at `/api/status` + +**Debug vs Release behavior:** +- **Debug builds**: Serve files directly from `viewer/` directory for live editing (using `CMAKE_VIEWER_SOURCE_DIR`) +- **Release builds**: Serve pre-embedded HTML from the binary (generated by [cmake/EmbedViewerHtml.cmake](../cmake/EmbedViewerHtml.cmake)) + +This gives developers the flexibility to edit viewer HTML/CSS/JS and see changes immediately by refreshing the browser, while maintaining the convenience of a single-binary deployment. + +**Important:** The debug file serving uses explicit file routes with regex patterns instead of httplib's `set_mount_point()` to avoid AddressSanitizer (ASAN) crashes in httplib's conditional request handling. + +### ViewerPanel + +[ViewerPanel.cpp](../src/ViewerPanel.cpp) is a wxPanel that embeds a wxWebView browser: + +- **`GoHome()`** - Navigates to `http://localhost:8888` +- **`UpdateShowData()`** - Calls JavaScript `loadCalChartShow()` to refresh show data without page reload +- **`RefreshViewer()`** - Full page reload (rarely used, UpdateShowData preferred) +- **`OnPageLoaded()`** - Triggered when page finishes loading, calls UpdateShowData to load initial show + +### JavaScript Integration + +The viewer's JavaScript ([viewer/js/](../viewer/js/)) is modified to support CalChart integration: + +**[viewer/js/viewer/ApplicationController.js](../viewer/js/viewer/ApplicationController.js):** +```javascript +ApplicationController.prototype.loadFromCalChart = function() { + $.ajax({ + url: '/api/show', + dataType: 'json', + success: function(data) { + var show = ShowUtils.fromJSON(data); + // ... load show into viewer + } + }); +}; +``` + +**[viewer/js/application.js](../viewer/js/application.js):** +```javascript +// Expose function for CalChart to call via RunScript() +window.loadCalChartShow = function() { + applicationController.loadFromCalChart(); +}; +``` + +### Auto-Update on Edits + +When editing a show in CalChart, the viewer automatically updates: + +**[CalChartFrame::OnUpdate()](../src/CalChartFrame.cpp):** +```cpp +void CalChartFrame::OnUpdate() { + // ... other update logic ... + + // If viewer is visible, update it with latest show data + if (mViewerPanel && mCurrentCenterView == CenterViewMode::Viewer) { + mViewerPanel->UpdateShowData(); + } +} +``` + +This means any edit (moving points, changing continuity, etc.) triggers a viewer refresh when the viewer is active. + +## Build System + +The embedding is handled by: + +1. **cmake/EmbedViewerHtml.cmake** - CMake function that reads `viewer/index.html` and generates `CalChartViewerHtml.h` +2. **src/CMakeLists.txt** - Calls `embed_viewer_html()` and adds the generated header to the CalChart target +3. **ViewerServer.cpp** - Uses `CalChart::ViewerHtml::GetViewerHtml()` to get the HTML content + +## Development Workflow + +### Initial Setup + +When cloning the repository: + +```bash +git submodule update --init --recursive +``` + +**The viewer is built automatically by CMake!** If you have Node.js installed, CMake will: +1. Run `npm install` to install dependencies (if needed) +2. Run `grunt build` to compile JavaScript and CSS +3. Create the viewer assets in `viewer/build/` + +No manual setup required - just configure and build as normal! + +### Building the Viewer + +The viewer build is integrated into CMake and happens automatically during configuration/build. + +**What CMake does:** +- Checks for Node.js and npm +- Installs npm dependencies (creates `viewer/node_modules/`) +- Builds JavaScript (`viewer/build/js/application.js`) +- Builds CSS (`viewer/build/css/app.css`, `graph.css`, etc.) + +**Manual build (if needed):** +```bash +cd viewer +npm install +npm run build +``` + +Or use the convenience script: +```bash +./scripts/setup-viewer.sh +``` + +### Editing the Viewer + +In debug builds, all viewer files (HTML, CSS, JS, images) are served directly from the `viewer/` directory. + +**For active development:** + +1. In one terminal, run the viewer's auto-rebuild: + ```bash + cd viewer + npm run watch + ``` + This will automatically rebuild JS/CSS when you edit files. + +2. Run CalChart in debug mode +3. Enable the viewer: **Preferences > General Setup > Enable Viewer (experimental)** +4. Switch to Viewer mode (Cmd+Return until you reach viewer, or **View > Preview in Viewer (experimental)...**) +5. Edit viewer files - the browser will automatically refresh via the viewer's built-in refresh mechanism +6. Make changes to CalChart - the viewer updates automatically via `UpdateShowData()` + +**What gets served in debug:** +- `/` → `viewer/index.html` +- `/build/js/application.js` → `viewer/build/js/application.js` +- `/build/css/app.css` → `viewer/build/css/app.css` +- `/img/*` → `viewer/img/*` +- All other static assets from `viewer/` + +### Testing Release Mode + +To test the embedded version (what end users will see): + +```bash +cd build/mac-release +cmake -DCMAKE_BUILD_TYPE=Release ../.. +cmake --build . +``` + +Release builds embed the HTML at compile time, so you need to rebuild after viewer changes. + +## Implementation Details + +The generated header (`build/*/src/generated/CalChartViewerHtml.h`) contains: + +- `GetEmbeddedHtml()` - Returns the pre-embedded HTML string +- `GetViewerHtml()` - Smart function that: + - In debug: Reads from `CMAKE_VIEWER_SOURCE_DIR/index.html` if available + - Falls back to embedded HTML if file read fails or in release mode + +The `CMAKE_VIEWER_SOURCE_DIR` preprocessor define is only set in Debug builds, making the filesystem path available. + +## Key Implementation Details + +### ASAN Fix for Static File Serving + +The debug build originally used httplib's `set_mount_point()` feature to serve static files, but this caused **AddressSanitizer (ASAN) buffer overflow crashes** when handling HTTP conditional requests (If-None-Match headers). + +**Solution:** Implemented explicit file serving with regex routes ([ViewerServer.cpp lines 98-127](../src/ViewerServer.cpp)): +```cpp +#ifdef CMAKE_VIEWER_SOURCE_DIR +mServer->Get(R"(.+\.(css|js|png|jpg|jpeg|gif|svg|ico|json|woff|woff2|ttf|eot))", + [](const httplib::Request& req, httplib::Response& res) { + auto path = std::string(CMAKE_VIEWER_SOURCE_DIR) + req.path; + std::ifstream file(path, std::ios::binary); + // ... manual file reading and Content-Type setting ... +}); +#endif +``` + +This avoids the buggy code path entirely while maintaining live file serving for development. + +### Data Flow + +**CalChart → Viewer:** +1. User edits show in CalChart +2. `CalChartFrame::OnUpdate()` called +3. If viewer visible: `mViewerPanel->UpdateShowData()` +4. wxWebView executes: `window.loadCalChartShow()` +5. JavaScript calls AJAX: `GET http://localhost:8888/api/show` +6. ViewerServer calls: `CalChartDoc::toViewerJSON()` +7. Viewer receives JSON and calls: `ShowUtils.fromJSON(data)` +8. Viewer re-renders with updated show + +**No page reloads needed** - the viewer updates its data model in-place, providing smooth live editing experience. + +## Future Enhancements + +Possible improvements: + +1. **~~Static asset serving~~** - ✅ **DONE** - Debug mode serves CSS/JS/images from `viewer/` directory +2. **Multiple file embedding** - Embed all viewer assets (CSS, JS) for offline use in release builds +3. **Build-time optimization** - Minify HTML/CSS/JS for release builds +4. **~~Auto-refresh on edits~~** - ✅ **DONE** - Viewer updates automatically when show is edited +5. **Configuration UI** - Add viewer-specific settings (refresh rate, layout options) +6. **Error handling** - Better UI feedback when viewer fails to load +7. **Remove experimental gate** - Once stable, enable viewer by default and remove AllowViewer flag + +## Updating the Viewer Submodule + +To update to the latest viewer version: + +```bash +cd viewer +git checkout main # or specific branch/tag +git pull +cd .. +git add viewer +git commit -m "Update viewer submodule to latest" +``` + +Or to update to a specific commit: + +```bash +cd viewer +git checkout +cd .. +git add viewer +git commit -m "Update viewer submodule to " +``` diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 6cd6984e..f21f56ed 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -114,8 +114,12 @@ The CalChart3 source code lives on [Github](https://github.com/calband/calchart) ``` git clone https://github.com/calband/calchart.git ./calchart +cd calchart +git submodule update --init --recursive ``` +**Note:** The CalChart viewer will be built automatically as part of the CMake build process if Node.js is installed. If you don't have Node.js and want the full viewer functionality, install it from [nodejs.org](https://nodejs.org/) before building. + You should see the project being downloaded, and it should appear to be similar to: ``` Cloning into './calchart'... diff --git a/LATEST_RELEASE_NOTES.md b/LATEST_RELEASE_NOTES.md index 037c09a8..9b906e49 100644 --- a/LATEST_RELEASE_NOTES.md +++ b/LATEST_RELEASE_NOTES.md @@ -6,3 +6,6 @@ Bugs addressed in this release: Other changes: +* [#773](../../issues/773) Add the minimal integration of CalChart-Viewer + + diff --git a/RELEASE_INSTRUCTIONS.md b/RELEASE_INSTRUCTIONS.md index 7c01d78d..82ce37f2 100644 --- a/RELEASE_INSTRUCTIONS.md +++ b/RELEASE_INSTRUCTIONS.md @@ -27,11 +27,9 @@ The current calchart version is 3.8.6. In all commands below, substitute that n awk '//; /^# Release notes/{while(getline<"LATEST_RELEASE_NOTES.md"){print}}' README.md > tmp && mv tmp README.md ``` - 5. Clear out LATEST_RELEASE_NOTES.md for next development effort. Update $CCVER+1 in RELEASE_INSTRUCTIONS.md. + 5. Merge the branch into main - 6. Merge the branch into main - - 7. Tag the depot + 6. Tag the depot ``` $ git tag -a v3.8.6 -m "calchart-3.8.6" @@ -40,6 +38,8 @@ $ git push origin v3.8.6 This should trigger the github action, which should publish release notes in Draft form. + 7. Do a comment where we clear out LATEST_RELEASE_NOTES.md for next development effort. Update $CCVER+1 in RELEASE_INSTRUCTIONS.md. + 8. Once the Release information looks good, Press the Publish Release button. 9. Download the Release artifacts to your machine. diff --git a/cmake/EmbedViewerHtml.cmake b/cmake/EmbedViewerHtml.cmake new file mode 100644 index 00000000..883ad1f7 --- /dev/null +++ b/cmake/EmbedViewerHtml.cmake @@ -0,0 +1,93 @@ +# CMake script to embed viewer HTML into a C++ header file +# +# This script reads the viewer/index.html file and generates a C++ header +# that contains the HTML as a string constant. +# +# In DEBUG builds, the generated header will include logic to optionally +# serve files directly from disk for live editing. + +function(embed_viewer_html TARGET_NAME VIEWER_SOURCE_DIR OUTPUT_HEADER) + set(VIEWER_HTML_FILE "${VIEWER_SOURCE_DIR}/index.html") + + # Read the HTML file + if(EXISTS "${VIEWER_HTML_FILE}") + file(READ "${VIEWER_HTML_FILE}" VIEWER_HTML_CONTENT) + + # Escape special characters for C++ string literal + string(REPLACE "\\" "\\\\" VIEWER_HTML_CONTENT "${VIEWER_HTML_CONTENT}") + string(REPLACE "\"" "\\\"" VIEWER_HTML_CONTENT "${VIEWER_HTML_CONTENT}") + string(REPLACE "\n" "\\n\"\n\"" VIEWER_HTML_CONTENT "${VIEWER_HTML_CONTENT}") + + # Generate the header file content + set(HEADER_CONTENT "// Auto-generated file - do not edit manually +// Generated from ${VIEWER_HTML_FILE} + +#pragma once + +#include +#include +#include + +namespace CalChart { +namespace ViewerHtml { + +// Path to viewer source directory (only available in debug builds) +#ifdef CMAKE_VIEWER_SOURCE_DIR +constexpr const char* kViewerSourceDir = CMAKE_VIEWER_SOURCE_DIR; +constexpr bool kUseFileSystem = true; +#else +constexpr const char* kViewerSourceDir = nullptr; +constexpr bool kUseFileSystem = false; +#endif + +// Embedded HTML content +inline const char* GetEmbeddedHtml() { + return \"${VIEWER_HTML_CONTENT}\"; +} + +// Get viewer HTML - from filesystem in debug, embedded in release +inline std::string GetViewerHtml() { + if constexpr (kUseFileSystem) { + if (kViewerSourceDir != nullptr) { + std::string path = std::string(kViewerSourceDir) + \"/index.html\"; + std::ifstream file(path); + if (file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + // Fall through to embedded if file can't be read + } + } + return GetEmbeddedHtml(); +} + +} // namespace ViewerHtml +} // namespace CalChart +") + + # Write the header file + file(WRITE "${OUTPUT_HEADER}" "${HEADER_CONTENT}") + + message(STATUS "Generated viewer HTML header: ${OUTPUT_HEADER}") + message(STATUS " Source: ${VIEWER_HTML_FILE}") + + else() + message(WARNING "Viewer HTML file not found: ${VIEWER_HTML_FILE}") + message(WARNING "Using fallback embedded HTML") + + # Create a minimal fallback header + set(HEADER_CONTENT "// Auto-generated fallback - viewer source not found +#pragma once +#include +namespace CalChart { +namespace ViewerHtml { +inline std::string GetViewerHtml() { + return \"

Viewer not available

viewer/index.html not found

\"; +} +} +} +") + file(WRITE "${OUTPUT_HEADER}" "${HEADER_CONTENT}") + endif() +endfunction() diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake index ff53993b..a196fc52 100644 --- a/cmake/dependencies.cmake +++ b/cmake/dependencies.cmake @@ -83,6 +83,9 @@ FetchContent_MakeAvailable(wxUI) set(JSON_BuildTests OFF CACHE INTERNAL "") try_find_or_fetch(nlohmann_json nlohmann_json https://github.com/nlohmann/json 55f93686c01528224f448c19128836e7df245f72) +# cpp-httplib (header-only). Prefer system (config package), fallback to fetch. +try_find_or_fetch(httplib httplib https://github.com/yhirose/cpp-httplib v0.9.8) + try_find_or_fetch(Catch2 Catch2 https://github.com/catchorg/Catch2 6e79e68) # v3.4.0 if(USE_SYSTEM_DEPENDENCIES) diff --git a/core/CalChartConfiguration.cpp b/core/CalChartConfiguration.cpp index 64da7a3e..2bca373e 100644 --- a/core/CalChartConfiguration.cpp +++ b/core/CalChartConfiguration.cpp @@ -319,6 +319,8 @@ IMPLEMENT_CONFIGURATION_FUNCTIONS(IgnoredUpdateVersion, std::string, ""); IMPLEMENT_CONFIGURATION_FUNCTIONS(GitHubToken, std::string, ""); +IMPLEMENT_CONFIGURATION_FUNCTIONS(AllowViewer, bool, false); + // OBSOLETE Settings // "MainFrameZoom" now obsolete with version post 3.2, use "MainFrameZoom2" // IMPLEMENT_CONFIGURATION_FUNCTIONS( MainFrameZoom, float, 0.5); diff --git a/core/CalChartConfiguration.h b/core/CalChartConfiguration.h index 16e4eaec..6657cc17 100644 --- a/core/CalChartConfiguration.h +++ b/core/CalChartConfiguration.h @@ -241,6 +241,9 @@ private: \ // GitHub token: personal access token for bug report submission DECLARE_CONFIGURATION_FUNCTIONS(GitHubToken, std::string); + // Viewer: enable experimental viewer feature + DECLARE_CONFIGURATION_FUNCTIONS(AllowViewer, bool); + public: // color palettes: The color Palettes allow you to set different "blocks" of // colors. diff --git a/scripts/setup-viewer.sh b/scripts/setup-viewer.sh new file mode 100755 index 00000000..5d1542b2 --- /dev/null +++ b/scripts/setup-viewer.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Setup script for calchart-viewer submodule +# +# Note: CMake will build the viewer automatically if Node.js is installed. +# This script is provided for manual builds or troubleshooting. + +set -e + +echo "Setting up CalChart Viewer..." +echo "" +echo "Note: CMake builds the viewer automatically during the build process." +echo "This script is only needed for manual builds or troubleshooting." +echo "" + +# Check if we're in the right directory +if [ ! -f "viewer/package.json" ]; then + echo "Error: Run this script from the CalChart root directory" + exit 1 +fi + +# Check for Node.js +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed" + echo "Please install Node.js from https://nodejs.org/" + exit 1 +fi + +# Check for npm +if ! command -v npm &> /dev/null; then + echo "Error: npm is not installed" + echo "npm should come with Node.js. Please reinstall Node.js." + exit 1 +fi + +cd viewer + +echo "Installing npm dependencies..." +npm install + +echo "Installing grunt-cli globally (may require sudo)..." +npm install -g grunt-cli 2>/dev/null || sudo npm install -g grunt-cli + +echo "Building viewer..." +grunt build + +echo "" +echo "✅ Viewer setup complete!" +echo "" +echo "To auto-rebuild during development, run:" +echo " cd viewer && grunt watch" +echo "" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index de41c7c6..be67d728 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,22 @@ # CalChart CMake +# Include viewer HTML embedding support +include(${PROJECT_SOURCE_DIR}/cmake/EmbedViewerHtml.cmake) + +# Build the viewer as a subdirectory (uses viewer/CMakeLists.txt) +add_subdirectory(${PROJECT_SOURCE_DIR}/viewer ${CMAKE_CURRENT_BINARY_DIR}/viewer) + +# Generate viewer HTML header from submodule +set(VIEWER_HTML_HEADER "${CMAKE_CURRENT_BINARY_DIR}/generated/CalChartViewerHtml.h") +embed_viewer_html(CalChart "${PROJECT_SOURCE_DIR}/viewer" "${VIEWER_HTML_HEADER}") + +# Inform developers about viewer integration mode +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + message(STATUS "CalChart Viewer: Debug mode - will serve from ${PROJECT_SOURCE_DIR}/viewer (live editing enabled)") +else() + message(STATUS "CalChart Viewer: Release mode - using embedded HTML") +endif() + # CalChart docs (excludes developer-only docs in DeveloperDocs/) file( GLOB CalChartDocs @@ -154,6 +171,11 @@ add_executable( TransitionSolverView.h WebViewDemoDialog.cpp WebViewDemoDialog.h + ViewerPanel.cpp + ViewerPanel.h + ViewerServer.cpp + ViewerServer.h + ${VIEWER_HTML_HEADER} basic_ui.cpp basic_ui.h calchart.rc @@ -184,6 +206,17 @@ target_include_directories( CalChart PRIVATE "${PROJECT_SOURCE_DIR}/resources/common" + "${CMAKE_CURRENT_BINARY_DIR}/generated" +) + +# In debug builds, define the viewer source directory for live editing +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions( + CalChart + PRIVATE + CMAKE_VIEWER_SOURCE_DIR="${PROJECT_SOURCE_DIR}/viewer" + ) +endif( "${cpp_httplib_SOURCE_DIR}" ) target_link_libraries( diff --git a/src/CalChartApp.cpp b/src/CalChartApp.cpp index 9087748c..aee5d096 100644 --- a/src/CalChartApp.cpp +++ b/src/CalChartApp.cpp @@ -31,6 +31,7 @@ #include "HostAppInterface.h" #include "SystemConfiguration.h" #include "UpdateChecker.h" +#include "ViewerServer.h" #include "basic_ui.h" #include "platconf.h" @@ -147,6 +148,11 @@ wxPrintDialogData& CalChartApp::GetGlobalPrintDialog() return *mPrintDialogData; } +ViewerServer& CalChartApp::GetViewerServer() +{ + return *mViewerServer; +} + CalChart::CircularLogBuffer CalChartApp::GetLogBuffer() const { return mLogTarget->GetLogBuffer(); @@ -154,8 +160,7 @@ CalChart::CircularLogBuffer CalChartApp::GetLogBuffer() const void CalChartApp::InitAppAsServer() { - wxLogDebug("Initializing CalChart as server..."); - + wxLogDebug("CalChartApp: Initializing as Server"); // Create log target (wxLog will take ownership and delete it) mLogTarget = new CalChartLogTarget(CalChart::CircularLogBuffer{ 100 }); @@ -177,6 +182,12 @@ void CalChartApp::InitAppAsServer() mHelpManager = std::make_unique(); mPrintDialogData = std::make_unique(); + // Start the viewer server + wxLogDebug("CalChartApp: Creating and starting ViewerServer"); + mViewerServer = std::make_unique(); + mViewerServer->Start(8888); + wxLogDebug("CalChartApp: ViewerServer started"); + //// Create the main frame window auto frame = new CalChartSplash(mDocManager, nullptr, "CalChart", wxCalChart::GetGlobalConfig()); @@ -226,7 +237,7 @@ void CalChartApp::InitAppAsServer() // Core invokes this callback on the worker thread. Marshal to the UI thread here. if (!wxTheApp) { // No wx application available; best-effort: log and do nothing. - std::cerr << "[CalChartApp] wxTheApp not available to show update dialog for tag=" << latestTag << "\n"; + wxLogWarning("CalChartApp: wxTheApp not available to show update dialog for tag=%s", latestTag.c_str()); } wxTheApp->CallAfter([frame, latestTag]() { // Build a small dialog with a checkbox "Never show this again for this release". @@ -277,6 +288,13 @@ void CalChartApp::ProcessArguments() void CalChartApp::ExitAppAsServer() { + // Stop the viewer server + wxLogDebug("CalChartApp: Stopping ViewerServer"); + if (mViewerServer) { + mViewerServer->Stop(); + wxLogDebug("CalChartApp: ViewerServer stopped"); + } + // Flush out the other commands wxCalChart::GetGlobalConfig().FlushWriteQueue(); // Get the file history diff --git a/src/CalChartApp.h b/src/CalChartApp.h index d62a5026..c65e53f4 100644 --- a/src/CalChartApp.h +++ b/src/CalChartApp.h @@ -24,13 +24,11 @@ #include #include -// CalChartApp represents the wxWidgets App for CalChart. The document manager creates -// the actual CalChart Document instances. This just serves the purpose of being the first -// thing that runs, holds much of the top level logic. - +// Forward declarations class CalChartApp; class HostAppInterface; class HelpManager; +class ViewerServer; class wxDocManager; class wxPrintDialogData; class CalChartLogTarget; @@ -62,6 +60,9 @@ class CalChartApp : public wxApp { // Get a copy of the global log buffer for bug reporting CalChart::CircularLogBuffer GetLogBuffer() const; + // Get the viewer server + ViewerServer& GetViewerServer(); + private: void ProcessArguments(); @@ -74,5 +75,6 @@ class CalChartApp : public wxApp { std::unique_ptr mHelpManager; std::unique_ptr mHostInterface; std::unique_ptr mPrintDialogData; + std::unique_ptr mViewerServer; CalChartLogTarget* mLogTarget = nullptr; // Owned by wxLog, don't delete }; diff --git a/src/CalChartDoc.cpp b/src/CalChartDoc.cpp index 4a3e54d0..b9c3293f 100644 --- a/src/CalChartDoc.cpp +++ b/src/CalChartDoc.cpp @@ -274,6 +274,11 @@ bool CalChartDoc::exportViewerFile(std::filesystem::path const& filepath) return true; } +nlohmann::json CalChartDoc::toViewerJSON() const +{ + return mShow->toOnlineViewerJSON(Animation(*mShow)); +} + void CalChartDoc::FlushAllTextWindows() { CalChartDoc_FlushAllViews flushMod; diff --git a/src/CalChartDoc.h b/src/CalChartDoc.h index 18ee2c23..66b25458 100644 --- a/src/CalChartDoc.h +++ b/src/CalChartDoc.h @@ -119,6 +119,12 @@ class CalChartDoc : public wxDocument { */ bool exportViewerFile(std::filesystem::path const& filepath); + /*! + * @brief Generates JSON representation of the show for the CalChart Online Viewer. + * @return A JSON object representing the show in viewer format. + */ + [[nodiscard]] nlohmann::json toViewerJSON() const; + private: template T& LoadObjectGeneric(T& stream); diff --git a/src/CalChartFrame.cpp b/src/CalChartFrame.cpp index ae948cc6..abd4dad2 100644 --- a/src/CalChartFrame.cpp +++ b/src/CalChartFrame.cpp @@ -57,6 +57,7 @@ #include "SystemConfiguration.h" #include "TransitionSolverFrame.h" #include "TransitionSolverView.h" +#include "ViewerPanel.h" #include "ccvers.h" #include "platconf.h" #include "ui_enums.h" @@ -287,6 +288,11 @@ CalChartFrame::CalChartFrame(wxDocument* doc, wxView* view, CalChart::Configurat } } .withProxy(mViewSwapFieldAndAnimate), wxUI::Separator{}, + wxUI::Item{ "Preview in Viewer (experimental)...", "Open the built-in CalChart Viewer preview", [this] { + OnToggleViewerPanel(); + } } + .withProxy(mToggleViewerPanel), + wxUI::Separator{}, wxUI::MenuForEach{ CalChart::Ranges::enumerate_view(kAUINames), [this](auto&& whichAndName) { auto&& [which, name] = whichAndName; return wxUI::Item{ std::string("Show ") + name, std::string("Controls Displaying ") + name, [this, which] { @@ -427,7 +433,7 @@ CalChartFrame::CalChartFrame(wxDocument* doc, wxView* view, CalChart::Configurat ChangePaneVisibility(mAUIManager->GetPane(mLookupSubWindows.at(i)).IsShown(), i); } - ChangeMainFieldVisibility(mMainFieldVisible); + SetCenterViewMode(mCurrentCenterView); SetTitle(static_cast(doc)->GetTitle()); @@ -471,7 +477,7 @@ void CalChartFrame::OnClose() mConfig.Set_FieldCanvasScrollY(mCanvas->GetViewStart().y); // just to make sure we never end up hiding the Field - ShowFieldAndHideAnimation(true); + SetCenterViewMode(CenterViewMode::Field); mConfig.Set_CalChartFrameAUILayout_3_6_1(mAUIManager->SavePerspective().ToStdString()); SetViewsOnComponents(nullptr); } @@ -779,14 +785,42 @@ void CalChartFrame::OnEditCurveAssignments() } } -void CalChartFrame::ShowFieldAndHideAnimation(bool showField) +void CalChartFrame::ShowCenterView(CenterViewMode mode) { - // when we are going to show field, pause the animation - if (showField) { + // Pause animation when leaving animation view + if (mCurrentCenterView == CenterViewMode::Animation) { mShadowAnimationPanel->SetPlayState(false); } - mAUIManager->GetPane("Field").Show(showField); - mAUIManager->GetPane("ShadowAnimation").Show(!showField); + + // Hide all center panes + mAUIManager->GetPane("Field").Show(false); + mAUIManager->GetPane("ShadowAnimation").Show(false); + if (mViewerPanel) { + mAUIManager->GetPane("ViewerPanel").Show(false); + } + + // Show the requested pane + switch (mode) { + case CenterViewMode::Field: + mAUIManager->GetPane("Field").Show(true); + break; + case CenterViewMode::Animation: + mAUIManager->GetPane("ShadowAnimation").Show(true); + break; + case CenterViewMode::Viewer: + // Create viewer panel if it doesn't exist + if (!mViewerPanel) { + mViewerPanel = new ViewerPanel(this, GetShow()); + mAUIManager->AddPane(mViewerPanel, wxAuiPaneInfo().Name("ViewerPanel").CenterPane().Hide()); + } else { + // If panel already exists, update the show data + // (new panels will load data automatically when page loads) + mViewerPanel->UpdateShowData(); + } + mAUIManager->GetPane("ViewerPanel").Show(true); + break; + } + mAUIManager->Update(); } @@ -1120,7 +1154,17 @@ void CalChartFrame::OnAdjustViews(size_t which) void CalChartFrame::OnSwapAnimation() { - ChangeMainFieldVisibility(!mMainFieldVisible); + CycleToNextCenterView(); +} + +void CalChartFrame::OnToggleViewerPanel() +{ + // Toggle to viewer mode (or back to field if already in viewer) + if (mCurrentCenterView == CenterViewMode::Viewer) { + SetCenterViewMode(CenterViewMode::Field); + } else { + SetCenterViewMode(CenterViewMode::Viewer); + } } void CalChartFrame::AUIIsClose(wxAuiManagerEvent& event) @@ -1144,15 +1188,45 @@ void CalChartFrame::ChangePaneVisibility(bool show, size_t which) mAUIManager->Update(); } -void CalChartFrame::ChangeMainFieldVisibility(bool show) +void CalChartFrame::SetCenterViewMode(CenterViewMode mode) { - mMainFieldVisible = show; - if (mMainFieldVisible) { + mCurrentCenterView = mode; + + // Update menu label based on what comes next + switch (mode) { + case CenterViewMode::Field: mViewSwapFieldAndAnimate->SetItemLabel("View Animation\tCTRL-RETURN"); - } else { + break; + case CenterViewMode::Animation: + mViewSwapFieldAndAnimate->SetItemLabel("View Viewer\tCTRL-RETURN"); + break; + case CenterViewMode::Viewer: mViewSwapFieldAndAnimate->SetItemLabel("View Field\tCTRL-RETURN"); + break; } - ShowFieldAndHideAnimation(mMainFieldVisible); + + ShowCenterView(mCurrentCenterView); +} + +void CalChartFrame::CycleToNextCenterView() +{ + auto* doc = dynamic_cast(GetDocument()); + bool allowViewer = doc && doc->GetConfiguration().Get_AllowViewer(); + + CenterViewMode nextMode; + switch (mCurrentCenterView) { + case CenterViewMode::Field: + nextMode = CenterViewMode::Animation; + break; + case CenterViewMode::Animation: + // Skip viewer if not allowed + nextMode = allowViewer ? CenterViewMode::Viewer : CenterViewMode::Field; + break; + case CenterViewMode::Viewer: + nextMode = CenterViewMode::Field; + break; + } + SetCenterViewMode(nextMode); } void CalChartFrame::OnResetReferencePoint() @@ -1384,6 +1458,11 @@ void CalChartFrame::OnUpdate() mShadowAnimationPanel->OnUpdate(); + // Update viewer panel if it exists and is visible + if (mViewerPanel && mCurrentCenterView == CenterViewMode::Viewer) { + mViewerPanel->UpdateShowData(); + } + refreshInUse(); } diff --git a/src/CalChartFrame.h b/src/CalChartFrame.h index c234977a..9c1e98f0 100644 --- a/src/CalChartFrame.h +++ b/src/CalChartFrame.h @@ -42,6 +42,7 @@ class FieldCanvas; class FieldFrameControls; class FieldThumbnailBrowser; class PrintContinuityEditor; +class ViewerPanel; class wxAuiToolBar; namespace CalChart { class Configuration; @@ -128,6 +129,7 @@ class CalChartFrame : public wxDocChildFrame { void OnCmd_MarcherSelection(wxCommandEvent& event); void OnAdjustViews(size_t which); void OnSwapAnimation(); + void OnToggleViewerPanel(); void OnResetReferencePoint(); @@ -178,9 +180,16 @@ class CalChartFrame : public wxDocChildFrame { private: void refreshGhostOptionStates(); void refreshInUse(); + enum class CenterViewMode { + Field, + Animation, + Viewer + }; + void ChangePaneVisibility(bool show, size_t itemid); - void ChangeMainFieldVisibility(bool show); - void ShowFieldAndHideAnimation(bool showField); + void SetCenterViewMode(CenterViewMode mode); + void CycleToNextCenterView(); + void ShowCenterView(CenterViewMode mode); void SetViewsOnComponents(CalChartView* showField); std::string BeatStatusText() const; std::string PointStatusText() const; @@ -191,6 +200,7 @@ class CalChartFrame : public wxDocChildFrame { ContinuityBrowser* mContinuityBrowser{}; AnimationErrorsPanel* mAnimationErrorsPanel{}; AnimationPanel* mAnimationPanel{}; + ViewerPanel* mViewerPanel{}; AnimationPanel* mShadowAnimationPanel{}; PrintContinuityEditor* mPrintContinuityEditor{}; wxAuiToolBar* mControls; @@ -203,12 +213,13 @@ class CalChartFrame : public wxDocChildFrame { CalChart::Configuration& mConfig; wxAuiManager* mAUIManager; - bool mMainFieldVisible = true; + CenterViewMode mCurrentCenterView = CenterViewMode::Field; wxUI::MenuItemProxy mShowBackgroundImages; wxUI::MenuItemProxy mAdjustBackgroundImageMode; wxUI::MenuItemProxy mGhostOff; wxUI::MenuItemProxy mViewSwapFieldAndAnimate; + wxUI::MenuItemProxy mToggleViewerPanel; wxUI::MenuItemProxy mDrawPaths; std::vector mAdjustPaneIndex; diff --git a/src/CalChartSplash.cpp b/src/CalChartSplash.cpp index 4a578067..c860a4c7 100644 --- a/src/CalChartSplash.cpp +++ b/src/CalChartSplash.cpp @@ -28,6 +28,7 @@ #include "HelpDialog.hpp" #include "StackDrawPlayground.h" #include "SystemConfiguration.h" +#include "WebViewDemoDialog.h" #include "basic_ui.h" #include "ccvers.h" diff --git a/src/PreferencesGeneralSetup.cpp b/src/PreferencesGeneralSetup.cpp index 73852e8d..49649c42 100644 --- a/src/PreferencesGeneralSetup.cpp +++ b/src/PreferencesGeneralSetup.cpp @@ -43,6 +43,7 @@ void GeneralSetup::CreateControls() wxUI::CheckBox{ "Scroll Direction: Natural" }.withProxy(mScroll_Natural), wxUI::CheckBox{ "Set Sheet is undo-able" }.withProxy(mSetSheet_Undo), wxUI::CheckBox{ "Point selection is undo-able" }.withProxy(mSelection_Undo), + wxUI::CheckBox{ "Enable Viewer (experimental)" }.withProxy(mAllowViewer), }, } @@ -60,6 +61,7 @@ void GeneralSetup::InitFromConfig() *mScroll_Natural = mConfig.Get_ScrollDirectionNatural(); *mSetSheet_Undo = mConfig.Get_CommandUndoSetSheet(); *mSelection_Undo = mConfig.Get_CommandUndoSelection(); + *mAllowViewer = mConfig.Get_AllowViewer(); } bool GeneralSetup::TransferDataToWindow() @@ -77,6 +79,7 @@ bool GeneralSetup::TransferDataFromWindow() mConfig.Set_ScrollDirectionNatural(*mScroll_Natural); mConfig.Set_CommandUndoSetSheet(*mSetSheet_Undo); mConfig.Set_CommandUndoSelection(*mSelection_Undo); + mConfig.Set_AllowViewer(*mAllowViewer); return true; } @@ -89,6 +92,7 @@ bool GeneralSetup::ClearValuesToDefault() mConfig.Clear_ScrollDirectionNatural(); mConfig.Clear_CommandUndoSetSheet(); mConfig.Clear_CommandUndoSelection(); + mConfig.Clear_AllowViewer(); // the extra ones to reset: mConfig.Clear_CalChartFrameAUILayout_3_6_1(); mConfig.Clear_FieldFrameZoom_3_6_0(); diff --git a/src/PreferencesGeneralSetup.h b/src/PreferencesGeneralSetup.h index ae8d67fd..94d3833b 100644 --- a/src/PreferencesGeneralSetup.h +++ b/src/PreferencesGeneralSetup.h @@ -73,4 +73,5 @@ class GeneralSetup : public PreferencePage { wxUI::CheckBox::Proxy mScroll_Natural{}; wxUI::CheckBox::Proxy mSetSheet_Undo{}; wxUI::CheckBox::Proxy mSelection_Undo{}; + wxUI::CheckBox::Proxy mAllowViewer{}; }; diff --git a/src/ViewerPanel.cpp b/src/ViewerPanel.cpp new file mode 100644 index 00000000..680822f3 --- /dev/null +++ b/src/ViewerPanel.cpp @@ -0,0 +1,112 @@ +#include "ViewerPanel.h" + +#include +#include +#include + +#include "CalChartApp.h" +#include "CalChartDoc.h" +#include "ViewerServer.h" + +wxBEGIN_EVENT_TABLE(ViewerPanel, wxPanel) + EVT_TIMER(wxID_ANY, ViewerPanel::OnRefreshTimer) + EVT_WEBVIEW_LOADED(wxID_ANY, ViewerPanel::OnPageLoaded) + wxEND_EVENT_TABLE() + + ViewerPanel::ViewerPanel(wxWindow* parent, CalChartDoc* doc) + : wxPanel(parent, wxID_ANY) + , mDoc(doc) + , mRefreshTimer(this) +{ + wxLogDebug("ViewerPanel: Constructing ViewerPanel"); + auto sizer = new wxBoxSizer(wxVERTICAL); + + // Create the wxWebView using the factory method with full parameters + // Start with a placeholder URL - we'll navigate to the actual viewer after creation + mWebView = wxWebView::New(this, wxID_ANY, wxWebViewDefaultURLStr, wxDefaultPosition, wxDefaultSize); + if (!mWebView) { + wxLogError("ViewerPanel: Failed to create wxWebView"); + return; + } + + sizer->Add(mWebView, 1, wxEXPAND | wxALL, 0); + SetSizer(sizer); + wxLogDebug("ViewerPanel: wxWebView created successfully"); + + // Don't auto-reload here - let the JavaScript in the page handle periodic updates + wxLogDebug("ViewerPanel: JavaScript will handle periodic refresh"); + + // Notify the server about the current doc + if (mDoc) { + auto& app = *wxGetApp().GetInstance(); + CalChartApp* pApp = static_cast(&app); + pApp->GetViewerServer().SetCurrentDoc(mDoc); + wxLogDebug("ViewerPanel: Set current doc on server"); + } else { + wxLogWarning("ViewerPanel: mDoc is null!"); + } + + // Navigate to the viewer + GoHome(); +} + +ViewerPanel::~ViewerPanel() +{ + mRefreshTimer.Stop(); + // Clear the server's current doc reference + if (mDoc) { + auto& app = *wxGetApp().GetInstance(); + CalChartApp* pApp = static_cast(&app); + pApp->GetViewerServer().SetCurrentDoc(nullptr); + } +} + +void ViewerPanel::RefreshViewer() +{ + if (mWebView) { + mWebView->Reload(); + } +} + +void ViewerPanel::UpdateShowData() +{ + if (mWebView) { + // Trigger the viewer's loadCalChartShow() function to refresh show data + // This function is exposed by the viewer's application.js + mWebView->RunScript("if (typeof loadCalChartShow === 'function') { loadCalChartShow(); }"); + } +} + +void ViewerPanel::GoHome() +{ + if (mWebView) { + auto& app = *wxGetApp().GetInstance(); + CalChartApp* pApp = static_cast(&app); + auto url = pApp->GetViewerServer().GetViewerUrl(); + wxLogDebug("ViewerPanel: Navigating to URL: %s", url.c_str()); + mWebView->LoadURL(url); + } else { + wxLogWarning("ViewerPanel: mWebView is null in GoHome()"); + } +} + +void ViewerPanel::OnDocumentChanged(wxNotifyEvent& event) +{ + RefreshViewer(); + event.Skip(); +} + +void ViewerPanel::OnRefreshTimer(wxTimerEvent&) +{ + // Timer disabled - JavaScript in the page handles periodic updates + // This allows the page to stay loaded and update content without full page reloads +} + +void ViewerPanel::OnPageLoaded(wxWebViewEvent& event) +{ + wxLogDebug("ViewerPanel: Page loaded event fired for: %s", event.GetURL().c_str()); + + // The viewer's HTML/JS is fully loaded, now trigger it to load the show data + // from CalChart's /api/show endpoint + UpdateShowData(); +} diff --git a/src/ViewerPanel.h b/src/ViewerPanel.h new file mode 100644 index 00000000..8b1cac12 --- /dev/null +++ b/src/ViewerPanel.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +class wxWebView; +class CalChartDoc; + +/** + * ViewerPanel is a wxPanel that displays the CalChart Viewer in an embedded wxWebView. + * It automatically refreshes when the current show changes. + */ +class ViewerPanel : public wxPanel { +public: + explicit ViewerPanel(wxWindow* parent, CalChartDoc* doc); + ~ViewerPanel() override; + + ViewerPanel(const ViewerPanel&) = delete; + ViewerPanel& operator=(const ViewerPanel&) = delete; + + /** + * Refresh the viewer with the current show data (full page reload). + */ + void RefreshViewer(); + + /** + * Update the show data without reloading the page. + * This triggers the JavaScript loadShow() function to fetch fresh data. + */ + void UpdateShowData(); + + /** + * Navigate to the viewer homepage. + */ + void GoHome(); + +private: + void OnDocumentChanged(wxNotifyEvent& event); + void OnRefreshTimer(wxTimerEvent& event); + void OnPageLoaded(wxWebViewEvent& event); + + wxWebView* mWebView = nullptr; + CalChartDoc* mDoc = nullptr; + wxTimer mRefreshTimer; + + wxDECLARE_EVENT_TABLE(); +}; diff --git a/src/ViewerServer.cpp b/src/ViewerServer.cpp new file mode 100644 index 00000000..e92579e6 --- /dev/null +++ b/src/ViewerServer.cpp @@ -0,0 +1,244 @@ +#include "ViewerServer.h" + +#include "CalChartViewerHtml.h" +#include +#include +#include +#include +#include +#include + +#include "CalChartDoc.h" + +class ViewerServer::Impl { +public: + Impl() + : mPort(8888) + , mIsRunning(false) + , mCurrentDoc(nullptr) + { + } + + ~Impl() + { + Stop(); + } + + void Start(int port) + { + std::lock_guard lock(mMutex); + + if (mIsRunning) { + wxLogDebug("ViewerServer: Server already running on port %d", mPort); + return; + } + + mPort = port; + mServer = std::make_unique(); + wxLogDebug("ViewerServer: Creating server on port %d", mPort); + + // Route: GET /api/show - returns the current show as JSON + mServer->Get("/api/show", [this](const httplib::Request&, httplib::Response& res) { + wxLogDebug("ViewerServer: GET /api/show requested"); + std::lock_guard showLock(mMutex); + + if (!mCurrentDoc) { + wxLogDebug("ViewerServer: No current doc loaded"); + res.set_content(R"({"error": "No show loaded"})", "application/json"); + res.status = 400; + return; + } + + try { + wxLogDebug("ViewerServer: Generating show JSON from CalChartDoc..."); + + // Get the viewer JSON from the document + auto response = mCurrentDoc->toViewerJSON(); + + res.set_content(response.dump(4), "application/json"); + res.status = 200; + wxLogDebug("ViewerServer: /api/show response sent successfully"); + } catch (const std::exception& e) { + wxLogError("ViewerServer: Exception in /api/show: %s", e.what()); + nlohmann::json error; + error["error"] = e.what(); + res.set_content(error.dump(), "application/json"); + res.status = 500; + } + }); + + // Route: GET /api/status - health check + mServer->Get("/api/status", [](const httplib::Request&, httplib::Response& res) { + wxLogDebug("ViewerServer: GET /api/status requested"); + res.set_content(R"({"status": "ok"})", "application/json"); + res.status = 200; + }); + + // Route: GET / - serve viewer HTML + mServer->Get("/", [](const httplib::Request&, httplib::Response& res) { + wxLogDebug("ViewerServer: GET / requested"); + auto html = CalChart::ViewerHtml::GetViewerHtml(); +#ifdef CMAKE_VIEWER_SOURCE_DIR + wxLogDebug("ViewerServer: Serving from filesystem: %s", CMAKE_VIEWER_SOURCE_DIR); +#else + wxLogDebug("ViewerServer: Serving embedded HTML"); +#endif + res.set_content(html, "text/html"); + res.status = 200; + wxLogDebug("ViewerServer: Root page served"); + }); + + // Route: GET /viewer - same as / + mServer->Get("/viewer", [](const httplib::Request&, httplib::Response& res) { + wxLogDebug("ViewerServer: GET /viewer requested"); + auto html = CalChart::ViewerHtml::GetViewerHtml(); + res.set_content(html, "text/html"); + res.status = 200; + }); + +#ifdef CMAKE_VIEWER_SOURCE_DIR + // In debug mode, serve static files from viewer directory + // We explicitly serve files instead of using set_mount_point to avoid + // ASAN crashes in httplib's conditional request handling + mServer->Get(R"(.+\.(css|js|png|jpg|jpeg|gif|svg|ico|json|woff|woff2|ttf|eot))", [](const httplib::Request& req, httplib::Response& res) { + auto path = std::string(CMAKE_VIEWER_SOURCE_DIR) + req.path; + std::ifstream file(path, std::ios::binary); + if (file) { + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + // Set content type based on extension + auto ext = req.path.substr(req.path.find_last_of('.') + 1); + std::string contentType = "application/octet-stream"; + if (ext == "css") + contentType = "text/css"; + else if (ext == "js") + contentType = "application/javascript"; + else if (ext == "json") + contentType = "application/json"; + else if (ext == "png") + contentType = "image/png"; + else if (ext == "jpg" || ext == "jpeg") + contentType = "image/jpeg"; + else if (ext == "gif") + contentType = "image/gif"; + else if (ext == "svg") + contentType = "image/svg+xml"; + else if (ext == "ico") + contentType = "image/x-icon"; + + res.set_content(content, contentType.c_str()); + res.status = 200; + } else { + res.status = 404; + res.set_content("File not found", "text/plain"); + } + }); + wxLogDebug("ViewerServer: Configured to serve static files from %s", CMAKE_VIEWER_SOURCE_DIR); +#endif + + // Start the server in a background thread + mThread = std::thread([this]() { + wxLogDebug("ViewerServer: Thread started, calling listen() on port %d", mPort); + bool result = mServer->listen("localhost", mPort); + wxLogDebug("ViewerServer: listen() returned: %d", result); + }); + + // Give the server a moment to start + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + mIsRunning = true; + wxLogDebug("ViewerServer: Server started successfully on localhost:%d", mPort); + } + + void Stop() + { + { + std::lock_guard lock(mMutex); + + if (!mIsRunning) { + return; + } + + mIsRunning = false; + + // Signal the server to stop, but don't destroy it yet + if (mServer) { + mServer->stop(); + } + } + + // Wait for the thread to finish (outside the lock to avoid deadlock) + if (mThread.joinable()) { + mThread.join(); + } + + // Now it's safe to destroy the server + { + std::lock_guard lock(mMutex); + mServer.reset(); + } + } + + bool IsRunning() const + { + std::lock_guard lock(mMutex); + return mIsRunning; + } + + int GetPort() const + { + std::lock_guard lock(mMutex); + return mPort; + } + + void SetCurrentDoc(CalChartDoc* doc) + { + std::lock_guard lock(mMutex); + mCurrentDoc = doc; + } + +private: + mutable std::mutex mMutex; + std::unique_ptr mServer; + std::thread mThread; + int mPort; + bool mIsRunning; + CalChartDoc* mCurrentDoc; +}; + +ViewerServer::ViewerServer() + : mImpl(std::make_unique()) +{ +} + +ViewerServer::~ViewerServer() = default; + +void ViewerServer::Start(int port) +{ + mImpl->Start(port); +} + +void ViewerServer::Stop() +{ + mImpl->Stop(); +} + +bool ViewerServer::IsRunning() const +{ + return mImpl->IsRunning(); +} + +int ViewerServer::GetPort() const +{ + return mImpl->GetPort(); +} + +void ViewerServer::SetCurrentDoc(CalChartDoc* doc) +{ + mImpl->SetCurrentDoc(doc); +} + +std::string ViewerServer::GetViewerUrl() const +{ + return "http://localhost:" + std::to_string(GetPort()) + "/"; +} diff --git a/src/ViewerServer.h b/src/ViewerServer.h new file mode 100644 index 00000000..e568823b --- /dev/null +++ b/src/ViewerServer.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include + +class CalChartDoc; + +namespace CalChart { +class Show; +} + +/** + * ViewerServer manages an HTTP server that serves the CalChart Viewer interface + * and provides an API for accessing the current show data. + * + * The server runs on localhost on a configurable port and serves: + * - /api/show - JSON representation of the current show + * - / - The viewer web interface (static files from calchart-viewer) + */ +class ViewerServer { +public: + ViewerServer(); + ~ViewerServer(); + + ViewerServer(const ViewerServer&) = delete; + ViewerServer& operator=(const ViewerServer&) = delete; + + /** + * Start the HTTP server on the given port. + * If the server is already running, this does nothing. + */ + void Start(int port = 8888); + + /** + * Stop the HTTP server. + */ + void Stop(); + + /** + * Check if the server is currently running. + */ + [[nodiscard]] bool IsRunning() const; + + /** + * Get the port the server is running on. + */ + [[nodiscard]] int GetPort() const; + + /** + * Set the current document to be served by the /api/show endpoint. + * Pass nullptr to clear the current document. + */ + void SetCurrentDoc(CalChartDoc* doc); + + /** + * Get the URL at which the viewer can be accessed. + */ + [[nodiscard]] std::string GetViewerUrl() const; + +private: + class Impl; + std::unique_ptr mImpl; +}; diff --git a/vcpkg.json b/vcpkg.json index e04fac4a..cce4020f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -20,6 +20,7 @@ "platform": "windows" }, "nlohmann-json", + "cpp-httplib", { "name": "catch2", "version>=": "3.4.0" @@ -28,4 +29,4 @@ "libwebp" ], "builtin-baseline": "365f6444ab40ee87c73c947b475b3a267b3cb77c" -} \ No newline at end of file +} diff --git a/viewer b/viewer new file mode 160000 index 00000000..42ab742d --- /dev/null +++ b/viewer @@ -0,0 +1 @@ +Subproject commit 42ab742d855243d39711da0a18ccde976478a885