From 0147e1ddfbab06cc45e27f2addb9da2023feceb6 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 13 Sep 2020 22:09:47 +0200 Subject: [PATCH] Fixes #4730 and also introduces repeat-pageup/pagedown when holding the mouse on the area above or below the scrollbar handle. --- .../java/appeng/client/gui/AEBaseScreen.java | 31 ++- .../client/gui/widgets/EventRepeater.java | 93 +++++++++ .../appeng/client/gui/widgets/Scrollbar.java | 182 +++++++++++++++--- 3 files changed, 277 insertions(+), 29 deletions(-) create mode 100644 src/main/java/appeng/client/gui/widgets/EventRepeater.java diff --git a/src/main/java/appeng/client/gui/AEBaseScreen.java b/src/main/java/appeng/client/gui/AEBaseScreen.java index d0d6e11a9..74e2e2668 100644 --- a/src/main/java/appeng/client/gui/AEBaseScreen.java +++ b/src/main/java/appeng/client/gui/AEBaseScreen.java @@ -229,7 +229,7 @@ public abstract class AEBaseScreen extends ContainerS RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F); if (this.getScrollBar() != null) { - this.getScrollBar().draw(this); + this.getScrollBar().draw(matrixStack, this); } this.drawFG(matrixStack, ox, oy, x, y); @@ -291,22 +291,36 @@ public abstract class AEBaseScreen extends ContainerS } } - if (this.getScrollBar() != null) { - this.getScrollBar().click(xCoord - this.guiLeft, yCoord - this.guiTop); + // Forward left mouse button down events to the scrollbar + if (btn == 0 && this.getScrollBar() != null) { + if (this.getScrollBar().mouseDown(xCoord - this.guiLeft, yCoord - this.guiTop)) { + return true; + } } return super.mouseClicked(xCoord, yCoord, btn); } @Override - public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double dragX, double dragY) { + public boolean mouseReleased(double mouseX, double mouseY, int button) { + // Forward left mouse button up events to the scrollbar + if (button == 0 && this.getScrollBar() != null) { + if (this.getScrollBar().mouseUp(mouseX - this.guiLeft, mouseY - this.guiTop)) { + return true; + } + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double dragX, double dragY) { final Slot slot = this.getSlot((int) mouseX, (int) mouseY); final ItemStack itemstack = getPlayer().inventory.getItemStack(); if (this.getScrollBar() != null) { // FIXME: Coordinate system of mouseX/mouseY is unclear - this.getScrollBar().click((int) mouseX - this.guiLeft, (int) mouseY - this.guiTop); + this.getScrollBar().mouseDragged((int) mouseX - this.guiLeft, (int) mouseY - this.guiTop); } if (slot instanceof FakeSlot && !itemstack.isEmpty()) { @@ -581,7 +595,7 @@ public abstract class AEBaseScreen extends ContainerS if (slot instanceof SlotME) { final IAEItemStack item = ((SlotME) slot).getAEStack(); if (item != null) { - ((AEBaseContainer) this.container).setTargetStack(item); + this.container.setTargetStack(item); final InventoryAction direction = wheel > 0 ? InventoryAction.ROLL_DOWN : InventoryAction.ROLL_UP; final int times = (int) Math.abs(wheel); final int inventorySize = this.getInventorySlots().size(); @@ -796,6 +810,11 @@ public abstract class AEBaseScreen extends ContainerS public void tick() { super.tick(); + + if (this.getScrollBar() != null) { + this.getScrollBar().tick(); + } + for (IGuiEventListener child : children) { if (child instanceof ITickingWidget) { ((ITickingWidget) child).tick(); diff --git a/src/main/java/appeng/client/gui/widgets/EventRepeater.java b/src/main/java/appeng/client/gui/widgets/EventRepeater.java new file mode 100644 index 000000000..bac50ec5c --- /dev/null +++ b/src/main/java/appeng/client/gui/widgets/EventRepeater.java @@ -0,0 +1,93 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.client.gui.widgets; + +import java.time.Duration; + +/** + * This class can be used to implement repeating events such as holding down a button to fire + * an event repeatedly while the button is still being held, or repeatedly scrolling down + * a page, while the mouse is held down on the scrollbar. + */ +public class EventRepeater { + + /** + * -1 if no repeat event is scheduled. + * Otherwise contains the {@link System#nanoTime()} at which the next event should occur. + */ + private long nextEventTime = -1; + + private EventCallback eventCallback = null; + + private final long eventDelay; // In nanoseconds + + private final long eventInterval; // In nanoseconds + + public EventRepeater(Duration delay, Duration interval) { + this.eventDelay = delay.toNanos(); + this.eventInterval = interval.toNanos(); + } + + public void tick() { + if (this.eventCallback == null) { + return; // No event scheduled + } + + // Use nanoTime here because it is monotonically increasing, while System.currentTimeMillis is not + long nanoTime = System.nanoTime(); + if (nanoTime < this.nextEventTime) { + return; // Event time not reached + } + + // Before triggering, recompute the next event, since + // the event callback itself may reschedule/cancel, and + // we should not overwrite that + this.nextEventTime = nanoTime + this.eventInterval; + this.eventCallback.trigger(); + } + + /** + * Schedule the given callback to be called after a given initial delay, and then + * after the given interval repeatedly. + * + *

Replaces any previously queued callback. + */ + public void repeat(EventCallback callback) { + long time = System.nanoTime(); + this.eventCallback = callback; + this.nextEventTime = time + eventDelay; + } + + public boolean isRepeating() { + return this.eventCallback != null; + } + + /** + * Stop repeating the event. + */ + public void stop() { + this.eventCallback = null; + } + + @FunctionalInterface + public interface EventCallback { + void trigger(); + } + +} diff --git a/src/main/java/appeng/client/gui/widgets/Scrollbar.java b/src/main/java/appeng/client/gui/widgets/Scrollbar.java index 4b842ed18..90ffff101 100644 --- a/src/main/java/appeng/client/gui/widgets/Scrollbar.java +++ b/src/main/java/appeng/client/gui/widgets/Scrollbar.java @@ -18,17 +18,62 @@ package appeng.client.gui.widgets; -import com.mojang.blaze3d.systems.RenderSystem; - -import net.minecraftforge.fml.client.gui.GuiUtils; - import appeng.client.gui.AEBaseScreen; +import com.mojang.blaze3d.matrix.MatrixStack; +import net.minecraft.client.gui.AbstractGui; +import net.minecraft.client.renderer.Rectangle2d; +import net.minecraft.util.ResourceLocation; -public class Scrollbar implements IScrollSource { +import java.time.Duration; +public class Scrollbar extends AbstractGui implements IScrollSource { + + /** + * Width of the scrollbar handle sprite in the source texture. + */ + private static final int HANDLE_WIDTH = 12; + + /** + * Height of the scrollbar handle sprite in the source texture. + */ + private static final int HANDLE_HEIGHT = 15; + + /** + * Texture containing the scrollbar handle sprites. + */ + private static final ResourceLocation TEXTURE = new ResourceLocation("minecraft", + "textures/gui/container/creative_inventory/tabs.png"); + + /** + * Rectangle in the source texture that contains the sprite for an enabled + * handle. + */ + private static final Rectangle2d ENABLED = new Rectangle2d(232, 0, HANDLE_WIDTH, HANDLE_HEIGHT); + + /** + * Rectangle in the source texture that contains the sprite for a disabled + * handle. + */ + private static final Rectangle2d DISABLED = new Rectangle2d(232 + HANDLE_WIDTH, 0, HANDLE_WIDTH, HANDLE_HEIGHT); + + /** + * The screen x-coordinate of the scrollbar's inner track. + */ private int displayX = 0; + + /** + * The screen y-coordinate of the scrollbar's inner track. + */ private int displayY = 0; - private int width = 12; + + /** + * The inner width of the scrollbar track. + */ + private int width = HANDLE_WIDTH; + + /** + * The inner height of the scrollbar track. + */ private int height = 16; private int pageSize = 1; @@ -36,18 +81,51 @@ public class Scrollbar implements IScrollSource { private int minScroll = 0; private int currentScroll = 0; - public void draw(final AEBaseScreen g) { - g.bindTexture("minecraft", "gui/container/creative_inventory/tabs.png"); - RenderSystem.color4f(1.0f, 1.0f, 1.0f, 1.0f); + /** + * True if the scrollbar's handle is currently being dragged. + */ + private boolean dragging; + private final EventRepeater eventRepeater = new EventRepeater( + Duration.ofMillis(250), + Duration.ofMillis(150) + ); + + /** + * Draws the handle of the scrollbar. + *

+ * The GUI is assumed to already contain a prebaked scrollbar track in its + * background. + */ + public void draw(MatrixStack matrices, final AEBaseScreen g) { + setBlitOffset(g.getBlitOffset()); + + // Draw the track (nice for debugging) + // fill(matrices, displayX, displayY, this.displayX + width, this.displayY + height, 0xffff0000); + + g.bindTexture(TEXTURE); + + int yOffset; + Rectangle2d sourceRect; if (this.getRange() == 0) { - GuiUtils.drawTexturedModalRect(this.displayX, this.displayY, 232 + this.width, 0, this.width, 15, - g.getBlitOffset()); + yOffset = 0; + sourceRect = DISABLED; } else { - final int offset = (this.currentScroll - this.minScroll) * (this.height - 15) / this.getRange(); - GuiUtils.drawTexturedModalRect(this.displayX, offset + this.displayY, 232, 0, this.width, 15, - g.getBlitOffset()); + yOffset = getHandleYOffset(); + sourceRect = ENABLED; } + + blit(matrices, this.displayX, this.displayY + yOffset, sourceRect.getX(), sourceRect.getY(), + sourceRect.getWidth(), sourceRect.getHeight()); + } + + /** + * Returns the y-position of the scrollbar handle in relation to the upper edge + * of the scrollbar's track. + */ + private int getHandleYOffset() { + int availableHeight = this.height - HANDLE_HEIGHT; + return (this.currentScroll - this.minScroll) * availableHeight / this.getRange(); } private int getRange() { @@ -111,18 +189,55 @@ public class Scrollbar implements IScrollSource { return this.currentScroll; } - public void click(final double x, final double y) { - if (this.getRange() == 0) { + public boolean mouseDown(double x, double y) { + this.dragging = false; + + // Clicks to the left or right of the scrollbar don't do anything + if (x < displayX || x >= displayX + width) { + return false; + } + + // Clicks to the top or bottom don't do anything either + int relY = (int) Math.round(y - displayY); + if (relY < 0 || relY >= height) { + return false; + } + + int handleYOffset = getHandleYOffset(); + + if (relY < handleYOffset) { + // Clicks above the handle will page up, repeatedly + repeatPageUp(); + eventRepeater.repeat(this::repeatPageUp); + + } else if (relY < handleYOffset + HANDLE_HEIGHT) { + // Clicks on the handle will initiate dragging it + this.dragging = true; + } else { + // Clicks below the handle will page down, repeatedly + repeatPageDown(); + eventRepeater.repeat(this::repeatPageDown); + } + + return true; + } + + public boolean mouseUp(double x, double y) { + this.dragging = false; + this.eventRepeater.stop(); + return false; + } + + public void mouseDragged(double x, double y) { + if (this.getRange() == 0 || !this.dragging || this.eventRepeater.isRepeating()) { return; } - if (x > this.displayX && x <= this.displayX + this.width) { - if (y > this.displayY && y <= this.displayY + this.height) { - this.currentScroll = (int) (y - this.displayY); - this.currentScroll = this.minScroll + ((this.currentScroll * 2 * this.getRange() / this.height)); - this.currentScroll = (this.currentScroll + 1) >> 1; - this.applyRange(); - } + if (y > this.displayY && y <= this.displayY + this.height) { + this.currentScroll = (int) (y - this.displayY); + this.currentScroll = this.minScroll + ((this.currentScroll * 2 * this.getRange() / this.height)); + this.currentScroll = (this.currentScroll + 1) >> 1; + this.applyRange(); } } @@ -131,4 +246,25 @@ public class Scrollbar implements IScrollSource { this.currentScroll += delta * this.pageSize; this.applyRange(); } + + /** + * Ticks the scrollbar for the purposes of input-repeats (since mouse-downs are not repeat-triggered), + * used to repeatedly page-up or page-down when the mouse is held in the area above or below the scrollbar handle. + */ + public void tick() { + this.eventRepeater.tick(); + } + + private void repeatPageUp() { + System.out.println("REPEAT PAGE UP"); + this.currentScroll -= this.pageSize; + this.applyRange(); + } + + private void repeatPageDown() { + System.out.println("REPEAT PAGE DOWN"); + this.currentScroll += this.pageSize; + this.applyRange(); + } + }