package net.minecraft.client.gui.screens; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.mojang.blaze3d.platform.InputConstants; import com.mojang.logging.LogUtils; import java.net.URI; import java.nio.file.Path; import java.util.Comparator; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.CrashReportDetail; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.NarratorStatus; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.CycleButton; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.components.TabOrderedElement; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.narration.ScreenNarrationCollector; import net.minecraft.client.gui.narration.NarratableEntry.NarrationPriority; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenDirection; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.navigation.FocusNavigationEvent.ArrowNavigation; import net.minecraft.client.gui.navigation.FocusNavigationEvent.InitialFocus; import net.minecraft.client.gui.navigation.FocusNavigationEvent.TabNavigation; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; import net.minecraft.client.renderer.CubeMap; import net.minecraft.client.renderer.PanoramaRenderer; import net.minecraft.client.renderer.RenderType; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.Music; import net.minecraft.util.FormattedCharSequence; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag.Default; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public abstract class Screen extends AbstractContainerEventHandler implements Renderable { private static final Logger LOGGER = LogUtils.getLogger(); private static final Component USAGE_NARRATION = Component.translatable("narrator.screen.usage"); protected static final CubeMap CUBE_MAP = new CubeMap(ResourceLocation.withDefaultNamespace("textures/gui/title/background/panorama")); protected static final PanoramaRenderer PANORAMA = new PanoramaRenderer(CUBE_MAP); public static final ResourceLocation MENU_BACKGROUND = ResourceLocation.withDefaultNamespace("textures/gui/menu_background.png"); public static final ResourceLocation HEADER_SEPARATOR = ResourceLocation.withDefaultNamespace("textures/gui/header_separator.png"); public static final ResourceLocation FOOTER_SEPARATOR = ResourceLocation.withDefaultNamespace("textures/gui/footer_separator.png"); private static final ResourceLocation INWORLD_MENU_BACKGROUND = ResourceLocation.withDefaultNamespace("textures/gui/inworld_menu_background.png"); public static final ResourceLocation INWORLD_HEADER_SEPARATOR = ResourceLocation.withDefaultNamespace("textures/gui/inworld_header_separator.png"); public static final ResourceLocation INWORLD_FOOTER_SEPARATOR = ResourceLocation.withDefaultNamespace("textures/gui/inworld_footer_separator.png"); protected final Component title; private final List children = Lists.newArrayList(); private final List narratables = Lists.newArrayList(); @Nullable protected Minecraft minecraft; private boolean initialized; public int width; public int height; private final List renderables = Lists.newArrayList(); protected Font font; private static final long NARRATE_SUPPRESS_AFTER_INIT_TIME = TimeUnit.SECONDS.toMillis(2L); private static final long NARRATE_DELAY_NARRATOR_ENABLED = NARRATE_SUPPRESS_AFTER_INIT_TIME; private static final long NARRATE_DELAY_MOUSE_MOVE = 750L; private static final long NARRATE_DELAY_MOUSE_ACTION = 200L; private static final long NARRATE_DELAY_KEYBOARD_ACTION = 200L; private final ScreenNarrationCollector narrationState = new ScreenNarrationCollector(); private long narrationSuppressTime = Long.MIN_VALUE; private long nextNarrationTime = Long.MAX_VALUE; @Nullable protected CycleButton narratorButton; @Nullable private NarratableEntry lastNarratable; @Nullable private Screen.DeferredTooltipRendering deferredTooltipRendering; protected final Executor screenExecutor = runnable -> this.minecraft.execute(() -> { if (this.minecraft.screen == this) { runnable.run(); } }); protected Screen(Component title) { this.title = title; } public Component getTitle() { return this.title; } public Component getNarrationMessage() { return this.getTitle(); } public final void renderWithTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { this.render(guiGraphics, mouseX, mouseY, partialTick); if (this.deferredTooltipRendering != null) { guiGraphics.renderTooltip(this.font, this.deferredTooltipRendering.tooltip(), this.deferredTooltipRendering.positioner(), mouseX, mouseY); this.deferredTooltipRendering = null; } } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); for (Renderable renderable : this.renderables) { renderable.render(guiGraphics, mouseX, mouseY, partialTick); } } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (keyCode == 256 && this.shouldCloseOnEsc()) { this.onClose(); return true; } else if (super.keyPressed(keyCode, scanCode, modifiers)) { return true; } else { FocusNavigationEvent focusNavigationEvent = (FocusNavigationEvent)(switch (keyCode) { case 258 -> this.createTabEvent(); default -> null; case 262 -> this.createArrowEvent(ScreenDirection.RIGHT); case 263 -> this.createArrowEvent(ScreenDirection.LEFT); case 264 -> this.createArrowEvent(ScreenDirection.DOWN); case 265 -> this.createArrowEvent(ScreenDirection.UP); }); if (focusNavigationEvent != null) { ComponentPath componentPath = super.nextFocusPath(focusNavigationEvent); if (componentPath == null && focusNavigationEvent instanceof TabNavigation) { this.clearFocus(); componentPath = super.nextFocusPath(focusNavigationEvent); } if (componentPath != null) { this.changeFocus(componentPath); } } return false; } } private TabNavigation createTabEvent() { boolean bl = !hasShiftDown(); return new TabNavigation(bl); } private ArrowNavigation createArrowEvent(ScreenDirection direction) { return new ArrowNavigation(direction); } protected void setInitialFocus() { if (this.minecraft.getLastInputType().isKeyboard()) { TabNavigation tabNavigation = new TabNavigation(true); ComponentPath componentPath = super.nextFocusPath(tabNavigation); if (componentPath != null) { this.changeFocus(componentPath); } } } protected void setInitialFocus(GuiEventListener listener) { ComponentPath componentPath = ComponentPath.path(this, listener.nextFocusPath(new InitialFocus())); if (componentPath != null) { this.changeFocus(componentPath); } } public void clearFocus() { ComponentPath componentPath = this.getCurrentFocusPath(); if (componentPath != null) { componentPath.applyFocus(false); } } @VisibleForTesting protected void changeFocus(ComponentPath path) { this.clearFocus(); path.applyFocus(true); } public boolean shouldCloseOnEsc() { return true; } public void onClose() { this.minecraft.setScreen(null); } protected T addRenderableWidget(T widget) { this.renderables.add(widget); return this.addWidget(widget); } protected T addRenderableOnly(T renderable) { this.renderables.add(renderable); return renderable; } protected T addWidget(T listener) { this.children.add(listener); this.narratables.add(listener); return listener; } protected void removeWidget(GuiEventListener listener) { if (listener instanceof Renderable) { this.renderables.remove((Renderable)listener); } if (listener instanceof NarratableEntry) { this.narratables.remove((NarratableEntry)listener); } this.children.remove(listener); } protected void clearWidgets() { this.renderables.clear(); this.children.clear(); this.narratables.clear(); } public static List getTooltipFromItem(Minecraft minecraft, ItemStack item) { return item.getTooltipLines( Item.TooltipContext.of(minecraft.level), minecraft.player, minecraft.options.advancedItemTooltips ? Default.ADVANCED : Default.NORMAL ); } protected void insertText(String text, boolean overwrite) { } public boolean handleComponentClicked(@Nullable Style style) { if (style == null) { return false; } else { ClickEvent clickEvent = style.getClickEvent(); if (hasShiftDown()) { if (style.getInsertion() != null) { this.insertText(style.getInsertion(), false); } } else if (clickEvent != null) { switch (clickEvent) { case ClickEvent.OpenUrl(URI var28): URI var19 = var28; if (!this.minecraft.options.chatLinks().get()) { return false; } if (this.minecraft.options.chatLinksPrompt().get()) { this.minecraft.setScreen(new ConfirmLinkScreen(bl -> { if (bl) { Util.getPlatform().openUri(var19); } this.minecraft.setScreen(this); }, var19.toString(), false)); } else { Util.getPlatform().openUri(var19); } break; case ClickEvent.OpenFile openFile: Util.getPlatform().openFile(openFile.file()); break; case ClickEvent.SuggestCommand(String var20): this.insertText(var20, true); break; case ClickEvent.RunCommand(String var24): String var21 = var24; String string2 = var21; if (var21.startsWith("/")) { string2 = var21.substring(1); } if (!this.minecraft.player.connection.sendUnsignedCommand(string2)) { LOGGER.error("Not allowed to run command with signed argument from click event: '{}'", string2); } break; case ClickEvent.CopyToClipboard(String var14): this.minecraft.keyboardHandler.setClipboard(var14); break; default: LOGGER.error("Don't know how to handle {}", clickEvent); } return true; } return false; } } public final void init(Minecraft minecraft, int width, int height) { this.minecraft = minecraft; this.font = minecraft.font; this.width = width; this.height = height; if (!this.initialized) { this.init(); this.setInitialFocus(); } else { this.repositionElements(); } this.initialized = true; this.triggerImmediateNarration(false); this.suppressNarration(NARRATE_SUPPRESS_AFTER_INIT_TIME); } protected void rebuildWidgets() { this.clearWidgets(); this.clearFocus(); this.init(); this.setInitialFocus(); } @Override public List children() { return this.children; } protected void init() { } public void tick() { } public void removed() { } public void added() { } public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { if (this.minecraft.level == null) { this.renderPanorama(guiGraphics, partialTick); } this.renderBlurredBackground(); this.renderMenuBackground(guiGraphics); } protected void renderBlurredBackground() { this.minecraft.gameRenderer.processBlurEffect(); } protected void renderPanorama(GuiGraphics guiGraphics, float partialTick) { PANORAMA.render(guiGraphics, this.width, this.height, 1.0F, partialTick); } protected void renderMenuBackground(GuiGraphics partialTick) { this.renderMenuBackground(partialTick, 0, 0, this.width, this.height); } protected void renderMenuBackground(GuiGraphics guiGraphics, int x, int y, int width, int height) { renderMenuBackgroundTexture(guiGraphics, this.minecraft.level == null ? MENU_BACKGROUND : INWORLD_MENU_BACKGROUND, x, y, 0.0F, 0.0F, width, height); } public static void renderMenuBackgroundTexture( GuiGraphics guiGraphics, ResourceLocation texture, int x, int y, float uOffset, float vOffset, int width, int height ) { int i = 32; guiGraphics.blit(RenderType::guiTextured, texture, x, y, uOffset, vOffset, width, height, 32, 32); } public void renderTransparentBackground(GuiGraphics guiGraphics) { guiGraphics.fillGradient(0, 0, this.width, this.height, -1072689136, -804253680); } public boolean isPauseScreen() { return true; } public static boolean hasControlDown() { return Minecraft.ON_OSX ? InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 343) || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 347) : InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 341) || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 345); } public static boolean hasShiftDown() { return InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 340) || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 344); } public static boolean hasAltDown() { return InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 342) || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 346); } public static boolean isCut(int keyCode) { return keyCode == 88 && hasControlDown() && !hasShiftDown() && !hasAltDown(); } public static boolean isPaste(int keyCode) { return keyCode == 86 && hasControlDown() && !hasShiftDown() && !hasAltDown(); } public static boolean isCopy(int keyCode) { return keyCode == 67 && hasControlDown() && !hasShiftDown() && !hasAltDown(); } public static boolean isSelectAll(int keyCode) { return keyCode == 65 && hasControlDown() && !hasShiftDown() && !hasAltDown(); } protected void repositionElements() { this.rebuildWidgets(); } public void resize(Minecraft minecraft, int width, int height) { this.width = width; this.height = height; this.repositionElements(); } public void fillCrashDetails(CrashReport crashReport) { CrashReportCategory crashReportCategory = crashReport.addCategory("Affected screen", 1); crashReportCategory.setDetail("Screen name", (CrashReportDetail)(() -> this.getClass().getCanonicalName())); } protected boolean isValidCharacterForName(String text, char charTyped, int cursorPos) { int i = text.indexOf(58); int j = text.indexOf(47); if (charTyped == ':') { return (j == -1 || cursorPos <= j) && i == -1; } else { return charTyped == '/' ? cursorPos > i : charTyped == '_' || charTyped == '-' || charTyped >= 'a' && charTyped <= 'z' || charTyped >= '0' && charTyped <= '9' || charTyped == '.'; } } @Override public boolean isMouseOver(double mouseX, double mouseY) { return true; } public void onFilesDrop(List packs) { } private void scheduleNarration(long delay, boolean stopSuppression) { this.nextNarrationTime = Util.getMillis() + delay; if (stopSuppression) { this.narrationSuppressTime = Long.MIN_VALUE; } } private void suppressNarration(long time) { this.narrationSuppressTime = Util.getMillis() + time; } public void afterMouseMove() { this.scheduleNarration(750L, false); } public void afterMouseAction() { this.scheduleNarration(200L, true); } public void afterKeyboardAction() { this.scheduleNarration(200L, true); } private boolean shouldRunNarration() { return this.minecraft.getNarrator().isActive(); } public void handleDelayedNarration() { if (this.shouldRunNarration()) { long l = Util.getMillis(); if (l > this.nextNarrationTime && l > this.narrationSuppressTime) { this.runNarration(true); this.nextNarrationTime = Long.MAX_VALUE; } } } public void triggerImmediateNarration(boolean onlyNarrateNew) { if (this.shouldRunNarration()) { this.runNarration(onlyNarrateNew); } } private void runNarration(boolean onlyNarrateNew) { this.narrationState.update(this::updateNarrationState); String string = this.narrationState.collectNarrationText(!onlyNarrateNew); if (!string.isEmpty()) { this.minecraft.getNarrator().sayNow(string); } } protected boolean shouldNarrateNavigation() { return true; } protected void updateNarrationState(NarrationElementOutput output) { output.add(NarratedElementType.TITLE, this.getNarrationMessage()); if (this.shouldNarrateNavigation()) { output.add(NarratedElementType.USAGE, USAGE_NARRATION); } this.updateNarratedWidget(output); } protected void updateNarratedWidget(NarrationElementOutput narrationElementOutput) { List list = this.narratables .stream() .flatMap(narratableEntry -> narratableEntry.getNarratables().stream()) .filter(NarratableEntry::isActive) .sorted(Comparator.comparingInt(TabOrderedElement::getTabOrderGroup)) .toList(); Screen.NarratableSearchResult narratableSearchResult = findNarratableWidget(list, this.lastNarratable); if (narratableSearchResult != null) { if (narratableSearchResult.priority.isTerminal()) { this.lastNarratable = narratableSearchResult.entry; } if (list.size() > 1) { narrationElementOutput.add(NarratedElementType.POSITION, Component.translatable("narrator.position.screen", narratableSearchResult.index + 1, list.size())); if (narratableSearchResult.priority == NarrationPriority.FOCUSED) { narrationElementOutput.add(NarratedElementType.USAGE, this.getUsageNarration()); } } narratableSearchResult.entry.updateNarration(narrationElementOutput.nest()); } } protected Component getUsageNarration() { return Component.translatable("narration.component_list.usage"); } @Nullable public static Screen.NarratableSearchResult findNarratableWidget(List entries, @Nullable NarratableEntry target) { Screen.NarratableSearchResult narratableSearchResult = null; Screen.NarratableSearchResult narratableSearchResult2 = null; int i = 0; for (int j = entries.size(); i < j; i++) { NarratableEntry narratableEntry = (NarratableEntry)entries.get(i); NarrationPriority narrationPriority = narratableEntry.narrationPriority(); if (narrationPriority.isTerminal()) { if (narratableEntry != target) { return new Screen.NarratableSearchResult(narratableEntry, i, narrationPriority); } narratableSearchResult2 = new Screen.NarratableSearchResult(narratableEntry, i, narrationPriority); } else if (narrationPriority.compareTo(narratableSearchResult != null ? narratableSearchResult.priority : NarrationPriority.NONE) > 0) { narratableSearchResult = new Screen.NarratableSearchResult(narratableEntry, i, narrationPriority); } } return narratableSearchResult != null ? narratableSearchResult : narratableSearchResult2; } public void updateNarratorStatus(boolean narratorEnabled) { if (narratorEnabled) { this.scheduleNarration(NARRATE_DELAY_NARRATOR_ENABLED, false); } if (this.narratorButton != null) { this.narratorButton.setValue(this.minecraft.options.narrator().get()); } } protected void clearTooltipForNextRenderPass() { this.deferredTooltipRendering = null; } public void setTooltipForNextRenderPass(List tooltip) { this.setTooltipForNextRenderPass(tooltip, DefaultTooltipPositioner.INSTANCE, true); } public void setTooltipForNextRenderPass(List tooltip, ClientTooltipPositioner positioner, boolean override) { if (this.deferredTooltipRendering == null || override) { this.deferredTooltipRendering = new Screen.DeferredTooltipRendering(tooltip, positioner); } } public void setTooltipForNextRenderPass(Component tooltip) { this.setTooltipForNextRenderPass(Tooltip.splitTooltip(this.minecraft, tooltip)); } public void setTooltipForNextRenderPass(Tooltip tooltip, ClientTooltipPositioner positioner, boolean override) { this.setTooltipForNextRenderPass(tooltip.toCharSequence(this.minecraft), positioner, override); } public Font getFont() { return this.font; } public boolean showsActiveEffects() { return false; } @Override public ScreenRectangle getRectangle() { return new ScreenRectangle(0, 0, this.width, this.height); } @Nullable public Music getBackgroundMusic() { return null; } @Environment(EnvType.CLIENT) record DeferredTooltipRendering(List tooltip, ClientTooltipPositioner positioner) { } @Environment(EnvType.CLIENT) public static class NarratableSearchResult { public final NarratableEntry entry; public final int index; public final NarrationPriority priority; public NarratableSearchResult(NarratableEntry entry, int index, NarrationPriority priority) { this.entry = entry; this.index = index; this.priority = priority; } } }