Added OptionsModule

This commit is contained in:
extio-de
2026-04-03 13:32:54 +02:00
parent 694f47600d
commit 7d140919f5
16 changed files with 2607 additions and 3 deletions

View File

@@ -1140,13 +1140,29 @@ None (utility classes)
### Menu System ### Menu System
#### Purpose and Overview #### Purpose and Overview
Framework for building in-game menus and UI screens as modules. Provides base components for menu rendering and interaction handling. Framework for building in-game menus and UI screens as modules. Provides reusable menu infrastructure, including a generic options dialog with auto-discovered tabs, a theme editor, and an HSB color picker dialog.
#### Setup / Autoconfiguration #### Setup / Autoconfiguration
Integrated with the module system. Menu components are implemented as `AbstractClientModule` instances. **Configuration Class**: `MenuAutoConfiguration`
**Properties**:
- `game-engine.menu.enabled` (default: `true`)
- `game-engine.menu.options.enabled` (default: `true`)
- `game-engine.menu.theme-editor.enabled` (default: `true`)
- `game-engine.menu.hsb-color-picker.enabled` (default: `true`)
The menu subsystem is auto-configured through explicit bean definitions, matching the rest of the engine. The options dialog auto-discovers all `OptionsTab` beans, so applications can contribute additional tabs from their own codebase without modifying the library. The theme editor implements `OptionsThemeEditorSupport`, and the reusable themes tab will use it when present.
`ThemeEditorModule` depends on `HsbColorPickerModule`, so disabling `game-engine.menu.hsb-color-picker.enabled` also prevents theme editor registration.
#### Exposed Spring Beans #### Exposed Spring Beans
None (menu implementations are modules) - `OptionsModule` when `game-engine.menu.options.enabled=true`
- `OptionsAudioTab`, `OptionsVideoTab`, `OptionsKeyboardTab`, and `OptionsThemesTab` when `game-engine.menu.options.enabled=true`
- `ThemeEditorModule` when `game-engine.menu.theme-editor.enabled=true`
- `HsbColorPickerModule` when `game-engine.menu.hsb-color-picker.enabled=true`
#### Integration Notes
All menu implementations are still regular `AbstractClientModule` modules at runtime. The auto-configuration only controls bean registration.
--- ---

View File

@@ -0,0 +1,105 @@
package de.extio.game_engine.menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import de.extio.game_engine.audio.AudioControl;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.i18n.LocalizationService;
import de.extio.game_engine.keyboard.KeycodeRegistry;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.ThemeManager;
import de.extio.game_engine.renderer.container.ScrollArea;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.storage.StorageService;
public abstract class AbstractOptionsTab {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private RenderingBoPool renderingBoPool;
@Autowired
private EventService eventService;
@Autowired
private LocalizationService localizationService;
@Autowired
private RendererControl rendererControl;
@Autowired
private AudioControl audioControl;
@Autowired
private ThemeManager themeManager;
@Autowired
private StorageService storageService;
@Autowired
private KeycodeRegistry keycodeRegistry;
protected final ApplicationContext applicationContext() {
return this.applicationContext;
}
protected final RenderingBoPool renderingBoPool() {
return this.renderingBoPool;
}
protected final EventService eventService() {
return this.eventService;
}
protected final LocalizationService localizationService() {
return this.localizationService;
}
protected final RendererControl rendererControl() {
return this.rendererControl;
}
protected final AudioControl audioControl() {
return this.audioControl;
}
protected final ThemeManager themeManager() {
return this.themeManager;
}
protected final StorageService storageService() {
return this.storageService;
}
protected final KeycodeRegistry keycodeRegistry() {
return this.keycodeRegistry;
}
protected final Window optionsWindow(final OptionsModuleContext context) {
return context.optionsWindow();
}
protected final ScrollArea contentScrollArea(final OptionsModuleContext context) {
return context.contentScrollArea();
}
protected final void refreshContent(final OptionsModuleContext context) {
context.refreshContent();
}
protected final void rebuildWindow(final OptionsModuleContext context) {
context.rebuildWindow();
}
protected final int contentWidth(final OptionsModuleContext context) {
return context.contentWidth();
}
protected static String sanitizeId(final String input) {
return input.replaceAll("[^a-zA-Z0-9]", "_");
}
}

View File

@@ -0,0 +1,314 @@
package de.extio.game_engine.menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import de.extio.game_engine.event.Event;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.module.AbstractClientModule;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderData;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.color.HSBColor;
import de.extio.game_engine.renderer.model.color.ImmutableRgbaColor;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
public class HsbColorPickerModule extends AbstractClientModule {
public record HsbColorPickerResponseEvent(String requestId, boolean confirmed, HSBColor color) implements Event {
}
private static final int WINDOW_WIDTH = 700;
private static final int WINDOW_HEIGHT = 520;
private static final int LABEL_HEIGHT = 34;
private static final int SLIDER_HEIGHT = 34;
private static final int BUTTON_HEIGHT = 40;
private static final int BUTTON_WIDTH = 110;
private static final int SPACING = 12;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private RenderingBoPool renderingBoPool;
@Autowired
private EventService eventService;
private Window dialogWindow;
private String requestId;
private String title;
private float hue;
private float saturation;
private float brightness;
@Override
public void onLoad() {
this.dialogWindow = this.applicationContext.getBean(Window.class);
this.dialogWindow.setNormalizedDimension(ImmutableCoordI2.create(WINDOW_WIDTH, WINDOW_HEIGHT));
this.dialogWindow.setNormalizedPosition(this.centeredPosition(this.dialogWindow.getNormalizedDimension()));
this.dialogWindow.setDraggable(true);
this.dialogWindow.setCloseButton(true);
this.dialogWindow.setOnCloseAction(this::onCancel);
}
@Override
public void onUnload() {
this.getModuleService().unloadModule(this.dialogWindow.getId());
}
@Override
public void onActivate() {
this.setupDialog();
this.getModuleService().changeActiveState(this.dialogWindow.getId(), true);
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
}
@Override
public void onDeactivate() {
this.eventService.unregisterAll(this.getId());
this.getModuleService().changeActiveState(this.dialogWindow.getId(), false);
}
@Override
public void onShow() {
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), true);
}
@Override
public void onHide() {
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), false);
}
public void open(final String requestId, final String title, final HSBColor initialColor, final Window parentWindow) {
this.requestId = requestId;
this.title = title;
if (initialColor != null) {
this.hue = initialColor.getHue();
this.saturation = initialColor.getSaturation();
this.brightness = initialColor.getBrightness();
}
else {
this.hue = 0.0f;
this.saturation = 0.0f;
this.brightness = 0.0f;
}
this.dialogWindow.setParent(parentWindow);
this.getModuleService().changeActiveState(this.getId(), true);
this.getModuleService().changeDisplayState(this.getId(), true);
}
private void onUiControlEvent(final UiControlEvent event) {
if (event.getId() == null) {
return;
}
switch (event.getId()) {
case "HsbColorPicker_Button_Ok" -> this.onOk();
case "HsbColorPicker_Button_Cancel" -> this.onCancel();
case "HsbColorPicker_Slider_Hue" -> this.updateSliderValue(event, SliderTarget.HUE);
case "HsbColorPicker_Slider_Saturation" -> this.updateSliderValue(event, SliderTarget.SATURATION);
case "HsbColorPicker_Slider_Brightness" -> this.updateSliderValue(event, SliderTarget.BRIGHTNESS);
}
}
private void updateSliderValue(final UiControlEvent event, final SliderTarget target) {
if (!(event.getPayload() instanceof final Double rawValue)) {
return;
}
final float value = Math.max(0.0f, Math.min(1.0f, rawValue.floatValue()));
switch (target) {
case HUE -> this.hue = value;
case SATURATION -> this.saturation = value;
case BRIGHTNESS -> this.brightness = value;
}
this.refreshPreview();
}
private void setupDialog() {
this.dialogWindow.clearRenderingBos();
final var contentWidth = WINDOW_WIDTH - Window.MARGIN_LEFT - Window.MARGIN_RIGHT;
var yOffset = Window.MARGIN_TOP;
if (this.title != null && !this.title.isBlank()) {
final var titleBo = this.renderingBoPool.acquire("HsbColorPicker_Title", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.title)
.setFontSize(28)
.setControlData(new LabelData(null, null, HorizontalAlignment.CENTER))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(titleBo);
yOffset += LABEL_HEIGHT + SPACING;
}
final var preview = this.renderingBoPool.acquire("HsbColorPicker_Preview", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(" ")
.setFontSize(20)
.setControlData(new LabelData(this.toRgbaColor(this.getCurrentColor()), null, HorizontalAlignment.CENTER))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, 80)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(preview);
yOffset += 80 + SPACING * 2;
yOffset = this.addSliderRow("Hue", "HsbColorPicker_Slider_Hue", "HsbColorPicker_Value_Hue", this.hue, yOffset, contentWidth);
yOffset = this.addSliderRow("Saturation", "HsbColorPicker_Slider_Saturation", "HsbColorPicker_Value_Saturation", this.saturation, yOffset, contentWidth);
yOffset = this.addSliderRow("Brightness", "HsbColorPicker_Slider_Brightness", "HsbColorPicker_Value_Brightness", this.brightness, yOffset, contentWidth);
yOffset += SPACING * 2;
final var buttonY = yOffset;
final var okButton = this.renderingBoPool.acquire("HsbColorPicker_Button_Ok", ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption("OK")
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT + contentWidth - (BUTTON_WIDTH * 2 + SPACING), buttonY)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(okButton);
final var cancelButton = this.renderingBoPool.acquire("HsbColorPicker_Button_Cancel", ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption("Cancel")
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT + contentWidth - BUTTON_WIDTH, buttonY)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(cancelButton);
this.dialogWindow.draw();
}
private int addSliderRow(final String label, final String sliderId, final String valueId, final float value, final int yOffset, final int contentWidth) {
final var labelWidth = 160;
final var valueWidth = 100;
final var sliderWidth = contentWidth - labelWidth - valueWidth - SPACING * 2;
var bo = this.renderingBoPool.acquire("HsbColorPicker_Label_" + label, ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(label)
.setFontSize(24)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(labelWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(sliderId, ControlRenderingBo.class)
.setType(SliderControl.class)
.setControlData(new SliderData(true, value, value, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(sliderWidth, SLIDER_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT + labelWidth + SPACING, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(valueId, ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.formatValue(value))
.setFontSize(22)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(valueWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT + labelWidth + SPACING + sliderWidth + SPACING, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(bo);
return yOffset + LABEL_HEIGHT + SPACING;
}
private void refreshPreview() {
final var preview = this.dialogWindow.getRenderingBo("HsbColorPicker_Preview", ControlRenderingBo.class);
if (preview != null) {
preview.setControlData(new LabelData(this.toRgbaColor(this.getCurrentColor()), null, HorizontalAlignment.CENTER));
this.dialogWindow.putRenderingBo(preview);
}
this.updateValueLabel("HsbColorPicker_Value_Hue", this.hue);
this.updateValueLabel("HsbColorPicker_Value_Saturation", this.saturation);
this.updateValueLabel("HsbColorPicker_Value_Brightness", this.brightness);
this.dialogWindow.draw();
}
private void updateValueLabel(final String id, final float value) {
final var bo = this.dialogWindow.getRenderingBo(id, ControlRenderingBo.class);
if (bo != null) {
bo.setCaption(this.formatValue(value));
this.dialogWindow.putRenderingBo(bo);
}
}
private String formatValue(final float value) {
return String.format("%.2f", value);
}
private HSBColor getCurrentColor() {
return new HSBColor(this.hue, this.saturation, this.brightness);
}
private ImmutableRgbaColor toRgbaColor(final HSBColor color) {
return new ImmutableRgbaColor(color.toColor());
}
private void onOk() {
this.eventService.fire(new HsbColorPickerResponseEvent(this.requestId, true, this.getCurrentColor()));
this.close();
}
private void onCancel() {
this.eventService.fire(new HsbColorPickerResponseEvent(this.requestId, false, null));
this.close();
}
private void close() {
this.getModuleService().changeActiveState(this.getId(), false);
this.getModuleService().changeDisplayState(this.getId(), false);
}
private ImmutableCoordI2 centeredPosition(final de.extio.game_engine.spatial2.model.CoordI2 dimension) {
final var referenceResolution = RendererControl.REFERENCE_RESOLUTION;
final var x = (referenceResolution.getX() - dimension.getX()) / 2;
final var y = (referenceResolution.getY() - dimension.getY()) / 2;
return ImmutableCoordI2.create(x, y);
}
private enum SliderTarget {
HUE,
SATURATION,
BRIGHTNESS
}
}

View File

@@ -0,0 +1,67 @@
package de.extio.game_engine.menu;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.i18n.LocalizationService;
import de.extio.game_engine.renderer.work.RenderingBoPool;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.menu.enabled", havingValue = "true", matchIfMissing = true)
public class MenuAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.options.enabled", havingValue = "true", matchIfMissing = true)
OptionsModule optionsModule(final ApplicationContext applicationContext, final RenderingBoPool renderingBoPool, final EventService eventService,
final LocalizationService localizationService) {
return new OptionsModule(applicationContext, renderingBoPool, eventService, localizationService);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.options.enabled", havingValue = "true", matchIfMissing = true)
OptionsAudioTab optionsAudioTab() {
return new OptionsAudioTab();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.options.enabled", havingValue = "true", matchIfMissing = true)
OptionsVideoTab optionsVideoTab() {
return new OptionsVideoTab();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.options.enabled", havingValue = "true", matchIfMissing = true)
OptionsKeyboardTab optionsKeyboardTab() {
return new OptionsKeyboardTab();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.options.enabled", havingValue = "true", matchIfMissing = true)
OptionsThemesTab optionsThemesTab() {
return new OptionsThemesTab();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.hsb-color-picker.enabled", havingValue = "true", matchIfMissing = true)
HsbColorPickerModule hsbColorPickerModule() {
return new HsbColorPickerModule();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "game-engine.menu.theme-editor.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(name = "game-engine.menu.hsb-color-picker.enabled", havingValue = "true", matchIfMissing = true)
ThemeEditorModule themeEditorModule() {
return new ThemeEditorModule();
}
}

View File

@@ -0,0 +1,7 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.event.Event;
public record OpenOptionsModuleEvent(String tab) implements Event {
}

View File

@@ -0,0 +1,208 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SwitchControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SwitchData;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
final class OptionsAudioTab extends AbstractOptionsTab implements OptionsTab {
static final String TAB_ID = "audio";
private static final String CONTROL_ID = "OptionsModule_Tab_Audio";
@Override
public String tabId() {
return TAB_ID;
}
@Override
public String controlId() {
return CONTROL_ID;
}
@Override
public String navigationLocalizationKey() {
return "ecyoa-85";
}
@Override
public String fallbackCaption() {
return "Audio";
}
@Override
public int navigationOrder() {
return 40;
}
@Override
public void render(final OptionsModuleContext context) {
var yPos = 10;
var bo = this.renderingBoPool().acquire("OptionsModule_Audio_Label_SFX", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-97", "Sound Effects:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 60;
final var sfxEnabled = this.audioControl().getAudioOptions().getSfxOptions().isEnable();
bo = this.renderingBoPool().acquire("OptionsModule_Audio_SFX_Enable", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-98", "Enable"))
.setFontSize(42)
.setType(SwitchControl.class)
.setControlData(new SwitchData(sfxEnabled, false, null, true))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(620, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 80;
final var sfxVolume = this.audioControl().getAudioOptions().getSfxOptions().getVolume();
bo = this.renderingBoPool().acquire("OptionsModule_Audio_Label_SFX_Volume", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-99", "Volume:"))
.setFontSize(42)
.setType(LabelControl.class)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(200, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
bo = this.renderingBoPool().acquire("OptionsModule_Audio_SFX_Volume_Slider", ControlRenderingBo.class)
.setCaption(String.format("%.2f", sfxVolume))
.setFontSize(42)
.setType(SliderControl.class)
.setControlData(new SliderData(true, sfxVolume, 0.0, null))
.setVisible(true)
.setEnabled(sfxEnabled)
.withDimensionAbsolute(400, 60)
.withPositionRelative(220, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 70;
final var sfxSoftwareMixing = this.audioControl().getAudioOptions().getSfxOptions().isSoftwareMixing();
bo = this.renderingBoPool().acquire("OptionsModule_Audio_SFX_SoftwareMixing", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-100", "Software Mixing"))
.setFontSize(42)
.setType(SwitchControl.class)
.setControlData(new SwitchData(sfxSoftwareMixing, false, null, true))
.setVisible(true)
.setEnabled(sfxEnabled)
.withDimensionAbsolute(620, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 110;
bo = this.renderingBoPool().acquire("OptionsModule_Audio_Label_Music", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-101", "Music:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 60;
final var musicEnabled = this.audioControl().getAudioOptions().getMusicOptions().isEnable();
bo = this.renderingBoPool().acquire("OptionsModule_Audio_Music_Enable", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-98", "Enable"))
.setFontSize(42)
.setType(SwitchControl.class)
.setControlData(new SwitchData(musicEnabled, false, null, true))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(620, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 80;
final var musicVolume = this.audioControl().getAudioOptions().getMusicOptions().getVolume();
bo = this.renderingBoPool().acquire("OptionsModule_Audio_Label_Music_Volume", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-99", "Volume:"))
.setFontSize(42)
.setType(LabelControl.class)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(200, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
bo = this.renderingBoPool().acquire("OptionsModule_Audio_Music_Volume_Slider", ControlRenderingBo.class)
.setCaption(String.format("%.2f", musicVolume))
.setFontSize(42)
.setType(SliderControl.class)
.setControlData(new SliderData(true, musicVolume, 0.0, null))
.setVisible(true)
.setEnabled(musicEnabled)
.withDimensionAbsolute(400, 60)
.withPositionRelative(220, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
}
@Override
public boolean handle(final UiControlEvent event, final OptionsModuleContext context) {
final var controlId = event.getId();
if (controlId == null) {
return false;
}
switch (controlId) {
case "OptionsModule_Audio_SFX_Enable" -> {
final var currentValue = this.audioControl().getAudioOptions().getSfxOptions().isEnable();
this.audioControl().getAudioOptions().getSfxOptions().setEnable(!currentValue);
this.audioControl().applyAudioOptions(this.audioControl().getAudioOptions());
this.refreshContent(context);
return true;
}
case "OptionsModule_Audio_Music_Enable" -> {
final var currentValue = this.audioControl().getAudioOptions().getMusicOptions().isEnable();
this.audioControl().getAudioOptions().getMusicOptions().setEnable(!currentValue);
this.audioControl().applyAudioOptions(this.audioControl().getAudioOptions());
this.refreshContent(context);
return true;
}
case "OptionsModule_Audio_SFX_SoftwareMixing" -> {
final var currentValue = this.audioControl().getAudioOptions().getSfxOptions().isSoftwareMixing();
this.audioControl().getAudioOptions().getSfxOptions().setSoftwareMixing(!currentValue);
this.audioControl().applyAudioOptions(this.audioControl().getAudioOptions());
this.refreshContent(context);
return true;
}
case "OptionsModule_Audio_Music_SoftwareMixing" -> {
final var currentValue = this.audioControl().getAudioOptions().getMusicOptions().isSoftwareMixing();
this.audioControl().getAudioOptions().getMusicOptions().setSoftwareMixing(!currentValue);
this.audioControl().applyAudioOptions(this.audioControl().getAudioOptions());
this.refreshContent(context);
return true;
}
default -> {
return false;
}
}
}
}

View File

@@ -0,0 +1,95 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldControl;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
final class OptionsKeyboardTab extends AbstractOptionsTab implements OptionsTab {
static final String TAB_ID = "keyboard";
private static final String CONTROL_ID = "OptionsModule_Tab_Keyboard";
@Override
public String tabId() {
return TAB_ID;
}
@Override
public String controlId() {
return CONTROL_ID;
}
@Override
public String navigationLocalizationKey() {
return "ecyoa-86";
}
@Override
public String fallbackCaption() {
return "Keyboard";
}
@Override
public int navigationOrder() {
return 50;
}
@Override
public boolean visibleInNavigation() {
return false;
}
@Override
public void render(final OptionsModuleContext context) {
final var keycodeRegistrations = this.keycodeRegistry().getAll();
var yPos = 10;
var bo = this.renderingBoPool().acquire("OptionsModule_Keyboard_Label_Header", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-102", "Keyboard Shortcuts:"))
.setFontSize(42)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
for (final var registration : keycodeRegistrations) {
final var controlId = "OptionsModule_Keyboard_" + sanitizeId(registration.getQualifier());
bo = this.renderingBoPool().acquire(controlId + "_Label", ControlRenderingBo.class)
.setCaption(registration.getQualifier())
.setFontSize(42)
.setType(LabelControl.class)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(400, 60)
.withPositionRelative(10, yPos + 10);
this.contentScrollArea(context).putRenderingBo(bo);
bo = this.renderingBoPool().acquire(controlId + "_Key", ControlRenderingBo.class)
.setCaption(registration.getKeyCodeDisplay() != null ? registration.getKeyCodeDisplay() : String.valueOf((char) registration.getCode()))
.setFontSize(42)
.setType(TextfieldControl.class)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(400, 60)
.withPositionRelative(420, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 70;
}
}
@Override
public boolean handle(final UiControlEvent event, final OptionsModuleContext context) {
return false;
}
}

View File

@@ -0,0 +1,280 @@
package de.extio.game_engine.menu;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.i18n.LocalizationService;
import de.extio.game_engine.module.AbstractClientModule;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.container.ScrollArea;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonData;
import de.extio.game_engine.renderer.model.event.MouseClickEvent;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.spatial2.model.CoordI2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
public class OptionsModule extends AbstractClientModule implements OptionsModuleContext {
private static final String TAB_GROUP = "OptionsModule_TabGroup";
private static final int TAB_BUTTON_HEIGHT = 60;
private static final int TAB_BUTTON_WIDTH = 220;
private static final int CONTENT_START_Y = 140;
private final ApplicationContext applicationContext;
private final RenderingBoPool renderingBoPool;
private final EventService eventService;
private final LocalizationService localizationService;
@Autowired
private List<OptionsTab> discoveredTabs;
OptionsModule(final ApplicationContext applicationContext, final RenderingBoPool renderingBoPool, final EventService eventService,
final LocalizationService localizationService) {
this.applicationContext = applicationContext;
this.renderingBoPool = renderingBoPool;
this.eventService = eventService;
this.localizationService = localizationService;
}
private List<OptionsTab> tabs = List.of();
private Map<String, OptionsTab> tabsById = Map.of();
private Map<String, OptionsTab> tabsByControlId = Map.of();
private Window optionsWindow;
private ScrollArea contentScrollArea;
private String activeTabId;
@Override
public void onLoad() {
this.initializeTabs();
this.optionsWindow = this.applicationContext.getBean(Window.class);
this.optionsWindow.setNormalizedDimension(RendererControl.REFERENCE_RESOLUTION.divide(9).multiply(7));
this.optionsWindow.setNormalizedPosition(centeredPosition(this.optionsWindow.getNormalizedDimension()));
this.optionsWindow.setDraggable(false);
this.optionsWindow.setCloseButton(true);
this.optionsWindow.setOnCloseAction(this::closeOptions);
this.contentScrollArea = this.applicationContext.getBean(ScrollArea.class);
this.optionsWindow.addComponent(this.contentScrollArea);
}
@Override
public void onUnload() {
this.getModuleService().unloadModule(this.optionsWindow.getId());
}
@Override
public void onActivate() {
this.setupOptionsWindow();
this.getModuleService().changeActiveState(this.optionsWindow.getId(), true);
this.getModuleService().hideExcept(this.getId(), this.optionsWindow.getId());
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
this.eventService.register(OptionsThemeEditorSavedEvent.class, this.getId(), this::onThemeEditorSaved);
this.eventService.register(MouseClickEvent.class, this.getId(), event -> {
if (event.isPressed() && (event.getButton() == 1 || event.getButton() == 3)) {
this.refreshContent();
}
});
this.eventService.register(OpenOptionsModuleEvent.class, this.getId(), event -> {
if (event.tab() != null && !event.tab().isBlank()) {
this.openTab(event.tab());
} else {
this.getModuleService().changeActiveState(this.getId(), true);
this.getModuleService().changeDisplayState(this.getId(), true);
}
});
}
@Override
public void onDeactivate() {
this.eventService.unregisterAll(this.getId());
this.getModuleService().changeActiveState(this.optionsWindow.getId(), false);
this.getModuleService().restoreVisibility();
}
@Override
public void onShow() {
this.getModuleService().changeDisplayState(this.optionsWindow.getId(), true);
}
@Override
public void onHide() {
this.getModuleService().changeDisplayState(this.optionsWindow.getId(), false);
}
public void openTab(final String tabId) {
this.initializeTabs();
if (this.tabsById.containsKey(tabId)) {
this.activeTabId = tabId;
}
this.getModuleService().changeActiveState(this.getId(), true);
this.getModuleService().changeDisplayState(this.getId(), true);
}
@Override
public void rebuildWindow() {
this.setupOptionsWindow();
}
@Override
public void refreshContent() {
for (final var boId : this.contentScrollArea.getRenderingBoIds().toArray(new String[0])) {
this.contentScrollArea.removeRenderingBo(boId);
}
for (final var tab : this.tabs) {
tab.onContentCleared(this);
}
this.activeTab().render(this);
this.optionsWindow.draw();
}
@Override
public Window optionsWindow() {
return this.optionsWindow;
}
@Override
public ScrollArea contentScrollArea() {
return this.contentScrollArea;
}
private void setupOptionsWindow() {
final var windowWidth = this.optionsWindow.getNormalizedDimension().getX();
final var tabsY = Window.MARGIN_TOP;
final var tabSpacing = 10;
var tabX = Window.MARGIN_LEFT;
for (final var tab : this.tabs) {
if (!tab.visibleInNavigation()) {
continue;
}
final var bo = this.renderingBoPool.acquire(tab.controlId(), ControlRenderingBo.class)
.setCaption(this.localizationService.translate(tab.navigationLocalizationKey(), tab.fallbackCaption()))
.setFontSize(36)
.setType(ToggleButtonControl.class)
.setControlGroup(TAB_GROUP)
.setControlData(new ToggleButtonData(tab.tabId().equals(this.activeTabId), false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(TAB_BUTTON_WIDTH, TAB_BUTTON_HEIGHT)
.withPositionRelative(tabX, tabsY);
this.optionsWindow.putRenderingBo(bo);
tabX += TAB_BUTTON_WIDTH + tabSpacing;
}
final var bottomButtonsY = this.optionsWindow.getNormalizedDimension().getY() - Window.MARGIN_BOTTOM - 60;
final var buttonWidth = 200;
final var buttonHeight = 60;
final var closeButton = this.renderingBoPool.acquire("OptionsModule_Button_Close", ControlRenderingBo.class)
.setCaption(this.localizationService.translate("ecyoa-67", "Cancel"))
.setFontSize(36)
.setType(ButtonControl.class)
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(buttonWidth, buttonHeight)
.withPositionRelative(windowWidth - Window.MARGIN_RIGHT - buttonWidth, bottomButtonsY);
this.optionsWindow.putRenderingBo(closeButton);
this.contentScrollArea.setRelativePosition(ImmutableCoordI2.create(Window.MARGIN_LEFT, CONTENT_START_Y));
this.contentScrollArea.setDimension(ImmutableCoordI2.create(
windowWidth - Window.MARGIN_LEFT - Window.MARGIN_RIGHT,
bottomButtonsY - CONTENT_START_Y - 20));
this.refreshContent();
this.optionsWindow.draw();
}
private void initializeTabs() {
if (!this.tabs.isEmpty()) {
return;
}
final var orderedTabs = new ArrayList<>(this.discoveredTabs);
orderedTabs.sort(Comparator.comparingInt(OptionsTab::navigationOrder));
this.tabs = List.copyOf(orderedTabs);
final var tabsById = new LinkedHashMap<String, OptionsTab>();
final var tabsByControlId = new LinkedHashMap<String, OptionsTab>();
for (final var tab : this.tabs) {
if (tabsById.put(tab.tabId(), tab) != null) {
throw new IllegalStateException("Duplicate options tab id: " + tab.tabId());
}
if (tabsByControlId.put(tab.controlId(), tab) != null) {
throw new IllegalStateException("Duplicate options tab control id: " + tab.controlId());
}
}
this.tabsById = Map.copyOf(tabsById);
this.tabsByControlId = Map.copyOf(tabsByControlId);
if (this.activeTabId == null && !this.tabs.isEmpty()) {
this.activeTabId = this.tabs.stream()
.filter(OptionsTab::visibleInNavigation)
.findFirst()
.orElse(this.tabs.getFirst())
.tabId();
}
}
private OptionsTab activeTab() {
this.initializeTabs();
final var activeTab = this.tabsById.get(this.activeTabId);
if (activeTab != null) {
return activeTab;
}
return this.tabs.getFirst();
}
private void onUiControlEvent(final UiControlEvent event) {
final var controlId = event.getId();
if (controlId == null) {
return;
}
final var selectedTab = this.tabsByControlId.get(controlId);
if (selectedTab != null) {
this.activeTabId = selectedTab.tabId();
this.setupOptionsWindow();
return;
}
switch (controlId) {
case "OptionsModule_Button_Close" -> this.closeOptions();
default -> this.activeTab().handle(event, this);
}
}
private void onThemeEditorSaved(final OptionsThemeEditorSavedEvent event) {
if (event.saved()) {
this.refreshContent();
}
}
private void closeOptions() {
this.getModuleService().changeActiveState(this.getId(), false);
this.getModuleService().changeDisplayState(this.getId(), false);
}
private static ImmutableCoordI2 centeredPosition(final CoordI2 dimension) {
final var referenceResolution = RendererControl.REFERENCE_RESOLUTION;
final var x = Math.max(0, (referenceResolution.getX() - dimension.getX()) / 2);
final var y = Math.max(0, (referenceResolution.getY() - dimension.getY()) / 2);
return ImmutableCoordI2.create(x, y);
}
}

View File

@@ -0,0 +1,19 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.renderer.container.ScrollArea;
import de.extio.game_engine.renderer.container.Window;
public interface OptionsModuleContext {
Window optionsWindow();
ScrollArea contentScrollArea();
void refreshContent();
void rebuildWindow();
default int contentWidth() {
return this.optionsWindow().getNormalizedDimension().getX() - Window.MARGIN_LEFT - Window.MARGIN_RIGHT;
}
}

View File

@@ -0,0 +1,26 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
public interface OptionsTab {
String tabId();
String controlId();
String navigationLocalizationKey();
String fallbackCaption();
int navigationOrder();
default boolean visibleInNavigation() {
return true;
}
void render(OptionsModuleContext context);
boolean handle(UiControlEvent event, OptionsModuleContext context);
default void onContentCleared(final OptionsModuleContext context) {
}
}

View File

@@ -0,0 +1,7 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.event.Event;
import de.extio.game_engine.renderer.model.Theme;
public record OptionsThemeEditorSavedEvent(boolean saved, Theme theme) implements Event {
}

View File

@@ -0,0 +1,11 @@
package de.extio.game_engine.menu;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.model.Theme;
public interface OptionsThemeEditorSupport {
void openNew(Theme baseTheme, String proposedName, Window parentWindow);
void openEdit(Theme theme, Window parentWindow);
}

View File

@@ -0,0 +1,387 @@
package de.extio.game_engine.menu;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import de.extio.game_engine.renderer.model.RenderingBoHasDimension;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.Theme;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
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.bo.ControlRenderingBo.PopupMenuItem;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonData;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
final class OptionsThemesTab extends AbstractOptionsTab implements OptionsTab {
static final String TAB_ID = "themes";
private static final String CONTROL_ID = "OptionsModule_Tab_Themes";
private static final List<String> CUSTOM_THEME_PATH = List.of("customThemes");
private static final String CUSTOM_THEME_MENU_ID = "OptionsModule_CustomTheme_Menu";
private static final String CUSTOM_THEME_MENU_SELECT = "OptionsModule_CustomThemeMenu_Select";
private static final String CUSTOM_THEME_MENU_EDIT = "OptionsModule_CustomThemeMenu_Edit";
private static final String CUSTOM_THEME_MENU_DELETE = "OptionsModule_CustomThemeMenu_Delete";
@Autowired
private ObjectProvider<OptionsThemeEditorSupport> themeEditorSupportProvider;
private final Map<String, String> themeSelectionByControlId = new HashMap<>();
private final Map<String, String> customThemeSelectionByControlId = new HashMap<>();
private String activeCustomThemeName;
@Override
public String tabId() {
return TAB_ID;
}
@Override
public String controlId() {
return CONTROL_ID;
}
@Override
public String navigationLocalizationKey() {
return "ecyoa-87";
}
@Override
public String fallbackCaption() {
return "Themes";
}
@Override
public int navigationOrder() {
return 20;
}
@Override
public void render(final OptionsModuleContext context) {
this.themeSelectionByControlId.clear();
this.customThemeSelectionByControlId.clear();
final var themeEditorSupport = this.themeEditorSupportProvider.getIfAvailable();
final var customThemeNames = this.getCustomThemeNames();
final var themeNames = this.applicationContext().getBeansOfType(Theme.class).values().stream()
.map(Theme::getName)
.sorted()
.toList();
final var currentTheme = this.themeManager().getCurrentTheme();
final var currentThemeName = currentTheme != null ? currentTheme.getName() : null;
var yPos = 10;
var bo = this.renderingBoPool().acquire("OptionsModule_Themes_Label_Header", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-103", "Available Themes:"))
.setFontSize(42)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
final var columnWidth = 330;
final var columnSpacing = 20;
var xPos = 10;
var columnIndex = 0;
var emptyRow = false;
for (final var themeName : themeNames) {
emptyRow = false;
final var controlId = "OptionsModule_Theme_" + sanitizeId(themeName);
this.themeSelectionByControlId.put(controlId, themeName);
bo = this.renderingBoPool().acquire(controlId, ControlRenderingBo.class)
.setCaption(themeName)
.setFontSize(32)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_ThemeGroup")
.setControlData(new ToggleButtonData(themeName.equals(currentThemeName), true, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(columnWidth, 60)
.withPositionRelative(xPos, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
columnIndex++;
if (columnIndex >= 4) {
columnIndex = 0;
xPos = 10;
yPos += 70;
emptyRow = true;
}
else {
xPos += columnWidth + columnSpacing;
}
}
yPos += emptyRow ? 20 : 90;
xPos = 10;
columnIndex = 0;
bo = this.renderingBoPool().acquire("OptionsModule_Themes_Label_Custom", ControlRenderingBo.class)
.setCaption("Custom Themes")
.setFontSize(40)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(600, 70)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
for (final var themeName : customThemeNames) {
final var controlId = "OptionsModule_CustomTheme_" + sanitizeId(themeName);
this.customThemeSelectionByControlId.put(controlId, themeName);
bo = this.renderingBoPool().acquire(controlId, ControlRenderingBo.class)
.setCaption(themeName)
.setFontSize(32)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_CustomThemeGroup")
.setControlData(new ToggleButtonData(themeName.equals(currentThemeName), true, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(columnWidth, 60)
.withPositionRelative(xPos, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
columnIndex++;
if (columnIndex >= 4) {
columnIndex = 0;
xPos = 10;
yPos += 70;
}
else {
xPos += columnWidth + columnSpacing;
}
}
if (themeEditorSupport != null) {
final var addBo = this.renderingBoPool().acquire("OptionsModule_CustomTheme_Add", ControlRenderingBo.class)
.setCaption("+")
.setFontSize(36)
.setType(ButtonControl.class)
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(columnWidth, 60)
.withPositionRelative(xPos, yPos);
this.contentScrollArea(context).putRenderingBo(addBo);
}
}
@Override
public boolean handle(final UiControlEvent event, final OptionsModuleContext context) {
final var controlId = event.getId();
if (controlId == null) {
return false;
}
switch (controlId) {
case "OptionsModule_CustomTheme_Add" -> {
this.openNewThemeEditor(context);
return true;
}
case CUSTOM_THEME_MENU_ID -> {
this.handleCustomThemeMenuSelection(event.getPayload(), context);
return true;
}
default -> {
}
}
final var customThemeName = this.customThemeSelectionByControlId.get(controlId);
if (customThemeName != null) {
final var bo = this.optionsWindow(context).getRenderingBo(controlId, ControlRenderingBo.class);
if (bo != null) {
this.showCustomThemeMenu(bo, customThemeName, context);
}
return true;
}
final var themeName = this.themeSelectionByControlId.get(controlId);
if (themeName != null) {
this.themeManager().setCurrentTheme(themeName);
this.refreshContent(context);
return true;
}
return false;
}
@Override
public void onContentCleared(final OptionsModuleContext context) {
this.hideCustomThemeMenu(context);
this.themeSelectionByControlId.clear();
this.customThemeSelectionByControlId.clear();
}
private List<String> getCustomThemeNames() {
final var descriptors = this.storageService().listPath(CUSTOM_THEME_PATH, false);
final var names = new ArrayList<String>(descriptors.size());
for (final var descriptor : descriptors) {
if (descriptor != null && descriptor.name() != null && !descriptor.name().isBlank()) {
names.add(descriptor.name());
}
}
names.sort(String::compareToIgnoreCase);
return List.copyOf(names);
}
private void showCustomThemeMenu(final ControlRenderingBo bo, final String themeName, final OptionsModuleContext context) {
if (themeName.equals(this.activeCustomThemeName)
&& this.optionsWindow(context).getRenderingBo(CUSTOM_THEME_MENU_ID, ControlRenderingBo.class) != null) {
this.hideCustomThemeMenu(context);
final var controlId = "OptionsModule_CustomTheme_" + sanitizeId(themeName);
final var control = this.optionsWindow(context).getRenderingBo(controlId, ControlRenderingBo.class);
if (control != null) {
final var currentTheme = this.themeManager().getCurrentTheme();
control.setControlData(new ToggleButtonData(currentTheme != null && themeName.equals(currentTheme.getName()), true, null));
this.optionsWindow(context).putRenderingBo(control);
}
this.optionsWindow(context).draw();
return;
}
this.hideCustomThemeMenu(context);
this.activeCustomThemeName = themeName;
if (!(bo instanceof final RenderingBoHasDimension boDim)) {
return;
}
final var items = new ArrayList<PopupMenuItem>();
items.add(new PopupMenuItem(CUSTOM_THEME_MENU_SELECT, "Select", true));
if (this.themeEditorSupportProvider.getIfAvailable() != null) {
items.add(new PopupMenuItem(CUSTOM_THEME_MENU_EDIT, "Edit", true));
}
items.add(new PopupMenuItem(CUSTOM_THEME_MENU_DELETE, "Delete", true));
final var rowHeight = 32;
final var padding = 8;
final var width = Math.min(240, boDim.getWidth());
final var height = (rowHeight * items.size()) + (padding * 2);
final var offset = this.contentScrollArea(context).getRelativePosition();
final var x = offset.getX() + bo.getLocalX();
final var y = offset.getY() + bo.getLocalY() + boDim.getHeight() + 6;
final var data = new PopupMenuData(List.copyOf(items), null, null, null, rowHeight, padding);
final var popup = this.renderingBoPool().acquire(CUSTOM_THEME_MENU_ID, ControlRenderingBo.class)
.setType(PopupMenuControl.class)
.setFontSize(22)
.setVisible(true)
.setEnabled(true)
.setControlData(data)
.withDimensionAbsolute(width, height)
.setLayer(RenderingBoLayer.UI1)
.withPositionRelative(x, y);
this.optionsWindow(context).putRenderingBo(popup);
this.optionsWindow(context).draw();
}
private void handleCustomThemeMenuSelection(final Object payload, final OptionsModuleContext context) {
if (this.activeCustomThemeName == null) {
return;
}
final String actionId;
if (payload instanceof final String id) {
actionId = id;
}
else if (payload instanceof final PopupMenuItem item) {
actionId = item.id();
}
else {
return;
}
switch (actionId) {
case CUSTOM_THEME_MENU_SELECT -> this.applyCustomTheme(this.activeCustomThemeName, context);
case CUSTOM_THEME_MENU_EDIT -> this.editCustomTheme(this.activeCustomThemeName, context);
case CUSTOM_THEME_MENU_DELETE -> this.deleteCustomTheme(this.activeCustomThemeName, context);
default -> {
return;
}
}
this.refreshContent(context);
}
private void hideCustomThemeMenu(final OptionsModuleContext context) {
this.optionsWindow(context).removeRenderingBo(CUSTOM_THEME_MENU_ID);
this.activeCustomThemeName = null;
}
private void applyCustomTheme(final String themeName, final OptionsModuleContext context) {
final var theme = this.loadCustomTheme(themeName);
if (theme == null) {
return;
}
this.themeManager().setCurrentTheme(theme);
this.refreshContent(context);
}
private void editCustomTheme(final String themeName, final OptionsModuleContext context) {
final var themeEditorSupport = this.themeEditorSupportProvider.getIfAvailable();
if (themeEditorSupport == null) {
return;
}
final var theme = this.loadCustomTheme(themeName);
if (theme == null) {
return;
}
themeEditorSupport.openEdit(theme, this.optionsWindow(context));
}
private void deleteCustomTheme(final String themeName, final OptionsModuleContext context) {
this.storageService().deleteByPath(CUSTOM_THEME_PATH, themeName);
final var currentTheme = this.themeManager().getCurrentTheme();
if (currentTheme != null && themeName.equalsIgnoreCase(currentTheme.getName())) {
this.themeManager().setCurrentTheme((String) null);
}
this.refreshContent(context);
}
private Theme loadCustomTheme(final String themeName) {
return this.storageService().loadByPath(Theme.class, CUSTOM_THEME_PATH, themeName).orElse(null);
}
private void openNewThemeEditor(final OptionsModuleContext context) {
final var themeEditorSupport = this.themeEditorSupportProvider.getIfAvailable();
if (themeEditorSupport == null) {
return;
}
final var name = this.generateNewThemeName();
themeEditorSupport.openNew(this.themeManager().getCurrentTheme(), name, this.optionsWindow(context));
}
private String generateNewThemeName() {
final var existing = this.getCustomThemeNames();
final var base = "Custom Theme";
if (!existing.contains(base)) {
return base;
}
var index = 2;
while (existing.contains(base + " " + index)) {
index++;
}
return base + " " + index;
}
}

View File

@@ -0,0 +1,287 @@
package de.extio.game_engine.menu;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonData;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.model.options.VideoOptions.VideoOptionsVideoMode;
final class OptionsVideoTab extends AbstractOptionsTab implements OptionsTab {
static final String TAB_ID = "video";
private static final String CONTROL_ID = "OptionsModule_Tab_Video";
private final Map<String, Integer> framerateSelectionByControlId = new HashMap<>();
private final Map<String, Integer> screenSelectionByControlId = new HashMap<>();
private final Map<String, Double> scaleFactorSelectionByControlId = new HashMap<>();
@Override
public String tabId() {
return TAB_ID;
}
@Override
public String controlId() {
return CONTROL_ID;
}
@Override
public String navigationLocalizationKey() {
return "ecyoa-84";
}
@Override
public String fallbackCaption() {
return "Video";
}
@Override
public int navigationOrder() {
return 30;
}
@Override
public void render(final OptionsModuleContext context) {
this.framerateSelectionByControlId.clear();
this.screenSelectionByControlId.clear();
this.scaleFactorSelectionByControlId.clear();
var yPos = 10;
var bo = this.renderingBoPool().acquire("OptionsModule_Video_Label_Mode", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-91", "Video Mode:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
final var currentMode = this.rendererControl().getVideoOptions().getVideoMode();
bo = this.renderingBoPool().acquire("OptionsModule_VideoMode_Window", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-92", "Windowed"))
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_VideoModeGroup")
.setControlData(new ToggleButtonData(currentMode == VideoOptionsVideoMode.WINDOW, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(300, 60)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
bo = this.renderingBoPool().acquire("OptionsModule_VideoMode_Borderless", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-93", "Borderless"))
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_VideoModeGroup")
.setControlData(new ToggleButtonData(currentMode == VideoOptionsVideoMode.BORDERLESS, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(300, 60)
.withPositionRelative(320, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
bo = this.renderingBoPool().acquire("OptionsModule_VideoMode_Fullscreen", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-94", "Fullscreen"))
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_VideoModeGroup")
.setControlData(new ToggleButtonData(currentMode == VideoOptionsVideoMode.FULLSCREEN, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(300, 60)
.withPositionRelative(630, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 100;
bo = this.renderingBoPool().acquire("OptionsModule_Video_Label_Screen", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-105", "Screen:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
final var screenDevices = this.rendererControl().getScreenDevicesCount();
final var currentScreen = this.rendererControl().getVideoOptions().getFullScreenNumber();
var screenXPos = 10;
for (var i = 0; i < screenDevices; i++) {
final var controlId = "OptionsModule_Video_Screen_" + i;
this.screenSelectionByControlId.put(controlId, i);
bo = this.renderingBoPool().acquire(controlId, ControlRenderingBo.class)
.setCaption(String.valueOf(i + 1))
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_ScreenGroup")
.setControlData(new ToggleButtonData(currentScreen == i, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(80, 60)
.withPositionRelative(screenXPos, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
screenXPos += 90;
}
yPos += 100;
bo = this.renderingBoPool().acquire("OptionsModule_Video_Label_ScaleFactor", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-95", "Scale Factor:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
final var currentScaleFactor = this.rendererControl().getVideoOptions().getScaleFactorModifier();
final var scaleFactors = List.of(0.75, 0.90, 1.00, 1.15, 1.25);
var scaleXPos = 10;
for (final var scaleFactor : scaleFactors) {
final var controlId = "OptionsModule_Video_ScaleFactor_" + (int) (scaleFactor * 100);
this.scaleFactorSelectionByControlId.put(controlId, scaleFactor);
bo = this.renderingBoPool().acquire(controlId, ControlRenderingBo.class)
.setCaption((int) (scaleFactor * 100) + "%")
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_ScaleFactorGroup")
.setControlData(new ToggleButtonData(Math.abs(currentScaleFactor - scaleFactor) < 0.01, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(150, 60)
.withPositionRelative(scaleXPos, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
scaleXPos += 160;
}
yPos += 100;
bo = this.renderingBoPool().acquire("OptionsModule_Video_Label_Framerate", ControlRenderingBo.class)
.setCaption(this.localizationService().translate("ecyoa-96", "Target Framerate:"))
.setFontSize(48)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(800, 80)
.withPositionRelative(10, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
yPos += 90;
final var currentFramerate = this.rendererControl().getVideoOptions().getFrameRateTarget();
final var framerates = List.of(30, 60, 120, 144, 165, 200);
var xPos = 10;
for (final var framerate : framerates) {
final var controlId = "OptionsModule_Video_Framerate_" + framerate;
this.framerateSelectionByControlId.put(controlId, framerate);
bo = this.renderingBoPool().acquire(controlId, ControlRenderingBo.class)
.setCaption(framerate + " FPS")
.setFontSize(42)
.setType(ToggleButtonControl.class)
.setControlGroup("OptionsModule_FramerateGroup")
.setControlData(new ToggleButtonData(currentFramerate == framerate, false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(220, 60)
.withPositionRelative(xPos, yPos);
this.contentScrollArea(context).putRenderingBo(bo);
xPos += 230;
}
}
@Override
public boolean handle(final UiControlEvent event, final OptionsModuleContext context) {
final var controlId = event.getId();
if (controlId == null) {
return false;
}
switch (controlId) {
case "OptionsModule_VideoMode_Window" -> {
this.rendererControl().getVideoOptions().setVideoMode(VideoOptionsVideoMode.WINDOW);
this.rendererControl().applyVideoOptions();
this.refreshContent(context);
return true;
}
case "OptionsModule_VideoMode_Borderless" -> {
this.rendererControl().getVideoOptions().setVideoMode(VideoOptionsVideoMode.BORDERLESS);
this.rendererControl().applyVideoOptions();
this.refreshContent(context);
return true;
}
case "OptionsModule_VideoMode_Fullscreen" -> {
this.rendererControl().getVideoOptions().setVideoMode(VideoOptionsVideoMode.FULLSCREEN);
this.rendererControl().applyVideoOptions();
this.refreshContent(context);
return true;
}
default -> {
}
}
final var framerate = this.framerateSelectionByControlId.get(controlId);
if (framerate != null) {
this.rendererControl().getVideoOptions().setFrameRateTarget(framerate);
this.rendererControl().setFrameRate(framerate);
this.refreshContent(context);
return true;
}
final var screenNumber = this.screenSelectionByControlId.get(controlId);
if (screenNumber != null) {
this.rendererControl().getVideoOptions().setFullScreenNumber(screenNumber);
this.rendererControl().applyVideoOptions();
this.refreshContent(context);
return true;
}
final var scaleFactor = this.scaleFactorSelectionByControlId.get(controlId);
if (scaleFactor != null) {
this.rendererControl().getVideoOptions().setScaleFactorModifier(scaleFactor);
this.rendererControl().setScaleFactorModifier(scaleFactor);
this.refreshContent(context);
return true;
}
return false;
}
@Override
public void onContentCleared(final OptionsModuleContext context) {
this.framerateSelectionByControlId.clear();
this.screenSelectionByControlId.clear();
this.scaleFactorSelectionByControlId.clear();
}
}

View File

@@ -0,0 +1,774 @@
package de.extio.game_engine.menu;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.menu.HsbColorPickerModule.HsbColorPickerResponseEvent;
import de.extio.game_engine.module.AbstractClientModule;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.ThemeManager;
import de.extio.game_engine.renderer.container.ScrollArea;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.model.RenderingBoHasDimension;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.Theme;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
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.bo.ControlRenderingBo.PopupMenuItem;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldData;
import de.extio.game_engine.renderer.model.bo.DrawEffectRenderingBo;
import de.extio.game_engine.renderer.model.bo.DrawEffectRenderingBoEffects;
import de.extio.game_engine.renderer.model.bo.DrawFontRenderingBo;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.color.HSBColor;
import de.extio.game_engine.renderer.model.color.ImmutableRgbaColor;
import de.extio.game_engine.renderer.model.event.MouseClickEvent;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.resource.StaticResourceService;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.storage.StorageService;
public class ThemeEditorModule extends AbstractClientModule implements OptionsThemeEditorSupport {
private static final List<String> CUSTOM_THEME_PATH = List.of("customThemes");
private static final int WINDOW_WIDTH = 1200;
private static final int WINDOW_HEIGHT = 820;
private static final int ROW_HEIGHT = 60;
private static final int LABEL_WIDTH = 320;
private static final int CONTROL_WIDTH = 620;
private static final int VALUE_WIDTH = 120;
private static final int SPACING = 14;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private RenderingBoPool renderingBoPool;
@Autowired
private EventService eventService;
@Autowired
private ThemeManager themeManager;
@Autowired
private StorageService storageService;
@Autowired
private StaticResourceService staticResourceService;
@Autowired
private HsbColorPickerModule hsbColorPickerModule;
private Window editorWindow;
private ScrollArea scrollArea;
private Theme originalTheme;
private String originalName;
private String name;
private String patternRendererName;
private String font;
private HSBColor borderOuter;
private HSBColor borderInner;
private HSBColor borderInnerDisabled;
private HSBColor backgroundNormal;
private HSBColor backgroundSelected;
private HSBColor textNormal;
private HSBColor textDisabled;
private HSBColor selectionPrimary;
private HSBColor selectionSecondary;
private HSBColor windowBackground;
private float hoverBrightnessAdjustment;
private float pressedBrightnessAdjustment;
private final Map<String, ColorSlot> colorSlotByControlId = new HashMap<>();
private PopupTarget popupTarget;
private String pendingColorRequestId;
@Override
public void onLoad() {
this.editorWindow = this.applicationContext.getBean(Window.class);
this.editorWindow.setNormalizedDimension(ImmutableCoordI2.create(WINDOW_WIDTH, WINDOW_HEIGHT));
this.editorWindow.setNormalizedPosition(this.centeredPosition(this.editorWindow.getNormalizedDimension()));
this.editorWindow.setDraggable(true);
this.editorWindow.setCloseButton(true);
this.editorWindow.setOnCloseAction(this::onCancel);
this.scrollArea = this.applicationContext.getBean(ScrollArea.class);
this.editorWindow.addComponent(this.scrollArea);
}
@Override
public void onUnload() {
this.getModuleService().unloadModule(this.editorWindow.getId());
}
@Override
public void onActivate() {
this.setupEditor();
this.getModuleService().changeActiveState(this.editorWindow.getId(), true);
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
this.eventService.register(HsbColorPickerResponseEvent.class, this.getId(), this::onColorPickerResponse);
this.eventService.register(MouseClickEvent.class, this.getId(), event -> {
if (event.isPressed() && (event.getButton() == 1 || event.getButton() == 3) && this.popupTarget != null) {
this.hidePopup();
this.editorWindow.draw();
}
});
}
@Override
public void onDeactivate() {
this.eventService.unregisterAll(this.getId());
this.getModuleService().changeActiveState(this.editorWindow.getId(), false);
}
@Override
public void onShow() {
this.getModuleService().changeDisplayState(this.editorWindow.getId(), true);
}
@Override
public void onHide() {
this.getModuleService().changeDisplayState(this.editorWindow.getId(), false);
}
@Override
public void openNew(final Theme baseTheme, final String proposedName, final Window parentWindow) {
this.originalTheme = this.themeManager.getCurrentTheme();
final var template = baseTheme != null ? baseTheme : this.themeManager.getCurrentTheme();
this.originalName = null;
this.name = proposedName;
this.initializeFromTheme(template);
// apply the template theme immediately for preview
this.applyCurrentThemePreview();
this.editorWindow.setParent(parentWindow);
this.getModuleService().changeActiveState(this.getId(), true);
this.getModuleService().changeDisplayState(this.getId(), true);
}
@Override
public void openEdit(final Theme theme, final Window parentWindow) {
if (theme == null) {
return;
}
this.originalTheme = this.themeManager.getCurrentTheme();
this.originalName = theme.getName();
this.name = theme.getName();
this.initializeFromTheme(theme);
// apply the selected theme immediately for preview
this.applyCurrentThemePreview();
this.editorWindow.setParent(parentWindow);
this.getModuleService().changeActiveState(this.getId(), true);
this.getModuleService().changeDisplayState(this.getId(), true);
}
private void initializeFromTheme(final Theme theme) {
this.patternRendererName = theme.getPatternRendererName();
this.font = theme.getFont();
this.borderOuter = theme.getBorderOuter();
this.borderInner = theme.getBorderInner();
this.borderInnerDisabled = theme.getBorderInnerDisabled();
this.backgroundNormal = theme.getBackgroundNormal();
this.backgroundSelected = theme.getBackgroundSelected();
this.textNormal = theme.getTextNormal();
this.textDisabled = theme.getTextDisabled();
this.selectionPrimary = theme.getSelectionPrimary();
this.selectionSecondary = theme.getSelectionSecondary();
this.windowBackground = theme.getWindowBackground();
this.hoverBrightnessAdjustment = theme.getHoverBrightnessAdjustment();
this.pressedBrightnessAdjustment = theme.getPressedBrightnessAdjustment();
}
private void setupEditor() {
this.editorWindow.clearRenderingBos();
for (final var boId : this.scrollArea.getRenderingBoIds().toArray(new String[0])) {
this.scrollArea.removeRenderingBo(boId);
}
this.colorSlotByControlId.clear();
this.popupTarget = null;
this.pendingColorRequestId = null;
final var contentWidth = WINDOW_WIDTH - Window.MARGIN_LEFT - Window.MARGIN_RIGHT;
final var bottomButtonsY = WINDOW_HEIGHT - Window.MARGIN_BOTTOM - 60;
this.scrollArea.setRelativePosition(ImmutableCoordI2.create(Window.MARGIN_LEFT, Window.MARGIN_TOP));
this.scrollArea.setDimension(ImmutableCoordI2.create(contentWidth, bottomButtonsY - Window.MARGIN_TOP - SPACING));
var yPos = 10;
var bo = this.renderingBoPool.acquire("ThemeEditor_Label_Header", DrawFontRenderingBo.class)
.setText("Theme Editor")
.setSize(36)
.setAlignment(HorizontalAlignment.LEFT)
.withDimensionAbsolute(600, 60)
.withPositionRelative(10, yPos);
this.scrollArea.putRenderingBo(bo);
yPos += 80;
yPos = this.addTextFieldRow("Name", "ThemeEditor_Field_Name", this.name, yPos);
yPos = this.addPopupRow("Pattern Renderer", "ThemeEditor_Field_Pattern", this.patternRendererName, yPos);
yPos = this.addPopupRow("Font", "ThemeEditor_Field_Font", this.font, yPos);
yPos = this.addSliderRow("Hover Brightness", "ThemeEditor_Field_Hover", this.hoverBrightnessAdjustment, yPos);
yPos = this.addSliderRow("Pressed Brightness", "ThemeEditor_Field_Pressed", this.pressedBrightnessAdjustment, yPos);
yPos += 20;
yPos = this.addColorRow("Border Outer", "ThemeEditor_Color_BorderOuter", this.borderOuter, yPos);
yPos = this.addColorRow("Border Inner", "ThemeEditor_Color_BorderInner", this.borderInner, yPos);
yPos = this.addColorRow("Border Inner Disabled", "ThemeEditor_Color_BorderInnerDisabled", this.borderInnerDisabled, yPos);
yPos = this.addColorRow("Background Normal", "ThemeEditor_Color_BackgroundNormal", this.backgroundNormal, yPos);
yPos = this.addColorRow("Background Selected", "ThemeEditor_Color_BackgroundSelected", this.backgroundSelected, yPos);
yPos = this.addColorRow("Text Normal", "ThemeEditor_Color_TextNormal", this.textNormal, yPos);
yPos = this.addColorRow("Text Disabled", "ThemeEditor_Color_TextDisabled", this.textDisabled, yPos);
yPos = this.addColorRow("Selection Primary", "ThemeEditor_Color_SelectionPrimary", this.selectionPrimary, yPos);
yPos = this.addColorRow("Selection Secondary", "ThemeEditor_Color_SelectionSecondary", this.selectionSecondary, yPos);
yPos = this.addColorRow("Window Background", "ThemeEditor_Color_WindowBackground", this.windowBackground, yPos);
final var buttonWidth = 200;
final var buttonHeight = 60;
bo = this.renderingBoPool.acquire("ThemeEditor_Button_Save", ControlRenderingBo.class)
.setCaption("Save")
.setFontSize(32)
.setType(ButtonControl.class)
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(buttonWidth, buttonHeight)
.withPositionRelative(WINDOW_WIDTH - Window.MARGIN_RIGHT - buttonWidth * 2 - SPACING, bottomButtonsY);
this.editorWindow.putRenderingBo(bo);
bo = this.renderingBoPool.acquire("ThemeEditor_Button_Cancel", ControlRenderingBo.class)
.setCaption("Cancel")
.setFontSize(32)
.setType(ButtonControl.class)
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(buttonWidth, buttonHeight)
.withPositionRelative(WINDOW_WIDTH - Window.MARGIN_RIGHT - buttonWidth, bottomButtonsY);
this.editorWindow.putRenderingBo(bo);
this.editorWindow.draw();
}
private int addTextFieldRow(final String label, final String fieldId, final String value, final int yPos) {
var bo = this.renderingBoPool.acquire("ThemeEditor_Label_" + fieldId, ControlRenderingBo.class)
.setCaption(label)
.setFontSize(30)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(LABEL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10, yPos + 10);
this.scrollArea.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(fieldId, ControlRenderingBo.class)
.setCaption(value != null ? value : "")
.setFontSize(28)
.setType(TextfieldControl.class)
.setControlData(new TextfieldData(false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(CONTROL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10 + LABEL_WIDTH + SPACING, yPos);
this.scrollArea.putRenderingBo(bo);
return yPos + ROW_HEIGHT + SPACING;
}
private int addPopupRow(final String label, final String fieldId, final String value, final int yPos) {
var bo = this.renderingBoPool.acquire("ThemeEditor_Label_" + fieldId, ControlRenderingBo.class)
.setCaption(label)
.setFontSize(30)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(LABEL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10, yPos + 10);
this.scrollArea.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(fieldId, ControlRenderingBo.class)
.setCaption(value != null ? value : "")
.setFontSize(28)
.setType(ButtonControl.class)
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(CONTROL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10 + LABEL_WIDTH + SPACING, yPos);
this.scrollArea.putRenderingBo(bo);
return yPos + ROW_HEIGHT + SPACING;
}
private int addSliderRow(final String label, final String fieldId, final float value, final int yPos) {
var bo = this.renderingBoPool.acquire("ThemeEditor_Label_" + fieldId, ControlRenderingBo.class)
.setCaption(label)
.setFontSize(30)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(LABEL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10, yPos + 10);
this.scrollArea.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(fieldId, ControlRenderingBo.class)
.setType(SliderControl.class)
.setControlData(new SliderData(true, value, value, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(CONTROL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10 + LABEL_WIDTH + SPACING, yPos);
this.scrollArea.putRenderingBo(bo);
bo = this.renderingBoPool.acquire(fieldId + "_Value", ControlRenderingBo.class)
.setCaption(this.formatValue(value))
.setFontSize(26)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(VALUE_WIDTH, ROW_HEIGHT)
.withPositionRelative(10 + LABEL_WIDTH + SPACING + CONTROL_WIDTH + SPACING, yPos + 10);
this.scrollArea.putRenderingBo(bo);
return yPos + ROW_HEIGHT + SPACING;
}
private int addColorRow(final String label, final String fieldId, final HSBColor color, final int yPos) {
var bo = this.renderingBoPool.acquire("ThemeEditor_Label_" + fieldId, ControlRenderingBo.class)
.setCaption(label)
.setFontSize(30)
.setType(LabelControl.class)
.setControlData(new LabelData(null, null, HorizontalAlignment.LEFT))
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(LABEL_WIDTH, ROW_HEIGHT)
.withPositionRelative(10, yPos + 10);
this.scrollArea.putRenderingBo(bo);
final int rectWidth = CONTROL_WIDTH - VALUE_WIDTH - SPACING;
final DrawEffectRenderingBo rectBo = this.renderingBoPool.acquire("ThemeEditor_ColorRect_" + fieldId, DrawEffectRenderingBo.class);
rectBo.setEffect(DrawEffectRenderingBoEffects.RECT_FILLED);
rectBo.setRelativeCoordinates(List.of(ImmutableCoordI2.create(rectWidth, ROW_HEIGHT)));
rectBo.setColor(this.toRgbaColor(color));
rectBo.withPositionRelative(10 + LABEL_WIDTH + SPACING, yPos);
this.scrollArea.putRenderingBo(rectBo);
bo = this.renderingBoPool.acquire(fieldId, ControlRenderingBo.class)
.setCaption("Edit")
.setFontSize(26)
.setType(ButtonControl.class)
.setControlData(new ControlRenderingBo.ButtonData(null, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(VALUE_WIDTH, ROW_HEIGHT)
.withPositionRelative(10 + LABEL_WIDTH + SPACING + rectWidth + SPACING, yPos);
this.scrollArea.putRenderingBo(bo);
this.colorSlotByControlId.put(fieldId, this.resolveColorSlot(fieldId));
return yPos + ROW_HEIGHT + SPACING;
}
private void onUiControlEvent(final UiControlEvent event) {
if (event.getId() == null) {
return;
}
switch (event.getId()) {
case "ThemeEditor_Button_Save" -> this.onSave();
case "ThemeEditor_Button_Cancel" -> this.onCancel();
case "ThemeEditor_Field_Name" -> this.onNameChanged(event.getPayload());
case "ThemeEditor_Field_Pattern" -> this.openPatternPopup();
case "ThemeEditor_Field_Font" -> this.openFontPopup();
case "ThemeEditor_Field_Hover" -> this.onHoverChanged(event.getPayload());
case "ThemeEditor_Field_Pressed" -> this.onPressedChanged(event.getPayload());
case "ThemeEditor_Popup_Menu" -> this.onPopupSelected(event.getPayload());
default -> this.handleColorButton(event.getId());
}
}
private void onNameChanged(final Object payload) {
if (payload instanceof final String text) {
this.name = text.trim();
}
}
private void onHoverChanged(final Object payload) {
if (payload instanceof final Double value) {
this.hoverBrightnessAdjustment = this.clamp01(value.floatValue());
this.updateValueLabel("ThemeEditor_Field_Hover_Value", this.hoverBrightnessAdjustment);
this.updateSliderValue("ThemeEditor_Field_Hover", value);
this.editorWindow.draw();
this.applyCurrentThemePreview();
}
}
private void onPressedChanged(final Object payload) {
if (payload instanceof final Double value) {
this.pressedBrightnessAdjustment = this.clamp01(value.floatValue());
this.updateValueLabel("ThemeEditor_Field_Pressed_Value", this.pressedBrightnessAdjustment);
this.updateSliderValue("ThemeEditor_Field_Pressed", value);
this.editorWindow.draw();
this.applyCurrentThemePreview();
}
}
private void handleColorButton(final String controlId) {
final var slot = this.colorSlotByControlId.get(controlId);
if (slot == null) {
return;
}
final var color = this.getColor(slot);
final var requestId = "ThemeEditor_ColorPicker_" + slot.name();
this.pendingColorRequestId = requestId;
this.hsbColorPickerModule.open(requestId, slot.displayName, color, this.editorWindow);
}
private void onColorPickerResponse(final HsbColorPickerResponseEvent event) {
if (this.pendingColorRequestId == null || !this.pendingColorRequestId.equals(event.requestId())) {
return;
}
if (!event.confirmed() || event.color() == null) {
return;
}
final var slot = ColorSlot.valueOf(event.requestId().substring("ThemeEditor_ColorPicker_".length()));
this.setColor(slot, event.color());
this.updateColorButton(slot);
this.applyCurrentThemePreview();
this.editorWindow.draw();
}
private void openPatternPopup() {
final List<String> items = this.themeManager.getPatternRendererNames();
if (this.popupTarget == null) {
this.popupTarget = PopupTarget.PATTERN_RENDERER;
this.showPopup(items, "ThemeEditor_Field_Pattern");
}
else {
this.hidePopup();
this.editorWindow.draw();
}
}
private void openFontPopup() {
final List<String> items = this.getAvailableFonts();
if (this.popupTarget == null) {
this.popupTarget = PopupTarget.FONT;
this.showPopup(items, "ThemeEditor_Field_Font");
}
else {
this.hidePopup();
this.editorWindow.draw();
}
}
private void onPopupSelected(final Object payload) {
final String value;
if (payload instanceof final String id) {
value = id;
}
else if (payload instanceof final PopupMenuItem item) {
value = item.id();
}
else {
return;
}
if (this.popupTarget == PopupTarget.PATTERN_RENDERER) {
this.patternRendererName = value;
this.updateButtonCaption("ThemeEditor_Field_Pattern", value);
}
else if (this.popupTarget == PopupTarget.FONT) {
this.font = value;
this.updateButtonCaption("ThemeEditor_Field_Font", value);
}
this.hidePopup();
this.editorWindow.draw();
this.applyCurrentThemePreview();
}
private void showPopup(final List<String> items, final String anchorId) {
if (items == null || items.isEmpty()) {
return;
}
final var bo = this.editorWindow.getRenderingBo(anchorId, ControlRenderingBo.class);
if (!(bo instanceof final RenderingBoHasDimension boDim)) {
return;
}
final var popupItems = items.stream()
.map(item -> new PopupMenuItem(item, item, true))
.toList();
final var rowHeight = 32;
final var padding = 8;
final var width = Math.min(420, boDim.getWidth());
final var height = (rowHeight * popupItems.size()) + (padding * 2);
final var offset = this.scrollArea.getRelativePosition();
final var x = offset.getX() + bo.getLocalX();
final var y = offset.getY() + bo.getLocalY() + boDim.getHeight() + 6;
final var data = new PopupMenuData(popupItems, null, null, null, rowHeight, padding);
final var popup = this.renderingBoPool.acquire("ThemeEditor_Popup_Menu", ControlRenderingBo.class)
.setType(PopupMenuControl.class)
.setFontSize(22)
.setVisible(true)
.setEnabled(true)
.setControlData(data)
.withDimensionAbsolute(width, height)
.setLayer(RenderingBoLayer.UI1)
.withPositionRelative(x, y);
this.editorWindow.putRenderingBo(popup);
this.editorWindow.draw();
}
private void hidePopup() {
this.editorWindow.removeRenderingBo("ThemeEditor_Popup_Menu");
this.popupTarget = null;
}
private void updateButtonCaption(final String id, final String caption) {
final var bo = this.editorWindow.getRenderingBo(id, ControlRenderingBo.class);
if (bo != null) {
bo.setCaption(caption != null ? caption : "");
this.editorWindow.putRenderingBo(bo);
}
}
private void updateValueLabel(final String id, final float value) {
final var bo = this.editorWindow.getRenderingBo(id, ControlRenderingBo.class);
if (bo != null) {
bo.setCaption(this.formatValue(value));
this.editorWindow.putRenderingBo(bo);
}
}
private void updateSliderValue(final String id, final double value) {
final var bo = this.editorWindow.getRenderingBo(id, ControlRenderingBo.class);
if (bo != null) {
final var sliderData = new SliderData(true, value, value, null);
bo.setControlData(sliderData);
this.editorWindow.putRenderingBo(bo);
}
}
private void updateColorButton(final ColorSlot slot) {
final var controlId = slot.controlId;
final var rectBo = this.editorWindow.getRenderingBo("ThemeEditor_ColorRect_" + controlId, DrawEffectRenderingBo.class);
if (rectBo != null) {
rectBo.setColor(this.toRgbaColor(this.getColor(slot)));
this.editorWindow.putRenderingBo(rectBo);
}
}
private List<String> getAvailableFonts() {
final var fonts = new ArrayList<String>();
for (final var name : this.staticResourceService.listPath(List.of("renderer"))) {
final var lower = name.toLowerCase();
if (lower.endsWith(".ttf") || lower.endsWith(".otf")) {
fonts.add(name);
}
}
fonts.sort(String::compareToIgnoreCase);
return fonts;
}
private void onSave() {
if (this.name == null || this.name.isBlank()) {
return;
}
final var theme = new Theme(
this.name.trim(),
this.patternRendererName,
this.font,
this.borderOuter,
this.borderInner,
this.borderInnerDisabled,
this.backgroundNormal,
this.backgroundSelected,
this.textNormal,
this.textDisabled,
this.selectionPrimary,
this.selectionSecondary,
this.windowBackground,
this.hoverBrightnessAdjustment,
this.pressedBrightnessAdjustment);
this.storageService.store(CUSTOM_THEME_PATH, theme.getName(), theme);
if (this.originalName != null && !this.originalName.equalsIgnoreCase(theme.getName())) {
this.storageService.deleteByPath(CUSTOM_THEME_PATH, this.originalName);
}
this.themeManager.setCurrentTheme(theme);
this.eventService.fire(new OptionsThemeEditorSavedEvent(true, theme));
this.close();
}
private void onCancel() {
if (this.originalTheme != null) {
this.themeManager.setCurrentTheme(this.originalTheme);
}
this.eventService.fire(new OptionsThemeEditorSavedEvent(false, null));
this.close();
}
private void applyCurrentThemePreview() {
final var previewTheme = new Theme(
this.name != null && !this.name.isBlank() ? this.name : "Preview",
this.patternRendererName,
this.font,
this.borderOuter,
this.borderInner,
this.borderInnerDisabled,
this.backgroundNormal,
this.backgroundSelected,
this.textNormal,
this.textDisabled,
this.selectionPrimary,
this.selectionSecondary,
this.windowBackground,
this.hoverBrightnessAdjustment,
this.pressedBrightnessAdjustment);
this.themeManager.setCurrentTheme(previewTheme);
}
private void close() {
this.getModuleService().changeActiveState(this.getId(), false);
this.getModuleService().changeDisplayState(this.getId(), false);
}
private float clamp01(final float value) {
return Math.max(0.0f, Math.min(1.0f, value));
}
private String formatValue(final float value) {
return String.format("%.2f", value);
}
private HSBColor getColor(final ColorSlot slot) {
return switch (slot) {
case BORDER_OUTER -> this.borderOuter;
case BORDER_INNER -> this.borderInner;
case BORDER_INNER_DISABLED -> this.borderInnerDisabled;
case BACKGROUND_NORMAL -> this.backgroundNormal;
case BACKGROUND_SELECTED -> this.backgroundSelected;
case TEXT_NORMAL -> this.textNormal;
case TEXT_DISABLED -> this.textDisabled;
case SELECTION_PRIMARY -> this.selectionPrimary;
case SELECTION_SECONDARY -> this.selectionSecondary;
case WINDOW_BACKGROUND -> this.windowBackground;
};
}
private void setColor(final ColorSlot slot, final HSBColor color) {
switch (slot) {
case BORDER_OUTER -> this.borderOuter = color;
case BORDER_INNER -> this.borderInner = color;
case BORDER_INNER_DISABLED -> this.borderInnerDisabled = color;
case BACKGROUND_NORMAL -> this.backgroundNormal = color;
case BACKGROUND_SELECTED -> this.backgroundSelected = color;
case TEXT_NORMAL -> this.textNormal = color;
case TEXT_DISABLED -> this.textDisabled = color;
case SELECTION_PRIMARY -> this.selectionPrimary = color;
case SELECTION_SECONDARY -> this.selectionSecondary = color;
case WINDOW_BACKGROUND -> this.windowBackground = color;
}
}
private ImmutableRgbaColor toRgbaColor(final HSBColor color) {
return new ImmutableRgbaColor(color.toColor());
}
private ColorSlot resolveColorSlot(final String controlId) {
return switch (controlId) {
case "ThemeEditor_Color_BorderOuter" -> ColorSlot.BORDER_OUTER;
case "ThemeEditor_Color_BorderInner" -> ColorSlot.BORDER_INNER;
case "ThemeEditor_Color_BorderInnerDisabled" -> ColorSlot.BORDER_INNER_DISABLED;
case "ThemeEditor_Color_BackgroundNormal" -> ColorSlot.BACKGROUND_NORMAL;
case "ThemeEditor_Color_BackgroundSelected" -> ColorSlot.BACKGROUND_SELECTED;
case "ThemeEditor_Color_TextNormal" -> ColorSlot.TEXT_NORMAL;
case "ThemeEditor_Color_TextDisabled" -> ColorSlot.TEXT_DISABLED;
case "ThemeEditor_Color_SelectionPrimary" -> ColorSlot.SELECTION_PRIMARY;
case "ThemeEditor_Color_SelectionSecondary" -> ColorSlot.SELECTION_SECONDARY;
case "ThemeEditor_Color_WindowBackground" -> ColorSlot.WINDOW_BACKGROUND;
default -> null;
};
}
private ImmutableCoordI2 centeredPosition(final de.extio.game_engine.spatial2.model.CoordI2 dimension) {
final var referenceResolution = RendererControl.REFERENCE_RESOLUTION;
final var x = Math.max(0, (referenceResolution.getX() - dimension.getX()) / 2);
final var y = Math.max(0, (referenceResolution.getY() - dimension.getY()) / 2);
return ImmutableCoordI2.create(x, y);
}
private enum PopupTarget {
PATTERN_RENDERER,
FONT
}
private enum ColorSlot {
BORDER_OUTER("ThemeEditor_Color_BorderOuter", "Border Outer"),
BORDER_INNER("ThemeEditor_Color_BorderInner", "Border Inner"),
BORDER_INNER_DISABLED("ThemeEditor_Color_BorderInnerDisabled", "Border Inner Disabled"),
BACKGROUND_NORMAL("ThemeEditor_Color_BackgroundNormal", "Background Normal"),
BACKGROUND_SELECTED("ThemeEditor_Color_BackgroundSelected", "Background Selected"),
TEXT_NORMAL("ThemeEditor_Color_TextNormal", "Text Normal"),
TEXT_DISABLED("ThemeEditor_Color_TextDisabled", "Text Disabled"),
SELECTION_PRIMARY("ThemeEditor_Color_SelectionPrimary", "Selection Primary"),
SELECTION_SECONDARY("ThemeEditor_Color_SelectionSecondary", "Selection Secondary"),
WINDOW_BACKGROUND("ThemeEditor_Color_WindowBackground", "Window Background");
private final String controlId;
private final String displayName;
ColorSlot(final String controlId, final String displayName) {
this.controlId = controlId;
this.displayName = displayName;
}
}
}

View File

@@ -7,3 +7,4 @@ de.extio.game_engine.module.ModuleServiceAutoConfiguration
de.extio.game_engine.i18n.LocalizationAutoConfiguration de.extio.game_engine.i18n.LocalizationAutoConfiguration
de.extio.game_engine.resource.StaticResourceAutoConfiguration de.extio.game_engine.resource.StaticResourceAutoConfiguration
de.extio.game_engine.audio.AudioAutoConfiguration de.extio.game_engine.audio.AudioAutoConfiguration
de.extio.game_engine.menu.MenuAutoConfiguration