Skip to content

Commit ba80090

Browse files
committed
Implement context menu functionality for markers in JLMap, enhancing user interaction with options to delete and show info.
Signed-off-by: makbn <[email protected]>
1 parent b916576 commit ba80090

File tree

5 files changed

+157
-11
lines changed

5 files changed

+157
-11
lines changed

jlmap-fx/src/main/java/io/github/makbn/jlmap/fx/JLMapView.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import javafx.util.Duration;
3434
import lombok.AccessLevel;
3535
import lombok.Builder;
36+
import lombok.Getter;
3637
import lombok.NonNull;
3738
import lombok.experimental.FieldDefaults;
3839
import lombok.experimental.NonFinal;
@@ -58,6 +59,7 @@
5859
public class JLMapView extends AnchorPane implements JLMap<Object> {
5960
JLMapOption mapOption;
6061
JLWebEngine<Object> jlWebEngine;
62+
@Getter
6163
WebView webView;
6264
JLMapEventHandler jlMapCallbackHandler;
6365
HashMap<Class<? extends LeafletLayer>, LeafletLayer> layers;

jlmap-fx/src/main/java/io/github/makbn/jlmap/fx/demo/LeafletTestJFX.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,35 @@ private void loadDemoElement(JLMapView map) {
8484
addPolyline(map);
8585
addPolygon(map);
8686

87-
map.getUiLayer()
87+
// Add marker with context menu
88+
JLMarker calgaryMarker = map.getUiLayer()
8889
.addMarker(JLLatLng.builder()
89-
.lat(35.63)
90-
.lng(51.45)
91-
.build(), "Tehran", true)
92-
.setOnActionListener(getListener());
90+
.lat(51.0447)
91+
.lng(-114.0719)
92+
.build(), "Calgary", true);
93+
94+
// Add context menu to the marker
95+
calgaryMarker.addContextMenu()
96+
.addItem("delete", "Delete Marker", "https://img.icons8.com/material-outlined/24/000000/trash--v1.png")
97+
.addItem("info", "Show Info", "https://img.icons8.com/material-outlined/24/000000/info--v1.png")
98+
.setOnMenuItemListener(item -> {
99+
log.info("Context menu item selected: {}", item.getText());
100+
switch (item.getId()) {
101+
case "delete" -> calgaryMarker.remove();
102+
case "info" -> map.getUiLayer().addPopup(
103+
JLLatLng.builder().lat(51.0447).lng(-114.0719).build(),
104+
"Calgary - AB",
105+
JLOptions.builder().autoClose(true).build()
106+
);
107+
}
108+
});
109+
110+
calgaryMarker.setOnActionListener(getListener());
93111

94112
map.getVectorLayer()
95113
.addCircleMarker(JLLatLng.builder()
96-
.lat(35.63)
97-
.lng(40.45)
114+
.lat(51.0447)
115+
.lng(-114.0719)
98116
.build());
99117

100118
map.getVectorLayer()

jlmap-fx/src/main/java/io/github/makbn/jlmap/fx/element/menu/JavaFXContextMenuMediator.java

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22

33
import io.github.makbn.jlmap.JLMap;
44
import io.github.makbn.jlmap.element.menu.JLContextMenuMediator;
5+
import io.github.makbn.jlmap.element.menu.JLHasContextMenu;
6+
import io.github.makbn.jlmap.fx.JLMapView;
57
import io.github.makbn.jlmap.model.JLObject;
8+
import javafx.scene.control.ContextMenu;
9+
import javafx.scene.control.MenuItem;
10+
import javafx.scene.image.Image;
11+
import javafx.scene.image.ImageView;
612
import lombok.NonNull;
13+
import lombok.experimental.FieldDefaults;
714
import lombok.extern.slf4j.Slf4j;
815

16+
import java.util.Objects;
17+
918
/**
1019
* JavaFX-specific implementation of the context menu mediator.
1120
* <p>
@@ -35,22 +44,129 @@
3544
* @since 2.0.0
3645
*/
3746
@Slf4j
47+
@FieldDefaults(level = lombok.AccessLevel.PRIVATE)
3848
public class JavaFXContextMenuMediator implements JLContextMenuMediator {
49+
public static final double ICON_SIZE = 14.0;
50+
ContextMenu universalContextMenu;
51+
boolean mouseHandlerRegistered = false;
52+
long lastMenuShowTime = 0;
3953

4054
/**
4155
* {@inheritDoc}
4256
*/
4357
@Override
44-
public <T extends JLObject<T>> void showContextMenu(@NonNull JLMap<?> map, @NonNull JLObject<T> object, double x, double y) {
58+
public synchronized <T extends JLObject<T>> void showContextMenu(@NonNull JLMap<?> map, @NonNull JLObject<T> object, double x, double y) {
59+
log.debug("Showing context menu for object: {} at ({}, {})", object.getJLId(), x, y);
60+
ensureContextMenuInitialized();
61+
62+
if (!(object instanceof JLHasContextMenu<?> objectWithContextMenu)) {
63+
return;
64+
}
65+
66+
logContextMenuDebugInfo(objectWithContextMenu);
67+
68+
if (shouldShowContextMenu(objectWithContextMenu)) {
69+
populateMenuItems(objectWithContextMenu);
70+
displayContextMenu(map, x, y);
71+
}
72+
}
73+
74+
private void ensureContextMenuInitialized() {
75+
if (universalContextMenu == null) {
76+
universalContextMenu = new ContextMenu();
77+
}
78+
}
79+
80+
private void logContextMenuDebugInfo(JLHasContextMenu<?> objectWithContextMenu) {
81+
log.debug("Object is JLHasContextMenu: true");
82+
log.debug("Has context menu: {}, Enabled: {}", objectWithContextMenu.hasContextMenu(), objectWithContextMenu.isContextMenuEnabled());
83+
log.debug("Context menu object: {}", objectWithContextMenu.getContextMenu());
84+
if (objectWithContextMenu.getContextMenu() != null) {
85+
log.debug("Context menu items count: {}", objectWithContextMenu.getContextMenu().getItemCount());
86+
}
87+
}
88+
89+
private boolean shouldShowContextMenu(JLHasContextMenu<?> objectWithContextMenu) {
90+
return objectWithContextMenu.hasContextMenu() && objectWithContextMenu.isContextMenuEnabled();
91+
}
92+
93+
private void populateMenuItems(JLHasContextMenu<?> objectWithContextMenu) {
94+
universalContextMenu.getItems().clear();
95+
Objects.requireNonNull(objectWithContextMenu.getContextMenu()).getItems().forEach(item -> {
96+
log.debug("Adding menu item: {}", item.getText());
97+
MenuItem menuItem = createMenuItem(item, objectWithContextMenu);
98+
universalContextMenu.getItems().add(menuItem);
99+
});
100+
}
45101

102+
private MenuItem createMenuItem(io.github.makbn.jlmap.element.menu.JLMenuItem item, JLHasContextMenu<?> objectWithContextMenu) {
103+
MenuItem menuItem = new MenuItem(item.getText());
104+
menuItem.setOnAction(e ->
105+
Objects.requireNonNull(objectWithContextMenu.getContextMenu()).getOnMenuItemListener().onMenuItemSelected(item));
106+
if (item.getIcon() != null && !item.getIcon().isBlank()) {
107+
menuItem.setGraphic(createIcon(item.getIcon()));
108+
}
109+
return menuItem;
110+
}
111+
112+
private void displayContextMenu(@NonNull JLMap<?> map, double x, double y) {
113+
if (!(map instanceof JLMapView jlMapView)) {
114+
log.warn("Map is not a JLMapView: {}", map.getClass());
115+
return;
116+
}
117+
118+
log.debug("Showing context menu with {} items at WebView-relative position ({}, {})",
119+
universalContextMenu.getItems().size(), x, y);
120+
121+
registerClickHandlerIfNeeded(jlMapView);
122+
showMenuAtPosition(jlMapView, x, y);
123+
}
124+
125+
private void registerClickHandlerIfNeeded(JLMapView jlMapView) {
126+
if (mouseHandlerRegistered) {
127+
return;
128+
}
129+
130+
jlMapView.getWebView().setOnMouseClicked(event -> {
131+
long timeSinceShow = System.currentTimeMillis() - lastMenuShowTime;
132+
if (universalContextMenu != null && universalContextMenu.isShowing() && timeSinceShow > 100) {
133+
log.debug("Hiding context menu due to WebView click (time since show: {}ms)", timeSinceShow);
134+
universalContextMenu.hide();
135+
}
136+
});
137+
mouseHandlerRegistered = true;
138+
}
139+
140+
private void showMenuAtPosition(JLMapView jlMapView, double x, double y) {
141+
javafx.geometry.Point2D screenCoords = jlMapView.getWebView().localToScreen(x, y);
142+
if (screenCoords != null) {
143+
log.debug("Screen coordinates: ({}, {})", screenCoords.getX(), screenCoords.getY());
144+
lastMenuShowTime = System.currentTimeMillis();
145+
universalContextMenu.show(jlMapView.getWebView(), screenCoords.getX(), screenCoords.getY());
146+
} else {
147+
log.warn("Could not convert coordinates to screen space");
148+
}
149+
}
150+
151+
private ImageView createIcon(String url) {
152+
Image image = new Image(url);
153+
ImageView imageView = new ImageView(image);
154+
imageView.setFitWidth(ICON_SIZE);
155+
imageView.setFitHeight(ICON_SIZE);
156+
imageView.setPreserveRatio(true);
157+
return imageView;
46158
}
47159

48160
/**
49161
* {@inheritDoc}
50162
*/
51163
@Override
52-
public <T extends JLObject<T>> void hideContextMenu(@NonNull JLMap<?> map, @NonNull JLObject<T> object) {
53-
164+
public synchronized <T extends JLObject<T>> void hideContextMenu(@NonNull JLMap<?> map, @NonNull JLObject<T> object) {
165+
log.debug("Hiding context menu for object: {}", object.getJLId());
166+
if (universalContextMenu != null) {
167+
universalContextMenu.hide();
168+
universalContextMenu.getItems().clear();
169+
}
54170
}
55171

56172
/**

jlmap-fx/src/main/java/io/github/makbn/jlmap/fx/layer/JLLayer.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import lombok.experimental.FieldDefaults;
1313
import org.jetbrains.annotations.NotNull;
1414

15+
import java.util.Arrays;
1516
import java.util.UUID;
1617
import java.util.function.Function;
18+
import java.util.stream.Collectors;
1719

1820
/**
1921
* Represents the basic layer.
@@ -48,7 +50,12 @@ protected final String removeLayerWithUUID(@NonNull String uuid) {
4850
return new JLJavaFxServerToClientTransporter() {
4951
@Override
5052
public Function<JLTransportRequest, Object> serverToClientTransport() {
51-
return transport -> engine.executeScript(transport.function());
53+
return transport -> {
54+
// Generate JavaScript method call: this.objectId.methodName(param1,param2,...)
55+
String script = "this.%1$s.%2$s(%3$s)".formatted(transport.self().getJLId(), transport.function(),
56+
transport.params().length > 0 ? Arrays.stream(transport.params()).map(String::valueOf).collect(Collectors.joining(",")) : "");
57+
return engine.executeScript(script);
58+
};
5259
}
5360
};
5461
}

jlmap-fx/src/main/java/io/github/makbn/jlmap/fx/layer/JLUiLayer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public JLMarker addMarker(JLLatLng latLng, String text, boolean draggable) {
5555
jlCallbackBuilder.on(JLAction.REMOVE);
5656
jlCallbackBuilder.on(JLAction.CLICK);
5757
jlCallbackBuilder.on(JLAction.DOUBLE_CLICK);
58+
jlCallbackBuilder.on(JLAction.CONTEXT_MENU);
5859
})
5960
.withOptions(JLOptions.DEFAULT.toBuilder().draggable(draggable).build());
6061

@@ -98,6 +99,7 @@ public JLPopup addPopup(JLLatLng latLng, String text, JLOptions options) {
9899
.withCallbacks(jlCallbackBuilder -> {
99100
jlCallbackBuilder.on(JLAction.CLICK);
100101
jlCallbackBuilder.on(JLAction.DOUBLE_CLICK);
102+
jlCallbackBuilder.on(JLAction.CONTEXT_MENU);
101103
jlCallbackBuilder.on(JLAction.ADD);
102104
jlCallbackBuilder.on(JLAction.REMOVE);
103105
})
@@ -153,6 +155,7 @@ public JLImageOverlay addImage(JLBounds bounds, String imageUrl, JLOptions optio
153155
.withCallbacks(jlCallbackBuilder -> {
154156
jlCallbackBuilder.on(JLAction.CLICK);
155157
jlCallbackBuilder.on(JLAction.DOUBLE_CLICK);
158+
jlCallbackBuilder.on(JLAction.CONTEXT_MENU);
156159
jlCallbackBuilder.on(JLAction.ADD);
157160
jlCallbackBuilder.on(JLAction.REMOVE);
158161
})

0 commit comments

Comments
 (0)