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