AbstractConfigScreen.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. /*
  2. * This file is part of Cloth Config.
  3. * Copyright (C) 2020 - 2021 shedaniel
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU Lesser General Public
  7. * License as published by the Free Software Foundation; either
  8. * version 3 of the License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. * Lesser General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Lesser General Public License
  16. * along with this program; if not, write to the Free Software Foundation,
  17. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. */
  19. package me.shedaniel.clothconfig2.gui;
  20. import com.google.common.collect.Lists;
  21. import com.mojang.blaze3d.platform.InputConstants;
  22. import com.mojang.blaze3d.systems.RenderSystem;
  23. import com.mojang.blaze3d.vertex.BufferBuilder;
  24. import com.mojang.blaze3d.vertex.DefaultVertexFormat;
  25. import com.mojang.blaze3d.vertex.PoseStack;
  26. import com.mojang.blaze3d.vertex.Tesselator;
  27. import com.mojang.math.Matrix4f;
  28. import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
  29. import me.shedaniel.clothconfig2.ClothConfigInitializer;
  30. import me.shedaniel.clothconfig2.api.*;
  31. import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
  32. import me.shedaniel.math.Rectangle;
  33. import net.minecraft.Util;
  34. import net.minecraft.client.Minecraft;
  35. import net.minecraft.client.gui.components.AbstractWidget;
  36. import net.minecraft.client.gui.components.events.GuiEventListener;
  37. import net.minecraft.client.gui.screens.ConfirmLinkScreen;
  38. import net.minecraft.client.gui.screens.ConfirmScreen;
  39. import net.minecraft.client.gui.screens.Screen;
  40. import net.minecraft.network.chat.ClickEvent;
  41. import net.minecraft.network.chat.Component;
  42. import net.minecraft.network.chat.Style;
  43. import net.minecraft.network.chat.TranslatableComponent;
  44. import net.minecraft.resources.ResourceLocation;
  45. import net.minecraft.world.level.block.entity.TickableBlockEntity;
  46. import org.jetbrains.annotations.ApiStatus;
  47. import org.jetbrains.annotations.Nullable;
  48. import java.net.URI;
  49. import java.net.URISyntaxException;
  50. import java.util.List;
  51. import java.util.Locale;
  52. import java.util.Map;
  53. import java.util.Optional;
  54. import java.util.function.Consumer;
  55. public abstract class AbstractConfigScreen extends Screen implements ConfigScreen {
  56. protected static final ResourceLocation CONFIG_TEX = new ResourceLocation("cloth-config2", "textures/gui/cloth_config.png");
  57. private boolean legacyEdited = false;
  58. private final ResourceLocation backgroundLocation;
  59. protected boolean legacyRequiresRestart = false;
  60. protected boolean confirmSave;
  61. protected final Screen parent;
  62. private boolean alwaysShowTabs = false;
  63. private boolean transparentBackground = false;
  64. @Nullable
  65. private Component defaultFallbackCategory = null;
  66. public int selectedCategoryIndex = 0;
  67. private boolean editable = true;
  68. private KeyCodeEntry focusedBinding;
  69. private ModifierKeyCode startedKeyCode = null;
  70. private final List<Tooltip> tooltips = Lists.newArrayList();
  71. @Nullable
  72. private Runnable savingRunnable = null;
  73. @Nullable
  74. protected Consumer<Screen> afterInitConsumer = null;
  75. protected AbstractConfigScreen(Screen parent, Component title, ResourceLocation backgroundLocation) {
  76. super(title);
  77. this.parent = parent;
  78. this.backgroundLocation = backgroundLocation;
  79. }
  80. @Override
  81. public void setSavingRunnable(@Nullable Runnable savingRunnable) {
  82. this.savingRunnable = savingRunnable;
  83. }
  84. @Override
  85. public void setAfterInitConsumer(@Nullable Consumer<Screen> afterInitConsumer) {
  86. this.afterInitConsumer = afterInitConsumer;
  87. }
  88. @Override
  89. public ResourceLocation getBackgroundLocation() {
  90. return backgroundLocation;
  91. }
  92. @Override
  93. public boolean isRequiresRestart() {
  94. if (legacyRequiresRestart) return true;
  95. for (List<AbstractConfigEntry<?>> entries : getCategorizedEntries().values()) {
  96. for (AbstractConfigEntry<?> entry : entries) {
  97. if (!entry.getConfigError().isPresent() && entry.isEdited() && entry.isRequiresRestart()) {
  98. return true;
  99. }
  100. }
  101. }
  102. return false;
  103. }
  104. public abstract Map<Component, List<AbstractConfigEntry<?>>> getCategorizedEntries();
  105. @Override
  106. public boolean isEdited() {
  107. if (legacyEdited) return true;
  108. for (List<AbstractConfigEntry<?>> entries : getCategorizedEntries().values()) {
  109. for (AbstractConfigEntry<?> entry : entries) {
  110. if (entry.isEdited()) {
  111. return true;
  112. }
  113. }
  114. }
  115. return false;
  116. }
  117. /**
  118. * Override #isEdited please
  119. */
  120. @Override
  121. @Deprecated
  122. @ApiStatus.ScheduledForRemoval
  123. public void setEdited(boolean edited) {
  124. this.legacyEdited = edited;
  125. }
  126. /**
  127. * Override #isEdited please
  128. */
  129. @Override
  130. @Deprecated
  131. @ApiStatus.ScheduledForRemoval
  132. public void setEdited(boolean edited, boolean legacyRequiresRestart) {
  133. setEdited(edited);
  134. if (!this.legacyRequiresRestart && legacyRequiresRestart)
  135. this.legacyRequiresRestart = legacyRequiresRestart;
  136. }
  137. public boolean isShowingTabs() {
  138. return isAlwaysShowTabs() || getCategorizedEntries().size() > 1;
  139. }
  140. public boolean isAlwaysShowTabs() {
  141. return alwaysShowTabs;
  142. }
  143. @ApiStatus.Internal
  144. public void setAlwaysShowTabs(boolean alwaysShowTabs) {
  145. this.alwaysShowTabs = alwaysShowTabs;
  146. }
  147. public boolean isTransparentBackground() {
  148. return transparentBackground && Minecraft.getInstance().level != null;
  149. }
  150. @ApiStatus.Internal
  151. public void setTransparentBackground(boolean transparentBackground) {
  152. this.transparentBackground = transparentBackground;
  153. }
  154. public Component getFallbackCategory() {
  155. if (defaultFallbackCategory != null)
  156. return defaultFallbackCategory;
  157. return getCategorizedEntries().keySet().iterator().next();
  158. }
  159. @ApiStatus.Internal
  160. public void setFallbackCategory(@Nullable Component defaultFallbackCategory) {
  161. this.defaultFallbackCategory = defaultFallbackCategory;
  162. List<Component> categories = Lists.newArrayList(getCategorizedEntries().keySet());
  163. for (int i = 0; i < categories.size(); i++) {
  164. Component category = categories.get(i);
  165. if (category.equals(getFallbackCategory())) {
  166. this.selectedCategoryIndex = i;
  167. break;
  168. }
  169. }
  170. }
  171. @Override
  172. public void saveAll(boolean openOtherScreens) {
  173. for (List<AbstractConfigEntry<?>> entries : Lists.newArrayList(getCategorizedEntries().values()))
  174. for (AbstractConfigEntry<?> entry : entries)
  175. entry.save();
  176. save();
  177. setEdited(false);
  178. if (openOtherScreens) {
  179. if (isRequiresRestart())
  180. AbstractConfigScreen.this.minecraft.setScreen(new ClothRequiresRestartScreen(parent));
  181. else
  182. AbstractConfigScreen.this.minecraft.setScreen(parent);
  183. }
  184. this.legacyRequiresRestart = false;
  185. }
  186. public void save() {
  187. Optional.ofNullable(this.savingRunnable).ifPresent(Runnable::run);
  188. }
  189. public boolean isEditable() {
  190. return editable;
  191. }
  192. @ApiStatus.Internal
  193. public void setEditable(boolean editable) {
  194. this.editable = editable;
  195. }
  196. @ApiStatus.Internal
  197. public void setConfirmSave(boolean confirmSave) {
  198. this.confirmSave = confirmSave;
  199. }
  200. public KeyCodeEntry getFocusedBinding() {
  201. return focusedBinding;
  202. }
  203. @ApiStatus.Internal
  204. public void setFocusedBinding(KeyCodeEntry focusedBinding) {
  205. this.focusedBinding = focusedBinding;
  206. if (focusedBinding != null) {
  207. startedKeyCode = this.focusedBinding.getValue();
  208. startedKeyCode.setKeyCodeAndModifier(InputConstants.UNKNOWN, Modifier.none());
  209. } else
  210. startedKeyCode = null;
  211. }
  212. @Override
  213. public boolean mouseReleased(double double_1, double double_2, int int_1) {
  214. if (this.focusedBinding != null && this.startedKeyCode != null && !this.startedKeyCode.isUnknown() && focusedBinding.isAllowMouse()) {
  215. focusedBinding.setValue(startedKeyCode);
  216. setFocusedBinding(null);
  217. return true;
  218. }
  219. return super.mouseReleased(double_1, double_2, int_1);
  220. }
  221. @Override
  222. public boolean keyReleased(int int_1, int int_2, int int_3) {
  223. if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowKey()) {
  224. focusedBinding.setValue(startedKeyCode);
  225. setFocusedBinding(null);
  226. return true;
  227. }
  228. return super.keyReleased(int_1, int_2, int_3);
  229. }
  230. @Override
  231. public boolean mouseClicked(double double_1, double double_2, int int_1) {
  232. if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowMouse()) {
  233. if (startedKeyCode.isUnknown())
  234. startedKeyCode.setKeyCode(InputConstants.Type.MOUSE.getOrCreate(int_1));
  235. else if (focusedBinding.isAllowModifiers()) {
  236. if (startedKeyCode.getType() == InputConstants.Type.KEYSYM) {
  237. int code = startedKeyCode.getKeyCode().getValue();
  238. if (Minecraft.ON_OSX ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
  239. Modifier modifier = startedKeyCode.getModifier();
  240. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
  241. startedKeyCode.setKeyCode(InputConstants.Type.MOUSE.getOrCreate(int_1));
  242. return true;
  243. } else if (code == 344 || code == 340) {
  244. Modifier modifier = startedKeyCode.getModifier();
  245. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
  246. startedKeyCode.setKeyCode(InputConstants.Type.MOUSE.getOrCreate(int_1));
  247. return true;
  248. } else if (code == 342 || code == 346) {
  249. Modifier modifier = startedKeyCode.getModifier();
  250. startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
  251. startedKeyCode.setKeyCode(InputConstants.Type.MOUSE.getOrCreate(int_1));
  252. return true;
  253. }
  254. }
  255. }
  256. return true;
  257. } else {
  258. if (this.focusedBinding != null)
  259. return true;
  260. return super.mouseClicked(double_1, double_2, int_1);
  261. }
  262. }
  263. @Override
  264. public boolean keyPressed(int int_1, int int_2, int int_3) {
  265. if (this.focusedBinding != null && (focusedBinding.isAllowKey() || int_1 == 256)) {
  266. if (int_1 != 256) {
  267. if (startedKeyCode.isUnknown())
  268. startedKeyCode.setKeyCode(InputConstants.getKey(int_1, int_2));
  269. else if (focusedBinding.isAllowModifiers()) {
  270. if (startedKeyCode.getType() == InputConstants.Type.KEYSYM) {
  271. int code = startedKeyCode.getKeyCode().getValue();
  272. if (Minecraft.ON_OSX ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
  273. Modifier modifier = startedKeyCode.getModifier();
  274. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
  275. startedKeyCode.setKeyCode(InputConstants.getKey(int_1, int_2));
  276. return true;
  277. } else if (code == 344 || code == 340) {
  278. Modifier modifier = startedKeyCode.getModifier();
  279. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
  280. startedKeyCode.setKeyCode(InputConstants.getKey(int_1, int_2));
  281. return true;
  282. } else if (code == 342 || code == 346) {
  283. Modifier modifier = startedKeyCode.getModifier();
  284. startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
  285. startedKeyCode.setKeyCode(InputConstants.getKey(int_1, int_2));
  286. return true;
  287. }
  288. }
  289. if (Minecraft.ON_OSX ? (int_1 == 343 || int_1 == 347) : (int_1 == 341 || int_1 == 345)) {
  290. Modifier modifier = startedKeyCode.getModifier();
  291. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
  292. return true;
  293. } else if (int_1 == 344 || int_1 == 340) {
  294. Modifier modifier = startedKeyCode.getModifier();
  295. startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
  296. return true;
  297. } else if (int_1 == 342 || int_1 == 346) {
  298. Modifier modifier = startedKeyCode.getModifier();
  299. startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
  300. return true;
  301. }
  302. }
  303. } else {
  304. focusedBinding.setValue(ModifierKeyCode.unknown());
  305. setFocusedBinding(null);
  306. }
  307. return true;
  308. }
  309. if (this.focusedBinding != null && int_1 != 256)
  310. return true;
  311. if (int_1 == 256 && this.shouldCloseOnEsc()) {
  312. return quit();
  313. }
  314. return super.keyPressed(int_1, int_2, int_3);
  315. }
  316. protected final boolean quit() {
  317. if (confirmSave && isEdited())
  318. minecraft.setScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableComponent("text.cloth-config.quit_config"), new TranslatableComponent("text.cloth-config.quit_config_sure"), new TranslatableComponent("text.cloth-config.quit_discard"), new TranslatableComponent("gui.cancel")));
  319. else
  320. minecraft.setScreen(parent);
  321. return true;
  322. }
  323. private class QuitSaveConsumer implements BooleanConsumer {
  324. @Override
  325. public void accept(boolean t) {
  326. if (!t)
  327. minecraft.setScreen(AbstractConfigScreen.this);
  328. else
  329. minecraft.setScreen(parent);
  330. }
  331. }
  332. @Override
  333. public void tick() {
  334. super.tick();
  335. boolean edited = isEdited();
  336. Optional.ofNullable(getQuitButton()).ifPresent(button -> button.setMessage(edited ? new TranslatableComponent("text.cloth-config.cancel_discard") : new TranslatableComponent("gui.cancel")));
  337. for (GuiEventListener child : children())
  338. if (child instanceof TickableBlockEntity)
  339. ((TickableBlockEntity) child).tick();
  340. }
  341. @Nullable
  342. protected AbstractWidget getQuitButton() {
  343. return null;
  344. }
  345. @Override
  346. public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
  347. super.render(matrices, mouseX, mouseY, delta);
  348. for (Tooltip tooltip : tooltips) {
  349. renderTooltip(matrices, tooltip.getText(), tooltip.getX(), tooltip.getY());
  350. }
  351. this.tooltips.clear();
  352. }
  353. @Override
  354. public void addTooltip(Tooltip tooltip) {
  355. this.tooltips.add(tooltip);
  356. }
  357. protected void overlayBackground(PoseStack matrices, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
  358. overlayBackground(matrices.last().pose(), rect, red, green, blue, startAlpha, endAlpha);
  359. }
  360. protected void overlayBackground(Matrix4f matrix, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
  361. if (isTransparentBackground())
  362. return;
  363. Tesselator tessellator = Tesselator.getInstance();
  364. BufferBuilder buffer = tessellator.getBuilder();
  365. minecraft.getTextureManager().bind(getBackgroundLocation());
  366. RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
  367. buffer.begin(7, DefaultVertexFormat.POSITION_TEX_COLOR);
  368. buffer.vertex(matrix, rect.getMinX(), rect.getMaxY(), 0.0F).uv(rect.getMinX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).endVertex();
  369. buffer.vertex(matrix, rect.getMaxX(), rect.getMaxY(), 0.0F).uv(rect.getMaxX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).endVertex();
  370. buffer.vertex(matrix, rect.getMaxX(), rect.getMinY(), 0.0F).uv(rect.getMaxX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).endVertex();
  371. buffer.vertex(matrix, rect.getMinX(), rect.getMinY(), 0.0F).uv(rect.getMinX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).endVertex();
  372. tessellator.end();
  373. }
  374. @Override // override to expose this protected method to config entries
  375. public void renderComponentHoverEffect(PoseStack matrices, Style style, int x, int y) {
  376. super.renderComponentHoverEffect(matrices, style, x, y);
  377. }
  378. @Override
  379. public boolean handleComponentClicked(@Nullable Style style) {
  380. if (style == null) return false;
  381. ClickEvent clickEvent = style.getClickEvent();
  382. if (clickEvent != null && clickEvent.getAction() == ClickEvent.Action.OPEN_URL) {
  383. try {
  384. URI uri = new URI(clickEvent.getValue());
  385. String string = uri.getScheme();
  386. if (string == null) {
  387. throw new URISyntaxException(clickEvent.getValue(), "Missing protocol");
  388. }
  389. if (!(string.equalsIgnoreCase("http") || string.equalsIgnoreCase("https"))) {
  390. throw new URISyntaxException(clickEvent.getValue(), "Unsupported protocol: " + string.toLowerCase(Locale.ROOT));
  391. }
  392. Minecraft.getInstance().setScreen(new ConfirmLinkScreen(openInBrowser -> {
  393. if (openInBrowser) {
  394. Util.getPlatform().openUri(uri);
  395. }
  396. Minecraft.getInstance().setScreen(this);
  397. }, clickEvent.getValue(), true));
  398. } catch (URISyntaxException e) {
  399. ClothConfigInitializer.LOGGER.error("Can't open url for {}", clickEvent, e);
  400. }
  401. return true;
  402. }
  403. return super.handleComponentClicked(style);
  404. }
  405. }