mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Pin folder headers, cut layers and see through blocks in the Objects view
Folder headers stay pinned at the top of the object tree while scrolling, one per nesting level; clicking one jumps to that folder. A faint slider over the 3D view (or Page Up/Down) cuts away the top layers so floors of large structures like the woodland mansion can be looked into. Clicking a block in the overview list makes that block type see-through, so whatever it hides shows behind it. Model used: Claude Opus 5.5
This commit is contained in:
1 parent
db3d42df26
commit
99258bfd98
9 files changed
+674
-29
No files matched your search
@@ -270,7 +270,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
viewport.setCoveredLeftWidth(sidebarIsland.getWidth());
|
||||
}
|
||||
});
|
||||
content.add(viewport, OBJECTS_CARD);
|
||||
content.add(objectExplorer.view(), OBJECTS_CARD);
|
||||
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ final class ObjectBrowser extends JPanel {
|
||||
countLabel.setFont(StudioTheme.smallFont());
|
||||
countLabel.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||
add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
|
||||
add(StudioToolbar.scroll(tree), BorderLayout.CENTER);
|
||||
add(StickyFolderHeaders.around(tree, StudioToolbar.scroll(tree)), BorderLayout.CENTER);
|
||||
add(StudioToolbar.padded(countLabel, 6, 12, 8, 12), BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,14 @@ import org.bukkit.util.BlockVector;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -46,13 +48,15 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
final class ObjectExplorer {
|
||||
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
|
||||
private static final int UNKNOWN_BLOCK_COLOR = 0x8A8A8A;
|
||||
private static final String CAMERA_HINT = "Drag to orbit · Shift-drag to pan · Scroll to zoom · R resets";
|
||||
private static final String CAMERA_HINT = "Drag to orbit · Shift-drag to pan · Scroll to zoom · Page Up/Down cuts layers · R resets";
|
||||
|
||||
private final Engine startEngine;
|
||||
private final UUID worldId;
|
||||
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
|
||||
private final ObjectViewport viewport = new ObjectViewport();
|
||||
private final ObjectOverviewPanel overview = new ObjectOverviewPanel();
|
||||
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> rebuildShownMesh());
|
||||
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
|
||||
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
|
||||
private final StudioStatusBar statusBar = new StudioStatusBar();
|
||||
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris Object Loader");
|
||||
@@ -61,11 +65,14 @@ final class ObjectExplorer {
|
||||
return thread;
|
||||
});
|
||||
private final AtomicLong objectRevision = new AtomicLong();
|
||||
private final AtomicLong meshRevision = new AtomicLong();
|
||||
private final Map<String, BlockModel> models = new ConcurrentHashMap<>();
|
||||
private final Runnable hotloadHook = this::reloadPack;
|
||||
private volatile BlockTextures textures;
|
||||
private volatile BiomeTintColors tintColors;
|
||||
private String shownKey;
|
||||
private LoadedObject shown;
|
||||
private Set<String> seeThroughTypes = Set.of();
|
||||
|
||||
ObjectExplorer(Engine engine, UUID worldId) {
|
||||
this.startEngine = engine;
|
||||
@@ -73,6 +80,8 @@ final class ObjectExplorer {
|
||||
GuiHost.interruptOnPluginShutdown(loader);
|
||||
GuiHost.registerHotloadHook(hotloadHook);
|
||||
statusBar.showPosition(CAMERA_HINT);
|
||||
viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1));
|
||||
viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1));
|
||||
}
|
||||
|
||||
JComponent browser() {
|
||||
@@ -83,6 +92,13 @@ final class ObjectExplorer {
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 3D view with the slider that cuts away its top layers.
|
||||
*/
|
||||
JComponent view() {
|
||||
return viewWithLayerCut;
|
||||
}
|
||||
|
||||
JComponent overview() {
|
||||
return overview;
|
||||
}
|
||||
@@ -133,6 +149,7 @@ final class ObjectExplorer {
|
||||
if (key == null) {
|
||||
viewport.showMessage("This pack has no objects");
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
return;
|
||||
}
|
||||
browser.select(key);
|
||||
@@ -141,6 +158,8 @@ final class ObjectExplorer {
|
||||
|
||||
private void showObject(String key) {
|
||||
shownKey = key;
|
||||
shown = null;
|
||||
meshRevision.incrementAndGet();
|
||||
long revision = objectRevision.incrementAndGet();
|
||||
viewport.showMessage("Loading " + key + "...");
|
||||
statusBar.showActivity("Loading " + key + "...");
|
||||
@@ -170,18 +189,73 @@ final class ObjectExplorer {
|
||||
});
|
||||
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
|
||||
ObjectSummary summary = ObjectSummary.of(key, mesh.bounds(), blocks.values(), this::iconOf);
|
||||
return new LoadedObject(mesh, summary);
|
||||
return new LoadedObject(blocks, mesh, summary);
|
||||
}
|
||||
|
||||
private void showLoaded(long revision, LoadedObject loaded) {
|
||||
if (revision != objectRevision.get()) {
|
||||
return;
|
||||
}
|
||||
shown = loaded;
|
||||
seeThroughTypes = Set.of();
|
||||
statusBar.showActivity("");
|
||||
viewport.show(loaded.mesh());
|
||||
overview.show(loaded.summary());
|
||||
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn",
|
||||
loaded.summary().key(), loaded.summary().blockCount(), loaded.mesh().faceCount()));
|
||||
layerCut.showHeight(loaded.mesh().bounds().sizeY());
|
||||
showMeshSummary(loaded.mesh());
|
||||
}
|
||||
|
||||
private void showMeshSummary(ObjectMesh mesh) {
|
||||
ObjectSummary summary = shown.summary();
|
||||
String cut = layerCut.isCutting()
|
||||
? String.format(Locale.ROOT, " · showing layers 1-%d of %d", layerCut.shownLayerCount(), layerCut.layerCount())
|
||||
: "";
|
||||
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn%s",
|
||||
summary.key(), summary.blockCount(), mesh.faceCount(), cut));
|
||||
}
|
||||
|
||||
private void showSeeThrough(Set<String> blockTypes) {
|
||||
seeThroughTypes = blockTypes;
|
||||
rebuildShownMesh();
|
||||
}
|
||||
|
||||
// Blocks under the cut that were covered by the ones above get their hidden faces back, so each floor shows, and
|
||||
// blocks behind see-through ones get theirs back too.
|
||||
private void rebuildShownMesh() {
|
||||
LoadedObject object = shown;
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
long revision = meshRevision.incrementAndGet();
|
||||
ObjectMesh.Bounds bounds = object.mesh().bounds();
|
||||
boolean cutting = layerCut.isCutting();
|
||||
int highestShownY = bounds.minY() + layerCut.shownLayerCount() - 1;
|
||||
Set<String> seeThrough = seeThroughTypes;
|
||||
loader.execute(() -> {
|
||||
if (revision != meshRevision.get()) {
|
||||
return;
|
||||
}
|
||||
ObjectMesh mesh = !cutting && seeThrough.isEmpty()
|
||||
? object.mesh()
|
||||
: ObjectMesh.build(cutting ? blocksUpTo(object.blocks(), highestShownY) : object.blocks(), this::modelOf, bounds,
|
||||
block -> seeThrough.contains(block.getMaterial().getKey().getKey()));
|
||||
EventQueue.invokeLater(() -> {
|
||||
if (revision == meshRevision.get() && shown == object) {
|
||||
viewport.replaceMesh(mesh);
|
||||
showMeshSummary(mesh);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static Map<BlockVector, BlockData> blocksUpTo(Map<BlockVector, BlockData> blocks, int highestY) {
|
||||
Map<BlockVector, BlockData> below = new HashMap<>();
|
||||
blocks.forEach((position, block) -> {
|
||||
if (position.getBlockY() <= highestY) {
|
||||
below.put(position, block);
|
||||
}
|
||||
});
|
||||
return below;
|
||||
}
|
||||
|
||||
private void showFailure(long revision, String key, Throwable error) {
|
||||
@@ -191,6 +265,7 @@ final class ObjectExplorer {
|
||||
statusBar.showActivity("");
|
||||
viewport.showMessage("Could not load " + key);
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
statusBar.showError(error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage());
|
||||
}
|
||||
|
||||
@@ -238,6 +313,6 @@ final class ObjectExplorer {
|
||||
return texture.tinted(colors.colorAt(texture.biomeTint(), 0, 0));
|
||||
}
|
||||
|
||||
private record LoadedObject(ObjectMesh mesh, ObjectSummary summary) {
|
||||
private record LoadedObject(Map<BlockVector, BlockData> blocks, ObjectMesh mesh, ObjectSummary summary) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSlider;
|
||||
import javax.swing.SwingConstants;
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
/**
|
||||
* A slim slider floating on the right of the 3D view that hides an object's top layers, to look inside buildings
|
||||
* floor by floor. Most objects never need it, so it stays faint until the mouse comes near or a cut is active.
|
||||
*/
|
||||
final class ObjectLayerCut extends JPanel {
|
||||
private static final long serialVersionUID = 4473820211915526120L;
|
||||
private static final int PANEL_WIDTH = 30;
|
||||
private static final int MAX_HEIGHT = 320;
|
||||
private static final double HEIGHT_SHARE_OF_VIEW = 0.5D;
|
||||
private static final int EDGE_MARGIN = 8;
|
||||
private static final float FAINT_OPACITY = 0.28F;
|
||||
|
||||
private final JSlider slider = new JSlider(SwingConstants.VERTICAL, 1, 1, 1);
|
||||
private final JLabel shownLayers = new JLabel(" ", SwingConstants.CENTER);
|
||||
private final IntConsumer onLayersChanged;
|
||||
private boolean resetting;
|
||||
private boolean hovered;
|
||||
|
||||
/**
|
||||
* @param onLayersChanged told how many layers from the bottom stay visible whenever the user moves the slider
|
||||
*/
|
||||
ObjectLayerCut(IntConsumer onLayersChanged) {
|
||||
super(new BorderLayout(0, StudioTheme.scale(4)));
|
||||
this.onLayersChanged = onLayersChanged;
|
||||
setOpaque(false);
|
||||
setToolTipText("Hide the top layers to look inside · Page Up / Page Down");
|
||||
shownLayers.setFont(StudioTheme.smallFont());
|
||||
shownLayers.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||
slider.setOpaque(false);
|
||||
slider.setFocusable(false);
|
||||
slider.setToolTipText(getToolTipText());
|
||||
slider.addChangeListener(event -> {
|
||||
showLayerLabel();
|
||||
if (!resetting) {
|
||||
onLayersChanged.accept(slider.getValue());
|
||||
}
|
||||
});
|
||||
MouseAdapter hover = new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent event) {
|
||||
setHovered(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent event) {
|
||||
setHovered(slider.getValueIsAdjusting());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent event) {
|
||||
setHovered(slider.contains(event.getPoint()));
|
||||
}
|
||||
};
|
||||
slider.addMouseListener(hover);
|
||||
addMouseListener(hover);
|
||||
add(shownLayers, BorderLayout.NORTH);
|
||||
add(slider, BorderLayout.CENTER);
|
||||
showHeight(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The view with this slider floating over its right edge, halfway down.
|
||||
*/
|
||||
JComponent floatingOver(JComponent view) {
|
||||
ObjectLayerCut cut = this;
|
||||
JPanel stack = new JPanel(null) {
|
||||
private static final long serialVersionUID = -1950381405658102347L;
|
||||
|
||||
// The slider overlaps the view, so the view may not repaint without the slider on top of it.
|
||||
@Override
|
||||
public boolean isOptimizedDrawingEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doLayout() {
|
||||
view.setBounds(0, 0, getWidth(), getHeight());
|
||||
int width = StudioTheme.scale(PANEL_WIDTH);
|
||||
int height = Math.min(StudioTheme.scale(MAX_HEIGHT), (int) (getHeight() * HEIGHT_SHARE_OF_VIEW));
|
||||
cut.setBounds(getWidth() - width - StudioTheme.scale(EDGE_MARGIN), (getHeight() - height) / 2, width, height);
|
||||
}
|
||||
};
|
||||
stack.add(cut);
|
||||
stack.add(view);
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows every layer of a newly chosen object without telling anyone, since nothing is hidden yet.
|
||||
*/
|
||||
void showHeight(int layers) {
|
||||
resetting = true;
|
||||
try {
|
||||
slider.setMaximum(Math.max(1, layers));
|
||||
slider.setValue(slider.getMaximum());
|
||||
} finally {
|
||||
resetting = false;
|
||||
}
|
||||
showLayerLabel();
|
||||
setVisible(layers > 1);
|
||||
}
|
||||
|
||||
void stepBy(int layers) {
|
||||
if (isVisible()) {
|
||||
slider.setValue(slider.getValue() + layers);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isCutting() {
|
||||
return slider.getValue() < slider.getMaximum();
|
||||
}
|
||||
|
||||
int shownLayerCount() {
|
||||
return slider.getValue();
|
||||
}
|
||||
|
||||
int layerCount() {
|
||||
return slider.getMaximum();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintChildren(Graphics graphics) {
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
if (!hovered && !isCutting()) {
|
||||
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, FAINT_OPACITY));
|
||||
}
|
||||
super.paintChildren(canvas);
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void setHovered(boolean nowHovered) {
|
||||
if (hovered != nowHovered) {
|
||||
hovered = nowHovered;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void showLayerLabel() {
|
||||
shownLayers.setText(isCutting() ? Integer.toString(slider.getValue()) : " ");
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* The faces of an object's blocks that can be seen from outside. A face is left out when the neighbour on its side
|
||||
@@ -34,22 +35,36 @@ import java.util.function.Function;
|
||||
* kept apart because they are drawn last, blended over everything behind them.
|
||||
*/
|
||||
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bounds) {
|
||||
private static final double SEE_THROUGH_OPACITY = 0.22D;
|
||||
private static final double FULL_OPACITY = 1D;
|
||||
|
||||
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
|
||||
return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bounds the whole object's extent, kept when only part of it is drawn so the view does not jump
|
||||
* @param isSeeThrough blocks drawn faintly, which hide nothing behind them, to look past them into the object
|
||||
*/
|
||||
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf, Bounds bounds,
|
||||
Predicate<BlockData> isSeeThrough) {
|
||||
List<Quad> solid = new ArrayList<>();
|
||||
List<Quad> translucent = new ArrayList<>();
|
||||
for (Map.Entry<BlockVector, BlockData> entry : blocks.entrySet()) {
|
||||
BlockVector position = entry.getKey();
|
||||
BlockData block = entry.getValue();
|
||||
BlockModel model = modelOf.apply(block);
|
||||
boolean seeThrough = isSeeThrough.test(block);
|
||||
for (Face face : model.faces()) {
|
||||
if (isHiddenByNeighbour(blocks, modelOf, position, block, model, face)) {
|
||||
if (isHiddenByNeighbour(blocks, modelOf, isSeeThrough, position, block, model, face)) {
|
||||
continue;
|
||||
}
|
||||
Quad quad = new Quad(face, position.getBlockX(), position.getBlockY(), position.getBlockZ());
|
||||
(face.texture().isTranslucent() ? translucent : solid).add(quad);
|
||||
Quad quad = new Quad(face, position.getBlockX(), position.getBlockY(), position.getBlockZ(),
|
||||
seeThrough ? SEE_THROUGH_OPACITY : FULL_OPACITY);
|
||||
(seeThrough || face.texture().isTranslucent() ? translucent : solid).add(quad);
|
||||
}
|
||||
}
|
||||
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), Bounds.of(blocks.keySet()));
|
||||
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), bounds);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
@@ -60,9 +75,11 @@ record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bou
|
||||
return solidQuads.size() + translucentQuads.size();
|
||||
}
|
||||
|
||||
// Glass next to glass and water next to water hide the side between them too, as they do in the game.
|
||||
// Glass next to glass and water next to water hide the side between them too, as they do in the game. A see-through
|
||||
// block only hides sides of other see-through blocks, so whatever it covers shows through it.
|
||||
private static boolean isHiddenByNeighbour(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf,
|
||||
BlockVector position, BlockData block, BlockModel model, Face face) {
|
||||
Predicate<BlockData> isSeeThrough, BlockVector position, BlockData block,
|
||||
BlockModel model, Face face) {
|
||||
CubeFace side = face.cullFace();
|
||||
if (side == null) {
|
||||
return false;
|
||||
@@ -71,7 +88,7 @@ record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bou
|
||||
position.getBlockX() + side.stepX(),
|
||||
position.getBlockY() + side.stepY(),
|
||||
position.getBlockZ() + side.stepZ()));
|
||||
if (neighbour == null) {
|
||||
if (neighbour == null || (isSeeThrough.test(neighbour) && !isSeeThrough.test(block))) {
|
||||
return false;
|
||||
}
|
||||
BlockModel neighbourModel = modelOf.apply(neighbour);
|
||||
@@ -82,9 +99,9 @@ record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, Bounds bou
|
||||
}
|
||||
|
||||
/**
|
||||
* A model face placed at a block in the object.
|
||||
* A model face placed at a block in the object, drawn at the given opacity on top of its texture's own.
|
||||
*/
|
||||
record Quad(Face face, int x, int y, int z) {
|
||||
record Quad(Face face, int x, int y, int z, double opacity) {
|
||||
double originX() {
|
||||
return x + face.origin().x();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ package com.volmit.iris.core.gui;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.JLabel;
|
||||
@@ -28,18 +30,28 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The chosen object's size and the blocks it is made of, most used first, like the overview in Procedural Objects.
|
||||
* Clicking a block makes it see-through in the 3D view, to look past walls into the object.
|
||||
*/
|
||||
final class ObjectOverviewPanel extends JPanel {
|
||||
private static final long serialVersionUID = -8811407613645718290L;
|
||||
@@ -50,6 +62,8 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
private static final int ROW_PADDING = 10;
|
||||
private static final int SHARE_WIDTH = 92;
|
||||
private static final int SHARE_BAR_HEIGHT = 4;
|
||||
private static final float SEE_THROUGH_ICON_OPACITY = 0.3F;
|
||||
private static final String SEE_THROUGH_HINT = "Click a block to see through it";
|
||||
|
||||
private final JLabel title = new JLabel(" ");
|
||||
private final JLabel path = new JLabel(" ");
|
||||
@@ -65,10 +79,18 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
private final JLabel seeThroughHint = new JLabel(SEE_THROUGH_HINT);
|
||||
private final JButton showAllSolid = new JButton("Show all solid");
|
||||
private final Set<String> seeThroughTypes = new LinkedHashSet<>();
|
||||
private final Consumer<Set<String>> onSeeThroughChanged;
|
||||
private ObjectSummary summary;
|
||||
|
||||
ObjectOverviewPanel() {
|
||||
/**
|
||||
* @param onSeeThroughChanged told which block types to draw see-through whenever the user changes them
|
||||
*/
|
||||
ObjectOverviewPanel(Consumer<Set<String>> onSeeThroughChanged) {
|
||||
super(new BorderLayout());
|
||||
this.onSeeThroughChanged = onSeeThroughChanged;
|
||||
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
@@ -85,6 +107,8 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
blockCount.setText(formatCount(nextSummary.blockCount()));
|
||||
blockTypes.setText(formatCount(nextSummary.blockUses().size()));
|
||||
filled.setText(String.format(Locale.ROOT, "%.1f%%", nextSummary.filledFraction() * 100D));
|
||||
seeThroughTypes.clear();
|
||||
showSeeThroughState();
|
||||
refilter();
|
||||
}
|
||||
|
||||
@@ -96,6 +120,8 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
value.setText("—");
|
||||
}
|
||||
rows.clear();
|
||||
seeThroughTypes.clear();
|
||||
showSeeThroughState();
|
||||
}
|
||||
|
||||
private JPanel buildHeader() {
|
||||
@@ -147,11 +173,68 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
list.setCellRenderer(new BlockUseRenderer());
|
||||
list.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||
list.setBorder(StudioTheme.emptyBorder(0, 6, 4, 6));
|
||||
blocks.add(StudioToolbar.padded(filterField, 4, 12, 6, 12), BorderLayout.NORTH);
|
||||
list.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
list.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent event) {
|
||||
int row = list.locationToIndex(event.getPoint());
|
||||
if (SwingUtilities.isLeftMouseButton(event) && row >= 0 && list.getCellBounds(row, row).contains(event.getPoint())) {
|
||||
toggleSeeThrough(rows.get(row).blockType());
|
||||
}
|
||||
}
|
||||
});
|
||||
JPanel top = new JPanel();
|
||||
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
|
||||
top.setOpaque(false);
|
||||
top.add(leftAligned(StudioToolbar.padded(filterField, 4, 12, 4, 12)));
|
||||
top.add(leftAligned(StudioToolbar.padded(seeThroughRow(), 0, 14, 6, 12)));
|
||||
blocks.add(top, BorderLayout.NORTH);
|
||||
blocks.add(StudioToolbar.scroll(list), BorderLayout.CENTER);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private JPanel seeThroughRow() {
|
||||
seeThroughHint.setFont(StudioTheme.smallFont());
|
||||
seeThroughHint.setForeground(StudioTheme.MUTED_TEXT);
|
||||
showAllSolid.setFont(StudioTheme.smallFont());
|
||||
showAllSolid.setForeground(StudioTheme.LINK);
|
||||
showAllSolid.setBorderPainted(false);
|
||||
showAllSolid.setContentAreaFilled(false);
|
||||
showAllSolid.setFocusable(false);
|
||||
showAllSolid.setMargin(new Insets(0, 0, 0, 0));
|
||||
showAllSolid.setBorder(BorderFactory.createEmptyBorder());
|
||||
showAllSolid.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
showAllSolid.addActionListener(event -> {
|
||||
seeThroughTypes.clear();
|
||||
seeThroughChanged();
|
||||
});
|
||||
JPanel row = new JPanel(new BorderLayout());
|
||||
row.setOpaque(false);
|
||||
row.add(seeThroughHint, BorderLayout.CENTER);
|
||||
row.add(showAllSolid, BorderLayout.EAST);
|
||||
showSeeThroughState();
|
||||
return row;
|
||||
}
|
||||
|
||||
private void toggleSeeThrough(String blockType) {
|
||||
if (!seeThroughTypes.remove(blockType)) {
|
||||
seeThroughTypes.add(blockType);
|
||||
}
|
||||
seeThroughChanged();
|
||||
}
|
||||
|
||||
private void seeThroughChanged() {
|
||||
showSeeThroughState();
|
||||
list.repaint();
|
||||
onSeeThroughChanged.accept(Set.copyOf(seeThroughTypes));
|
||||
}
|
||||
|
||||
private void showSeeThroughState() {
|
||||
int count = seeThroughTypes.size();
|
||||
seeThroughHint.setText(count == 0 ? SEE_THROUGH_HINT : count == 1 ? "1 block see-through" : count + " blocks see-through");
|
||||
showAllSolid.setVisible(count > 0);
|
||||
}
|
||||
|
||||
private void refilter() {
|
||||
rows.clear();
|
||||
if (summary == null) {
|
||||
@@ -199,6 +282,7 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
private final class BlockUseRenderer extends DefaultListCellRenderer {
|
||||
private static final long serialVersionUID = 2785337045812613941L;
|
||||
private transient BufferedImage icon;
|
||||
private boolean seeThrough;
|
||||
private double share;
|
||||
private String percent = "";
|
||||
|
||||
@@ -206,9 +290,12 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
|
||||
ObjectSummary.BlockUse use = (ObjectSummary.BlockUse) value;
|
||||
super.getListCellRendererComponent(owner, use.blockType(), index, false, false);
|
||||
seeThrough = seeThroughTypes.contains(use.blockType());
|
||||
setFont(StudioTheme.font());
|
||||
setForeground(seeThrough ? StudioTheme.MUTED_TEXT : StudioTheme.TEXT);
|
||||
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + ICON_SIZE + ICON_GAP, 0, ROW_PADDING + SHARE_WIDTH));
|
||||
setToolTipText(use.blockType() + " · " + formatCount(use.count()) + " blocks");
|
||||
setToolTipText(use.blockType() + " · " + formatCount(use.count()) + " blocks · click to "
|
||||
+ (seeThrough ? "make solid again" : "see through it"));
|
||||
icon = use.icon();
|
||||
share = summary == null || summary.blockCount() == 0 ? 0D : use.count() / (double) summary.blockCount();
|
||||
percent = share > 0D && share < 0.001D ? "<0.1%" : String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||
@@ -225,7 +312,12 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
int iconY = (getHeight() - iconSize) / 2;
|
||||
if (icon != null) {
|
||||
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
||||
Composite solid = canvas.getComposite();
|
||||
if (seeThrough) {
|
||||
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, SEE_THROUGH_ICON_OPACITY));
|
||||
}
|
||||
canvas.drawImage(icon, iconX, iconY, iconSize, iconSize, null);
|
||||
canvas.setComposite(solid);
|
||||
} else {
|
||||
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||
canvas.drawRect(iconX, iconY, iconSize - 1, iconSize - 1);
|
||||
@@ -248,7 +340,7 @@ final class ObjectOverviewPanel extends JPanel {
|
||||
int barY = (getHeight() - barHeight) / 2;
|
||||
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||
canvas.fillRoundRect(barX, barY, barWidth, barHeight, barHeight, barHeight);
|
||||
canvas.setColor(StudioTheme.ACCENT);
|
||||
canvas.setColor(seeThrough ? StudioTheme.CONTROL_BORDER : StudioTheme.ACCENT);
|
||||
canvas.fillRoundRect(barX, barY, Math.max(barHeight, (int) Math.round(barWidth * share)), barHeight, barHeight, barHeight);
|
||||
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||
int textWidth = canvas.getFontMetrics().stringWidth(percent);
|
||||
|
||||
@@ -193,7 +193,7 @@ final class ObjectRenderer {
|
||||
}
|
||||
int color = BlockPreviewColors.shade(texels[texel], shade);
|
||||
if (blended) {
|
||||
pixels[index] = blend(color, pixels[index], alphas == null ? 1D : alphas[texel] / FULL_ALPHA);
|
||||
pixels[index] = blend(color, pixels[index], (alphas == null ? 1D : alphas[texel] / FULL_ALPHA) * quad.opacity());
|
||||
continue;
|
||||
}
|
||||
depths[index] = depth;
|
||||
|
||||
@@ -86,6 +86,26 @@ final class ObjectViewport extends JComponent {
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps in another mesh of the same object, like one with its top layers cut away, keeping the camera.
|
||||
*/
|
||||
void replaceMesh(ObjectMesh nextMesh) {
|
||||
mesh = nextMesh;
|
||||
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||
requestFrame();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void bindKey(int keyCode, String name, Runnable action) {
|
||||
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(keyCode, 0), name);
|
||||
getActionMap().put(name, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showMessage(String text) {
|
||||
mesh = null;
|
||||
message = text;
|
||||
@@ -218,12 +238,6 @@ final class ObjectViewport extends JComponent {
|
||||
}
|
||||
|
||||
private void bindKey(int keyCode, String name, UnaryOperator<ObjectCamera> move) {
|
||||
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(keyCode, 0), name);
|
||||
getActionMap().put(name, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
moveCamera(move);
|
||||
}
|
||||
});
|
||||
bindKey(keyCode, name, () -> moveCamera(move));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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.CellRendererPane;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLayer;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTree;
|
||||
import javax.swing.JViewport;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.plaf.LayerUI;
|
||||
import javax.swing.plaf.basic.BasicTreeUI;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.awt.AWTEvent;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.GradientPaint;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Keeps the folders around the top row of a scrolled tree pinned at the top, so a long folder never hides where it
|
||||
* is. The next folder pushes the last header up as it arrives, and clicking a header jumps to its folder.
|
||||
*/
|
||||
final class StickyFolderHeaders extends LayerUI<JScrollPane> {
|
||||
private static final long serialVersionUID = -2205474187531254372L;
|
||||
private static final int SHADOW_HEIGHT = 4;
|
||||
private static final Color SHADOW = new Color(0, 0, 0, 90);
|
||||
private static final Color NO_SHADOW = new Color(0, 0, 0, 0);
|
||||
// The folder handle sits one indent to the left of the folder's name, as the tree draws it.
|
||||
private static final int HANDLE_NUDGE = 1;
|
||||
|
||||
private final JTree tree;
|
||||
private final CellRendererPane rendererPane = new CellRendererPane();
|
||||
private transient JLayer<JScrollPane> layer;
|
||||
private int hoveredHeader = -1;
|
||||
|
||||
private StickyFolderHeaders(JTree tree) {
|
||||
this.tree = tree;
|
||||
}
|
||||
|
||||
static JLayer<JScrollPane> around(JTree tree, JScrollPane scrollPane) {
|
||||
// Copying the scrolled pixels would drag the pinned headers along, so every scroll repaints instead.
|
||||
scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
||||
StickyFolderHeaders headers = new StickyFolderHeaders(tree);
|
||||
JLayer<JScrollPane> layer = new JLayer<>(scrollPane, headers);
|
||||
scrollPane.getViewport().addChangeListener(event -> layer.repaint());
|
||||
return layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void installUI(JComponent component) {
|
||||
super.installUI(component);
|
||||
layer = (JLayer<JScrollPane>) component;
|
||||
layer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
|
||||
tree.add(rendererPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstallUI(JComponent component) {
|
||||
layer.setLayerEventMask(0);
|
||||
tree.remove(rendererPane);
|
||||
layer = null;
|
||||
super.uninstallUI(component);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics graphics, JComponent component) {
|
||||
super.paint(graphics, component);
|
||||
List<Header> headers = pinnedHeaders();
|
||||
if (headers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||
try {
|
||||
Rectangle viewport = viewportBounds();
|
||||
canvas.clipRect(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
paintHeader(canvas, headers.get(index), index == hoveredHeader, viewport);
|
||||
}
|
||||
Header last = headers.get(headers.size() - 1);
|
||||
int shadowTop = viewport.y + last.top() + tree.getRowHeight();
|
||||
canvas.setPaint(new GradientPaint(0, shadowTop, SHADOW, 0, shadowTop + StudioTheme.scale(SHADOW_HEIGHT), NO_SHADOW));
|
||||
canvas.fillRect(viewport.x, shadowTop, viewport.width, StudioTheme.scale(SHADOW_HEIGHT));
|
||||
} finally {
|
||||
canvas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void paintHeader(Graphics2D canvas, Header header, boolean hovered, Rectangle viewport) {
|
||||
int rowHeight = tree.getRowHeight();
|
||||
int top = viewport.y + header.top();
|
||||
Rectangle row = tree.getRowBounds(header.row());
|
||||
int left = viewport.x + row.x - scrolledView().x;
|
||||
canvas.setColor(hovered ? StudioTheme.HOVER_BACKGROUND : tree.getBackground());
|
||||
canvas.fillRect(viewport.x, top, viewport.width, rowHeight);
|
||||
paintFolderHandle(canvas, left, top + rowHeight / 2);
|
||||
Component renderer = tree.getCellRenderer().getTreeCellRendererComponent(
|
||||
tree, header.path().getLastPathComponent(), false, true, false, header.row(), false);
|
||||
rendererPane.paintComponent(canvas, renderer, tree, left, top, viewport.x + viewport.width - left, rowHeight, true);
|
||||
}
|
||||
|
||||
private void paintFolderHandle(Graphics2D canvas, int folderLeft, int middleY) {
|
||||
Icon handle = UIManager.getIcon("Tree.expandedIcon");
|
||||
if (handle == null || !(tree.getUI() instanceof BasicTreeUI treeUI)) {
|
||||
return;
|
||||
}
|
||||
int middleX = folderLeft - treeUI.getRightChildIndent() + HANDLE_NUDGE;
|
||||
handle.paintIcon(tree, canvas, middleX - handle.getIconWidth() / 2, middleY - handle.getIconHeight() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processMouseEvent(MouseEvent event, JLayer<? extends JScrollPane> source) {
|
||||
if (event.getID() == MouseEvent.MOUSE_EXITED) {
|
||||
hover(-1);
|
||||
return;
|
||||
}
|
||||
int index = headerAt(event);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
event.consume();
|
||||
if (event.getID() == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(event)) {
|
||||
jumpTo(pinnedHeaders().get(index), index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processMouseMotionEvent(MouseEvent event, JLayer<? extends JScrollPane> source) {
|
||||
hover(headerAt(event));
|
||||
}
|
||||
|
||||
private void hover(int index) {
|
||||
if (index == hoveredHeader) {
|
||||
return;
|
||||
}
|
||||
hoveredHeader = index;
|
||||
tree.setCursor(index >= 0 ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null);
|
||||
layer.repaint();
|
||||
}
|
||||
|
||||
// The folder lands just below the headers of the folders around it.
|
||||
private void jumpTo(Header header, int level) {
|
||||
Rectangle row = tree.getRowBounds(header.row());
|
||||
JViewport viewport = layer.getView().getViewport();
|
||||
int top = Math.max(0, row.y - level * tree.getRowHeight());
|
||||
int bottomLimit = Math.max(0, tree.getHeight() - viewport.getExtentSize().height);
|
||||
viewport.setViewPosition(new Point(viewport.getViewPosition().x, Math.min(top, bottomLimit)));
|
||||
}
|
||||
|
||||
private int headerAt(MouseEvent event) {
|
||||
if (layer == null || event.getComponent() == null) {
|
||||
return -1;
|
||||
}
|
||||
Point point = SwingUtilities.convertPoint(event.getComponent(), event.getPoint(), layer);
|
||||
Rectangle viewport = viewportBounds();
|
||||
if (!viewport.contains(point)) {
|
||||
return -1;
|
||||
}
|
||||
List<Header> headers = pinnedHeaders();
|
||||
for (int index = headers.size() - 1; index >= 0; index--) {
|
||||
int top = viewport.y + headers.get(index).top();
|
||||
if (point.y >= top && point.y < top + tree.getRowHeight()) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each depth, the folder holding the row just under the headers above it, as long as that folder's own row
|
||||
* has scrolled out of sight.
|
||||
*/
|
||||
private List<Header> pinnedHeaders() {
|
||||
List<Header> headers = new ArrayList<>();
|
||||
int rowHeight = tree.getRowHeight();
|
||||
if (layer == null || rowHeight <= 0 || tree.getRowCount() == 0) {
|
||||
return headers;
|
||||
}
|
||||
Rectangle view = scrolledView();
|
||||
for (int level = 0; ; level++) {
|
||||
int slotTop = level * rowHeight;
|
||||
int row = tree.getClosestRowForLocation(view.x, view.y + slotTop);
|
||||
TreePath path = row < 0 ? null : tree.getPathForRow(row);
|
||||
if (path == null || !isInsideFolderAt(path, level)) {
|
||||
return headers;
|
||||
}
|
||||
TreePath folder = ancestorAt(path, level);
|
||||
Rectangle lastRow = tree.getRowBounds(lastRowInside(folder, row));
|
||||
int top = Math.min(slotTop, lastRow.y + lastRow.height - view.y - rowHeight);
|
||||
headers.add(new Header(folder, tree.getRowForPath(folder), top));
|
||||
if (top < slotTop) {
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hidden root is the path's first element, so the folder at depth 0 is its second.
|
||||
private static boolean isInsideFolderAt(TreePath path, int level) {
|
||||
return path.getPathCount() > level + 2;
|
||||
}
|
||||
|
||||
private static TreePath ancestorAt(TreePath path, int level) {
|
||||
TreePath ancestor = path;
|
||||
while (ancestor.getPathCount() > level + 2) {
|
||||
ancestor = ancestor.getParentPath();
|
||||
}
|
||||
return ancestor;
|
||||
}
|
||||
|
||||
// A folder's rows sit together, so the last one is found by halving the rows after a row known to be inside.
|
||||
private int lastRowInside(TreePath folder, int rowInside) {
|
||||
int low = rowInside;
|
||||
int high = tree.getRowCount() - 1;
|
||||
while (low < high) {
|
||||
int middle = (low + high + 1) >>> 1;
|
||||
if (folder.isDescendant(tree.getPathForRow(middle))) {
|
||||
low = middle;
|
||||
} else {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
private Rectangle scrolledView() {
|
||||
return layer.getView().getViewport().getViewRect();
|
||||
}
|
||||
|
||||
private Rectangle viewportBounds() {
|
||||
JViewport viewport = layer.getView().getViewport();
|
||||
Point origin = SwingUtilities.convertPoint(viewport, 0, 0, layer);
|
||||
return new Rectangle(origin.x, origin.y, viewport.getWidth(), viewport.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* A pinned folder, its row in the tree and how far below the top of the visible rows it is drawn.
|
||||
*/
|
||||
private record Header(TreePath path, int row, int top) {
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user