mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Explain empty object views
Icon, where to add them, path chip.
This commit is contained in:
1 parent
0b5b75e936
commit
4ac268047e
8 files changed
+150
-4
No files matched your search
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
/**
|
||||
* What an empty view says, drawn in the middle of it the way IntelliJ fills an empty tool window: a large icon, what
|
||||
* is missing, and where to add it.
|
||||
*
|
||||
* @param location the pack folder or file the hint points to, drawn as code
|
||||
*/
|
||||
record EmptyState(Icon icon, String title, String hint, String location, String footer) {
|
||||
private static final int ICON_GAP = 18;
|
||||
private static final int TITLE_GAP = 8;
|
||||
private static final int LOCATION_GAP = 10;
|
||||
private static final int FOOTER_GAP = 14;
|
||||
private static final int CHIP_PADDING_X = 8;
|
||||
private static final int CHIP_PADDING_Y = 4;
|
||||
private static final int CHIP_ARC = 8;
|
||||
|
||||
/**
|
||||
* Draws the empty state centred on the given point.
|
||||
*/
|
||||
void paint(Graphics2D graphics, Component owner, int centerX, int centerY) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
int top = centerY - height(canvas) / 2;
|
||||
icon.paintIcon(owner, canvas, centerX - icon.getIconWidth() / 2, top);
|
||||
top += icon.getIconHeight() + StudioTheme.scale(ICON_GAP);
|
||||
top = drawCentered(canvas, title, StudioTheme.titleFont(), StudioTheme.TEXT, centerX, top);
|
||||
top = drawCentered(canvas, hint, StudioTheme.font(), StudioTheme.SECONDARY_TEXT, centerX,
|
||||
top + StudioTheme.scale(TITLE_GAP));
|
||||
top = drawLocationChip(canvas, centerX, top + StudioTheme.scale(LOCATION_GAP));
|
||||
drawCentered(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, centerX, top + StudioTheme.scale(FOOTER_GAP));
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private int height(Graphics2D canvas) {
|
||||
return icon.getIconHeight() + StudioTheme.scale(ICON_GAP)
|
||||
+ canvas.getFontMetrics(StudioTheme.titleFont()).getHeight()
|
||||
+ StudioTheme.scale(TITLE_GAP) + canvas.getFontMetrics(StudioTheme.font()).getHeight()
|
||||
+ StudioTheme.scale(LOCATION_GAP) + chipHeight(canvas)
|
||||
+ StudioTheme.scale(FOOTER_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||
}
|
||||
|
||||
private int chipHeight(Graphics2D canvas) {
|
||||
return canvas.getFontMetrics(StudioTheme.monospacedFont()).getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2;
|
||||
}
|
||||
|
||||
private int drawLocationChip(Graphics2D canvas, int centerX, int top) {
|
||||
canvas.setFont(StudioTheme.monospacedFont());
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
int width = metrics.stringWidth(location) + StudioTheme.scale(CHIP_PADDING_X) * 2;
|
||||
int height = chipHeight(canvas);
|
||||
int left = centerX - width / 2;
|
||||
RoundRectangle2D chip = new RoundRectangle2D.Double(left + 0.5D, top + 0.5D, width - 1D, height - 1D,
|
||||
StudioTheme.scale(CHIP_ARC), StudioTheme.scale(CHIP_ARC));
|
||||
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
|
||||
canvas.fill(chip);
|
||||
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||
canvas.draw(chip);
|
||||
canvas.setColor(StudioTheme.TEXT);
|
||||
canvas.drawString(location, left + StudioTheme.scale(CHIP_PADDING_X), top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent());
|
||||
return top + height;
|
||||
}
|
||||
|
||||
private static int drawCentered(Graphics2D canvas, String text, Font font, Color color, int centerX, int top) {
|
||||
canvas.setFont(font);
|
||||
canvas.setColor(color);
|
||||
FontMetrics metrics = canvas.getFontMetrics();
|
||||
canvas.drawString(text, centerX - metrics.stringWidth(text) / 2, top + metrics.getAscent());
|
||||
return top + metrics.getHeight();
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,18 @@ final class MapViewIcon {
|
||||
}
|
||||
|
||||
static Icon of(MapView view) {
|
||||
return LOADED.computeIfAbsent(view, MapViewIcon::load);
|
||||
return LOADED.computeIfAbsent(view, shown -> load(shown, SIZE));
|
||||
}
|
||||
|
||||
private static Icon load(MapView view) {
|
||||
/**
|
||||
* The view's icon drawn larger, for an empty view.
|
||||
*/
|
||||
static Icon large(MapView view, int size) {
|
||||
return load(view, size);
|
||||
}
|
||||
|
||||
private static Icon load(MapView view, int size) {
|
||||
String fileName = view.name().toLowerCase(Locale.ROOT).replace('_', '-') + ".svg";
|
||||
return new FlatSVGIcon(ICON_FOLDER + fileName, SIZE, SIZE, MapViewIcon.class.getClassLoader());
|
||||
return new FlatSVGIcon(ICON_FOLDER + fileName, size, size, MapViewIcon.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ final class ObjectExplorer {
|
||||
browser.showObjects(keys);
|
||||
String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject();
|
||||
if (key == null) {
|
||||
viewport.showMessage("This pack has no " + source.noun() + "s");
|
||||
viewport.showEmptyState(source.emptyState());
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
if (comparingVariants) {
|
||||
|
||||
@@ -31,6 +31,9 @@ interface ObjectSource {
|
||||
/** What one entry is called, such as "object", for the browser's texts. */
|
||||
String noun();
|
||||
|
||||
/** What the view shows when the pack has no entries. */
|
||||
EmptyState emptyState();
|
||||
|
||||
/** The key of every entry, where a slash separates folders. */
|
||||
List<String> keys(IrisData data);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ final class ObjectViewport extends JComponent {
|
||||
private ObjectCamera camera = ObjectCamera.START;
|
||||
private BufferedImage frame;
|
||||
private String message = "Choose an object";
|
||||
private EmptyState emptyState;
|
||||
private boolean rendering;
|
||||
private boolean renderAgain;
|
||||
private Point dragPoint;
|
||||
@@ -98,6 +99,7 @@ final class ObjectViewport extends JComponent {
|
||||
mesh = nextMesh;
|
||||
camera = ObjectCamera.START;
|
||||
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||
emptyState = null;
|
||||
if (nextMesh.isEmpty()) {
|
||||
frame = null;
|
||||
finishLoading();
|
||||
@@ -116,6 +118,7 @@ final class ObjectViewport extends JComponent {
|
||||
}
|
||||
loadingText = text;
|
||||
message = null;
|
||||
emptyState = null;
|
||||
loadingTicker.start();
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@ final class ObjectViewport extends JComponent {
|
||||
void replaceMesh(ObjectMesh nextMesh) {
|
||||
mesh = nextMesh;
|
||||
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||
emptyState = null;
|
||||
requestFrame();
|
||||
repaint();
|
||||
}
|
||||
@@ -147,6 +151,16 @@ final class ObjectViewport extends JComponent {
|
||||
void showMessage(String text) {
|
||||
mesh = null;
|
||||
message = text;
|
||||
emptyState = null;
|
||||
frame = null;
|
||||
finishLoading();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void showEmptyState(EmptyState state) {
|
||||
mesh = null;
|
||||
message = null;
|
||||
emptyState = state;
|
||||
frame = null;
|
||||
finishLoading();
|
||||
repaint();
|
||||
@@ -223,6 +237,9 @@ final class ObjectViewport extends JComponent {
|
||||
if (message != null) {
|
||||
drawMessage((Graphics2D) graphics);
|
||||
}
|
||||
if (emptyState != null) {
|
||||
emptyState.paint((Graphics2D) graphics, this, coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2, getHeight() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLoading(Graphics2D graphics) {
|
||||
|
||||
@@ -27,11 +27,19 @@ import java.util.List;
|
||||
* The pack's object files, by their load keys.
|
||||
*/
|
||||
final class PackObjectSource implements ObjectSource {
|
||||
private static final int EMPTY_ICON_SIZE = 40;
|
||||
|
||||
@Override
|
||||
public String noun() {
|
||||
return "object";
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmptyState emptyState() {
|
||||
return new EmptyState(MapViewIcon.large(MapView.OBJECTS, EMPTY_ICON_SIZE), "No objects yet",
|
||||
"Put the pack's .iob objects in", "objects/", "They appear here once the pack reloads.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> keys(IrisData data) {
|
||||
return Arrays.asList(data.getObjectLoader().getPossibleKeys());
|
||||
|
||||
File renamed without changes.
@@ -39,6 +39,8 @@ final class ProceduralObjectSource implements ObjectSource {
|
||||
"fungi", "Fungi",
|
||||
"ruins", "Ruins");
|
||||
|
||||
private static final int EMPTY_ICON_SIZE = 40;
|
||||
|
||||
private Map<String, ResolvedProceduralPlacement> definitionsByKey = Map.of();
|
||||
|
||||
@Override
|
||||
@@ -46,6 +48,13 @@ final class ProceduralObjectSource implements ObjectSource {
|
||||
return "procedural object";
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmptyState emptyState() {
|
||||
return new EmptyState(MapViewIcon.large(MapView.PROCEDURAL, EMPTY_ICON_SIZE), "No procedural objects yet",
|
||||
"Declare them in a biome or region, or save a settings file per object, like",
|
||||
"snippet/" + ProceduralObjectCatalog.TREE_FOLDER + "/oak.json", "They appear here once the pack reloads.");
|
||||
}
|
||||
|
||||
// The type folder gets its label; the rest of the path stays as the pack has it.
|
||||
@Override
|
||||
public List<String> keys(IrisData data) {
|
||||
|
||||
Reference in new issue
Block a user