Add support for macOS
Some checks failed
Build / build-gentoo (push) Failing after 14s
Build / download-system-deps (push) Successful in 3m28s
Build / get-source-code (push) Successful in 9m18s
Build / build-deb (push) Failing after 3m41s
Build / build-appimage (push) Successful in 4m12s
Build / build-android (push) Failing after 2m47s
Build / build-windows (push) Failing after 7m10s

- 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
This commit is contained in:
Zachary Hall 2025-02-09 10:11:30 -08:00
parent e9c3a93e16
commit 51d4ed39ab
26 changed files with 875 additions and 81 deletions

View file

@ -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()
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()

34
assets/looper.plist.in Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
</dict>
</plist>

View file

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python3
import os
import sys
import subprocess

View file

@ -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)

View file

@ -1,6 +1,5 @@
#pragma once
#include "playback_backend.hpp"
#include <omp.h>
#include <cstdint>
#include <stddef.h>
#include <stdint.h>

View file

@ -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)

View file

@ -1,6 +1,5 @@
#pragma once
#include "playback_backend.hpp"
#include <omp.h>
#include <cstdint>
#include <stddef.h>
#include <stdint.h>

View file

@ -1,5 +1,9 @@
#include "sdl_mixer_x.hpp"
#ifdef __MACOSX__
#include <machine/endian.h>
#else
#include <endian.h>
#endif
#include <filesystem>
#include "file_backend.hpp"
#include <stddef.h>

View file

@ -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)

View file

@ -1,6 +1,5 @@
#pragma once
#include "playback_backend.hpp"
#include <omp.h>
#include "x16emu/ymglue.h"
#include <cstdint>
#include <stddef.h>

View file

@ -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")

View file

@ -19,6 +19,7 @@
#include <web_functions.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <SDL_syswm.h>
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();
}
#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;

View file

@ -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;

View file

@ -0,0 +1,63 @@
#include <Foundation/Foundation.h>
#include <Cocoa/Cocoa.h>
#include <SDL.h>
#include <log.hpp>
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]];
}
}

View file

@ -39,6 +39,9 @@ const char *GetPickedFile() {
void ClearSelected() {
env->CallStaticVoidMethod(MainActivity, ClearSelected_Method, 0);
}
#elif defined(__MACOSX__)
extern const char *OpenDialog(std::vector<std::string> allowedFileTypes, std::string initialDirectory);
extern const char *SaveDialog(std::vector<std::string> 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<string> 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();

View file

@ -0,0 +1,52 @@
#include <Foundation/Foundation.h>
#include <Cocoa/Cocoa.h>
#include <UniformTypeIdentifiers/UTCoreTypes.h>
#include <vector>
#include <string>
extern "C++" {
UTType *GetUTIFromExtension(NSString *extension) {
return [UTType typeWithFilenameExtension:extension];
}
NSMutableArray *ConvertExtensions(std::vector<std::string> 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<std::string> 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<std::string> 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;
}
}
}

View file

@ -10,6 +10,7 @@
#include "imgui/misc/cpp/imgui_stdlib.h"
#include <web_functions.hpp>
#include <cats.hpp>
#include <menus.hpp>
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<double>("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<bool>("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<ImGuiMenuData> 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<double>("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<bool>("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)) {

View file

@ -33,6 +33,8 @@
#endif
#include "../../../backend.hpp"
#include "ui_backend.hpp"
#include <menus.hpp>
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<UIBackend*> backends;
UIBackend *cur_backend;

View file

@ -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})

View file

@ -0,0 +1,5 @@
#include <menus.hpp>
bool LooperUI::PublishMenu(Menu*, PlatformData*) {
// No-op
return false;
}

View file

@ -0,0 +1,103 @@
#include <menus.hpp>
#include <Foundation/Foundation.h>
#include <AppKit/AppKit.h>
#include "../../log.hpp"
#include <stack>
using namespace LooperUI;
@interface MenuHelper : NSObject {
std::map<id, MenuItem*> 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<NSMenu*> 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;
}
}

213
liblooperui/menus.cpp Normal file
View file

@ -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<MenuType, bool> 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<void(MenuItem*, void*)> 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<void(MenuItem*, void*)> 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<void(MenuItem*)> 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<void()> 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;
}
}

106
liblooperui/menus.hpp Normal file
View file

@ -0,0 +1,106 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <functional>
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<void(MenuItem*, void*)> callback;
virtual void RunAction() {
callback(this, userdata);
}
MenuItem(std::string title, std::string keybindString, std::function<void(MenuItem*, void*)> callback, void *userdata = nullptr);
};
class Menu : public MenuItemBase {
using container = std::vector<MenuItemBase*>;
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<void(MenuItem*, void*)> callback, void *userdata = nullptr, OSOptions allowedOS = OSOptions::AnyOS);
MenuBuilder &AddItemNoUserdata(std::string title, std::string keybindString, std::function<void(MenuItem*)> callback_no_userdata, OSOptions allowedOS = OSOptions::AnyOS);
MenuBuilder &AddItemSimple(std::string title, std::string keybindString, std::function<void()> callback_simple, OSOptions allowedOS = OSOptions::AnyOS);
Menu *Build();
MenuBuilder(std::string rootTitle = "<root>");
};
// 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);
}

View file

@ -15,6 +15,9 @@
#include <os/AppKit.h>
#include <image.h>
#endif
#ifdef __MACOSX__
#include <mach-o/dyld.h>
#endif
#include "web_functions.hpp"
#include "cats.hpp"
#include <json/value.h>
@ -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

20
mkicns.sh Executable file
View file

@ -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"

View file

@ -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;