Skip to content

Context Menu Guide

Matt Akbarian edited this page Oct 4, 2025 · 1 revision

Context Menu System Guide

A comprehensive guide to using and understanding the context menu system in Java Leaflet.

Table of Contents

  1. Overview
  2. Key Components
  3. Architecture
  4. Basic Usage
  5. Advanced Usage
  6. Platform-Specific Considerations
  7. Best Practices
  8. Troubleshooting
  9. Complete Working Example
  10. Extension Points

Overview

The context menu system provides a platform-independent way to add interactive context menus to all map elements (markers, polygons, polylines, etc.) in the Java Leaflet library. It uses a sophisticated multi-layered architecture based on the mediator pattern to abstract platform-specific implementations while maintaining a consistent API.

Design Goals

  • Platform Independence: Works seamlessly across different UI frameworks (Vaadin, JavaFX)
  • Type Safety: Leverages Java's type system to prevent runtime errors
  • Performance: Minimizes overhead through lazy initialization and caching
  • Extensibility: Supports new UI frameworks without modifying existing code
  • Thread Safety: Handles concurrent access in multi-threaded environments
  • Memory Efficiency: Prevents memory leaks through proper resource management

Supported Platforms

  • Vaadin: Native integration with Vaadin's ContextMenu component
  • JavaFX: Direct integration with JavaFX scene graph
  • Custom: Extensible architecture supports custom UI framework implementations

Key Components

1. JLHasContextMenu Interface

All JL objects implement this interface, which provides the core context menu functionality:

public interface JLHasContextMenu<T extends JLObject<T>> {
    @NonNull JLContextMenu<T> getContextMenu();
    boolean hasContextMenu();
    @NonNull T setContextMenuEnabled(boolean enabled);
    boolean isContextMenuEnabled();
}

Key Methods:

  • getContextMenu() - Gets or creates the context menu (lazy initialization)
  • hasContextMenu() - Checks if the object has visible menu items
  • setContextMenuEnabled(boolean) - Enables/disables the context menu
  • isContextMenuEnabled() - Checks if the context menu is enabled

Design Features:

  • Type Parameter Constraint: T extends JLObject<T> ensures type safety
  • Lazy Initialization: Context menus are created only when first accessed
  • Enable/Disable Support: Allows runtime control of context menu behavior

2. JLContextMenu Class

Central coordinator for menu items, event handling, and mediator interaction.

public class JLContextMenu<T extends JLObject<T>> {
    private final T owner;
    private final Map<String, JLMenuItem> menuItems = new ConcurrentHashMap<>();
    private OnJLContextMenuItemListener onMenuItemListener;
    private boolean enabled = true;
}

Responsibilities:

  • Add, remove, and update menu items
  • Handle menu item selections
  • Manage menu lifecycle events
  • Coordinate with platform-specific mediators

Thread Safety:

  • Uses ConcurrentHashMap for menu items storage
  • All public methods are thread-safe
  • Atomic operations for menu modifications

3. JLMenuItem Class

Immutable value object representing a menu item with builder pattern support:

@Value
@Builder(toBuilder = true)
public class JLMenuItem {
    @NonNull String id;
    @NonNull String text;
    String icon;
    @Builder.Default boolean enabled = true;
    @Builder.Default boolean visible = true;
}

Features:

  • Immutability: Prevents accidental modifications after creation
  • Builder Pattern: Flexible object creation with default values
  • Value Semantics: Equality based on content, not identity
  • Properties: ID, text, icon, enabled/disabled state, visible/hidden state

4. Context Menu Mediators

Platform-specific implementations that bridge JL objects with UI frameworks:

  • VaadinContextMenuMediator - For Vaadin applications
  • JavaFXContextMenuMediator - For JavaFX applications
  • Custom Mediators - Extensible for new platforms

Discovery Mechanism:

  • Automatically discovered via Java's ServiceLoader
  • Priority-based selection when multiple mediators are available
  • Runtime capability checking

Architecture

Architectural Patterns

1. Mediator Pattern

The system employs the Mediator pattern to decouple JL objects from platform-specific UI implementations:

┌─────────────────┐    ┌──────────────────────┐    ┌─────────────────────┐
│   JL Objects    │◄──►│ Context Menu System  │◄──►│ Platform Mediators  │
│                 │    │                      │    │                     │
│ - JLMarker      │    │ - JLContextMenu      │    │ - VaadinMediator    │
│ - JLPolygon     │    │ - JLMenuItem         │    │ - JavaFXMediator    │
│ - JLPolyline    │    │ - Event System       │    │ - CustomMediator    │
└─────────────────┘    └──────────────────────┘    └─────────────────────┘

Benefits:

  • JL objects remain platform-agnostic
  • New UI frameworks can be added without modifying existing code
  • Platform-specific optimizations can be implemented independently

2. Service Locator Pattern

The JLContextMenuMediatorLocator implements the Service Locator pattern for dynamic mediator discovery:

ServiceLoader<JLContextMenuMediator> → Filter by support & availability →
Sort by priority → Cache result → Return mediator

3. Observer Pattern

Event notification uses the Observer pattern for loose coupling between menu lifecycle and application logic:

JLContextMenu → OnJLActionListener → Application Code
    ↓
ContextMenuEvent → Event Handler → Business Logic

Event Flow Architecture

Complete event flow from user interaction to application response:

1. User right-clicks on object
   ↓
2. Platform UI detects mouse event
   ↓
3. Mediator receives platform event
   ↓
4. JLContextMenu processes event
   ↓
5. Application listener is notified
   ↓
6. User clicks menu item
   ↓
7. Platform UI detects selection
   ↓
8. Mediator notifies JLContextMenu
   ↓
9. OnJLContextMenuItemListener is invoked
   ↓
10. Menu closes and cleanup occurs

Event Types:

  • Menu Lifecycle Events: Handled through OnJLActionListener
    • ContextMenuEvent.OPEN - Menu is opened
    • ContextMenuEvent.CLOSE - Menu is closed
  • Item Selection Events: Handled through OnJLContextMenuItemListener
    • Contains the selected JLMenuItem

Service Discovery Mechanism

The system uses Java's built-in ServiceLoader mechanism for mediator discovery:

Service Provider Configuration:

For Vaadin:

META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator
io.github.makbn.jlmap.vaadin.element.menu.VaadinContextMenuMediator

For JavaFX:

META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator
io.github.makbn.jlmap.fx.element.menu.JavaFXContextMenuMediator

Discovery Algorithm:

public <T extends JLObject<T>> JLContextMenuMediator findMediator(@NonNull Class<T> objectType) {
    // 1. Check cache
    JLContextMenuMediator cached = mediatorCache.get(objectType);
    if (cached != null) return cached;

    // 2. Discover and filter
    List<JLContextMenuMediator> candidates = getAvailableMediators().stream()
        .filter(m -> m.supportsObjectType(objectType) && m.isAvailable())
        .sorted((m1, m2) -> Integer.compare(m2.getPriority(), m1.getPriority()))
        .toList();

    // 3. Select and cache
    JLContextMenuMediator selected = candidates.get(0);
    mediatorCache.put(objectType, selected);
    return selected;
}

Priority-Based Selection:

Priority Range Usage
100+ Framework-specific mediators (Vaadin, JavaFX)
50-99 General-purpose mediators
1-49 Fallback or test mediators

Basic Usage

Creating a Context Menu

// Create a marker
JLMarker marker = JLMarkerBuilder.create()
    .latLng(new JLLatLng(51.505, -0.09))
    .build();

// Get the context menu (created lazily)
JLContextMenu<JLMarker> contextMenu = marker.getContextMenu();

// Add menu items
contextMenu
    .addItem("edit", "Edit Marker", "edit-icon.png")
    .addItem("info", "Show Info")
    .addItem("delete", "Delete Marker");

Handling Menu Item Selections

contextMenu.setOnMenuItemListener(selectedItem -> {
    switch (selectedItem.getId()) {
        case "edit" -> openEditDialog(marker);
        case "info" -> showMarkerInfo(marker);
        case "delete" -> marker.remove();
    }
});

Handling Context Menu Events

Track when the context menu is opened or closed:

marker.setOnActionListener((source, event) -> {
    if (event instanceof ContextMenuEvent contextEvent) {
        if (contextEvent.isOpen()) {
            System.out.println("Context menu opened");
            // Perform actions when menu opens
        } else if (contextEvent.isClose()) {
            System.out.println("Context menu closed");
            // Perform cleanup when menu closes
        }
    }
});

Working with Different Object Types

All JL objects support context menus with the same API:

// Polygon with context menu
JLPolygon polygon = JLPolygonBuilder.create()
    .latLngs(coordinates)
    .build();

JLContextMenu<JLPolygon> polygonMenu = polygon.getContextMenu();
polygonMenu
    .addItem("edit-shape", "Edit Shape")
    .addItem("change-color", "Change Color")
    .addItem("delete", "Delete Polygon");

// Polyline with context menu
JLPolyline polyline = JLPolylineBuilder.create()
    .latLngs(coordinates)
    .build();

JLContextMenu<JLPolyline> polylineMenu = polyline.getContextMenu();
polylineMenu.addItem("edit-path", "Edit Path");

Advanced Usage

Creating Complex Menu Items

Use the builder pattern for detailed configuration:

JLMenuItem complexItem = JLMenuItem.builder()
    .id("advanced-action")
    .text("Advanced Action")
    .icon("advanced-icon.png")
    .enabled(userHasPermission)
    .visible(featureEnabled)
    .build();

contextMenu.addItem(complexItem);

Dynamic Menu Updates

Update menu items at runtime:

// Add items dynamically
contextMenu.addItem("new-action", "New Action");

// Remove items
contextMenu.removeItem("unwanted-action");

// Update existing items using toBuilder()
JLMenuItem existingItem = contextMenu.getItem("edit");
JLMenuItem updatedItem = existingItem.toBuilder()
    .text("Updated Text")
    .enabled(false)
    .build();
contextMenu.updateItem(updatedItem);

// Clear all items
contextMenu.clearItems();

Conditional Menu Display

Control when context menus appear:

// Enable/disable context menu based on conditions
marker.setContextMenuEnabled(userHasPermission);

// Check menu state
if (marker.hasContextMenu() && marker.isContextMenuEnabled()) {
    // Context menu will be shown on right-click
}

// Conditionally show/hide specific items
contextMenu.getItem("delete").toBuilder()
    .visible(userCanDelete)
    .build();
contextMenu.updateItem(updatedItem);

State-Dependent Menu Items

Update menu items based on application state:

// Update menu based on marker state
marker.setOnActionListener((source, event) -> {
    if (event instanceof ContextMenuEvent contextEvent && contextEvent.isOpen()) {
        // Update items before menu is shown
        JLMenuItem editItem = contextMenu.getItem("edit").toBuilder()
            .enabled(!marker.isLocked())
            .build();

        JLMenuItem deleteItem = contextMenu.getItem("delete").toBuilder()
            .visible(userHasDeletePermission())
            .build();

        contextMenu.updateItem(editItem);
        contextMenu.updateItem(deleteItem);
    }
});

Programmatic Menu Display

Show or hide the context menu programmatically:

// Show context menu at specific coordinates
marker.getContextMenu().show(screenX, screenY);

// Hide context menu
marker.getContextMenu().hide();

Platform-Specific Considerations

Vaadin Applications

Integration:

  • Context menus are automatically integrated with Vaadin's component tree
  • Uses Vaadin's ContextMenu component for native look and feel
  • Events are handled through Vaadin's server-side event system

Threading Model:

  • Server-side execution on UI thread
  • Event handling through Vaadin's session lock
  • No additional synchronization required

Features:

  • Native integration with Vaadin's ContextMenu component
  • Automatic UI updates through Vaadin's push mechanism
  • Server-side event handling

Example:

// Vaadin-specific mediator automatically handles component binding
VaadinContextMenuMediator mediator = new VaadinContextMenuMediator();
mediator.registerContextMenu(marker, contextMenu);
// Mediator creates Vaadin ContextMenu and binds to component

JavaFX Applications

Integration:

  • Context menus are attached to JavaFX nodes in the scene graph
  • Supports icons through JavaFX ImageView components
  • Events are handled on the JavaFX Application Thread

Threading Model:

// Mediator ensures operations run on JavaFX Application Thread
if (!Platform.isFxApplicationThread()) {
    Platform.runLater(() -> registerContextMenu(object, contextMenu));
    return;
}

Features:

  • Direct integration with JavaFX scene graph
  • Mouse event handling for right-click detection
  • Icon support through ImageView components

Example:

// JavaFX-specific mediator handles scene graph integration
JavaFXContextMenuMediator mediator = new JavaFXContextMenuMediator();
mediator.registerContextMenu(marker, contextMenu);
// Mediator creates JavaFX ContextMenu and attaches to Node

Cross-Platform Development

Best Practices:

  • Use platform-agnostic APIs provided by JLContextMenu
  • Avoid direct access to platform-specific UI components
  • Test on all target platforms
  • Use priority-based mediator selection for flexibility

Platform Detection:

// Get active mediator information
JLContextMenuMediatorLocator locator = JLContextMenuMediatorLocator.getInstance();
JLContextMenuMediator mediator = locator.findMediator(marker.getClass());
String platformName = mediator.getName(); // "Vaadin" or "JavaFX"

Best Practices

1. Menu Item Design

Clear and Actionable Text:

// Good: Clear action verbs
contextMenu
    .addItem("edit", "Edit Marker")
    .addItem("delete", "Delete Marker")
    .addItem("share", "Share Location");

// Avoid: Vague or unclear text
contextMenu
    .addItem("item1", "Thing")
    .addItem("item2", "Do stuff");

Meaningful IDs:

// Good: Descriptive IDs
contextMenu.addItem("export-geojson", "Export as GeoJSON");

// Avoid: Generic IDs
contextMenu.addItem("action1", "Export as GeoJSON");

Icon Usage:

// Include icons when they enhance usability
contextMenu
    .addItem("zoom-in", "Zoom In", "zoom-in-icon.png")
    .addItem("zoom-out", "Zoom Out", "zoom-out-icon.png");

Disable vs. Hide:

// Prefer disabling over hiding for unavailable actions
JLMenuItem saveItem = JLMenuItem.builder()
    .id("save")
    .text("Save Changes")
    .enabled(hasUnsavedChanges) // Disabled if no changes
    .visible(true) // Always visible
    .build();

2. Event Handling

Lightweight Handlers:

// Good: Lightweight handler delegates to methods
contextMenu.setOnMenuItemListener(item -> {
    switch (item.getId()) {
        case "edit" -> handleEdit(marker);
        case "delete" -> handleDelete(marker);
    }
});

// Avoid: Heavy processing in handlers
contextMenu.setOnMenuItemListener(item -> {
    // Don't perform expensive operations directly
    performLongRunningOperation();
});

Exception Handling:

// Graceful error handling
contextMenu.setOnMenuItemListener(item -> {
    try {
        switch (item.getId()) {
            case "export" -> exportData(marker);
            case "process" -> processMarker(marker);
        }
    } catch (Exception e) {
        showErrorMessage("Action failed: " + e.getMessage());
        log.error("Menu action failed", e);
    }
});

Method Delegation:

// Separate business logic from event handling
private void handleMarkerEdit(JLMarker marker) {
    validateMarkerState(marker);
    openEditDialog(marker);
    logUserAction("edit", marker.getId());
}

contextMenu.setOnMenuItemListener(item -> {
    if ("edit".equals(item.getId())) {
        handleMarkerEdit(marker);
    }
});

3. Performance

Lazy Initialization:

// Context menus are created only when needed
// Don't access getContextMenu() unless you need it
if (needsContextMenu) {
    marker.getContextMenu().addItem("action", "Action");
}

Efficient Updates:

// Batch updates when possible
List<JLMenuItem> items = List.of(
    JLMenuItem.builder().id("edit").text("Edit").build(),
    JLMenuItem.builder().id("delete").text("Delete").build()
);

items.forEach(contextMenu::addItem);

Resource Cleanup:

// Clean up when objects are no longer needed
@Override
public void dispose() {
    if (hasContextMenu()) {
        getContextMenu().clearItems();
        setContextMenuEnabled(false);
    }
}

4. Accessibility

Descriptive Text:

// Use descriptive text for screen readers
contextMenu.addItem("zoom", "Zoom to Marker Location");
// Not just "Zoom"

Keyboard Support:

// Consider providing keyboard shortcuts
// Document keyboard alternatives in UI
contextMenu.addItem("delete", "Delete Marker (Del)");

Alternative Access:

// Provide alternative access to critical functions
// Don't rely solely on context menus for essential operations
// Also provide toolbar buttons, menu bar items, etc.

5. Thread Safety

Concurrent Access:

// JLContextMenu is thread-safe
// But consider synchronizing complex operations
synchronized (marker) {
    if (!marker.hasContextMenu()) {
        JLContextMenu<JLMarker> menu = marker.getContextMenu();
        menu.addItem("action", "Action");
    }
}

Platform Threading:

// Respect platform threading requirements
// Mediators handle platform-specific threading automatically
// But external resources may need explicit threading
Platform.runLater(() -> {
    updateExternalResource(marker);
});

Troubleshooting

Context Menu Not Appearing

Check if context menu is enabled:

if (!marker.isContextMenuEnabled()) {
    marker.setContextMenuEnabled(true);
}

Verify menu has visible items:

if (!marker.getContextMenu().hasVisibleItems()) {
    // Add at least one visible item
    marker.getContextMenu().addItem("action", "Action");
}

Ensure appropriate mediator is available:

try {
    JLContextMenuMediatorLocator locator = JLContextMenuMediatorLocator.getInstance();
    JLContextMenuMediator mediator = locator.findMediator(marker.getClass());
    System.out.println("Using mediator: " + mediator.getName());
} catch (Exception e) {
    System.err.println("No mediator found: " + e.getMessage());
}

Check console for errors:

  • Look for mediator registration errors
  • Verify ServiceLoader configuration files
  • Check for platform availability issues

Menu Items Not Working

Verify listener is set:

if (marker.getContextMenu().getOnMenuItemListener() == null) {
    marker.getContextMenu().setOnMenuItemListener(item -> {
        // Handle item selection
    });
}

Check item state:

JLMenuItem item = contextMenu.getItem("action");
if (item != null) {
    System.out.println("Enabled: " + item.isEnabled());
    System.out.println("Visible: " + item.isVisible());
}

Verify unique IDs:

// Ensure all menu items have unique IDs within the same menu
Set<String> ids = new HashSet<>();
for (JLMenuItem item : contextMenu.getItems()) {
    if (!ids.add(item.getId())) {
        System.err.println("Duplicate ID: " + item.getId());
    }
}

Check for exceptions:

// Add try-catch in handler to catch and log exceptions
contextMenu.setOnMenuItemListener(item -> {
    try {
        handleMenuItem(item);
    } catch (Exception e) {
        e.printStackTrace();
        System.err.println("Error handling menu item: " + e.getMessage());
    }
});

Performance Issues

Avoid creating too many context menus:

// Bad: Creating context menus for thousands of markers
for (JLMarker marker : thousands_of_markers) {
    marker.getContextMenu().addItem("action", "Action");
}

// Good: Only create context menus when needed
marker.setOnActionListener((source, event) -> {
    if (event instanceof ContextMenuEvent) {
        source.getContextMenu().addItem("action", "Action");
    }
});

Remove unused context menus:

// Clean up when markers are removed
void removeMarker(JLMarker marker) {
    if (marker.hasContextMenu()) {
        marker.getContextMenu().clearItems();
    }
    marker.remove();
}

Use lazy initialization:

// Don't access getContextMenu() unless needed
if (markerRequiresContextMenu(marker)) {
    marker.getContextMenu().addItem("action", "Action");
}

Consider item pooling:

// Reuse menu item objects for frequently changing menus
private final Map<String, JLMenuItem> itemPool = new HashMap<>();

private JLMenuItem getOrCreateItem(String id, String text) {
    return itemPool.computeIfAbsent(id, k ->
        JLMenuItem.builder().id(id).text(text).build()
    );
}

Mediator Discovery Issues

Verify ServiceLoader configuration:

# Check that service provider file exists
ls -la META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator

# Verify file contains correct class name
cat META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator

Check mediator availability:

JLContextMenuMediatorLocator locator = JLContextMenuMediatorLocator.getInstance();
List<MediatorInfo> mediators = locator.getMediatorInfo();

for (MediatorInfo info : mediators) {
    System.out.println("Mediator: " + info.getName());
    System.out.println("  Class: " + info.getClassName());
    System.out.println("  Priority: " + info.getPriority());
    System.out.println("  Available: " + info.isAvailable());
}

Clear mediator cache:

// If mediator discovery seems incorrect, clear cache
JLContextMenuMediatorLocator.getInstance().clearCache();

Complete Working Example

Here's a comprehensive example demonstrating all features of the context menu system:

import io.github.makbn.jlmap.element.JLMarker;
import io.github.makbn.jlmap.element.JLPolygon;
import io.github.makbn.jlmap.element.builder.JLMarkerBuilder;
import io.github.makbn.jlmap.element.builder.JLPolygonBuilder;
import io.github.makbn.jlmap.element.menu.JLContextMenu;
import io.github.makbn.jlmap.element.menu.JLMenuItem;
import io.github.makbn.jlmap.event.ContextMenuEvent;
import io.github.makbn.jlmap.model.JLLatLng;

import java.util.List;

public class ContextMenuExample {

    public static void main(String[] args) {
        // Example 1: Basic marker with context menu
        basicMarkerExample();

        // Example 2: Polygon with dynamic menu
        polygonWithDynamicMenuExample();

        // Example 3: Advanced menu with state management
        advancedMenuExample();

        // Example 4: Menu lifecycle events
        menuLifecycleExample();
    }

    /**
     * Basic example: Create a marker with a simple context menu
     */
    private static void basicMarkerExample() {
        // Create marker
        JLMarker marker = JLMarkerBuilder.create()
            .latLng(new JLLatLng(51.505, -0.09))
            .draggable(true)
            .build();

        // Get context menu and add items
        JLContextMenu<JLMarker> contextMenu = marker.getContextMenu();
        contextMenu
            .addItem("edit", "Edit Marker", "edit-icon.png")
            .addItem("info", "Show Info")
            .addItem("delete", "Delete Marker");

        // Handle menu item selections
        contextMenu.setOnMenuItemListener(selectedItem -> {
            System.out.println("Selected: " + selectedItem.getText());

            switch (selectedItem.getId()) {
                case "edit" -> editMarker(marker);
                case "info" -> showMarkerInfo(marker);
                case "delete" -> deleteMarker(marker);
            }
        });
    }

    /**
     * Polygon example: Dynamic menu based on polygon state
     */
    private static void polygonWithDynamicMenuExample() {
        List<JLLatLng> coordinates = List.of(
            new JLLatLng(51.509, -0.08),
            new JLLatLng(51.503, -0.06),
            new JLLatLng(51.51, -0.047)
        );

        JLPolygon polygon = JLPolygonBuilder.create()
            .latLngs(coordinates)
            .build();

        JLContextMenu<JLPolygon> contextMenu = polygon.getContextMenu();

        // Add initial menu items
        contextMenu
            .addItem("edit-vertices", "Edit Vertices")
            .addItem("change-color", "Change Color")
            .addItem("calculate-area", "Calculate Area")
            .addItem("delete", "Delete Polygon");

        // Update menu dynamically based on state
        polygon.setOnActionListener((source, event) -> {
            if (event instanceof ContextMenuEvent contextEvent && contextEvent.isOpen()) {
                // Update menu items before showing
                updatePolygonMenu(polygon);
            }
        });

        // Handle selections
        contextMenu.setOnMenuItemListener(item -> {
            switch (item.getId()) {
                case "edit-vertices" -> editPolygonVertices(polygon);
                case "change-color" -> changePolygonColor(polygon);
                case "calculate-area" -> calculateArea(polygon);
                case "delete" -> deletePolygon(polygon);
            }
        });
    }

    /**
     * Advanced example: Complex menu items with state management
     */
    private static void advancedMenuExample() {
        JLMarker marker = JLMarkerBuilder.create()
            .latLng(new JLLatLng(51.5, -0.09))
            .build();

        JLContextMenu<JLMarker> contextMenu = marker.getContextMenu();

        // Create complex menu items using builder
        JLMenuItem editItem = JLMenuItem.builder()
            .id("edit")
            .text("Edit Marker")
            .icon("edit-icon.png")
            .enabled(hasEditPermission())
            .visible(true)
            .build();

        JLMenuItem shareItem = JLMenuItem.builder()
            .id("share")
            .text("Share Location")
            .icon("share-icon.png")
            .enabled(isOnline())
            .visible(hasShareFeature())
            .build();

        JLMenuItem exportItem = JLMenuItem.builder()
            .id("export")
            .text("Export Data")
            .enabled(hasData())
            .build();

        // Add items
        contextMenu
            .addItem(editItem)
            .addItem(shareItem)
            .addItem(exportItem)
            .addItem("delete", "Delete Marker");

        // Handle selections with error handling
        contextMenu.setOnMenuItemListener(item -> {
            try {
                handleAdvancedMenuItem(marker, item);
            } catch (Exception e) {
                System.err.println("Error handling menu item: " + e.getMessage());
                showErrorDialog("Action failed: " + e.getMessage());
            }
        });

        // Update items dynamically
        updateMenuItemStates(contextMenu);
    }

    /**
     * Lifecycle example: Track menu open/close events
     */
    private static void menuLifecycleExample() {
        JLMarker marker = JLMarkerBuilder.create()
            .latLng(new JLLatLng(51.505, -0.09))
            .build();

        // Setup context menu
        JLContextMenu<JLMarker> contextMenu = marker.getContextMenu();
        contextMenu
            .addItem("action1", "Action 1")
            .addItem("action2", "Action 2");

        // Track lifecycle events
        marker.setOnActionListener((source, event) -> {
            if (event instanceof ContextMenuEvent contextEvent) {
                if (contextEvent.isOpen()) {
                    System.out.println("Context menu opened");
                    onMenuOpen(source);
                } else if (contextEvent.isClose()) {
                    System.out.println("Context menu closed");
                    onMenuClose(source);
                }
            }
        });

        // Handle item selections
        contextMenu.setOnMenuItemListener(item -> {
            System.out.println("Item selected: " + item.getText());
            executeAction(item.getId());
        });

        // Enable/disable menu based on conditions
        marker.setContextMenuEnabled(shouldEnableMenu());
    }

    // Helper methods

    private static void editMarker(JLMarker marker) {
        System.out.println("Editing marker at " + marker.getLatLng());
        // Implementation
    }

    private static void showMarkerInfo(JLMarker marker) {
        System.out.println("Marker info: " + marker.getLatLng());
        // Implementation
    }

    private static void deleteMarker(JLMarker marker) {
        System.out.println("Deleting marker");
        marker.remove();
    }

    private static void updatePolygonMenu(JLPolygon polygon) {
        JLContextMenu<JLPolygon> menu = polygon.getContextMenu();

        // Update edit item based on lock state
        JLMenuItem editItem = menu.getItem("edit-vertices").toBuilder()
            .enabled(!polygon.isLocked())
            .build();
        menu.updateItem(editItem);

        // Show/hide calculate area based on polygon validity
        JLMenuItem areaItem = menu.getItem("calculate-area").toBuilder()
            .visible(polygon.isValid())
            .build();
        menu.updateItem(areaItem);
    }

    private static void editPolygonVertices(JLPolygon polygon) {
        System.out.println("Editing polygon vertices");
        // Implementation
    }

    private static void changePolygonColor(JLPolygon polygon) {
        System.out.println("Changing polygon color");
        // Implementation
    }

    private static void calculateArea(JLPolygon polygon) {
        System.out.println("Calculating polygon area");
        // Implementation
    }

    private static void deletePolygon(JLPolygon polygon) {
        System.out.println("Deleting polygon");
        polygon.remove();
    }

    private static void handleAdvancedMenuItem(JLMarker marker, JLMenuItem item) {
        switch (item.getId()) {
            case "edit" -> editMarker(marker);
            case "share" -> shareLocation(marker);
            case "export" -> exportMarkerData(marker);
            case "delete" -> deleteMarker(marker);
        }
    }

    private static void shareLocation(JLMarker marker) {
        System.out.println("Sharing location: " + marker.getLatLng());
        // Implementation
    }

    private static void exportMarkerData(JLMarker marker) {
        System.out.println("Exporting marker data");
        // Implementation
    }

    private static void updateMenuItemStates(JLContextMenu<?> contextMenu) {
        // Update edit item
        JLMenuItem editItem = contextMenu.getItem("edit").toBuilder()
            .enabled(hasEditPermission())
            .build();
        contextMenu.updateItem(editItem);

        // Update share item
        JLMenuItem shareItem = contextMenu.getItem("share").toBuilder()
            .enabled(isOnline())
            .build();
        contextMenu.updateItem(shareItem);
    }

    private static void onMenuOpen(JLMarker marker) {
        System.out.println("Preparing menu for marker: " + marker.getId());
        // Refresh menu items, load dynamic data, etc.
    }

    private static void onMenuClose(JLMarker marker) {
        System.out.println("Cleaning up menu for marker: " + marker.getId());
        // Cleanup, release resources, etc.
    }

    private static void executeAction(String actionId) {
        System.out.println("Executing action: " + actionId);
        // Implementation
    }

    private static void showErrorDialog(String message) {
        System.err.println("Error: " + message);
        // Show UI error dialog
    }

    // State check methods
    private static boolean hasEditPermission() {
        return true; // Check actual permissions
    }

    private static boolean isOnline() {
        return true; // Check network status
    }

    private static boolean hasShareFeature() {
        return true; // Check feature flags
    }

    private static boolean hasData() {
        return true; // Check if data is available
    }

    private static boolean shouldEnableMenu() {
        return true; // Check application state
    }
}

Key Takeaways from Example

  1. Simple Setup: Creating context menus requires minimal code
  2. Flexible Configuration: Builder pattern for complex scenarios
  3. Event Handling: Both item selection and lifecycle events
  4. Dynamic Updates: Menu items can be updated based on state
  5. Error Handling: Proper exception management in handlers
  6. Type Safety: Generic types ensure compile-time safety

Extension Points

Creating a Custom Mediator

To add support for a new UI framework, implement the JLContextMenuMediator interface:

package your.package;

import io.github.makbn.jlmap.element.JLObject;
import io.github.makbn.jlmap.element.menu.JLContextMenu;
import io.github.makbn.jlmap.element.menu.JLContextMenuMediator;
import lombok.NonNull;

public class CustomFrameworkMediator implements JLContextMenuMediator {

    @Override
    public <T extends JLObject<T>> void registerContextMenu(
            @NonNull T object,
            @NonNull JLContextMenu<T> contextMenu) {
        // Create framework-specific context menu
        CustomFrameworkMenu menu = new CustomFrameworkMenu();

        // Populate menu items
        for (JLMenuItem item : contextMenu.getVisibleItems()) {
            CustomFrameworkMenuItem menuItem = menu.addItem(item.getText());
            menuItem.setIcon(item.getIcon());
            menuItem.setEnabled(item.isEnabled());

            // Handle item selection
            menuItem.setOnAction(() ->
                contextMenu.handleMenuItemSelection(item));
        }

        // Attach to framework component
        CustomFrameworkComponent component = getComponent(object);
        component.setContextMenu(menu);

        // Store for later updates
        contextMenuCache.put(object, menu);
    }

    @Override
    public <T extends JLObject<T>> void unregisterContextMenu(@NonNull T object) {
        CustomFrameworkMenu menu = contextMenuCache.remove(object);
        if (menu != null) {
            menu.dispose();
        }
    }

    @Override
    public <T extends JLObject<T>> void updateContextMenu(
            @NonNull T object,
            @NonNull JLContextMenu<T> contextMenu) {
        // Clear and rebuild menu
        unregisterContextMenu(object);
        registerContextMenu(object, contextMenu);
    }

    @Override
    public <T extends JLObject<T>> void showContextMenu(
            @NonNull T object,
            double x,
            double y) {
        CustomFrameworkMenu menu = contextMenuCache.get(object);
        if (menu != null) {
            menu.show(x, y);
        }
    }

    @Override
    public <T extends JLObject<T>> void hideContextMenu(@NonNull T object) {
        CustomFrameworkMenu menu = contextMenuCache.get(object);
        if (menu != null) {
            menu.hide();
        }
    }

    @Override
    public boolean supportsObjectType(@NonNull Class<? extends JLObject<?>> objectType) {
        // Return true for supported object types
        return true;
    }

    @Override
    public boolean isAvailable() {
        // Check if framework is available
        return CustomFramework.isInitialized();
    }

    @Override
    public int getPriority() {
        // Return priority (100+ for framework-specific)
        return 100;
    }

    @Override
    @NonNull
    public String getName() {
        return "CustomFramework";
    }

    private CustomFrameworkComponent getComponent(JLObject<?> object) {
        // Get or create framework-specific component
        return null; // Implementation specific
    }
}

Registering the Custom Mediator

Create a service provider configuration file:

File: META-INF/services/io.github.makbn.jlmap.element.menu.JLContextMenuMediator

Content:

your.package.CustomFrameworkMediator

Custom Event Types

Extend the event system for framework-specific events:

public class CustomContextMenuEvent implements Event {
    private final String customData;

    public CustomContextMenuEvent(String customData) {
        this.customData = customData;
    }

    @Override
    public JLAction action() {
        return JLAction.CUSTOM_CONTEXT_ACTION;
    }

    public String getCustomData() {
        return customData;
    }
}

// Use in your mediator
contextMenu.getOwner().notifyActionListener(
    new CustomContextMenuEvent("data")
);

Performance Monitoring

Add custom monitoring to track menu performance:

public class MonitoredContextMenuMediator implements JLContextMenuMediator {
    private final JLContextMenuMediator delegate;
    private final PerformanceMonitor monitor;

    @Override
    public <T extends JLObject<T>> void registerContextMenu(
            T object,
            JLContextMenu<T> contextMenu) {
        long start = System.nanoTime();
        try {
            delegate.registerContextMenu(object, contextMenu);
        } finally {
            long duration = System.nanoTime() - start;
            monitor.recordRegistration(duration);
        }
    }

    // Implement other methods with similar monitoring
}

Additional Resources

Related Documentation

Code Examples

  • Complete working examples: ContextMenuExample.java
  • Unit tests: JLContextMenuTest.java
  • Integration tests: ContextMenuIntegrationTest.java

Support

  • GitHub Issues: Report bugs and request features
  • Discussions: Ask questions and share ideas
  • Wiki: Additional guides and tutorials

Last Updated: 2025-10-03 Version: 2.0.0

Clone this wiki locally