From 7f5399a1c4d6a2d1b9aef49c984e4eb5cd4ca308 Mon Sep 17 00:00:00 2001 From: D-Cysteine <54219287+D-Cysteine@users.noreply.github.com> Date: Sat, 18 May 2024 18:32:51 -0600 Subject: [PATCH 1/4] Replace indentation tabs with spaces --- .../gui/panels/content/PanelTextBox.java | 840 ++++++++---------- 1 file changed, 390 insertions(+), 450 deletions(-) diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java index d06312611..d8692e871 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java @@ -29,462 +29,402 @@ import static betterquesting.api.storage.BQ_Settings.textWidthCorrection; -public class PanelTextBox implements IGuiPanel -{ - private static final Pattern url = Pattern.compile("\\[url(?: text=([^]]+))?] *(.*?) *\\[/url]"); - private static final String defaultUrlProtocol = "https"; - private static final Set supportedUrlProtocol = ImmutableSet.of("http", "https"); - private final GuiRectText transform; - private final List hotZones = new ArrayList<>(); - private boolean enabled = true; - - // List of colors set for specific themes, usually dark ones, to improve readability - private static final ImmutableMap urlColors = ImmutableMap.of( - "betterquesting:dark", "§9§n", - "betterquesting:stronghold", "§9§n", - "betterquesting:overworld", "§9§n"); - - private static final ImmutableMap warningColors = ImmutableMap.of( - "betterquesting:dark", "§c", - "betterquesting:stronghold", "§c", - "betterquesting:overworld", "§c"); - - private static final ImmutableMap noteColors = ImmutableMap.of( - "betterquesting:stronghold", "§b", - "betterquesting:ender", "§b", - "betterquesting:overworld", "§b"); - - private static final ImmutableMap questReferenceColors = ImmutableMap.of( - "betterquesting:dark", "§a§n", - "betterquesting:stronghold", "§a§n", - "betterquesting:overworld", "§a§n"); - - private static final Pattern warningTagStart = Pattern.compile("\\[warn]"); - private static final Pattern noteTagStart = Pattern.compile("\\[note]"); - private static final Pattern questRefTagStart = Pattern.compile("\\[quest]"); - private static final Pattern allTagEnds = Pattern.compile("\\[/warn]|\\[/note]|\\[/quest]"); - - private String text = "", rawText = ""; - private boolean shadow = false; - private IGuiColor color = new GuiColorStatic(255, 255, 255, 255); - private final boolean autoFit; - private int align = 0; - private int fontScale = 12; - - private int lines = 1; // Cached number of lines - private boolean hyperlinkAware; - - public PanelTextBox(IGuiRect rect, String text) - { - this(rect, text, false); - } - - public PanelTextBox(IGuiRect rect, String text, boolean autoFit) - { - this(rect, text, autoFit, false); - } - - public PanelTextBox(IGuiRect rect, String text, boolean autoFit, boolean hyperlinkAware) - { - this.transform = new GuiRectText(rect, autoFit); - this.autoFit = autoFit; - this.hyperlinkAware = hyperlinkAware; - this.setText(text); - } - - public boolean isHyperlinkAware() - { - return hyperlinkAware; - } - - public PanelTextBox setHyperlinkAware(boolean hyperlinkAware) - { - this.hyperlinkAware = hyperlinkAware; - bakeHotZones(null); - return this; - } - - public PanelTextBox setText(String text) - { - if(hyperlinkAware) - { - String coloredText = ""; - String currentTheme = BQ_Settings.curTheme; - String urlColor = urlColors.getOrDefault(currentTheme, "§1§n"); // default dark blue + underlined - String warningColor = warningColors.getOrDefault(currentTheme, "§4"); // default dark red - String noteColor = noteColors.getOrDefault(currentTheme, "§3"); // default dark aqua - String questRefColor = questReferenceColors.getOrDefault(currentTheme, "§2§n"); // default dark green + bold - - coloredText = warningTagStart.matcher(text).replaceAll(warningColor); - coloredText = noteTagStart.matcher(coloredText).replaceAll(noteColor); - coloredText = questRefTagStart.matcher(coloredText).replaceAll(questRefColor); - coloredText = allTagEnds.matcher(coloredText).replaceAll("§r"); - coloredText = url.matcher(coloredText).replaceAll(urlColor + "$0§r"); - this.rawText = coloredText; - StringBuilder sb = new StringBuilder(); - Matcher matcher = url.matcher(coloredText); - int last = 0; - while(matcher.find()) - { - sb.append(coloredText, last, matcher.start()); - if (matcher.start(1) != -1) - { - sb.append(coloredText, matcher.start(1), matcher.end(1)); - } - else - { - sb.append(coloredText, matcher.start(2), matcher.end(2)); - } - last = matcher.end(); - } - if (last < coloredText.length()) - { - sb.append(coloredText, last, coloredText.length()); - } - this.text = sb.toString(); - } else - { - this.text = text; - } - - IGuiRect bounds = this.getTransform(); - FontRenderer fr = Minecraft.getMinecraft().fontRenderer; - - if(autoFit) - { - float scale = fontScale / 12F; - List sl = RenderUtils.splitStringWithoutFormat(this.text, (int)Math.floor(bounds.getWidth() / scale / textWidthCorrection), fr); - lines = sl.size() - 1; - - this.transform.h = fr.FONT_HEIGHT * sl.size(); - - bakeHotZones(sl); - } else - { - lines = (bounds.getHeight() / fr.FONT_HEIGHT) - 1; - } - - return this; - } - - private void bakeHotZones(List lines) { - hotZones.clear(); - if (!isHyperlinkAware()) return; // not enabled - if (StringUtils.isBlank(text)) return; // nothing to do - FontRenderer fr = Minecraft.getMinecraft().fontRenderer; - IGuiRect fullbox = getTransform(); - if (lines == null) - { - float scale = fontScale / 12F; - lines = RenderUtils.splitStringWithoutFormat(this.text, (int) Math.floor(fullbox.getWidth() / scale / textWidthCorrection), fr); - } - - Matcher matcher = url.matcher(rawText); - // removal of [url] and whitespace on either side of the url can affect string pos - int toDeduct = 0; - - while(matcher.find()) - { - String url = matcher.group(2); - int displayTextLength = matcher.start(1) == -1 ? url.length() : matcher.end(1) - matcher.start(1); - int start = matcher.start() - toDeduct; - int end = start + displayTextLength; - - int currentPos = 0; - boolean foundUrlStart = false; - for(int lineIndex = 0, lineCount = lines.size(); lineIndex < lineCount; currentPos += lines.get(lineIndex++).length()) - { - String line = lines.get(lineIndex); - if(!foundUrlStart) - { - if(start < currentPos + line.length()) - { - int left = RenderUtils.getStringWidth(line.substring(0, start - currentPos), fr); - if (end <= currentPos + line.length()) - { - // url on same line, early exit - int right = RenderUtils.getStringWidth(line.substring(0, end - currentPos), fr); - GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, left, fr.FONT_HEIGHT * lineIndex, right - left, fr.FONT_HEIGHT, 0); - location.setParent(fullbox); - hotZones.add(new HotZone(location, url)); - break; - } - // url span multiple lines - foundUrlStart = true; - GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, left, fr.FONT_HEIGHT * lineIndex, fullbox.getWidth(), fr.FONT_HEIGHT, 0); - location.setParent(fullbox); - hotZones.add(new HotZone(location, url)); - } - } else - { - if (end <= currentPos + line.length()) - { - // url ends at current line - GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, 0, fr.FONT_HEIGHT * lineIndex, RenderUtils.getStringWidth(line.substring(0, end - currentPos), fr), fr.FONT_HEIGHT, 0); - location.setParent(fullbox); - hotZones.add(new HotZone(location, url)); - break; - } else - { - // url still going... - GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, 0, fr.FONT_HEIGHT * lineIndex, fullbox.getWidth(), fr.FONT_HEIGHT, 0); - location.setParent(fullbox); - hotZones.add(new HotZone(location, url)); - } - } - } - toDeduct += matcher.end() - matcher.start() - displayTextLength; - } - } - - public PanelTextBox setColor(IGuiColor color) - { - this.color = color; - return this; - } - - public PanelTextBox setAlignment(int align) - { - this.align = MathHelper.clamp_int(align, 0, 2); - return this; - } - - public PanelTextBox setFontSize(int size) - { +public class PanelTextBox implements IGuiPanel { + private static final Pattern url = Pattern.compile("\\[url(?: text=([^]]+))?] *(.*?) *\\[/url]"); + private static final String defaultUrlProtocol = "https"; + private static final Set supportedUrlProtocol = ImmutableSet.of("http", "https"); + private final GuiRectText transform; + private final List hotZones = new ArrayList<>(); + private boolean enabled = true; + + // List of colors set for specific themes, usually dark ones, to improve readability + private static final ImmutableMap urlColors = ImmutableMap.of( + "betterquesting:dark", "§9§n", + "betterquesting:stronghold", "§9§n", + "betterquesting:overworld", "§9§n"); + + private static final ImmutableMap warningColors = ImmutableMap.of( + "betterquesting:dark", "§c", + "betterquesting:stronghold", "§c", + "betterquesting:overworld", "§c"); + + private static final ImmutableMap noteColors = ImmutableMap.of( + "betterquesting:stronghold", "§b", + "betterquesting:ender", "§b", + "betterquesting:overworld", "§b"); + + private static final ImmutableMap questReferenceColors = ImmutableMap.of( + "betterquesting:dark", "§a§n", + "betterquesting:stronghold", "§a§n", + "betterquesting:overworld", "§a§n"); + + private static final Pattern warningTagStart = Pattern.compile("\\[warn]"); + private static final Pattern noteTagStart = Pattern.compile("\\[note]"); + private static final Pattern questRefTagStart = Pattern.compile("\\[quest]"); + private static final Pattern allTagEnds = Pattern.compile("\\[/warn]|\\[/note]|\\[/quest]"); + + private String text = "", rawText = ""; + private boolean shadow = false; + private IGuiColor color = new GuiColorStatic(255, 255, 255, 255); + private final boolean autoFit; + private int align = 0; + private int fontScale = 12; + + private int lines = 1; // Cached number of lines + private boolean hyperlinkAware; + + public PanelTextBox(IGuiRect rect, String text) { + this(rect, text, false); + } + + public PanelTextBox(IGuiRect rect, String text, boolean autoFit) { + this(rect, text, autoFit, false); + } + + public PanelTextBox(IGuiRect rect, String text, boolean autoFit, boolean hyperlinkAware) { + this.transform = new GuiRectText(rect, autoFit); + this.autoFit = autoFit; + this.hyperlinkAware = hyperlinkAware; + this.setText(text); + } + + public boolean isHyperlinkAware() { + return hyperlinkAware; + } + + public PanelTextBox setHyperlinkAware(boolean hyperlinkAware) { + this.hyperlinkAware = hyperlinkAware; + bakeHotZones(null); + return this; + } + + public PanelTextBox setText(String text) { + if (hyperlinkAware) { + String coloredText = ""; + String currentTheme = BQ_Settings.curTheme; + String urlColor = urlColors.getOrDefault(currentTheme, "§1§n"); // default dark blue + underlined + String warningColor = warningColors.getOrDefault(currentTheme, "§4"); // default dark red + String noteColor = noteColors.getOrDefault(currentTheme, "§3"); // default dark aqua + String questRefColor = questReferenceColors.getOrDefault(currentTheme, "§2§n"); // default dark green + bold + + coloredText = warningTagStart.matcher(text).replaceAll(warningColor); + coloredText = noteTagStart.matcher(coloredText).replaceAll(noteColor); + coloredText = questRefTagStart.matcher(coloredText).replaceAll(questRefColor); + coloredText = allTagEnds.matcher(coloredText).replaceAll("§r"); + coloredText = url.matcher(coloredText).replaceAll(urlColor + "$0§r"); + this.rawText = coloredText; + StringBuilder sb = new StringBuilder(); + Matcher matcher = url.matcher(coloredText); + int last = 0; + while (matcher.find()) { + sb.append(coloredText, last, matcher.start()); + if (matcher.start(1) != -1) { + sb.append(coloredText, matcher.start(1), matcher.end(1)); + } else { + sb.append(coloredText, matcher.start(2), matcher.end(2)); + } + last = matcher.end(); + } + if (last < coloredText.length()) { + sb.append(coloredText, last, coloredText.length()); + } + this.text = sb.toString(); + } else { + this.text = text; + } + + IGuiRect bounds = this.getTransform(); + FontRenderer fr = Minecraft.getMinecraft().fontRenderer; + + if (autoFit) { + float scale = fontScale / 12F; + List sl = RenderUtils.splitStringWithoutFormat(this.text, (int) Math.floor(bounds.getWidth() / scale / textWidthCorrection), fr); + lines = sl.size() - 1; + + this.transform.h = fr.FONT_HEIGHT * sl.size(); + + bakeHotZones(sl); + } else { + lines = (bounds.getHeight() / fr.FONT_HEIGHT) - 1; + } + + return this; + } + + private void bakeHotZones(List lines) { + hotZones.clear(); + if (!isHyperlinkAware()) return; // not enabled + if (StringUtils.isBlank(text)) return; // nothing to do + FontRenderer fr = Minecraft.getMinecraft().fontRenderer; + IGuiRect fullbox = getTransform(); + if (lines == null) { + float scale = fontScale / 12F; + lines = RenderUtils.splitStringWithoutFormat(this.text, (int) Math.floor(fullbox.getWidth() / scale / textWidthCorrection), fr); + } + + Matcher matcher = url.matcher(rawText); + // removal of [url] and whitespace on either side of the url can affect string pos + int toDeduct = 0; + + while (matcher.find()) { + String url = matcher.group(2); + int displayTextLength = matcher.start(1) == -1 ? url.length() : matcher.end(1) - matcher.start(1); + int start = matcher.start() - toDeduct; + int end = start + displayTextLength; + + int currentPos = 0; + boolean foundUrlStart = false; + for (int lineIndex = 0, lineCount = lines.size(); lineIndex < lineCount; currentPos += lines.get(lineIndex++).length()) { + String line = lines.get(lineIndex); + if (!foundUrlStart) { + if (start < currentPos + line.length()) { + int left = RenderUtils.getStringWidth(line.substring(0, start - currentPos), fr); + if (end <= currentPos + line.length()) { + // url on same line, early exit + int right = RenderUtils.getStringWidth(line.substring(0, end - currentPos), fr); + GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, left, fr.FONT_HEIGHT * lineIndex, right - left, fr.FONT_HEIGHT, 0); + location.setParent(fullbox); + hotZones.add(new HotZone(location, url)); + break; + } + // url span multiple lines + foundUrlStart = true; + GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, left, fr.FONT_HEIGHT * lineIndex, fullbox.getWidth(), fr.FONT_HEIGHT, 0); + location.setParent(fullbox); + hotZones.add(new HotZone(location, url)); + } + } else { + if (end <= currentPos + line.length()) { + // url ends at current line + GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, 0, fr.FONT_HEIGHT * lineIndex, RenderUtils.getStringWidth(line.substring(0, end - currentPos), fr), fr.FONT_HEIGHT, 0); + location.setParent(fullbox); + hotZones.add(new HotZone(location, url)); + break; + } else { + // url still going... + GuiTransform location = new GuiTransform(GuiAlign.FULL_BOX, 0, fr.FONT_HEIGHT * lineIndex, fullbox.getWidth(), fr.FONT_HEIGHT, 0); + location.setParent(fullbox); + hotZones.add(new HotZone(location, url)); + } + } + } + toDeduct += matcher.end() - matcher.start() - displayTextLength; + } + } + + public PanelTextBox setColor(IGuiColor color) { + this.color = color; + return this; + } + + public PanelTextBox setAlignment(int align) { + this.align = MathHelper.clamp_int(align, 0, 2); + return this; + } + + public PanelTextBox setFontSize(int size) { this.fontScale = size; return this; } - - public PanelTextBox enableShadow(boolean enable) - { - this.shadow = enable; - return this; - } - - @Override - public IGuiRect getTransform() - { - return transform; - } - - @Override - public void initPanel() - { - IGuiRect bounds = this.getTransform(); - FontRenderer fr = Minecraft.getMinecraft().fontRenderer; - float scale = fontScale / 12F; - - if(!autoFit) - { - lines = (int)Math.floor(bounds.getHeight() / (fr.FONT_HEIGHT * scale)) - 1; - return; - } - - List sl = RenderUtils.splitStringWithoutFormat(text, (int)Math.floor(bounds.getWidth() / scale / textWidthCorrection), fr); - lines = sl.size() - 1; - bakeHotZones(sl); - - this.transform.h = (int)Math.floor(fr.FONT_HEIGHT * sl.size() * scale); - } - - @Override - public void setEnabled(boolean state) - { - this.enabled = state; - } - - @Override - public boolean isEnabled() - { - return this.enabled; - } - - @Override - public void drawPanel(int mx, int my, float partialTick) - { - IGuiRect bounds = this.getTransform(); - FontRenderer fr = Minecraft.getMinecraft().fontRenderer; - - float s = fontScale / 12F; - int w = (int)Math.ceil(RenderUtils.getStringWidth(text, fr) * s); - int bw = (int)Math.floor(bounds.getWidth() / s / textWidthCorrection); - - if(bw <= 0) return; - + + public PanelTextBox enableShadow(boolean enable) { + this.shadow = enable; + return this; + } + + @Override + public IGuiRect getTransform() { + return transform; + } + + @Override + public void initPanel() { + IGuiRect bounds = this.getTransform(); + FontRenderer fr = Minecraft.getMinecraft().fontRenderer; + float scale = fontScale / 12F; + + if (!autoFit) { + lines = (int) Math.floor(bounds.getHeight() / (fr.FONT_HEIGHT * scale)) - 1; + return; + } + + List sl = RenderUtils.splitStringWithoutFormat(text, (int) Math.floor(bounds.getWidth() / scale / textWidthCorrection), fr); + lines = sl.size() - 1; + bakeHotZones(sl); + + this.transform.h = (int) Math.floor(fr.FONT_HEIGHT * sl.size() * scale); + } + + @Override + public void setEnabled(boolean state) { + this.enabled = state; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + @Override + public void drawPanel(int mx, int my, float partialTick) { + IGuiRect bounds = this.getTransform(); + FontRenderer fr = Minecraft.getMinecraft().fontRenderer; + + float s = fontScale / 12F; + int w = (int) Math.ceil(RenderUtils.getStringWidth(text, fr) * s); + int bw = (int) Math.floor(bounds.getWidth() / s / textWidthCorrection); + + if (bw <= 0) return; + GL11.glPushMatrix(); GL11.glTranslatef(bounds.getX(), bounds.getY(), 1); GL11.glScalef(s, s, 1F); - - if(align == 2 && bw >= w) - { - RenderUtils.drawSplitString(fr, text, bw - w, 0, bw, color.getRGB(), shadow, 0, lines); - } else if(align == 1 && bw >= w) - { - RenderUtils.drawSplitString(fr, text, bw/2 - w/2, 0, bw, color.getRGB(), shadow, 0, lines); - } else - { - RenderUtils.drawSplitString(fr, text, 0, 0, bw, color.getRGB(), shadow, 0, lines); - } - - if(BQ_Settings.urlDebug) - { - for(int i = 0, hotZonesSize = hotZones.size(); i < hotZonesSize; i++) - { - RenderUtils.drawHighlightBox(hotZones.get(i).location, new GuiColorStatic(i % 3 == 0 ? 255 : 0, i % 3 == 1 ? 255 : 0, i % 3 == 2 ? 255 : 0, 255)); - } - } + + if (align == 2 && bw >= w) { + RenderUtils.drawSplitString(fr, text, bw - w, 0, bw, color.getRGB(), shadow, 0, lines); + } else if (align == 1 && bw >= w) { + RenderUtils.drawSplitString(fr, text, bw / 2 - w / 2, 0, bw, color.getRGB(), shadow, 0, lines); + } else { + RenderUtils.drawSplitString(fr, text, 0, 0, bw, color.getRGB(), shadow, 0, lines); + } + + if (BQ_Settings.urlDebug) { + for (int i = 0, hotZonesSize = hotZones.size(); i < hotZonesSize; i++) { + RenderUtils.drawHighlightBox(hotZones.get(i).location, new GuiColorStatic(i % 3 == 0 ? 255 : 0, i % 3 == 1 ? 255 : 0, i % 3 == 2 ? 255 : 0, 255)); + } + } GL11.glPopMatrix(); - } - - @Override - public boolean onMouseClick(int mx, int my, int click) - { - int mxt = mx + getTransform().getX(), myt = my + getTransform().getY(); - for(HotZone hotZone : hotZones) - { - if (hotZone.location.contains(mxt, myt)) { - URI uri; - try { - URI tmp; - tmp = new URI(hotZone.url); - if (tmp.getScheme() == null) - tmp = new URI(defaultUrlProtocol + "://" + hotZone.url); - uri = tmp; - } catch(URISyntaxException ex) { - return false; - } - Predicate handler = URIHandlers.get(uri.getScheme()); - if (handler == null) return false; - return handler.test(uri); - } - } - return false; - } - - private static void openURL(URI p_146407_1_) - { - try - { - Class oclass = Class.forName("java.awt.Desktop"); - Object object = oclass.getMethod("getDesktop").invoke(null); - oclass.getMethod("browse", URI.class).invoke(object, p_146407_1_); - } - catch (Throwable throwable) - { - BetterQuesting.logger.error("Couldn't open link", throwable); - } - } - - @Override - public boolean onMouseRelease(int mx, int my, int click) - { - return false; - } - - @Override - public boolean onMouseScroll(int mx, int my, int scroll) - { - return false; - } - - @Override - public boolean onKeyTyped(char c, int keycode) - { - return false; - } - - @Override - public List getTooltip(int mx, int my) - { - return null; - } - - private static class GuiRectText implements IGuiRect - { - private final IGuiRect proxy; - private final boolean useH; - private int h; - - public GuiRectText(IGuiRect proxy, boolean useH) - { - this.proxy = proxy; - this.useH = useH; - } - - @Override - public int getX() - { - return proxy.getX(); - } - - @Override - public int getY() - { - return proxy.getY(); - } - - @Override - public int getWidth() - { - return proxy.getWidth(); - } - - @Override - public int getHeight() - { - return useH ? h : proxy.getHeight(); - } - - @Override - public int getDepth() - { - return proxy.getDepth(); - } - - @Override - public IGuiRect getParent() - { - return proxy.getParent(); - } - - @Override - public void setParent(IGuiRect rect) - { - proxy.setParent(rect); - } - - @Override - public boolean contains(int x, int y) - { - int x1 = this.getX(); - int x2 = x1 + this.getWidth(); - int y1 = this.getY(); - int y2 = y1 + this.getHeight(); - return x >= x1 && x < x2 && y >= y1 && y < y2; - } - - /*@Override - public void translate(int x, int y) - { - proxy.translate(x, y); - }*/ - - @Override - public int compareTo(IGuiRect o) - { - return proxy.compareTo(o); - } - } - - private static class HotZone { - final IGuiRect location; - final String url; - - public HotZone(IGuiRect location, String url) - { - this.location = location; - this.url = url; - } - } + } + + @Override + public boolean onMouseClick(int mx, int my, int click) { + int mxt = mx + getTransform().getX(), myt = my + getTransform().getY(); + for (HotZone hotZone : hotZones) { + if (hotZone.location.contains(mxt, myt)) { + URI uri; + try { + URI tmp; + tmp = new URI(hotZone.url); + if (tmp.getScheme() == null) + tmp = new URI(defaultUrlProtocol + "://" + hotZone.url); + uri = tmp; + } catch (URISyntaxException ex) { + return false; + } + Predicate handler = URIHandlers.get(uri.getScheme()); + if (handler == null) return false; + return handler.test(uri); + } + } + return false; + } + + private static void openURL(URI p_146407_1_) { + try { + Class oclass = Class.forName("java.awt.Desktop"); + Object object = oclass.getMethod("getDesktop").invoke(null); + oclass.getMethod("browse", URI.class).invoke(object, p_146407_1_); + } catch (Throwable throwable) { + BetterQuesting.logger.error("Couldn't open link", throwable); + } + } + + @Override + public boolean onMouseRelease(int mx, int my, int click) { + return false; + } + + @Override + public boolean onMouseScroll(int mx, int my, int scroll) { + return false; + } + + @Override + public boolean onKeyTyped(char c, int keycode) { + return false; + } + + @Override + public List getTooltip(int mx, int my) { + return null; + } + + private static class GuiRectText implements IGuiRect { + private final IGuiRect proxy; + private final boolean useH; + private int h; + + public GuiRectText(IGuiRect proxy, boolean useH) { + this.proxy = proxy; + this.useH = useH; + } + + @Override + public int getX() { + return proxy.getX(); + } + + @Override + public int getY() { + return proxy.getY(); + } + + @Override + public int getWidth() { + return proxy.getWidth(); + } + + @Override + public int getHeight() { + return useH ? h : proxy.getHeight(); + } + + @Override + public int getDepth() { + return proxy.getDepth(); + } + + @Override + public IGuiRect getParent() { + return proxy.getParent(); + } + + @Override + public void setParent(IGuiRect rect) { + proxy.setParent(rect); + } + + @Override + public boolean contains(int x, int y) { + int x1 = this.getX(); + int x2 = x1 + this.getWidth(); + int y1 = this.getY(); + int y2 = y1 + this.getHeight(); + return x >= x1 && x < x2 && y >= y1 && y < y2; + } + + /* + @Override + public void translate(int x, int y) + { + proxy.translate(x, y); + } + */ + + @Override + public int compareTo(IGuiRect o) { + return proxy.compareTo(o); + } + } + + private static class HotZone { + final IGuiRect location; + final String url; + + public HotZone(IGuiRect location, String url) { + this.location = location; + this.url = url; + } + } } From d714f4c580ecb7298f4c323fe190b39cde5a5861 Mon Sep 17 00:00:00 2001 From: D-Cysteine <54219287+D-Cysteine@users.noreply.github.com> Date: Wed, 22 May 2024 02:48:36 -0600 Subject: [PATCH 2/4] Implement nested formatting tags --- .../gui/panels/content/FormattingTag.java | 205 ++++++++++++++++++ .../gui/panels/content/PanelTextBox.java | 182 ++++++++++------ 2 files changed, 320 insertions(+), 67 deletions(-) create mode 100644 src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java b/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java new file mode 100644 index 000000000..f31b3143a --- /dev/null +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java @@ -0,0 +1,205 @@ +package betterquesting.api2.client.gui.panels.content; + +import betterquesting.api.storage.BQ_Settings; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableTable; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public enum FormattingTag { + NOTE("note"), + WARNING("warning"), + QUEST("quest"), + + BOLD("bold"), + ITALIC("italic"), + UNDERLINE("underline"), + STRIKETHROUGH("strikethrough"), + OBFUSCATED("obfuscated"), + + /** + * Special tag which forcibly resets all formatting, even within other tags. + * + *

Unlike most tags, the reset tag should not be closed. + * + *

The active tags' formatting will be restored by the next encountered tag or literal + * {@code "§r"}. + */ + RESET("reset"), + + /** + * Defines a clickable hyperlink. + * + *

Params: + *

    + *
  • {@code link}: If provided, sets the hyperlink target. If not provided, then the + * exact text contents within the {@code [url]} tags will be the hyperlink target.
  • + *
+ * + *

Example usage: + *

    + *
  • {@code [url]https://www.example.com[/url]}
  • + *
  • {@code [url link=https://www.example.com]Click me![/url]}
  • + *
+ * + *

URL tags cannot be nested, and will break if you try. + */ + URL("url"), + ; + + public static final ImmutableMap NAME_TO_VALUE_MAP = + ImmutableMap.copyOf( + Arrays.stream(values()) + .collect( + Collectors.toMap(FormattingTag::getName, Function.identity()))); + + private static final Pattern OPENING_TAG_PATTERN = + Pattern.compile("\\[([0-9a-zA-Z]+)((?: [0-9a-zA-Z]+=[^ ]+)*)]"); + private static final Pattern OPENING_TAG_PARAMS_PATTERN = + Pattern.compile(" ([0-9a-zA-Z]+)=([^ ]+)"); + private static final Pattern CLOSING_TAG_PATTERN = Pattern.compile("\\[/([0-9a-zA-Z]+)]"); + + private static final String BQ_DARK_THEME = "betterquesting:dark"; + private static final String BQ_ENDER_THEME = "betterquesting:ender"; + private static final String BQ_OVERWORLD_THEME = "betterquesting:overworld"; + private static final String BQ_STRONGHOLD_THEME = "betterquesting:stronghold"; + + private static final ImmutableMap DEFAULT_FORMATTING_STRING_MAP; + private static final ImmutableTable THEME_FORMATTING_STRING_TABLE; + private static final ImmutableMap TEXT_FORMATTING_STRING_MAP; + + static { + ImmutableMap.Builder defaultFormattingStringMapBuilder = + ImmutableMap.builder(); + ImmutableTable.Builder themeFormattingStringTableBuilder = + ImmutableTable.builder(); + ImmutableMap.Builder textFormattingStringMapBuilder = + ImmutableMap.builder(); + + defaultFormattingStringMapBuilder.put(NOTE, "§3"); + themeFormattingStringTableBuilder.put(NOTE, BQ_ENDER_THEME, "§b"); + themeFormattingStringTableBuilder.put(NOTE, BQ_OVERWORLD_THEME, "§b"); + themeFormattingStringTableBuilder.put(NOTE, BQ_STRONGHOLD_THEME, "§b"); + + defaultFormattingStringMapBuilder.put(WARNING, "§4"); + themeFormattingStringTableBuilder.put(WARNING, BQ_DARK_THEME, "§c"); + themeFormattingStringTableBuilder.put(WARNING, BQ_OVERWORLD_THEME, "§c"); + themeFormattingStringTableBuilder.put(WARNING, BQ_STRONGHOLD_THEME, "§c"); + + defaultFormattingStringMapBuilder.put(QUEST, "§2"); + themeFormattingStringTableBuilder.put(QUEST, BQ_DARK_THEME, "§a"); + themeFormattingStringTableBuilder.put(QUEST, BQ_OVERWORLD_THEME, "§a"); + themeFormattingStringTableBuilder.put(QUEST, BQ_STRONGHOLD_THEME, "§a"); + + defaultFormattingStringMapBuilder.put(URL, "§1"); + themeFormattingStringTableBuilder.put(URL, BQ_DARK_THEME, "§9"); + themeFormattingStringTableBuilder.put(URL, BQ_OVERWORLD_THEME, "§9"); + themeFormattingStringTableBuilder.put(URL, BQ_STRONGHOLD_THEME, "§9"); + + textFormattingStringMapBuilder.put(BOLD, "§l"); + textFormattingStringMapBuilder.put(ITALIC, "§o"); + textFormattingStringMapBuilder.put(UNDERLINE, "§n"); + textFormattingStringMapBuilder.put(STRIKETHROUGH, "§m"); + textFormattingStringMapBuilder.put(OBFUSCATED, "§k"); + + textFormattingStringMapBuilder.put(QUEST, "§n"); + textFormattingStringMapBuilder.put(URL, "§n"); + + DEFAULT_FORMATTING_STRING_MAP = defaultFormattingStringMapBuilder.build(); + THEME_FORMATTING_STRING_TABLE = themeFormattingStringTableBuilder.build(); + TEXT_FORMATTING_STRING_MAP = textFormattingStringMapBuilder.build(); + } + + private final String name; + + FormattingTag(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getColourFormattingString() { + return Objects.firstNonNull( + THEME_FORMATTING_STRING_TABLE.get(this, BQ_Settings.curTheme), + DEFAULT_FORMATTING_STRING_MAP.getOrDefault(this, "")); + } + + /** + * Unfortunately, in Minecraft 1.7, text formatting codes (bold, italic, etc.) must be + * re-applied after colour formatting codes. + * + *

Having this separate method allows us to handle re-applying these text formatting codes + * where needed. + */ + public String getTextFormattingString() { + return TEXT_FORMATTING_STRING_MAP.getOrDefault(this, ""); + } + + public static Optional parseOpeningTag(String text) { + Matcher matcher = OPENING_TAG_PATTERN.matcher(text); + if (!matcher.matches()) { + return Optional.empty(); + } + + FormattingTag tag = NAME_TO_VALUE_MAP.get(matcher.group(1)); + if (tag == null) { + return Optional.empty(); + } + + ImmutableMap.Builder paramsBuilder = ImmutableMap.builder(); + String params = matcher.group(2); + if (!params.isEmpty()) { + matcher = OPENING_TAG_PARAMS_PATTERN.matcher(params); + while (matcher.find()) { + paramsBuilder.put(matcher.group(1), matcher.group(2)); + } + } + + return Optional.of(new TagInstance(tag, paramsBuilder.build())); + } + + public static Optional parseClosingTag(String text) { + Matcher matcher = CLOSING_TAG_PATTERN.matcher(text); + if (!matcher.matches()) { + return Optional.empty(); + } + + return Optional.ofNullable(NAME_TO_VALUE_MAP.get(matcher.group(1))); + } + + /** + * Helper object which represents an instance of an opening formatting tag, complete with + * optional parameters. + * + *

Parameters are specified using the form {@code paramName=value}. The parameter value is + * not allowed to contain spaces or square brackets. + * + *

The reason why parameter values cannot contain square brackets, is because they would + * interfere with the string tokenization logic in {@link PanelTextBox}. + */ + public static class TagInstance { + private final FormattingTag tag; + private final ImmutableMap params; + + private TagInstance(FormattingTag tag, ImmutableMap params) { + this.tag = tag; + this.params = params; + } + + public FormattingTag getTag() { + return tag; + } + + public ImmutableMap getParams() { + return params; + } + } +} diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java index d8692e871..efa9b9243 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java @@ -10,7 +10,6 @@ import betterquesting.api2.client.gui.resources.colors.GuiColorStatic; import betterquesting.api2.client.gui.resources.colors.IGuiColor; import betterquesting.core.BetterQuesting; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; @@ -20,50 +19,38 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.List; +import java.util.Optional; +import java.util.Scanner; import java.util.Set; import java.util.function.Predicate; -import java.util.regex.Matcher; import java.util.regex.Pattern; import static betterquesting.api.storage.BQ_Settings.textWidthCorrection; public class PanelTextBox implements IGuiPanel { - private static final Pattern url = Pattern.compile("\\[url(?: text=([^]]+))?] *(.*?) *\\[/url]"); + /** + * Tokenizer pattern which is used to tokenize raw text into literal text fragments, + * (potential) formatting tags, and formatting codes. + * + *

This is accomplished by matching empty strings which immediately precede or follow a + * square bracket or formatting code. + */ + private static final Pattern TOKEN_DELIMITER = Pattern.compile("(?=\\[)|(?=§.)|(?<=])|(?<=§.)"); + private static final Pattern COLOUR_FORMATTING_CODE_PATTERN = Pattern.compile("§[0-9a-f]"); + private static final String FORMATTING_CODE_RESET = "§r"; + private static final String defaultUrlProtocol = "https"; private static final Set supportedUrlProtocol = ImmutableSet.of("http", "https"); private final GuiRectText transform; + private final List urlRanges = new ArrayList<>(); private final List hotZones = new ArrayList<>(); private boolean enabled = true; - // List of colors set for specific themes, usually dark ones, to improve readability - private static final ImmutableMap urlColors = ImmutableMap.of( - "betterquesting:dark", "§9§n", - "betterquesting:stronghold", "§9§n", - "betterquesting:overworld", "§9§n"); - - private static final ImmutableMap warningColors = ImmutableMap.of( - "betterquesting:dark", "§c", - "betterquesting:stronghold", "§c", - "betterquesting:overworld", "§c"); - - private static final ImmutableMap noteColors = ImmutableMap.of( - "betterquesting:stronghold", "§b", - "betterquesting:ender", "§b", - "betterquesting:overworld", "§b"); - - private static final ImmutableMap questReferenceColors = ImmutableMap.of( - "betterquesting:dark", "§a§n", - "betterquesting:stronghold", "§a§n", - "betterquesting:overworld", "§a§n"); - - private static final Pattern warningTagStart = Pattern.compile("\\[warn]"); - private static final Pattern noteTagStart = Pattern.compile("\\[note]"); - private static final Pattern questRefTagStart = Pattern.compile("\\[quest]"); - private static final Pattern allTagEnds = Pattern.compile("\\[/warn]|\\[/note]|\\[/quest]"); - - private String text = "", rawText = ""; + private String text = ""; private boolean shadow = false; private IGuiColor color = new GuiColorStatic(255, 255, 255, 255); private final boolean autoFit; @@ -100,35 +87,90 @@ public PanelTextBox setHyperlinkAware(boolean hyperlinkAware) { public PanelTextBox setText(String text) { if (hyperlinkAware) { - String coloredText = ""; - String currentTheme = BQ_Settings.curTheme; - String urlColor = urlColors.getOrDefault(currentTheme, "§1§n"); // default dark blue + underlined - String warningColor = warningColors.getOrDefault(currentTheme, "§4"); // default dark red - String noteColor = noteColors.getOrDefault(currentTheme, "§3"); // default dark aqua - String questRefColor = questReferenceColors.getOrDefault(currentTheme, "§2§n"); // default dark green + bold - - coloredText = warningTagStart.matcher(text).replaceAll(warningColor); - coloredText = noteTagStart.matcher(coloredText).replaceAll(noteColor); - coloredText = questRefTagStart.matcher(coloredText).replaceAll(questRefColor); - coloredText = allTagEnds.matcher(coloredText).replaceAll("§r"); - coloredText = url.matcher(coloredText).replaceAll(urlColor + "$0§r"); - this.rawText = coloredText; - StringBuilder sb = new StringBuilder(); - Matcher matcher = url.matcher(coloredText); - int last = 0; - while (matcher.find()) { - sb.append(coloredText, last, matcher.start()); - if (matcher.start(1) != -1) { - sb.append(coloredText, matcher.start(1), matcher.end(1)); - } else { - sb.append(coloredText, matcher.start(2), matcher.end(2)); + StringBuilder textBuilder = new StringBuilder(); + urlRanges.clear(); + + int currUrlStart = -1; + Deque tags = new ArrayDeque<>(); + Scanner scanner = new Scanner(text).useDelimiter(TOKEN_DELIMITER); + while (scanner.hasNext()) { + String token = scanner.next(); + if (token.equals(FORMATTING_CODE_RESET)) { + // Reset the formatting, then reapply all active tags + // in order of outermost to innermost (reverse of stack order). + textBuilder.append(FORMATTING_CODE_RESET); + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getColourFormattingString())); + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getTextFormattingString())); + continue; + } else if (COLOUR_FORMATTING_CODE_PATTERN.matcher(token).matches()) { + textBuilder.append(token); + // Re-apply text formatting codes since we just changed the colour. + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getTextFormattingString())); + continue; } - last = matcher.end(); - } - if (last < coloredText.length()) { - sb.append(coloredText, last, coloredText.length()); + + Optional openingTagOptional = + FormattingTag.parseOpeningTag(token); + if (openingTagOptional.isPresent()) { + FormattingTag.TagInstance openingTag = openingTagOptional.get(); + if (openingTag.getTag() == FormattingTag.RESET) { + textBuilder.append(FORMATTING_CODE_RESET); + continue; + } + + tags.push(openingTag); + textBuilder.append(openingTag.getTag().getColourFormattingString()); + // Re-apply text formatting codes since we may have just changed the colour. + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getTextFormattingString())); + + if (openingTag.getTag() == FormattingTag.URL) { + currUrlStart = textBuilder.length(); + } + + continue; + } + + Optional closingTagOptional = FormattingTag.parseClosingTag(token); + if (closingTagOptional.isPresent()) { + FormattingTag closingTag = closingTagOptional.get(); + + if (!tags.isEmpty() && closingTag == tags.peek().getTag()) { + FormattingTag.TagInstance openingTag = tags.pop(); + if (closingTag == FormattingTag.URL) { + String url = + openingTag.getParams() + .getOrDefault( + "link", textBuilder.substring(currUrlStart)); + urlRanges.add(new UrlRange(currUrlStart, textBuilder.length(), url)); + } + + // Reset the formatting, then reapply all active tags + // in order of outermost to innermost (reverse of stack order). + // Note that the tag we just closed was already popped off the stack. + textBuilder.append(FORMATTING_CODE_RESET); + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getColourFormattingString())); + tags.descendingIterator() + .forEachRemaining( + t -> textBuilder.append(t.getTag().getTextFormattingString())); + } // Else the closing tag doesn't match the current tag, so ignore it. + + continue; + } + + textBuilder.append(token); } - this.text = sb.toString(); + + this.text = textBuilder.toString(); } else { this.text = text; } @@ -162,15 +204,10 @@ private void bakeHotZones(List lines) { lines = RenderUtils.splitStringWithoutFormat(this.text, (int) Math.floor(fullbox.getWidth() / scale / textWidthCorrection), fr); } - Matcher matcher = url.matcher(rawText); - // removal of [url] and whitespace on either side of the url can affect string pos - int toDeduct = 0; - - while (matcher.find()) { - String url = matcher.group(2); - int displayTextLength = matcher.start(1) == -1 ? url.length() : matcher.end(1) - matcher.start(1); - int start = matcher.start() - toDeduct; - int end = start + displayTextLength; + for (UrlRange urlRange : urlRanges) { + String url = urlRange.url; + int start = urlRange.start; + int end = urlRange.end; int currentPos = 0; boolean foundUrlStart = false; @@ -208,7 +245,6 @@ private void bakeHotZones(List lines) { } } } - toDeduct += matcher.end() - matcher.start() - displayTextLength; } } @@ -418,6 +454,18 @@ public int compareTo(IGuiRect o) { } } + private static class UrlRange { + final int start; + final int end; + final String url; + + public UrlRange(int start, int end, String url) { + this.start = start; + this.end = end; + this.url = url; + } + } + private static class HotZone { final IGuiRect location; final String url; From 575eb1843cc2c216f1b9a7ed7999dbf32d216ace Mon Sep 17 00:00:00 2001 From: D-Cysteine <54219287+D-Cysteine@users.noreply.github.com> Date: Wed, 22 May 2024 11:03:53 -0600 Subject: [PATCH 3/4] Delete `[reset]` tag I think it wouldn't work properly anyway without more extensive code changes --- .../api2/client/gui/panels/content/FormattingTag.java | 10 ---------- .../api2/client/gui/panels/content/PanelTextBox.java | 5 ----- 2 files changed, 15 deletions(-) diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java b/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java index f31b3143a..f82e4c7e0 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/FormattingTag.java @@ -23,16 +23,6 @@ public enum FormattingTag { STRIKETHROUGH("strikethrough"), OBFUSCATED("obfuscated"), - /** - * Special tag which forcibly resets all formatting, even within other tags. - * - *

Unlike most tags, the reset tag should not be closed. - * - *

The active tags' formatting will be restored by the next encountered tag or literal - * {@code "§r"}. - */ - RESET("reset"), - /** * Defines a clickable hyperlink. * diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java index efa9b9243..4732f994f 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java @@ -119,11 +119,6 @@ public PanelTextBox setText(String text) { FormattingTag.parseOpeningTag(token); if (openingTagOptional.isPresent()) { FormattingTag.TagInstance openingTag = openingTagOptional.get(); - if (openingTag.getTag() == FormattingTag.RESET) { - textBuilder.append(FORMATTING_CODE_RESET); - continue; - } - tags.push(openingTag); textBuilder.append(openingTag.getTag().getColourFormattingString()); // Re-apply text formatting codes since we may have just changed the colour. From 40f645fbde96f48eaa90ebd8247762a0df0f8bf6 Mon Sep 17 00:00:00 2001 From: D-Cysteine <54219287+D-Cysteine@users.noreply.github.com> Date: Sun, 26 May 2024 15:55:21 -0600 Subject: [PATCH 4/4] Gracefully handle `[/url]` without `[url]` --- .../api2/client/gui/panels/content/PanelTextBox.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java index 4732f994f..47c7cfcfe 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/content/PanelTextBox.java @@ -139,7 +139,7 @@ public PanelTextBox setText(String text) { if (!tags.isEmpty() && closingTag == tags.peek().getTag()) { FormattingTag.TagInstance openingTag = tags.pop(); - if (closingTag == FormattingTag.URL) { + if (closingTag == FormattingTag.URL && currUrlStart >= 0) { String url = openingTag.getParams() .getOrDefault(