feat(renderer): implement PopupMenu control and integrate CustomPopupMenu for enhanced UI interactions

This commit is contained in:
extio-de
2026-01-18 18:28:56 +01:00
parent 680dc99ba2
commit 7bc3807f63
5 changed files with 529 additions and 3 deletions

View File

@@ -23,6 +23,7 @@ import de.extio.game_engine.renderer.g2d.control.impl.G2DToggleButtonControlImpl
import de.extio.game_engine.renderer.g2d.control.impl.G2DTooltipControl;
import de.extio.game_engine.renderer.g2d.control.impl.G2DWindowCloseButtonControlImpl;
import de.extio.game_engine.renderer.g2d.control.impl.G2DWindowPanelControlImpl;
import de.extio.game_engine.renderer.g2d.control.impl.G2DPopupMenuControlImpl;
import de.extio.game_engine.renderer.model.RenderingBo;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
@@ -67,6 +68,8 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
private TableData tableData;
private PopupMenuData popupMenuData;
private SetFocusData setFocusData;
private Object customData;
@@ -143,6 +146,9 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
else if (data instanceof final TableData tableData) {
this.tableData = tableData;
}
else if (data instanceof final PopupMenuData popupMenuData) {
this.popupMenuData = popupMenuData;
}
else if (data instanceof final SetFocusData setFocusData) {
this.setFocusData = setFocusData;
}
@@ -241,6 +247,9 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
else if (this.clazz == TableControl.class) {
control = new G2DTableControlImpl();
}
else if (this.clazz == PopupMenuControl.class) {
control = new G2DPopupMenuControlImpl();
}
else if (this.clazz == SetFocusControl.class) {
control = new G2DSetFocusControl();
}
@@ -334,6 +343,9 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
else if (this.clazz == TableControl.class && this.tableData != null) {
((TableControl) control).setCustomData(this.tableData);
}
else if (this.clazz == PopupMenuControl.class && this.popupMenuData != null) {
((PopupMenuControl) control).setCustomData(this.popupMenuData);
}
else if (this.clazz == SetFocusControl.class && this.setFocusData != null) {
((SetFocusControl) control).setCustomData(this.setFocusData);
}
@@ -373,6 +385,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
this.textfieldData = o.textfieldData;
this.sliderData = o.sliderData;
this.tableData = o.tableData;
this.popupMenuData = o.popupMenuData;
this.setFocusData = o.setFocusData;
this.customData = o.customData;
this.visible = o.visible;
@@ -403,6 +416,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen
this.textfieldData = null;
this.sliderData = null;
this.tableData = null;
this.popupMenuData = null;
this.setFocusData = null;
this.customData = null;
this.visible = false;

View File

@@ -0,0 +1,294 @@
package de.extio.game_engine.renderer.g2d.control.components;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.List;
import java.util.Objects;
import de.extio.game_engine.renderer.ThemeManager;
import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawFont;
import de.extio.game_engine.renderer.g2d.theme.G2DThemeManager;
import de.extio.game_engine.renderer.g2d.theme.Theme;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.PopupMenuItem;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
@SuppressWarnings("serial")
public class CustomPopupMenu extends Component {
private static final int DEFAULT_PADDING = 6;
private List<PopupMenuItem> items = List.of();
private int fontSize;
private double scaleFactor;
private boolean enabled;
private Color backgroundColor;
private Color foregroundColor;
private Color selectionColor;
private int rowHeight;
private int padding = DEFAULT_PADDING;
private boolean dirty = true;
private int hoveredIndex = -1;
private CoordI2 lastMousePosition;
private String lastSelectedItemId;
private final ActionListener actionListener;
private final ThemeManager themeManager;
public CustomPopupMenu(final ActionListener listener, final ThemeManager themeManager) {
this.actionListener = listener;
this.themeManager = themeManager;
this.setIgnoreRepaint(true);
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent e) {
CustomPopupMenu.this.dirty = true;
}
@Override
public void mouseReleased(final MouseEvent e) {
if (!CustomPopupMenu.this.enabled) {
return;
}
final var index = CustomPopupMenu.this.getHoveredIndex();
if (index < 0 || index >= CustomPopupMenu.this.items.size()) {
return;
}
final var item = CustomPopupMenu.this.items.get(index);
if (item == null || !item.enabled()) {
return;
}
CustomPopupMenu.this.lastSelectedItemId = item.id();
if (CustomPopupMenu.this.actionListener != null) {
CustomPopupMenu.this.actionListener.actionPerformed(new ActionEvent(this, 0, item.id()));
}
CustomPopupMenu.this.dirty = true;
}
@Override
public void mouseEntered(final MouseEvent e) {
CustomPopupMenu.this.lastMousePosition = ImmutableCoordI2.create(e.getX(), e.getY());
CustomPopupMenu.this.updateHover(e.getY());
}
@Override
public void mouseExited(final MouseEvent e) {
CustomPopupMenu.this.lastMousePosition = null;
CustomPopupMenu.this.setHoveredIndex(-1);
}
});
this.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(final MouseEvent e) {
CustomPopupMenu.this.lastMousePosition = ImmutableCoordI2.create(e.getX(), e.getY());
CustomPopupMenu.this.updateHover(e.getY());
}
@Override
public void mouseDragged(final MouseEvent e) {
CustomPopupMenu.this.lastMousePosition = ImmutableCoordI2.create(e.getX(), e.getY());
CustomPopupMenu.this.updateHover(e.getY());
}
});
}
public void setItems(final List<PopupMenuItem> items) {
if (Objects.equals(this.items, items)) {
return;
}
this.items = items == null ? List.of() : List.copyOf(items);
this.setHoveredIndex(-1);
this.dirty = true;
}
public void setFontSize(final int fontSize) {
this.fontSize = fontSize;
}
public void setScaleFactor(final double scaleFactor) {
this.scaleFactor = scaleFactor;
}
@Override
public void setEnabled(final boolean enabled) {
if (!enabled) {
this.lastMousePosition = null;
this.setHoveredIndex(-1);
}
this.enabled = enabled;
}
public void setBackgroundColor(final Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setForegroundColor(final Color foregroundColor) {
this.foregroundColor = foregroundColor;
}
public void setSelectionColor(final Color selectionColor) {
this.selectionColor = selectionColor;
}
public void setRowHeight(final int rowHeight) {
this.rowHeight = rowHeight;
}
public void setPadding(final int padding) {
this.padding = Math.max(0, padding);
}
public String getLastSelectedItemId() {
return this.lastSelectedItemId;
}
public CoordI2 getLastMousePosition() {
return this.lastMousePosition;
}
public boolean isDirty() {
return this.dirty;
}
public void setDirty(final boolean dirty) {
this.dirty = dirty;
}
@Override
public void update(final Graphics g) {
}
@Override
public void paint(final Graphics g) {
if (this.themeManager == null) {
return;
}
final var g2d = (Graphics2D) g;
final Theme theme = this.themeManager.getCurrentTheme();
final var patternRenderer = ((G2DThemeManager) this.themeManager).getPatternRenderer(theme.getPatternRendererName());
final var background = this.backgroundColor != null ? this.backgroundColor : theme.getWindowBackground().toColor();
final var borderColor = theme.getBorderOuter().toColor();
if (patternRenderer != null) {
patternRenderer.drawDecorativeBorderFilled(g2d, 0, 0, this.getWidth(), this.getHeight(), 2, borderColor, background);
}
else {
g2d.setColor(background);
g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
}
final var selection = this.selectionColor != null ? this.selectionColor : theme.getSelectionPrimary().toColor();
final var normalText = this.foregroundColor != null ? this.foregroundColor : theme.getTextNormal().toColor();
final var disabledText = theme.getTextDisabled().toColor();
final var rowHeight = this.getEffectiveRowHeight(g2d);
final int paddedWidth = Math.max(1, this.getWidth() - (this.padding * 2));
for (int i = 0; i < this.items.size(); i++) {
final var item = this.items.get(i);
if (item == null) {
continue;
}
final var rowY = this.padding + i * rowHeight;
if (rowY > this.getHeight()) {
break;
}
if (i == this.hoveredIndex) {
g2d.setColor(selection);
g2d.fillRect(this.padding, rowY, paddedWidth, rowHeight);
}
if (!item.enabled() || !this.enabled) {
g2d.setColor(disabledText);
}
else if (i == this.hoveredIndex) {
g2d.setColor(theme.getTextNormal().adjustBrightness(theme.getHoverBrightnessAdjustment()).toColor());
}
else {
g2d.setColor(normalText);
}
final var text = item.label();
final var textDim = G2DDrawFont.getTextDimensions(text, g2d, this.fontSize, this.scaleFactor);
final int textX = this.padding;
final int textY = rowY + (rowHeight - textDim.getY()) / 2;
G2DDrawFont.renderText(g2d,
textDim,
this.scaleFactor,
(int) (textX / this.scaleFactor),
(int) (textY / this.scaleFactor),
this.fontSize,
text);
}
this.dirty = false;
}
private void updateHover(final int mouseY) {
if (!this.enabled) {
this.setHoveredIndex(-1);
return;
}
final var rowHeight = this.getEffectiveRowHeight(null);
if (rowHeight <= 0) {
this.setHoveredIndex(-1);
return;
}
final int index = (mouseY - this.padding) / rowHeight;
if (mouseY < this.padding || index < 0 || index >= this.items.size()) {
this.setHoveredIndex(-1);
}
else {
this.setHoveredIndex(index);
}
}
private int getHoveredIndex() {
return this.hoveredIndex;
}
private void setHoveredIndex(final int index) {
if (this.hoveredIndex != index) {
this.hoveredIndex = index;
this.dirty = true;
}
}
private int getEffectiveRowHeight(final Graphics2D g2d) {
if (this.rowHeight > 0) {
return this.rowHeight;
}
if (g2d == null) {
return (int) (Math.max(G2DDrawFont.FONT_SIZE_MIN, this.fontSize) * G2DDrawFont.FONT_LEADING);
}
final var textDim = G2DDrawFont.getTextDimensions("M", g2d, this.fontSize, 1.0);
return (int) (textDim.getY() * G2DDrawFont.FONT_LEADING);
}
}

View File

@@ -0,0 +1,151 @@
package de.extio.game_engine.renderer.g2d.control.impl;
import java.util.Objects;
import de.extio.game_engine.renderer.g2d.G2DRenderer;
import de.extio.game_engine.renderer.g2d.control.components.CustomPopupMenu;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.PopupMenuControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.PopupMenuData;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.model.color.RgbaColor;
public class G2DPopupMenuControlImpl extends G2DBaseControlImpl implements PopupMenuControl {
private CustomPopupMenu control;
private PopupMenuData popupMenuData;
private RgbaColor backgroundColor;
private RgbaColor foregroundColor;
private RgbaColor selectionColor;
private int rowHeight;
private int padding;
private String lastSelectedItemId;
@Override
public void setCustomData(final PopupMenuData data) {
this.modified |= !Objects.equals(this.popupMenuData, data);
this.popupMenuData = data;
if (data != null) {
this.setBackgroundColor(data.backgroundColor());
this.setForegroundColor(data.foregroundColor());
this.setSelectionColor(data.selectionColor());
this.setRowHeight(data.rowHeight());
this.setPadding(data.padding());
}
}
@Override
public void build() {
super.build();
this.createControl();
this.initControl();
final var mainFrame = ((G2DRenderer) this.rendererData.getRenderer()).getMainFrame();
mainFrame.add(this.control);
this.updateAllComponentZOrder();
}
@Override
public void performAction() {
if (this.enabled) {
this.rendererData.getEventService().fire(new UiControlEvent(this.id, this.lastSelectedItemId));
}
}
@Override
public void render() {
if (this.modified || this.control.isDirty()) {
this.initControl();
this.rebuildBufferedImage();
this.control.paint(this.bufferedImageGraphics);
this.control.setDirty(false);
this.updateAllComponentZOrder();
}
else if (this.positionModified) {
this.control.setLocation(this.x, this.y);
this.positionModified = false;
}
this.tooltipMousePosition = this.control.getLastMousePosition();
super.render();
}
@Override
public void close() {
if (this.control.isFocusOwner()) {
((G2DRenderer) this.rendererData.getRenderer()).getMainFrame().requestFocus();
}
this.control.setVisible(false);
this.control.invalidate();
((G2DRenderer) this.rendererData.getRenderer()).getMainFrame().remove(this.control);
super.close();
}
protected void createControl() {
this.control = new CustomPopupMenu(event -> {
this.lastSelectedItemId = this.control.getLastSelectedItemId();
if (this.enabled) {
this.performAction();
}
}, this.rendererData.getThemeManager());
this.control.setName(this.id);
}
protected void initControl() {
this.control.setLocation(this.x, this.y);
this.positionModified = false;
this.control.setSize(this.width, this.height);
this.control.setFontSize(this.fontSize);
this.control.setScaleFactor(this.scaleFactor);
this.control.setEnabled(this.enabled);
this.control.setVisible(this.visible);
this.control.setItems(this.popupMenuData != null ? this.popupMenuData.items() : null);
this.control.setBackgroundColor(this.backgroundColor != null ? this.backgroundColor.toAwtColor() : null);
this.control.setForegroundColor(this.foregroundColor != null ? this.foregroundColor.toAwtColor() : null);
this.control.setSelectionColor(this.selectionColor != null ? this.selectionColor.toAwtColor() : null);
if (this.rowHeight > 0) {
this.control.setRowHeight((int) (this.rowHeight * this.scaleFactor));
}
else {
this.control.setRowHeight(0);
}
if (this.padding > 0) {
this.control.setPadding((int) (this.padding * this.scaleFactor));
}
else {
this.control.setPadding(0);
}
}
private void setBackgroundColor(final RgbaColor backgroundColor) {
this.modified |= !Objects.equals(this.backgroundColor, backgroundColor);
this.backgroundColor = backgroundColor;
}
private void setForegroundColor(final RgbaColor foregroundColor) {
this.modified |= !Objects.equals(this.foregroundColor, foregroundColor);
this.foregroundColor = foregroundColor;
}
private void setSelectionColor(final RgbaColor selectionColor) {
this.modified |= !Objects.equals(this.selectionColor, selectionColor);
this.selectionColor = selectionColor;
}
private void setRowHeight(final int rowHeight) {
this.modified |= this.rowHeight != rowHeight;
this.rowHeight = rowHeight;
}
private void setPadding(final int padding) {
this.modified |= this.padding != padding;
this.padding = padding;
}
}

View File

@@ -30,6 +30,10 @@ public interface ControlRenderingBo extends RenderingBoHasDimension {
record TableData(List<Object> data, int rows, long version, boolean firstColDoubleSize) {}
record PopupMenuItem(String id, String label, boolean enabled) {}
record PopupMenuData(List<PopupMenuItem> items, RgbaColor backgroundColor, RgbaColor foregroundColor, RgbaColor selectionColor, int rowHeight, int padding) {}
record SetFocusData(String focusId) {}
/**
@@ -177,6 +181,12 @@ public interface ControlRenderingBo extends RenderingBoHasDimension {
}
public static interface PopupMenuControl extends BaseControl {
void setCustomData(PopupMenuData data);
}
/**
* Sets focus on a certain control
*/