/* * Copyright 2014 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.nui.internal; import com.google.common.base.Objects; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.utilities.Assets; import org.terasology.assets.ResourceUrn; import org.terasology.assets.management.AssetManager; import org.terasology.assets.module.ModuleAwareAssetTypeManager; import org.terasology.context.Context; import org.terasology.engine.SimpleUri; import org.terasology.engine.module.ModuleManager; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.entitySystem.event.EventPriority; import org.terasology.entitySystem.event.ReceiveEvent; import org.terasology.entitySystem.systems.BaseComponentSystem; import org.terasology.i18n.TranslationSystem; import org.terasology.input.BindButtonEvent; import org.terasology.input.InputSystem; import org.terasology.input.device.KeyboardDevice; import org.terasology.input.device.MouseDevice; import org.terasology.input.events.KeyEvent; import org.terasology.input.events.MouseAxisEvent; import org.terasology.input.events.MouseButtonEvent; import org.terasology.input.events.MouseWheelEvent; import org.terasology.logic.players.LocalPlayer; import org.terasology.module.ModuleEnvironment; import org.terasology.network.ClientComponent; import org.terasology.reflection.metadata.ClassLibrary; import org.terasology.registry.InjectionHelper; import org.terasology.rendering.nui.ControlWidget; import org.terasology.rendering.nui.CoreScreenLayer; import org.terasology.rendering.nui.NUIManager; import org.terasology.rendering.nui.ScreenLayerClosedEvent; import org.terasology.rendering.nui.UIScreenLayer; import org.terasology.rendering.nui.UIWidget; import org.terasology.rendering.nui.asset.UIElement; import org.terasology.rendering.nui.events.NUIKeyEvent; import org.terasology.rendering.nui.layers.hud.HUDScreenLayer; import java.util.ArrayList; import java.util.Deque; import java.util.LinkedList; import java.util.Map; import java.util.Optional; import java.util.Set; /** */ public class NUIManagerInternal extends BaseComponentSystem implements NUIManager { private Logger logger = LoggerFactory.getLogger(NUIManagerInternal.class); private Deque<UIScreenLayer> screens = Queues.newArrayDeque(); private HUDScreenLayer hudScreenLayer; private BiMap<ResourceUrn, UIScreenLayer> screenLookup = HashBiMap.create(); private CanvasControl canvas; private WidgetLibrary widgetsLibrary; private UIWidget focus; private KeyboardDevice keyboard; private MouseDevice mouse; private boolean forceReleaseMouse; private Map<ResourceUrn, ControlWidget> overlays = Maps.newLinkedHashMap(); private Context context; private AssetManager assetManager; public NUIManagerInternal(CanvasRenderer renderer, Context context) { this.context = context; this.hudScreenLayer = new HUDScreenLayer(); InjectionHelper.inject(hudScreenLayer, context); this.canvas = new CanvasImpl(this, context, renderer); this.keyboard = context.get(InputSystem.class).getKeyboard(); this.mouse = context.get(InputSystem.class).getMouseDevice(); this.assetManager = context.get(AssetManager.class); refreshWidgetsLibrary(); TranslationSystem system = context.get(TranslationSystem.class); system.subscribe(proj -> invalidate()); // All UIElement instances are disposed so that they are not automatically reloaded // by the AssetTypeManager. Reloading would not trigger the initialise() method // and UI screens should be created on demand anyway. ModuleAwareAssetTypeManager maaTypeManager = context.get(ModuleAwareAssetTypeManager.class); maaTypeManager.getAssetType(UIElement.class).ifPresent(type -> type.disposeAll()); } public void refreshWidgetsLibrary() { widgetsLibrary = new WidgetLibrary(context); ModuleEnvironment environment = context.get(ModuleManager.class).getEnvironment(); for (Class<? extends UIWidget> type : environment.getSubtypesOf(UIWidget.class)) { widgetsLibrary.register(new SimpleUri(environment.getModuleProviding(type), type.getSimpleName()), type); } } @Override public HUDScreenLayer getHUD() { return hudScreenLayer; } @Override public boolean isHUDVisible() { return !screens.isEmpty() && screens.getLast() == hudScreenLayer; } @Override public void setHUDVisible(boolean visible) { if (visible) { if (!isHUDVisible()) { screens.addLast(hudScreenLayer); } } else { if (isHUDVisible()) { screens.removeLast(); } } } @Override public boolean isOpen(String screenUri) { return isOpen(new ResourceUrn(screenUri)); } @Override public boolean isOpen(ResourceUrn screenUri) { return screenLookup.containsKey(screenUri); } @Override public boolean isOpen(UIElement element) { return isOpen(element.getUrn()); } @Override public UIScreenLayer getScreen(ResourceUrn screenUri) { return screenLookup.get(screenUri); } @Override public UIScreenLayer getScreen(String screenUri) { return getScreen(new ResourceUrn(screenUri)); } @Override public void closeScreen(String screenUri) { closeScreen(new ResourceUrn(screenUri)); } @Override public void closeScreen(ResourceUrn screenUri) { boolean sendEvents = true; closeScreen(screenUri, sendEvents); } private void closeScreen(ResourceUrn screenUri, boolean sendEvents) { UIScreenLayer screen = screenLookup.remove(screenUri); if (screen != null) { screens.remove(screen); onCloseScreen(screen, screenUri, sendEvents); } } private void closeScreenWithoutEvent(ResourceUrn screenUri) { boolean sendEvents = false; closeScreen(screenUri, sendEvents); } @Override public void closeScreen(UIScreenLayer screen) { if (screens.remove(screen)) { ResourceUrn screenUri = screenLookup.inverse().remove(screen); onCloseScreen(screen, screenUri, true); } } private void onCloseScreen(UIScreenLayer screen, ResourceUrn screenUri, boolean sendEvents) { screen.onClosed(); if (sendEvents) { LocalPlayer localPlayer = context.get(LocalPlayer.class); if (localPlayer != null) { localPlayer.getClientEntity().send(new ScreenLayerClosedEvent(screenUri)); } } } @Override public void closeScreen(UIElement element) { closeScreen(element.getUrn()); } @Override public void closeAllScreens() { for (UIScreenLayer screen: screens) { if (screen.isLowerLayerVisible()) { closeScreen(screen); } } } @Override public void toggleScreen(String screenUri) { toggleScreen(new ResourceUrn(screenUri)); } @Override public void toggleScreen(ResourceUrn screenUri) { if (isOpen(screenUri)) { closeScreen(screenUri); } else { pushScreen(screenUri); } } @Override public void toggleScreen(UIElement element) { toggleScreen(element.getUrn()); } @Override public UIScreenLayer createScreen(String screenUri) { return createScreen(screenUri, CoreScreenLayer.class); } @Override public UIScreenLayer createScreen(ResourceUrn screenUri) { return createScreen(screenUri, CoreScreenLayer.class); } @Override public <T extends CoreScreenLayer> T createScreen(String screenUri, Class<T> expectedType) { Set<ResourceUrn> urns = assetManager.resolve(screenUri, UIElement.class); switch (urns.size()) { case 0: logger.warn("No asset found for screen '{}'", screenUri); return null; case 1: ResourceUrn urn = urns.iterator().next(); return createScreen(urn, expectedType); default: logger.warn("Multiple matches for screen '{}': {}", screenUri, urns); return null; } } @Override public <T extends CoreScreenLayer> T createScreen(ResourceUrn screenUri, Class<T> expectedType) { boolean existsAlready = !screenUri.isInstance() && assetManager.isLoaded(screenUri, UIElement.class); Optional<UIElement> opt = Assets.get(screenUri, UIElement.class); if (!opt.isPresent()) { logger.error("Can't find screen '{}'", screenUri); } else { UIElement element = opt.get(); UIWidget root = element.getRootWidget(); if (expectedType.isInstance(root)) { T screen = expectedType.cast(root); if (!existsAlready) { initialiseScreen(screen, screenUri); } return screen; } else { logger.error("Screen '{}' is a '{}' and not a '{}'", screenUri, root.getClass(), expectedType); } } return null; } @Override public CoreScreenLayer pushScreen(ResourceUrn screenUri) { return pushScreen(screenUri, CoreScreenLayer.class); } @Override public <T extends CoreScreenLayer> T pushScreen(ResourceUrn screenUri, Class<T> expectedType) { T layer = createScreen(screenUri, expectedType); if (layer != null) { pushScreen(layer); } return layer; } @Override public CoreScreenLayer pushScreen(String screenUri) { return pushScreen(screenUri, CoreScreenLayer.class); } @Override public <T extends CoreScreenLayer> T pushScreen(String screenUri, Class<T> expectedType) { T screen = createScreen(screenUri, expectedType); if (screen != null) { pushScreen(screen); } return screen; } @Override public CoreScreenLayer pushScreen(UIElement element) { return pushScreen(element, CoreScreenLayer.class); } @Override public <T extends CoreScreenLayer> T pushScreen(UIElement element, Class<T> expectedType) { if (element != null && expectedType.isInstance(element.getRootWidget())) { @SuppressWarnings("unchecked") T result = (T) element.getRootWidget(); initialiseScreen(result, element.getUrn()); pushScreen(result); return result; } return null; } private void initialiseScreen(CoreScreenLayer screen, ResourceUrn uri) { InjectionHelper.inject(screen); screen.setId(uri.toString()); screen.setManager(this); screen.initialise(); } @Override public void pushScreen(UIScreenLayer screen) { if (!screen.isLowerLayerVisible()) { UIScreenLayer current = screens.peek(); if (current != null) { current.onHide(); } } screens.push(screen); screen.onOpened(); String id = screen.getId(); if (ResourceUrn.isValid(id)) { ResourceUrn uri = new ResourceUrn(id); screenLookup.put(uri, screen); } } @Override public void popScreen() { if (!screens.isEmpty()) { UIScreenLayer top = screens.peek(); closeScreen(top); if (!top.isLowerLayerVisible()) { UIScreenLayer current = screens.peek(); if (current != null) { current.onShow(); } } } } @Override public <T extends ControlWidget> T addOverlay(String overlayUri, Class<T> expectedType) { Set<ResourceUrn> urns = assetManager.resolve(overlayUri, UIElement.class); switch (urns.size()) { case 0: logger.warn("No asset found for overlay '{}'", overlayUri); return null; case 1: ResourceUrn urn = urns.iterator().next(); return addOverlay(urn, expectedType); default: logger.warn("Multiple matches for overlay '{}': {}", overlayUri, urns); return null; } } @Override public <T extends ControlWidget> T addOverlay(ResourceUrn overlayUri, Class<T> expectedType) { boolean existsAlready = assetManager.isLoaded(overlayUri, UIElement.class); Optional<UIElement> opt = Assets.get(overlayUri, UIElement.class); if (!opt.isPresent()) { logger.error("Can't find overlay '{}'", overlayUri); } else { UIElement element = opt.get(); UIWidget root = element.getRootWidget(); if (expectedType.isInstance(root)) { T overlay = expectedType.cast(root); if (!existsAlready) { initialiseOverlay(overlay, overlayUri); } addOverlay(overlay, overlayUri); return overlay; } else { logger.error("Screen '{}' is a '{}' and not a '{}'", overlayUri, root.getClass(), expectedType); } } return null; } private <T extends ControlWidget> void initialiseOverlay(T overlay, ResourceUrn screenUri) { InjectionHelper.inject(overlay); overlay.initialise(); } @Override public <T extends ControlWidget> T addOverlay(UIElement element, Class<T> expectedType) { if (element != null && expectedType.isInstance(element.getRootWidget())) { T result = expectedType.cast(element.getRootWidget()); addOverlay(result, element.getUrn()); return result; } return null; } private void addOverlay(ControlWidget overlay, ResourceUrn uri) { overlay.onOpened(); overlays.put(uri, overlay); } @Override public void removeOverlay(UIElement overlay) { removeOverlay(overlay.getUrn()); } @Override public void removeOverlay(String uri) { Set<ResourceUrn> assetUri = Assets.resolveAssetUri(uri, UIElement.class); if (assetUri.size() == 1) { removeOverlay(assetUri.iterator().next()); } } @Override public void removeOverlay(ResourceUrn uri) { ControlWidget widget = overlays.remove(uri); if (widget != null) { widget.onClosed(); } } @Override public void clear() { overlays.values().forEach(ControlWidget::onClosed); overlays.clear(); hudScreenLayer.clear(); screens.forEach(ControlWidget::onClosed); screens.clear(); screenLookup.clear(); focus = null; forceReleaseMouse = false; } @Override public void render() { canvas.preRender(); Deque<UIScreenLayer> screensToRender = Queues.newArrayDeque(); for (UIScreenLayer layer : screens) { screensToRender.push(layer); if (!layer.isLowerLayerVisible()) { break; } } for (UIScreenLayer screen : screensToRender) { canvas.drawWidget(screen, canvas.getRegion()); } for (ControlWidget overlay : overlays.values()) { canvas.drawWidget(overlay); } canvas.postRender(); } @Override public void update(float delta) { canvas.processMousePosition(mouse.getPosition()); // part of the update could be adding/removing screens // modifying a collection while iterating of it is typically not supported for (UIScreenLayer screen : new ArrayList<>(screens)) { screen.update(delta); } for (ControlWidget widget : overlays.values()) { widget.update(delta); } InputSystem inputSystem = context.get(InputSystem.class); inputSystem.getMouseDevice().setGrabbed(inputSystem.isCapturingMouse() && !(this.isReleasingMouse())); } @Override public ClassLibrary<UIWidget> getWidgetMetadataLibrary() { return widgetsLibrary; } @Override public void setFocus(UIWidget widget) { if (widget != null && !widget.canBeFocus()) { return; } if (!Objects.equal(widget, focus)) { if (focus != null) { focus.onLoseFocus(); } focus = widget; if (focus != null) { focus.onGainFocus(); } } } @Override public UIWidget getFocus() { return focus; } @Override public boolean isReleasingMouse() { for (UIScreenLayer screen : screens) { if (screen.isReleasingMouse()) { return true; } } return forceReleaseMouse; } @Override public boolean isForceReleasingMouse() { return forceReleaseMouse; } @Override public void setForceReleasingMouse(boolean value) { forceReleaseMouse = value; } /* The following events will capture the mouse and keyboard inputs. They have high priority so the GUI will have first pick of input */ @ReceiveEvent(components = ClientComponent.class, priority = EventPriority.PRIORITY_HIGH) public void mouseAxisEvent(MouseAxisEvent event, EntityRef entity) { if (isReleasingMouse()) { event.consume(); } } //mouse button events @ReceiveEvent(components = ClientComponent.class, priority = EventPriority.PRIORITY_HIGH) public void mouseButtonEvent(MouseButtonEvent event, EntityRef entity) { if (!mouse.isVisible()) { return; } if (focus != null) { focus.onMouseButtonEvent(event); if (event.isConsumed()) { return; } } if (event.isDown()) { if (canvas.processMouseClick(event.getButton(), event.getMousePosition())) { event.consume(); } } else { if (canvas.processMouseRelease(event.getButton(), event.getMousePosition())) { event.consume(); } } if (isReleasingMouse()) { event.consume(); } } //mouse wheel events @ReceiveEvent(components = ClientComponent.class, priority = EventPriority.PRIORITY_HIGH) public void mouseWheelEvent(MouseWheelEvent event, EntityRef entity) { if (!mouse.isVisible()) { return; } if (focus != null) { focus.onMouseWheelEvent(event); if (event.isConsumed()) { return; } } if (canvas.processMouseWheel(event.getWheelTurns(), mouse.getPosition())) { event.consume(); } if (isReleasingMouse()) { event.consume(); } } //raw input events @ReceiveEvent(components = ClientComponent.class, priority = EventPriority.PRIORITY_HIGH) public void keyEvent(KeyEvent ev, EntityRef entity) { NUIKeyEvent nuiEvent = new NUIKeyEvent(mouse, keyboard, ev.getKey(), ev.getKeyCharacter(), ev.getState()); if (focus != null) { if (focus.onKeyEvent(nuiEvent)) { ev.consume(); } } // send event to screen stack if not yet consumed if (!ev.isConsumed()) { for (UIScreenLayer screen : screens) { if (screen != focus) { // explicit identity check if (screen.onKeyEvent(nuiEvent)) { ev.consume(); break; } } if (screen.isModal()) { break; } } } } //bind input events (will be send after raw input events, if a bind button was pressed and the raw input event hasn't consumed the event) @ReceiveEvent(components = ClientComponent.class, priority = EventPriority.PRIORITY_HIGH) public void bindEvent(BindButtonEvent event, EntityRef entity) { if (focus != null) { focus.onBindEvent(event); } if (!event.isConsumed()) { for (UIScreenLayer layer : screens) { if (layer.isReleasingMouse()) { layer.onBindEvent(event); if (event.isConsumed() || !layer.isLowerLayerVisible()) { break; } } } } for (UIScreenLayer screen : screens) { if (screen.isModal()) { event.consume(); return; } } } @Override public void invalidate() { assetManager.getLoadedAssets(UIElement.class).forEach(UIElement::dispose); boolean hudVisible = isHUDVisible(); if (hudVisible) { setHUDVisible(false); } Deque<ResourceUrn> reverseUrns = new LinkedList<>(); Map<UIScreenLayer, ResourceUrn> inverseLookup = screenLookup.inverse(); for (UIScreenLayer screen : screens) { screen.onClosed(); reverseUrns.addFirst(inverseLookup.get(screen)); } screens.clear(); screenLookup.clear(); reverseUrns.forEach(this::pushScreen); if (hudVisible) { setHUDVisible(true); } } }