From a0c44bcf24fffa05484786cb9c0dc9a02eddc51d Mon Sep 17 00:00:00 2001 From: Zachary Hall Date: Sun, 9 Feb 2025 10:11:30 -0800 Subject: [PATCH] Add support for macOS - Remove unused OpenMP from playback backend - Fix update_assets on macOS - Add support for Objective C++ on macOS - Add macOS-specific code into the Dear ImGui backend - Add macOS .app packaging - Add support for global menus, and include support for macOS global menu into the Dear ImGui backend --- CMakeLists.txt | 37 ++- assets/looper.plist.in | 34 +++ assets/update_assets.py | 2 +- backends/playback/fluidsynth/CMakeLists.txt | 3 +- .../fluidsynth/fluidsynth_backend.hpp | 1 - backends/playback/gme/CMakeLists.txt | 3 +- backends/playback/gme/gme_backend.hpp | 1 - backends/playback/sdl_mixer_x/sdl_mixer_x.cpp | 4 + backends/playback/zsm/CMakeLists.txt | 3 +- backends/playback/zsm/zsm_backend.hpp | 1 - backends/ui/imgui/CMakeLists.txt | 9 + backends/ui/imgui/RendererBackend.cpp | 72 +++++- backends/ui/imgui/RendererBackend.h | 1 + backends/ui/imgui/RendererBackendOSX.mm | 63 ++++++ backends/ui/imgui/file_browser.cpp | 30 ++- backends/ui/imgui/file_browser_osx.mm | 52 +++++ backends/ui/imgui/main.cpp | 158 +++++++++---- backends/ui/imgui/main.h | 7 + liblooperui/CMakeLists.txt | 13 ++ liblooperui/backends/noop_backend.cpp | 5 + liblooperui/backends/osx_backend.mm | 103 +++++++++ liblooperui/menus.cpp | 213 ++++++++++++++++++ liblooperui/menus.hpp | 106 +++++++++ main.cpp | 13 ++ mkicns.sh | 20 ++ playback.cpp | 2 +- 26 files changed, 875 insertions(+), 81 deletions(-) create mode 100644 assets/looper.plist.in create mode 100644 backends/ui/imgui/RendererBackendOSX.mm create mode 100644 backends/ui/imgui/file_browser_osx.mm create mode 100644 liblooperui/CMakeLists.txt create mode 100644 liblooperui/backends/noop_backend.cpp create mode 100644 liblooperui/backends/osx_backend.mm create mode 100644 liblooperui/menus.cpp create mode 100644 liblooperui/menus.hpp create mode 100755 mkicns.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ed3056..a1c1daf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,13 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten") set(EMSCRIPTEN ON) message("Building for WASM.") endif() +if(APPLE) +enable_language(OBJCXX) +set(extra_linker_flags "-undefined dynamic_lookup") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${extra_linker_flags}") +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${extra_linker_flags}") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${extra_linker_flags}") +endif() option(TESTS "Enables unit testing" OFF) find_package(Threads) list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) @@ -309,13 +316,13 @@ prefix_all(LIBRARY_SOURCES playback_backend.hpp translation.cpp translation.hpp - playback_process.cpp - playback_process.hpp base85.cpp base85.h ) +add_subdirectory(liblooperui) run_protoc(OUTDIR ${CMAKE_BINARY_DIR}/google/protobuf SOURCE google/protobuf/any.proto OUTVAR _src) add_library(liblooper SHARED ${LIBRARY_SOURCES}) +target_link_libraries(liblooper PUBLIC liblooper_ui) set_target_properties(liblooper PROPERTIES PREFIX "") if(FOR_WASMER) target_compile_definitions(liblooper PUBLIC FOR_WASMER) @@ -531,7 +538,7 @@ playback_backend_subdir(NAME "ZSM" READABLE_NAME "ZSM" SUBDIR backends/playback/ playback_backend_subdir(NAME "FLUIDSYNTH" READABLE_NAME "Fluidsynth" SUBDIR backends/playback/fluidsynth) playback_backend_subdir(NAME "GME" READABLE_NAME "Game Music Emu" SUBDIR backends/playback/gme) execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${CMAKE_CURRENT_BINARY_DIR} --ui ${ENABLED_UIS} --playback ${ENABLED_PLAYBACK_BACKENDS}) -prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ main.cpp daemon_backend.cpp daemon_backend.hpp proxy_backend.cpp proxy_backend.hpp) +prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ playback_process.cpp playback_process.hpp main.cpp daemon_backend.cpp daemon_backend.hpp proxy_backend.cpp proxy_backend.hpp) list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/backend_glue.cpp) if(DEFINED EMSCRIPTEN) set(CMAKE_EXECUTABLE_SUFFIX ".html") @@ -555,7 +562,20 @@ else() list(APPEND SOURCES ${CMAKE_BINARY_DIR}/haiku-res.rsrc) add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/haiku-res.rsrc COMMAND rc -o ${CMAKE_BINARY_DIR}/haiku-res.rsrc ${HAIKU_RES} DEPENDS ${HAIKU_RES} COMMENT Compiling resources) endif() - add_executable(${TARGET_NAME} ${SOURCES}) + if(APPLE) + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${SOURCES}) + set_target_properties(${TARGET_NAME} PROPERTIES + BUNDLE True + MACOSX_BUNDLE_GUI_IDENTIFIES com.complecwaft.catmeow.Looper + MACOSX_BUNDLE_NAME "Looper" + MACOSX_BUNDLE_ICON_FILE icon + MACOSX_BUNDLE_VERSION ${TAG} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${TAG} + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/assets/looper.plist.in + ) + else() + add_executable(${TARGET_NAME} ${SOURCES}) + endif() if(HAIKU) # add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND resattr -o ${TARGET_NAME} ${CMAKE_BINARY_DIR}/haiku-res.rsrc DEPENDS ${TARGET_NAME} ${CMAKE_BINARY_DIR}/haiku-res.rsrc COMMENT Adding resources to target attributes) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND xres -o ${TARGET_NAME} ${CMAKE_BINARY_DIR}/haiku-res.rsrc DEPENDS ${TARGET_NAME} ${CMAKE_BINARY_DIR}/haiku-res.rsrc COMMENT Adding resources to target) @@ -576,7 +596,14 @@ if(DEFINED EMSCRIPTEN) copy_to_bindir(assets/ForkAwesome/css/fork-awesome.min.css.map fork-awesome.min.css.map) endif() target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS} ${PLAYBACK_BACKENDS}) -install(TARGETS ${TARGET_NAME} liblooper ${EXTRA_LIBS} ${UI_BACKENDS} ${PLAYBACK_BACKENDS}) +if(APPLE) +install(TARGETS ${TARGET_NAME} BUNDLE DESTINATION Looper.app) +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_SOURCE_DIR}/mkicns.sh ${CMAKE_SOURCE_DIR}/assets/icon.svg ${CMAKE_BINARY_DIR}/${TARGET_NAME}.app/Contents/Resources/icon) +copy_to_bindir(assets/icon.svg ${TARGET_NAME}.app/Contents/icon.svg) +else() +install(TARGETS ${TARGET_NAME}) +endif() +install(TARGETS liblooper ${EXTRA_LIBS} ${UI_BACKENDS} ${PLAYBACK_BACKENDS}) if (UNIX AND NOT APPLE) install(FILES assets/zsm-mime.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/audio RENAME x-zsound.xml) endif() diff --git a/assets/looper.plist.in b/assets/looper.plist.in new file mode 100644 index 0000000..a4009bc --- /dev/null +++ b/assets/looper.plist.in @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/assets/update_assets.py b/assets/update_assets.py index 623f6e6..d18650c 100755 --- a/assets/update_assets.py +++ b/assets/update_assets.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 import os import sys import subprocess diff --git a/backends/playback/fluidsynth/CMakeLists.txt b/backends/playback/fluidsynth/CMakeLists.txt index 629c7a0..9c78d99 100644 --- a/backends/playback/fluidsynth/CMakeLists.txt +++ b/backends/playback/fluidsynth/CMakeLists.txt @@ -2,8 +2,7 @@ set(BACKEND_FLUIDSYNTH_SRC ${CMAKE_CURRENT_SOURCE_DIR}/fluidsynth_backend.cpp) set(BACKEND_FLUIDSYNTH_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) add_playback_backend(fluidsynth_backend ${BACKEND_FLUIDSYNTH_SRC}) target_include_directories(fluidsynth_backend PRIVATE ${BACKEND_FLUIDSYNTH_INC}) -find_package(OpenMP) find_package(PkgConfig) pkg_check_modules(fluidsynth fluidsynth IMPORTED_TARGET) -target_link_libraries(fluidsynth_backend PUBLIC OpenMP::OpenMP_CXX PkgConfig::fluidsynth) +target_link_libraries(fluidsynth_backend PUBLIC PkgConfig::fluidsynth) diff --git a/backends/playback/fluidsynth/fluidsynth_backend.hpp b/backends/playback/fluidsynth/fluidsynth_backend.hpp index a100715..746c31a 100644 --- a/backends/playback/fluidsynth/fluidsynth_backend.hpp +++ b/backends/playback/fluidsynth/fluidsynth_backend.hpp @@ -1,6 +1,5 @@ #pragma once #include "playback_backend.hpp" -#include #include #include #include diff --git a/backends/playback/gme/CMakeLists.txt b/backends/playback/gme/CMakeLists.txt index 9992cee..31a18f2 100644 --- a/backends/playback/gme/CMakeLists.txt +++ b/backends/playback/gme/CMakeLists.txt @@ -3,5 +3,4 @@ set(BACKEND_GME_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) pkg_check_modules(libgme IMPORTED_TARGET libgme) add_playback_backend(gme_backend ${BACKEND_GME_SRC}) target_include_directories(gme_backend PRIVATE ${BACKEND_GME_INC}) -find_package(OpenMP) -target_link_libraries(gme_backend PUBLIC PkgConfig::libgme OpenMP::OpenMP_CXX) +target_link_libraries(gme_backend PUBLIC PkgConfig::libgme) diff --git a/backends/playback/gme/gme_backend.hpp b/backends/playback/gme/gme_backend.hpp index 7e7f40a..644256e 100644 --- a/backends/playback/gme/gme_backend.hpp +++ b/backends/playback/gme/gme_backend.hpp @@ -1,6 +1,5 @@ #pragma once #include "playback_backend.hpp" -#include #include #include #include diff --git a/backends/playback/sdl_mixer_x/sdl_mixer_x.cpp b/backends/playback/sdl_mixer_x/sdl_mixer_x.cpp index 85210d9..e187f25 100644 --- a/backends/playback/sdl_mixer_x/sdl_mixer_x.cpp +++ b/backends/playback/sdl_mixer_x/sdl_mixer_x.cpp @@ -1,5 +1,9 @@ #include "sdl_mixer_x.hpp" +#ifdef __MACOSX__ +#include +#else #include +#endif #include #include "file_backend.hpp" #include diff --git a/backends/playback/zsm/CMakeLists.txt b/backends/playback/zsm/CMakeLists.txt index f6f5cd5..f4fc6cb 100644 --- a/backends/playback/zsm/CMakeLists.txt +++ b/backends/playback/zsm/CMakeLists.txt @@ -4,5 +4,4 @@ set(BACKEND_ZSM_SRC ${CMAKE_CURRENT_SOURCE_DIR}/zsm_backend.cpp ${X16_DIR}/vera_ set(BACKEND_ZSM_INC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${YMFM_DIR} ${X16_DIR}) add_playback_backend(zsm_backend ${BACKEND_ZSM_SRC}) target_include_directories(zsm_backend PRIVATE ${BACKEND_ZSM_INC}) -find_package(OpenMP) -target_link_libraries(zsm_backend PUBLIC OpenMP::OpenMP_CXX) +target_link_libraries(zsm_backend PUBLIC) diff --git a/backends/playback/zsm/zsm_backend.hpp b/backends/playback/zsm/zsm_backend.hpp index af71e96..de31a59 100644 --- a/backends/playback/zsm/zsm_backend.hpp +++ b/backends/playback/zsm/zsm_backend.hpp @@ -1,6 +1,5 @@ #pragma once #include "playback_backend.hpp" -#include #include "x16emu/ymglue.h" #include #include diff --git a/backends/ui/imgui/CMakeLists.txt b/backends/ui/imgui/CMakeLists.txt index 549fbe6..14a988d 100644 --- a/backends/ui/imgui/CMakeLists.txt +++ b/backends/ui/imgui/CMakeLists.txt @@ -10,6 +10,12 @@ option(GLES_VERSION "Version of OpenGL ES" 3) set(IMGUI_SRC imgui_demo.cpp imgui_draw.cpp imgui_tables.cpp imgui_widgets.cpp imgui.cpp misc/cpp/imgui_stdlib.cpp) set(IMGUI_BACKEND_SRC imgui_impl_sdlrenderer2.cpp imgui_impl_sdl2.cpp) set(BACKEND_IMGUI_SRC_BASE main.cpp file_browser.cpp main.cpp RendererBackend.cpp theme.cpp base85.h file_browser.h main.h RendererBackend.h ui_backend.hpp theme.h) +set(BACKEND_IMGUI_OSX_SRC_BASE RendererBackendOSX.mm file_browser_osx.mm) +if(APPLE) + foreach(SRC IN ITEMS ${BACKEND_IMGUI_OSX_SRC_BASE}) + list(APPEND BACKEND_IMGUI_SRC_BASE ${SRC}) + endforeach() +endif() foreach(SRC IN ITEMS ${IMGUI_BACKEND_SRC}) list(APPEND IMGUI_SRC backends/${SRC}) endforeach() @@ -50,6 +56,9 @@ add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC}) if(USE_GLES) target_compile_definitions(imgui_ui PRIVATE IMGUI_IMPL_OPENGL_ES${GLES_VERSION}) endif() +if(APPLE) + target_link_libraries(imgui_ui PRIVATE "-framework Cocoa" "-framework Foundation" "-framework AppKit" "-framework UniformTypeIdentifiers") +endif() if(DEFINED EMSCRIPTEN) target_compile_options(imgui_ui PRIVATE "-sUSE_SDL_IMAGE=2") target_link_options(imgui_ui PUBLIC "-sMAX_WEBGL_VERSION=2" "-sMIN_WEBGL_VERSION=2" "-sFULL_ES3") diff --git a/backends/ui/imgui/RendererBackend.cpp b/backends/ui/imgui/RendererBackend.cpp index 34b441f..f89e396 100644 --- a/backends/ui/imgui/RendererBackend.cpp +++ b/backends/ui/imgui/RendererBackend.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using std::vector; using namespace Looper::Options; void RendererBackend::on_resize() { @@ -29,6 +30,15 @@ void RendererBackend::on_resize() { UpdateScale(); #endif } +#ifdef __MACOSX__ +extern SDL_Window *CreateWindow(); +extern void SetWindowProperties(bool enableBorderless); +extern float GetPostButtonPos(); +extern float GetTitlebarHeight(); +extern void SetTitle(const char *str); +extern void SetSubtitle(const char *str); +extern void SetMenubarWidth(float width); +#endif static RendererBackend *renderer_backend; void RendererBackend::resize_static() { @@ -337,8 +347,12 @@ void RendererBackend::BackendInit() { SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); #endif SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN | SDL_WINDOW_BORDERLESS); +#ifdef __MACOSX__ + window = CreateWindow(); + rend = SDL_CreateRenderer(window, -1, 0); +#else SDL_CreateWindowAndRenderer(window_width, window_height, window_flags, &window, &rend); - +#endif #ifndef __ANDROID__ SDL_SetWindowMinimumSize(window, window_width, window_height); if (enable_kms) { @@ -417,18 +431,38 @@ bool RendererBackend::UsingSystemTitlebar() { } void RendererBackend::EnableSystemTitlebar(bool enabled) { enable_system_title_bar = enabled; +#ifdef __MACOSX__ + SetWindowProperties(!enable_system_title_bar); +#else SDL_SetWindowBordered(window, enable_system_title_bar ? SDL_TRUE : SDL_FALSE); +#endif } bool RendererBackend::BeginMainMenuBar() { main_menu_bar_used = true; + float fontHeight = ImGui::GetFontSize(); +#ifdef __MACOSX__ + float titlebarHeight = GetTitlebarHeight(); + titlebar_height = (int)titlebarHeight; + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(ImGui::GetStyle().ItemInnerSpacing.x, titlebarHeight - fontHeight)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, titlebarHeight - fontHeight)); +#endif if (ImGui::BeginMainMenuBar()) { if (!enable_system_title_bar) { - ImVec2 winsize = ImGui::GetWindowSize(); - float texsize = winsize.y; + ImVec2 windowSize = ImGui::GetWindowSize(); + float posY = (windowSize.y - fontHeight) / 2.0f; +#ifndef __MACOSX__ + float texsize = windowSize.y; ImGui::SetCursorPosX(0); ImGui::SetCursorPosY(0); ImGui::Image((ImTextureID)(icon_texture), ImVec2(texsize, texsize)); + ImGui::SetCursorPosY(posY); ImGui::TextUnformatted(title_text.c_str()); +#else + float posX = GetPostButtonPos() + ImGui::GetStyle().ItemSpacing.x; + ImGui::SetCursorPosX(posX); + ImGui::SetCursorPosY(posY); + ImGui::TextUnformatted(title_text.c_str()); +#endif } menubar_start = ImGui::GetCursorPosX(); return true; @@ -475,7 +509,11 @@ SDL_HitTestResult RendererBackend::HitTest(SDL_Window *window, const SDL_Point * return SDL_HITTEST_RESIZE_RIGHT; } } - if (area->y < (16 * this->scale) && (area->x < menubar_start || (area->x > menubar_end && area->x < title_btn_start))) { + if (area->y < (titlebar_height * this->scale) && (area->x < menubar_start || (area->x > menubar_end +#ifndef __MACOSX__ + && area->x < title_btn_start +#endif + ))) { return SDL_HITTEST_DRAGGABLE; } else { return SDL_HITTEST_NORMAL; @@ -487,23 +525,27 @@ void RendererBackend::SetSubtitle(const char *subtitle) { update_real_title(); } void RendererBackend::update_real_title() { +#ifdef __MACOSX__ + ::SetTitle(title_text.c_str()); + ::SetSubtitle(subtitle.c_str()); +#else if (subtitle == "") { SDL_SetWindowTitle(window, title_text.c_str()); } else { SDL_SetWindowTitle(window, fmt::format("{} - {}", subtitle, title_text).c_str()); } +#endif } void RendererBackend::EndMainMenuBar() { #ifndef __EMSCRIPTEN__ if (!enable_system_title_bar) { menubar_end = ImGui::GetCursorPosX(); ImVec2 size = ImGui::GetWindowSize(); - if (subtitle != "") { - ImVec4 text_color = Theme::GetColor(LooperCol_Subtitle); - ImGui::PushStyleColor(ImGuiCol_Text, text_color); - ImGui::TextUnformatted(subtitle.c_str()); - ImGui::PopStyleColor(); - } + ImVec4 text_color = Theme::GetColor(LooperCol_Subtitle); + ImGui::PushStyleColor(ImGuiCol_Text, text_color); + ImGui::TextUnformatted(subtitle.c_str()); + ImGui::PopStyleColor(); +#ifndef __MACOSX__ float btnw = size.y; int btn_count = 3; ImVec2 btn_size(btnw, btnw); @@ -524,7 +566,12 @@ void RendererBackend::EndMainMenuBar() { else tmp -= spacing; tmp -= ImGui::CalcTextSize(titlebar_icons[i]).x; } +#else + float tmp = size.x; + SetMenubarWidth(menubar_end-menubar_start); +#endif title_btn_start = std::ceil(tmp); +#ifndef __MACOSX__ ImGui::SetCursorPosX(title_btn_start); if (ImGui::MenuItem(titlebar_icons[0])) { SDL_MinimizeWindow(window); @@ -536,9 +583,14 @@ void RendererBackend::EndMainMenuBar() { if (ImGui::MenuItem(titlebar_icons[2])) { done = true; } +#endif } #endif ImGui::EndMainMenuBar(); +#ifdef __MACOSX__ +ImGui::PopStyleVar(); +ImGui::PopStyleVar(); +#endif } int RendererBackend::Run() { framerate = 60; diff --git a/backends/ui/imgui/RendererBackend.h b/backends/ui/imgui/RendererBackend.h index ee1ed60..9d4daab 100644 --- a/backends/ui/imgui/RendererBackend.h +++ b/backends/ui/imgui/RendererBackend.h @@ -30,6 +30,7 @@ class RendererBackend { int menubar_start; int menubar_end; int title_btn_start; + int titlebar_height = 16; std::string subtitle; std::string title_text; bool enable_system_title_bar; diff --git a/backends/ui/imgui/RendererBackendOSX.mm b/backends/ui/imgui/RendererBackendOSX.mm new file mode 100644 index 0000000..0e33574 --- /dev/null +++ b/backends/ui/imgui/RendererBackendOSX.mm @@ -0,0 +1,63 @@ +#include +#include +#include +#include +NSWindow *window; +NSTitlebarAccessoryViewController *controller; +NSView *titlebarDummyView; +extern "C++" { +SDL_Window *CreateWindow() { + window = [[NSWindow alloc] init]; + controller = [[NSTitlebarAccessoryViewController alloc] init]; + titlebarDummyView = [[NSView alloc] init]; + [controller setView:titlebarDummyView]; + [controller setLayoutAttribute:NSLayoutAttributeLeft]; + [window addTitlebarAccessoryViewController:controller]; + return SDL_CreateWindowFrom((void*)[window contentView]); +} +void SetWindowProperties(bool enableBorderless) { + NSWindowStyleMask windowMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled; + if (enableBorderless) { + windowMask |= NSWindowStyleMaskFullSizeContentView; + } + [window setTitleVisibility: enableBorderless ? NSWindowTitleHidden : NSWindowTitleVisible]; + [window setStyleMask: windowMask]; + [window setTitlebarAppearsTransparent: enableBorderless]; + NSView *contentView = [window contentView]; + NSView *frameView = [contentView superview]; + for (NSTrackingArea *trackingArea : [frameView trackingAreas]) { + [contentView addTrackingArea:trackingArea]; + DEBUG.writeln("Adding tracking area..."); + } +} +float GetPostButtonPos() { + NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton]; + NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton]; + NSButton *miniatureButton = [window standardWindowButton:NSWindowMiniaturizeButton]; + NSRect closeRect = closeButton.frame; + NSRect zoomRect = zoomButton.frame; + NSRect miniatureRect = miniatureButton.frame; + float closeX = NSMinX(closeRect); + float zoomX = NSMinX(zoomRect); + float miniatureX = NSMinX(miniatureRect); + float zoomEndX = NSMaxX(zoomRect); + return zoomEndX; +} +void SetMenubarWidth(float width) { + [titlebarDummyView setBounds:NSMakeRect(0, 0, width, 0)]; +} +float GetTitlebarHeight() { + NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton]; + NSRect closeRect = closeButton.frame; + float topY = NSMinY(closeRect); + float bottomY = NSMaxY(closeRect); + return bottomY;// + topY; +} + +void SetSubtitle(const char *str) { + [window setSubtitle: [NSString stringWithUTF8String:str]]; +} +void SetTitle(const char *str) { + [window setTitle: [NSString stringWithUTF8String:str]]; +} +} diff --git a/backends/ui/imgui/file_browser.cpp b/backends/ui/imgui/file_browser.cpp index 9dce4f8..9c5139d 100644 --- a/backends/ui/imgui/file_browser.cpp +++ b/backends/ui/imgui/file_browser.cpp @@ -39,6 +39,9 @@ const char *GetPickedFile() { void ClearSelected() { env->CallStaticVoidMethod(MainActivity, ClearSelected_Method, 0); } +#elif defined(__MACOSX__) +extern const char *OpenDialog(std::vector allowedFileTypes, std::string initialDirectory); +extern const char *SaveDialog(std::vector allowedFileTypes, std::string initialDirectory); #endif FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags) { #ifdef PORTALS @@ -92,12 +95,12 @@ void FileBrowser::SetTypeFilters(string name, vector filters) { } void FileBrowser::SetPwd(path path) { pwd = path; - #if !(defined(PORTALS) || defined(__ANDROID__)) + #if !(defined(PORTALS) || defined(__ANDROID__) || defined(__MACOSX__)) fallback.SetPwd(path); #endif } bool FileBrowser::HasSelected() { - #ifdef PORTALS + #if defined(PORTALS) || defined(__MACOSX__) return selected.has_value(); #elif defined(__ANDROID__) return strlen(GetPickedFile()) > 0; @@ -108,7 +111,7 @@ bool FileBrowser::HasSelected() { #endif } path FileBrowser::GetSelected() { - #ifdef PORTALS + #if defined(PORTALS) || defined(__MACOSX__) return selected.value_or(path()); #elif defined(__ANDROID__) const char *file = GetPickedFile(); @@ -146,6 +149,19 @@ void FileBrowser::Open() { } else { xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this); } + #elif defined(__MACOSX__) + open = true; + const char *output = nullptr; + if (save) { + output = SaveDialog(filters, pwd.string()); + } else { + output = OpenDialog(filters, pwd.string()); + } + if (output == nullptr) { + selected = {}; + } else { + selected = path(output); + } #elif defined(__ANDROID__) ClearSelected(); open = true; @@ -293,7 +309,7 @@ void FileBrowser::Display() { } ImGui::EndPopup(); } - #else + #elif !defined(__MACOSX__) fallback.Display(); #endif } @@ -312,14 +328,12 @@ void FileBrowser::ClearSelected() { } void FileBrowser::SetTitle(string title) { this->title = title; - #ifndef PORTALS + #if !defined(PORTALS) && !defined(__MACOSX__) fallback.SetTitle(title); #endif } bool FileBrowser::IsOpened() { - #ifdef PORTALS - return open; - #elif defined(__ANDROID__) + #if defined(PORTALS) || defined(__ANDROID__) || defined(__MACOSX__) return open; #elif defined(__EMSCRIPTEN__) return !file_picker_closed() || file_picker_confirmed(); diff --git a/backends/ui/imgui/file_browser_osx.mm b/backends/ui/imgui/file_browser_osx.mm new file mode 100644 index 0000000..307f828 --- /dev/null +++ b/backends/ui/imgui/file_browser_osx.mm @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include + +extern "C++" { +UTType *GetUTIFromExtension(NSString *extension) { + return [UTType typeWithFilenameExtension:extension]; +} +NSMutableArray *ConvertExtensions(std::vector input) { + NSMutableArray *arr = [[[NSMutableArray alloc] init] autorelease]; + for (auto &str : input) { + UTType *maybeUTI = GetUTIFromExtension([NSString stringWithUTF8String:str.c_str()]); + if (maybeUTI != nil) { + [arr addObject:maybeUTI]; + } + } + return arr; +} +const char *OpenDialog(std::vector fileTypes, std::string initialDirectory) { + NSMutableArray *arr = ConvertExtensions(fileTypes); + NSOpenPanel* openFileDialog = [[[NSOpenPanel alloc] init] autorelease]; + [openFileDialog setCanChooseFiles:YES]; + [openFileDialog setCanChooseDirectories:NO]; + [openFileDialog setAllowsMultipleSelection:NO]; + [openFileDialog setAllowedContentTypes:arr]; + [openFileDialog setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:initialDirectory.c_str()]]]; + + NSModalResponse response = [openFileDialog runModal]; + if (response == NSModalResponseOK) { + return [[(NSURL*)[[openFileDialog URLs] objectAtIndex:0] path] UTF8String]; + } else { + return nullptr; + } +} + +const char *SaveDialog(std::vector fileTypes, std::string initialDirectory) { + NSSavePanel *panel = [[[NSSavePanel alloc] init] autorelease]; + [panel setCanCreateDirectories:YES]; + NSMutableArray *arr = ConvertExtensions(fileTypes); + [panel setAllowedContentTypes:arr]; + [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:initialDirectory.c_str()]]]; + + NSModalResponse response = [panel runModal]; + if (response == NSModalResponseOK) { + return [[[panel URL] path] UTF8String]; + } else { + return nullptr; + } +} +} diff --git a/backends/ui/imgui/main.cpp b/backends/ui/imgui/main.cpp index d6a4192..19206f7 100644 --- a/backends/ui/imgui/main.cpp +++ b/backends/ui/imgui/main.cpp @@ -10,6 +10,7 @@ #include "imgui/misc/cpp/imgui_stdlib.h" #include #include +#include using namespace Looper::Options; void MainLoop::Init() { #ifdef PORTALS @@ -153,6 +154,64 @@ void MainLoop::Init() { theme = new Theme(darkPath); } } + { + MenuBuilder builder; + auto quitHandler = [this]() { + done = true; + }; + auto aboutHandler = [this]() { + about_window = !about_window; + }; + auto prefsHandler = [this]() { + prefs_window = !prefs_window; + }; + rootMenu = builder + .BeginSubmenu("Looper", MenuType_Application, OSOptions::OnlyMac) + .AddItemSimple("About Looper", "", aboutHandler) + .AddItemSimple("Preferences...", ",", prefsHandler) + .AddEmptySubmenu("Services...", MenuType_Services) + .AddItemSimple("Quit", "q", quitHandler) + .EndSubmenu() + .BeginSubmenu("File") + .AddItemSimple("Open", "o", [this]() { + this->fileDialog.SetTitle(_TR_CTX("File dialog title", "Open...")); + this->fileDialog.SetTypeFilters(_TR_CTX("File dialog filter name", "Audio files"), { ".wav", ".ogg", ".mp3", ".qoa", ".flac", ".xm", ".mod"}); + this->fileDialog.Open(); + }) +#ifdef __EMSCRIPTEN__ + .AddItemSimple("Update", "", [this]() { + if (serviceworker_registered()) { + update(); + } + }) +#endif + .AddItemSimple("Quit", "", quitHandler, OSOptions::ExcludeMac) + .EndSubmenu() + .BeginSubmenu("Edit", MenuType_Default, OSOptions::ExcludeMac) + .AddItemSimple("Preferences", "", prefsHandler) + .EndSubmenu() + .BeginSubmenu("Debug") + .AddItemSimple("Show ImGui Demo Window", "", [this]() { + show_demo_window = !show_demo_window; + set_option("ui.imgui.demo_window", show_demo_window); + }) + .AddItemSimple("Property Editor", "", [this]() { + property_editor = !property_editor; + }) + .EndSubmenu() + .AddEmptySubmenu("Window", MenuType_Window, OSOptions::OnlyMac) + .BeginSubmenu("Help", MenuType_Help) + .AddItemSimple("About", "", aboutHandler, OSOptions::ExcludeMac) + .EndSubmenu() + .Build(); + debugMenu = (Menu*)FindMenu(rootMenu, "Debug"); + quitItem = (MenuItem*)FindMenu(rootMenu, "Quit"); +#ifdef __EMSCRIPTEN__ + updateItem = (MenuItem*)FindMenu(rootMenu, "Update"); +#endif + debugMenu->visible = debug_mode; + menu_showing = PublishMenu(rootMenu, nullptr); + } EnableSystemTitlebar(get_option("ui.imgui.enable_system_titlebar", false)); theme->Apply(accent_color, (float)scale); SetWindowTitle("Looper"); @@ -235,6 +294,49 @@ void MainLoop::FileLoaded() { } } } +struct ImGuiMenuData { + bool showing; + Menu *menu; +}; +struct ImGuiMenuCallbacks : public MenuIteratorCallbacks { + std::stack menuData; + bool shouldIncludeNextMenuItems() { + return menuData.top().showing; + } + void Init(Menu *root) { + menuData.push({.showing = true, .menu = root}); + } + void Finalize() { + while (menuData.size() > 1) { + EndSubmenu(); + } + } + void BeginSubmenu(Menu *menu) { + bool showing = false; + if (shouldIncludeNextMenuItems()) { + showing = ImGui::BeginMenu(menu->title.c_str()); + } + menuData.push({.showing = showing, .menu = menu}); + } + void EndSubmenu() { + if (shouldIncludeNextMenuItems()) { + ImGui::EndMenu(); + } + menuData.pop(); + } + void AddSeparator() { + if (shouldIncludeNextMenuItems()) { + ImGui::Separator(); + } + } + void AddNormalItem(MenuItem *item) { + if (shouldIncludeNextMenuItems()) { + if (ImGui::MenuItem(item->title.c_str())) { + item->RunAction(); + } + } + } +}; void MainLoop::GuiFunction() { #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) playback->LoopHook(); @@ -251,53 +353,13 @@ void MainLoop::GuiFunction() { if (show_demo_window) ImGui::ShowDemoWindow(&show_demo_window); if (BeginMainMenuBar()) { - if (ImGui::BeginMenu(_TRI_CTX(ICON_FK_FILE, "Main menu", "File"))) { - if (ImGui::MenuItem(_TRI_CTX(ICON_FK_FOLDER_OPEN, "Main menu | File", "Open"))) { - // Set translatable strings here so that they are in the correct language even when it changes at runtime. - fileDialog.SetTitle(_TR_CTX("File dialog title", "Open...")); - fileDialog.SetTypeFilters(_TR_CTX("File dialog filter name", "Audio files"), { ".wav", ".ogg", ".mp3", ".qoa", ".flac", ".xm", ".mod"}); - fileDialog.Open(); - } - #ifdef __EMSCRIPTEN__ - if (serviceworker_registered()) { - if (ImGui::MenuItem(_TRI_CTX(ICON_FK_DOWNLOAD, "Main menu | File", "Update"))) { - update(); - } - } - if (is_puter_enabled()) { - #endif - if (ImGui::MenuItem(_TRI_CTX(ICON_FK_WINDOW_CLOSE, "Main menu | File", "Quit"))) { - done = true; - } - #ifdef __EMSCRIPTEN__ - } - #endif - ImGui::EndMenu(); - } - if (ImGui::BeginMenu(_TRI_CTX(ICON_FK_SCISSORS,"Main menu", "Edit"))) { - if (ImGui::MenuItem(_TRI_CTX(ICON_FK_COG, "Main menu | Edit", "Preferences..."))) { - prefs_window = true; - } - ImGui::EndMenu(); - } - #ifndef DEBUG_MODE - if (debug_mode) - #endif - if (ImGui::BeginMenu(_TRI_CTX(ICON_FK_COG, "Main menu (in debug builds)", "Debug"))) { - if (ImGui::MenuItem(_TR_CTX("Main menu | Debug", "Show ImGui Demo Window"), nullptr, show_demo_window)) { - show_demo_window = !show_demo_window; - set_option("ui.imgui.demo_window", show_demo_window); - } - if (ImGui::MenuItem(_TR_CTX("Main menu | Debug", "Edit properties"), nullptr, property_editor)) { - property_editor = !property_editor; - } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu(_TRI_CTX(ICON_FK_INFO_CIRCLE, "Main menu", "Help"))) { - if (ImGui::MenuItem(_TRI_CTX(ICON_FK_INFO, "Main menu | Help", "About"), nullptr, about_window)) { - about_window = !about_window; - } - ImGui::EndMenu(); + if (!menu_showing) { +#ifdef __EMSCRIPTEN__ + updateItem->visible = serviceworker_registered(); + quitItem->visible = is_puter_enabled(); +#endif + ImGuiMenuCallbacks callbacks; + IterateMenu(rootMenu, callbacks); } EndMainMenuBar(); } @@ -596,6 +658,8 @@ void MainLoop::GuiFunction() { } if (ImGui::Checkbox(_TR_CTX("Preference | Debug menu enable", "Enable debug menu in release builds"), &debug_mode)) { set_option("ui.imgui.debug_mode", debug_mode); + debugMenu->visible = debug_mode; + PublishMenu(rootMenu); } bool tmp_enable_system_title_bar = UsingSystemTitlebar(); if (ImGui::Checkbox(_TR_CTX("Preference | System title bar", "Enable system title bar"), &tmp_enable_system_title_bar)) { diff --git a/backends/ui/imgui/main.h b/backends/ui/imgui/main.h index 8e1ddff..27b30a2 100644 --- a/backends/ui/imgui/main.h +++ b/backends/ui/imgui/main.h @@ -33,6 +33,8 @@ #endif #include "../../../backend.hpp" #include "ui_backend.hpp" +#include +using namespace LooperUI; using namespace std::filesystem; using std::string; #define IMGUI_FRONTEND @@ -50,6 +52,11 @@ class MainLoop : public RendererBackend { bool restart_needed = false; bool stopped = true; bool enable_cat = false; + Menu *debugMenu; + MenuItem *quitItem; + MenuItem *updateItem; + Menu *rootMenu; + bool menu_showing; std::string cat_setting = "__default__"; std::vector backends; UIBackend *cur_backend; diff --git a/liblooperui/CMakeLists.txt b/liblooperui/CMakeLists.txt new file mode 100644 index 0000000..cfd76eb --- /dev/null +++ b/liblooperui/CMakeLists.txt @@ -0,0 +1,13 @@ +prefix_all(LIBLOOPERUI_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ menus.cpp menus.hpp) +if(APPLE) + set(backend_src backends/osx_backend.mm) +# TODO: Implement dbusmenu +#elseif(UNIX AND ENABLE_DBUS) +# set(backend_src backends/dbus_backend.cpp) +else() + set(backend_src backends/noop_backend.cpp) +endif() +list(APPEND LIBLOOPERUI_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/${backend_src}) +add_library(liblooper_ui SHARED ${LIBLOOPERUI_SRCS}) +target_link_libraries(liblooper_ui PUBLIC fmt::fmt) +target_include_directories(liblooper_ui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) diff --git a/liblooperui/backends/noop_backend.cpp b/liblooperui/backends/noop_backend.cpp new file mode 100644 index 0000000..186102f --- /dev/null +++ b/liblooperui/backends/noop_backend.cpp @@ -0,0 +1,5 @@ +#include +bool LooperUI::PublishMenu(Menu*, PlatformData*) { + // No-op + return false; +} \ No newline at end of file diff --git a/liblooperui/backends/osx_backend.mm b/liblooperui/backends/osx_backend.mm new file mode 100644 index 0000000..dc855c8 --- /dev/null +++ b/liblooperui/backends/osx_backend.mm @@ -0,0 +1,103 @@ +#include +#include +#include +#include "../../log.hpp" +#include +using namespace LooperUI; +@interface MenuHelper : NSObject { + std::map menuItems; +} +- (void)menuItemClicked:(id)sender; +- (void)addMenuItem:(id)menuItem :(MenuItem*)looperItem; +@end +@implementation MenuHelper +- (void)addMenuItem:(id)menuItem :(MenuItem*)looperItem { + menuItems[menuItem] = looperItem; +} +- (void)menuItemClicked:(id)sender { + if (!menuItems.contains(sender)) { + NSLog(@"Failed to find menu item!"); + return; + } + MenuItem *looper_item = menuItems[sender]; + looper_item->RunAction(); +} +@end +MenuHelper *menuHelper; +NSMenuItem *windowItem; +extern "C++" { + struct PublishMenuCallbacks : public MenuIteratorCallbacks { + std::stack menu_stack; + void print_spaces() { + for (size_t i = 1; i < menu_stack.size(); i++) { + DEBUG.writes(" "); + } + } + void Init(Menu *root) { + menuHelper = [[[MenuHelper alloc] init] autorelease]; + NSMenu *osx_root = [[[NSMenu alloc] init] autorelease]; + [NSApp setMainMenu:osx_root]; + menu_stack.push(osx_root); + } + void Finalize() { + } + void AddNormalItem(MenuItem *item) { + NSMenuItem *osx_item = [[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:item->title.c_str()] action:@selector(menuItemClicked:) keyEquivalent:[NSString stringWithUTF8String:item->keybindString.c_str()]] autorelease]; + [osx_item setHidden:!item->visible]; + [osx_item setEnabled:item->enabled]; + [osx_item setTarget:menuHelper]; + [menuHelper addMenuItem:osx_item:item]; + [menu_stack.top() addItem:osx_item]; + print_spaces(); + DEBUG.writefln("[Item] %s", item->title.c_str()); + } + void BeginSubmenu(Menu *menu) { + print_spaces(); + NSMenu *submenu; + NSMenuItem *menuItem; + if (menu->type == MenuType_Window && windowItem != nullptr) { + menuItem = windowItem; + submenu = [windowItem submenu]; + [[menuItem menu] removeItem:menuItem]; + } else { + submenu = [[[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menu->title.c_str()]] autorelease]; + menuItem = [[[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:menu->title.c_str()] action:nil keyEquivalent:@""] autorelease]; + } + [menuItem setHidden:!menu->visible]; + [menuItem setEnabled:menu->enabled]; + [menuItem setSubmenu:submenu]; + [menu_stack.top() addItem:menuItem]; + if (menu->type == MenuType_Services) { + [NSApp setServicesMenu:submenu]; + DEBUG.writefln("[Services] %s", menu->title.c_str()); + } else if (menu->type == MenuType_Window) { + if (windowItem == nullptr) { + [NSApp setWindowsMenu:submenu]; + windowItem = menuItem; + } + DEBUG.writefln("[Window] %s", menu->title.c_str()); + } else if (menu->type == MenuType_Help) { + [NSApp setHelpMenu:submenu]; + DEBUG.writefln("[Help] %s", menu->title.c_str()); + } else if (menu->type == MenuType_Application) { + DEBUG.writefln("[Application] %s", menu->title.c_str()); + } else { + DEBUG.writefln("[Submenu] %s", menu->title.c_str()); + } + menu_stack.push(submenu); + } + void EndSubmenu() { + menu_stack.pop(); + } + void AddSeparator() { + print_spaces(); + DEBUG.writeln("[Separator]"); + [menu_stack.top() addItem:[[NSMenuItem separatorItem] autorelease]]; + } + }; + bool LooperUI::PublishMenu(Menu *menu, PlatformData*) { + PublishMenuCallbacks callbacks; + IterateMenu(menu, callbacks); + return true; + } +} diff --git a/liblooperui/menus.cpp b/liblooperui/menus.cpp new file mode 100644 index 0000000..289a145 --- /dev/null +++ b/liblooperui/menus.cpp @@ -0,0 +1,213 @@ +#include "menus.hpp" +#ifdef __APPLE__ +#define IS_MACOSX true +#else +#define IS_MACOSX false +#endif +namespace LooperUI { +static std::unordered_map menu_type_data { + {MenuType_Default, true}, + {MenuType_Item, true}, + {MenuType_Application, IS_MACOSX}, + {MenuType_Window, IS_MACOSX}, + {MenuType_Help, true}, + {MenuType_Services, IS_MACOSX} +}; +bool MenuTypeValidForSystem(MenuType type) { + if (menu_type_data.contains(type)) { + return menu_type_data[type]; + } else { + return false; + } +} +MenuItemBase::MenuItemBase(std::string title) + : title(title) + , type(MenuType_Default) +{ + this->enabled = true; + this->visible = true; +} +MenuItemBase::MenuItemBase(MenuType type) + : MenuItemBase("") { + this->type = type; +} +MenuItem::MenuItem(std::string title, std::string keybindString, std::function callback, void *userdata) + : MenuItemBase(title) +{ + this->keybindString = keybindString; + this->userdata = userdata; + this->callback = callback; + this->type = MenuType_Item; +} +Menu::iterator Menu::begin() { + return children.begin(); +} +Menu::iterator Menu::end() { + return children.end(); +} +Menu::const_iterator Menu::cbegin() const { + return children.cbegin(); +} +Menu::const_iterator Menu::cend() const { + return children.cend(); +} +Menu::reverse_iterator Menu::rbegin() { + return children.rbegin(); +} +Menu::reverse_iterator Menu::rend() { + return children.rend(); +} +Menu::const_reverse_iterator Menu::crbegin() const { + return children.crbegin(); +} +Menu::const_reverse_iterator Menu::crend() const { + return children.crend(); +} +MenuItemBase *Menu::operator[](size_t index) { + return children[index]; +} +void Menu::Prepend(MenuItemBase *menu) { + children.insert(children.begin(), menu); +} +void Menu::Insert(MenuItemBase *menu, size_t index) { + if (index >= children.size()) { + Append(menu); + } else { + children.insert(children.begin() + index, menu); + } +} +bool MenuItemBase::IsSubmenu() { + return false; +} +bool Menu::IsSubmenu() { + return true; +} +Menu::Menu(std::string title, MenuType type) + : MenuItemBase(title) +{ + this->type = type; +} +bool MenuItemBase::HasParent() { + return parent != nullptr; +} +Menu *MenuItemBase::GetParent() { + return parent; +} +void Menu::Append(MenuItemBase *menu) { + if (menu->parent != nullptr) { + throw std::exception(); + } + menu->parent = this; + children.push_back(menu); +} +MenuBuilder &MenuBuilder::BeginSubmenu(std::string title, MenuType type, OSOptions allowedOS) { + throwIfBuilt(); + Menu *submenu = new Menu(title); + submenu->os_options = allowedOS; + submenu->type = type; + current_menu->Append(submenu); + current_menu = submenu; + return *this; +} + +MenuBuilder &MenuBuilder::EndSubmenu() { + throwIfBuilt(); + if (current_menu->HasParent()) { + current_menu = current_menu->GetParent(); + } else { + throw std::exception(); + } + return *this; +} +MenuBuilder &MenuBuilder::AddEmptySubmenu(std::string title, MenuType type, OSOptions allowedOS) { + BeginSubmenu(title, type, allowedOS).EndSubmenu(); + return *this; +} +MenuBuilder &MenuBuilder::AddItem(std::string title, std::string keybindString, std::function callback, void *userdata, OSOptions allowedOS) { + throwIfBuilt(); + MenuItem *newItem = new MenuItem(title, keybindString, callback, userdata); + newItem->os_options = allowedOS; + current_menu->Append(newItem); + return *this; +} +MenuBuilder &MenuBuilder::AddSeparator(OSOptions allowedOS) { + throwIfBuilt(); + MenuItemBase *newItem = new MenuItemBase(MenuType_Separator); + newItem->os_options = allowedOS; + current_menu->Append(newItem); + return *this; +} +MenuBuilder &MenuBuilder::AddItemNoUserdata(std::string title, std::string keybindString, std::function callback_no_userdata, OSOptions allowedOS) { + AddItem(title, keybindString, [callback_no_userdata](MenuItem *item, void*) { + callback_no_userdata(item); + }, nullptr, allowedOS); + return *this; +} +MenuBuilder &MenuBuilder::AddItemSimple(std::string title, std::string keybindString, std::function callback_simple, OSOptions allowedOS) { + AddItem(title, keybindString, [callback_simple](MenuItem*, void*) { + callback_simple(); + }, nullptr, allowedOS); + return *this; +} +MenuBuilder::MenuBuilder(std::string rootTitle) { + root = new Menu(rootTitle); + current_menu = root; + built = false; +} +Menu *MenuBuilder::Build() { + throwIfBuilt(); + built = true; + return root; +} +void MenuBuilder::throwIfBuilt() { + if (built) throw std::exception(); +} +static void _IterateMenu(Menu *menu, MenuIteratorCallbacks &callbacks) { + + for (MenuItemBase *item : *menu) { + bool allowed = (item->os_options == OSOptions::AnyOS) || (item->os_options == +#ifdef __APPLE__ + OSOptions::OnlyMac +#else + OSOptions::ExcludeMac +#endif + ); + if (!allowed) continue; + if (item->type == MenuType_Separator) { + callbacks.AddSeparator(); + } else if (item->IsSubmenu()) { + callbacks.BeginSubmenu((Menu*)item); + _IterateMenu((Menu*)item, callbacks); + callbacks.EndSubmenu(); + } else { + callbacks.AddNormalItem((MenuItem*)item); + } + } +} +void IterateMenu(Menu *menu, MenuIteratorCallbacks &callbacks) { + callbacks.Init(menu); + _IterateMenu(menu, callbacks); + callbacks.Finalize(); +} +MenuItemBase *FindMenu(Menu *menu, std::string title) { + for (MenuItemBase *item : *menu) { + bool allowed = item->os_options == OSOptions::AnyOS || item->os_options == +#ifdef __APPLE__ + OSOptions::OnlyMac +#else + OSOptions::ExcludeMac +#endif + ; + if (!allowed) continue; + if (item->title == title) { + return item; + } else if (item->IsSubmenu()) { + MenuItemBase *maybe_output = FindMenu((Menu*)item, title); + if (maybe_output != nullptr) { + return maybe_output; + } + } + } + return nullptr; +} +} diff --git a/liblooperui/menus.hpp b/liblooperui/menus.hpp new file mode 100644 index 0000000..85f0d1b --- /dev/null +++ b/liblooperui/menus.hpp @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include +namespace LooperUI { + enum MenuType { + MenuType_Default, + MenuType_Item, + MenuType_Separator, + MenuType_Application, + MenuType_Window, + MenuType_Help, + MenuType_Services, + }; + bool MenuTypeValidForSystem(MenuType type); + class Menu; + enum class OSOptions { + AnyOS, + OnlyMac, + ExcludeMac + }; + class MenuItemBase { + friend class Menu; + Menu *parent; + public: + std::string id; + MenuType type; + OSOptions os_options; + virtual bool IsSubmenu(); + Menu *GetParent(); + bool HasParent(); + bool visible; + bool enabled; + std::string title; + MenuItemBase(std::string title); + MenuItemBase(MenuType type = MenuType_Separator); + }; + class MenuItem : public MenuItemBase { + public: + void *userdata; + std::string keybindString; + std::function callback; + virtual void RunAction() { + callback(this, userdata); + } + MenuItem(std::string title, std::string keybindString, std::function callback, void *userdata = nullptr); + }; + class Menu : public MenuItemBase { + using container = std::vector; + using iterator = container::iterator; + using const_iterator = container::const_iterator; + using reverse_iterator = container::reverse_iterator; + using const_reverse_iterator = container::const_reverse_iterator; + container children; + friend class MenuIterator; + public: + bool IsSubmenu() override; + void Append(MenuItemBase *menu); + iterator begin(); + iterator end(); + const_iterator cbegin() const; + const_iterator cend() const; + reverse_iterator rbegin(); + reverse_iterator rend(); + const_reverse_iterator crbegin() const; + const_reverse_iterator crend() const; + MenuItemBase *operator [](size_t index); + void Prepend(MenuItemBase *menu); + void Insert(MenuItemBase *menu, size_t index); + Menu(std::string title, MenuType type = MenuType_Default); + }; + class Menubar { + + }; + class MenuBuilder { + private: + Menu *root; + Menu *current_menu; + bool built; + void throwIfBuilt(); + public: + MenuBuilder &BeginSubmenu(std::string title, MenuType type = MenuType_Default, OSOptions allowedOS = OSOptions::AnyOS); + MenuBuilder &EndSubmenu(); + MenuBuilder &AddSeparator(OSOptions allowedOS = OSOptions::AnyOS); + MenuBuilder &AddEmptySubmenu(std::string title, MenuType type = MenuType_Default, OSOptions allowedOS = OSOptions::AnyOS); + MenuBuilder &AddItem(std::string title, std::string keybindString, std::function callback, void *userdata = nullptr, OSOptions allowedOS = OSOptions::AnyOS); + MenuBuilder &AddItemNoUserdata(std::string title, std::string keybindString, std::function callback_no_userdata, OSOptions allowedOS = OSOptions::AnyOS); + MenuBuilder &AddItemSimple(std::string title, std::string keybindString, std::function callback_simple, OSOptions allowedOS = OSOptions::AnyOS); + Menu *Build(); + MenuBuilder(std::string rootTitle = ""); + }; + // TODO: For DBusmenu + struct PlatformData; + struct MenuIteratorCallbacks { + inline virtual void Init(Menu *root) { } + inline virtual void Finalize() { } + inline virtual void AddNormalItem(MenuItem* item) { } + inline virtual void BeginSubmenu(Menu *menu) { } + inline virtual void EndSubmenu() { } + inline virtual void AddSeparator() { } + }; + MenuItemBase *FindMenu(Menu *menu, std::string title); + void IterateMenu(Menu *menu, MenuIteratorCallbacks &callbacks); + bool PublishMenu(Menu *menu, PlatformData *platform_data = nullptr); +} \ No newline at end of file diff --git a/main.cpp b/main.cpp index fcb58ac..e74e4c5 100644 --- a/main.cpp +++ b/main.cpp @@ -15,6 +15,9 @@ #include #include #endif +#ifdef __MACOSX__ +#include +#endif #include "web_functions.hpp" #include "cats.hpp" #include @@ -366,6 +369,16 @@ int main(int argc, char **argv) { } } } +#elif defined(__MACOSX__) + { + uint32_t path_len = 16; + char *path = (char*)malloc(path_len); + _NSGetExecutablePath(path, &path_len); + path = (char*)realloc(path, path_len); + _NSGetExecutablePath(path, &path_len); + executable_path = strdup(fs::canonical(path).c_str()); + free((void*)path); + } #else executable_path = strdup(fs::canonical("/proc/self/exe").c_str()); #endif diff --git a/mkicns.sh b/mkicns.sh new file mode 100755 index 0000000..06b2ae4 --- /dev/null +++ b/mkicns.sh @@ -0,0 +1,20 @@ +#!/bin/sh +OLDDIR="$(pwd)" +while [ -n "$1" ]; do + mkdir -p $2.iconset + for size in 16 24 32 48 64 128 256 512 1024; do + for scale in 1 2; do + export realsize=$((size * scale)) + export name="$2.iconset/$(basename $2)_$size" + if [ "$scale" -gt 1 ]; then + export name="$name@${scale}x" + fi + export name="$name.png" + rsvg-convert "$1" -w "$size" -h "$size" -o "$name" + done + done + iconutil -c icns --output "$2.icns" "$2.iconset" + rm -r "$2.iconset" + shift 2 +done +cd "$OLDDIR" \ No newline at end of file diff --git a/playback.cpp b/playback.cpp index 601df41..ac0089f 100644 --- a/playback.cpp +++ b/playback.cpp @@ -231,7 +231,7 @@ void PlaybackInstance::InitLoopFunction() { SDL_AudioSpec desired; desired.format = SDL_SAMPLE_FMT; desired.freq = 48000; - desired.samples = 400; + desired.samples = 1000; desired.channels = 2; desired.callback = PlaybackInstance::SDLCallback; desired.userdata = this;