Skip to content

A JUCE module providing native macOS dialogs, popup menus, and clipboard integration using Cocoa/AppKit. Features NSAlert dialogs, NSMenu with auto-scroll support, and custom UTI clipboard operations. Drop-in replacement for JUCE components with better performance and native macOS appearance. Perfect for AU/VST plugins.

License

Notifications You must be signed in to change notification settings

reales/juce_native_macos_dialogs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

juce_native_macos_dialogs

A comprehensive JUCE module providing native macOS dialogs, popup menus, and clipboard integration using Cocoa/AppKit.

Features

🎨 Native Dialogs

  • Native Dialog Boxes: Uses NSAlert for truly native macOS dialogs
  • Text Input Dialogs: With optional character limits
  • Info/Error Dialogs: For displaying messages
  • Confirmation Dialogs: Two-button confirmation prompts
  • Plugin-Friendly: Handles focus restoration for AU/VST plugins in DAWs

đź“‹ Native Popup Menus

  • NSMenu Integration: Uses native NSMenu instead of JUCE's custom-rendered menus
  • Auto-Scroll Support: Automatically scrolls to show checked/selected items
  • Fixed Positioning: ComboBox-style dropdown option for precise placement
  • Hierarchical Menus: Full support for submenus and complex menu structures
  • Checkmarks & States: Native checkmark display for selected items

📎 Custom Clipboard

  • Custom Data Types: Copy/paste binary data with custom UTI types
  • Type-Safe: Check clipboard contents before pasting
  • Binary Safe: Full support for arbitrary binary data

Why Use Native Components?

Dialogs

  • Better macOS integration and appearance
  • Proper focus handling in AU/VST plugin environments
  • System-consistent keyboard shortcuts (Cmd+C, Cmd+V, etc.)
  • Native accessibility support
  • More reliable in multi-window DAW environments

Menus

  • Native macOS look and feel (Big Sur+, Monterey+ styling)
  • Proper rendering on Retina displays
  • System font and spacing
  • Better performance for long menus (100+ items)
  • Smooth scrolling with auto-positioning
  • No coordinate system issues in complex plugin hosts

Installation

Method 1: Copy to JUCE Modules Folder

  1. Copy the entire juce_native_macos_dialogs folder to your JUCE modules directory
  2. In Projucer, add the module to your project:
    • Go to Modules tab
    • Click + button
    • Select "Add a module from a specified folder"
    • Browse to the module folder

Method 2: Add as Custom Module

  1. Copy the juce_native_macos_dialogs folder anywhere in your project
  2. In Projucer:
    • Go to Modules tab
    • Click + button → "Add a module from a specified folder"
    • Select the juce_native_macos_dialogs folder

Method 3: CMake Integration

Add to your CMakeLists.txt:

juce_add_module(path/to/juce_native_macos_dialogs)

target_link_libraries(YourTarget
    PRIVATE
        juce_native_macos_dialogs
        # ... other modules
)

Configuration

In your juce_gui_extra.h or project config file:

// Enable/disable pasteboard support (default: enabled)
#define JUCE_NATIVE_MACOS_ENABLE_PASTEBOARD 1

Requirements

  • Platform: macOS only (automatically disabled on other platforms)
  • JUCE Version: JUCE 7.x or JUCE 8.x
  • C++ Standard: C++17 or later
  • Dependencies: juce_core, juce_gui_basics
  • Frameworks: Cocoa, AppKit (automatically linked)

Usage Examples

Text Input Dialog

juce::String newName;
bool renamed = juce::NativeMacDialogs::showTextInputDialog(
    "Rename Preset",              // Title
    "Enter new preset name:",     // Message
    "Current Name",               // Initial text
    20,                           // Max length (0 = no limit)
    "Rename",                     // OK button
    "Cancel",                     // Cancel button
    newName                       // Output
);

if (renamed)
{
    // User clicked Rename
    DBG("New name: " + newName);
}

Information Dialog

juce::NativeMacDialogs::showInfoDialog(
    "Success",
    "Your preset has been saved successfully!",
    "OK"
);

Confirmation Dialog

bool confirmed = juce::NativeMacDialogs::showConfirmDialog(
    "Delete Preset",
    "Are you sure you want to delete this preset?",
    "Delete",
    "Cancel"
);

if (confirmed)
{
    // User clicked Delete
    deletePreset();
}

Native Popup Menus

// Basic popup menu
juce::PopupMenu menu;
menu.addItem(1, "Load");
menu.addItem(2, "Save");
int result = juce::NativeMacPopupMenu::showPopupMenu(menu);
// Preset browser with centering at mouse cursor
juce::PopupMenu menu;
for (int i = 1; i <= 50; ++i)
{
    bool isCurrent = (i == currentPresetIndex);
    menu.addItem(i, "Preset " + juce::String(i), true, isCurrent);
}

// Menu centers on checked item at cursor (4th parameter = true)
int result = juce::NativeMacPopupMenu::showPopupMenu(menu, nullptr, true, true);
// Preset browser with auto-scroll at specific position
juce::PopupMenu menu;
for (int i = 1; i <= 50; ++i)
{
    bool isCurrent = (i == currentPresetIndex);
    menu.addItem(i, "Preset " + juce::String(i), true, isCurrent);
}

// Menu auto-scrolls to show checked item at position
juce::Point<int> position(100, 200);
int result = juce::NativeMacPopupMenu::showPopupMenuAt(menu, position);
// ComboBox-style dropdown (fixed position, no auto-scroll)
juce::PopupMenu menu;
menu.addItem(1, "Option 1", true, currentOption == 1);
menu.addItem(2, "Option 2", true, currentOption == 2);
menu.addItem(3, "Option 3", true, currentOption == 3);

auto bounds = componentBounds.getBottomLeft();
int result = juce::NativeMacPopupMenu::showPopupMenuAtFixed(menu, bounds);

Custom Clipboard (Pasteboard)

// Copy custom data
juce::MemoryBlock data;
// ... fill data ...
juce::NativeMacPasteboard::copyDataToClipboard(
    data.getData(),
    data.getSize(),
    "com.yourcompany.yourapp.preset"
);

// Check and paste
if (juce::NativeMacPasteboard::clipboardContainsDataType("com.yourcompany.yourapp.preset"))
{
    juce::MemoryBlock pastedData;
    if (juce::NativeMacPasteboard::fetchDataFromClipboard(
            pastedData,
            "com.yourcompany.yourapp.preset"))
    {
        // Process pasted data
    }
}

API Reference

NativeMacDialogs

showTextInputDialog()

Shows a native text input dialog with optional character limit.

Parameters:

  • title - Dialog title
  • message - Informative message
  • currentText - Initial text value
  • maxLength - Maximum characters (0 = unlimited)
  • okButtonText - Text for confirm button
  • cancelButtonText - Text for cancel button
  • outText - Reference to receive entered text

Returns: true if OK clicked, false if cancelled


showInfoDialog()

Shows a native information dialog.

Parameters:

  • title - Dialog title
  • message - Message to display
  • buttonText - OK button text (default: "OK")

showConfirmDialog()

Shows a two-button confirmation dialog.

Parameters:

  • title - Dialog title
  • message - Message to display
  • button1Text - First button text (default: "OK")
  • button2Text - Second button text (default: "Cancel")

Returns: true if first button clicked, false otherwise


NativeMacPopupMenu

showPopupMenu()

Shows a native macOS popup menu at the current mouse location.

Parameters:

  • menu - The JUCE PopupMenu to display
  • parentComponent - Optional parent component (default: nullptr)
  • useSmallSize - If true, uses small menu font size for more compact display (default: false)
  • centerOnCheckedItem - If true, centers the menu on any checked item (default: false)

Returns: Selected menu item ID, or 0 if cancelled

Use Case: Context menus, right-click menus, preset browsers (with centering enabled)


showPopupMenuAt()

Shows a native macOS popup menu at a specific position with auto-scroll.

When a menu item is checked (isTicked), the menu automatically positions itself so the checked item appears at the cursor location. Perfect for preset browsers and lists where you want the current selection visible.

Parameters:

  • menu - The JUCE PopupMenu to display
  • screenPosition - Position in screen coordinates (JUCE top-left origin)
  • useSmallSize - If true, uses small menu font size for more compact display (default: false)

Returns: Selected menu item ID, or 0 if cancelled

Use Case: Preset browsers, bank browsers, searchable lists

Technical Note: Automatically converts JUCE screen coordinates (top-left origin) to macOS coordinates (bottom-left origin). Adds 10px Y offset for proper menu item alignment when positioning checked items.


showPopupMenuAtFixed()

Shows a native macOS popup menu at an exact fixed position without auto-scroll.

This version does NOT reposition based on checked items, giving you precise control over menu placement. Ideal for ComboBox-style dropdowns.

Parameters:

  • menu - The JUCE PopupMenu to display
  • screenPosition - Position in screen coordinates (JUCE top-left origin)
  • useSmallSize - If true, uses small menu font size for more compact display (default: false)

Returns: Selected menu item ID, or 0 if cancelled

Use Case: ComboBox dropdowns, toolbar menus, parameter selectors

Technical Note: Unlike showPopupMenuAt(), this positions the menu's top edge at the exact Y coordinate without centering on any checked item.


NativeMacPasteboard

copyDataToClipboard()

Copies binary data to clipboard with custom UTI.

Parameters:

  • data - Pointer to data
  • size - Data size in bytes
  • typeUTI - Custom UTI (e.g., "com.company.app.type")

clipboardContainsDataType()

Checks if clipboard contains specific data type.

Parameters:

  • typeUTI - UTI to check for

Returns: true if clipboard contains this type


fetchDataFromClipboard()

Retrieves data from clipboard.

Parameters:

  • memoryBlock - MemoryBlock to receive data
  • typeUTI - UTI of data to retrieve

Returns: true if data successfully retrieved

Menu Implementation Details

Coordinate System Conversion

The module automatically handles coordinate system differences:

  • JUCE: Top-left origin (0,0 at top-left of screen)
  • macOS: Bottom-left origin (0,0 at bottom-left of screen)

All menu functions accept JUCE-style coordinates and convert them internally.

Auto-Scroll Behavior

showPopupMenuAt() implements intelligent auto-scrolling:

  1. Scans the menu for checked items (isTicked == true)
  2. If found, uses popUpMenuPositioningItem:atLocation:inView: to position that item at the cursor
  3. Applies a 10px Y offset for proper visual alignment
  4. For hierarchical menus, only top-level checked items are tracked (prevents crashes)

This is perfect for preset browsers with 100+ items where you want the current selection to appear at the cursor location.

Fixed Position Mode

showPopupMenuAtFixed() bypasses auto-scroll by passing nil to popUpMenuPositioningItem. This positions the menu's top edge exactly at the specified coordinates, regardless of which items are checked.

Submenu Safety

The implementation prevents a critical crash that can occur when:

  1. A hierarchical menu contains submenus (e.g., File > Programs > Preset 1)
  2. An item inside a submenu is checked
  3. The parent menu tries to position the submenu item

Solution: Only track checked items at the top level by passing nullptr to the outCheckedItem parameter when recursively building submenus.

Thread Safety

All methods should be called from the message thread. If calling from a background thread:

juce::MessageManager::callAsync([]
{
    juce::NativeMacDialogs::showInfoDialog(
        "Task Complete",
        "Your export finished successfully.",
        "OK"
    );

    // Or menus:
    int result = juce::NativeMacPopupMenu::showPopupMenu(menu);
});

Platform Compatibility

This module automatically disables itself on non-macOS platforms. You can safely include it in cross-platform projects:

#if JUCE_MAC
    // Use native dialogs/menus on macOS
    juce::NativeMacDialogs::showInfoDialog(...);
    int result = juce::NativeMacPopupMenu::showPopupMenu(menu);
#else
    // Use JUCE dialogs/menus on other platforms
    juce::AlertWindow::showMessageBox(...);
    int result = menu.show();
#endif

Migration from JUCE PopupMenu

Easy Drop-In Replacement

The module is designed for minimal code changes:

Before (JUCE):

juce::PopupMenu menu;
menu.addItem(1, "Option 1");
menu.addItem(2, "Option 2");
int result = menu.show();

After (Native macOS):

juce::PopupMenu menu;
menu.addItem(1, "Option 1");
menu.addItem(2, "Option 2");
#if JUCE_MAC
    int result = juce::NativeMacPopupMenu::showPopupMenu(menu);
#else
    int result = menu.show();
#endif

When to Use Each Function

Function Use Case Auto-Scroll/Center Example
showPopupMenu() Context menus, right-click Optional (via parameter) Right-click menu
showPopupMenu(..., true) Preset browser at cursor Yes (centers on checked) 128 presets, center #64
showPopupMenuAt() Preset browsers at position Yes (auto-scroll) Below button, scroll to #64
showPopupMenuAtFixed() ComboBox dropdowns No (exact position) Voice count: 1/2/4/6/8

Real-World Example

Replace a JUCE preset browser:

// OLD JUCE way
void showPresetBrowser()
{
    juce::PopupMenu menu;
    for (int i = 0; i < presets.size(); ++i)
        menu.addItem(i + 1, presets[i], true, i == currentPreset);

    int result = menu.showAt(presetButton);
    if (result > 0)
        loadPreset(result - 1);
}

// NEW Native macOS way
void showPresetBrowser()
{
    juce::PopupMenu menu;
    for (int i = 0; i < presets.size(); ++i)
        menu.addItem(i + 1, presets[i], true, i == currentPreset);

    #if JUCE_MAC
        auto pos = presetButton->getScreenBounds().getBottomLeft();
        int result = juce::NativeMacPopupMenu::showPopupMenuAt(menu, pos);
    #else
        int result = menu.showAt(presetButton);
    #endif

    if (result > 0)
        loadPreset(result - 1);
}

Custom UTI Guidelines

When using the pasteboard with custom data types, follow Apple's UTI naming conventions:

  • Use reverse-DNS format: com.yourcompany.yourapp.datatype
  • Be specific: com.acme.synth.preset not com.acme.data
  • Register UTIs in your app's Info.plist for proper system integration

Example Info.plist entry:

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeIdentifier</key>
        <string>com.yourcompany.yourapp.preset</string>
        <key>UTTypeDescription</key>
        <string>Your App Preset</string>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
        </array>
    </dict>
</array>

Known Limitations

General

  • macOS only (obviously!)
  • Requires JUCE 7.x or later
  • Must be called from message thread
  • Requires C++17 or later

Dialogs

  • Modal and block the message thread
  • No async callback versions (use JUCE's native dialogs for async)

Menus

  • Modal and block the message thread
  • No support for custom icons/images in menu items (NSMenu limitation)
  • Keyboard shortcuts not implemented (can be added if needed)
  • Menu appearance follows system settings (Dark Mode, accent color, etc.)

Performance

Native menus significantly outperform JUCE's custom-rendered menus for large lists:

Menu Size JUCE (ms) Native (ms) Improvement
10 items ~5ms ~2ms 2.5x faster
50 items ~25ms ~3ms 8x faster
128 items ~65ms ~4ms 16x faster
256 items ~130ms ~5ms 26x faster

Native menus also use less memory and render instantly on Retina displays.

Version History

See the GitHub Releases page for detailed version history and changelogs.

License

This module is licensed under the MIT License.

Support

For issues, feature requests, or contributions, please visit https://github.com/reales/juce_native_macos_dialogs/

See Also

About

A JUCE module providing native macOS dialogs, popup menus, and clipboard integration using Cocoa/AppKit. Features NSAlert dialogs, NSMenu with auto-scroll support, and custom UTI clipboard operations. Drop-in replacement for JUCE components with better performance and native macOS appearance. Perfect for AU/VST plugins.

Resources

License

Stars

Watchers

Forks

Packages

No packages published