mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Add light theme
Follows the system by default.
This commit is contained in:
1 parent
46f5472081
commit
59626d2434
12 files changed
+394
-50
No files matched your search
@@ -266,6 +266,7 @@ public class IrisSettings {
|
|||||||
public boolean useServerLaunchedGuis = true;
|
public boolean useServerLaunchedGuis = true;
|
||||||
public boolean maximumPregenGuiFPS = false;
|
public boolean maximumPregenGuiFPS = false;
|
||||||
public boolean colorMode = true;
|
public boolean colorMode = true;
|
||||||
|
public String theme = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ public final class GuiHost {
|
|||||||
public void windowClosed(WindowEvent event) {
|
public void windowClosed(WindowEvent event) {
|
||||||
MANAGED_FRAMES.remove(frame);
|
MANAGED_FRAMES.remove(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowActivated(WindowEvent event) {
|
||||||
|
StudioTheme.followSystem();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
installMacQuitGuard();
|
installMacQuitGuard();
|
||||||
return frame;
|
return frame;
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ final class MapCanvas extends JPanel {
|
|||||||
private static final Color MOB_COLOR = StudioTheme.ERROR;
|
private static final Color MOB_COLOR = StudioTheme.ERROR;
|
||||||
private static final Color GRID_MINOR = new Color(255, 255, 255, 18);
|
private static final Color GRID_MINOR = new Color(255, 255, 255, 18);
|
||||||
private static final Color GRID_MAJOR = new Color(255, 255, 255, 38);
|
private static final Color GRID_MAJOR = new Color(255, 255, 255, 38);
|
||||||
private static final Color TILE_EMPTY_A = new Color(0x232427);
|
private static final Color TILE_EMPTY_A = new ThemeColor(0xF0F1F3, 0x232427);
|
||||||
private static final Color TILE_EMPTY_B = new Color(0x26272B);
|
private static final Color TILE_EMPTY_B = new ThemeColor(0xEBECEF, 0x26272B);
|
||||||
private static final int CARD_PADDING = 12;
|
private static final int CARD_PADDING = 12;
|
||||||
private static final int NOTIFICATION_MILLIS = 2_500;
|
private static final int NOTIFICATION_MILLIS = 2_500;
|
||||||
private static final double DEFAULT_BLOCKS_PER_PIXEL = 4D;
|
private static final double DEFAULT_BLOCKS_PER_PIXEL = 4D;
|
||||||
@@ -585,6 +585,13 @@ final class MapCanvas extends JPanel {
|
|||||||
canvas.drawImage(currentTileLayer(), 0, 0, null);
|
canvas.drawImage(currentTileLayer(), 0, 0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The theme changed: the cached layer holds the old theme's background.
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
super.updateUI();
|
||||||
|
tileLayerStale = true;
|
||||||
|
}
|
||||||
|
|
||||||
private BufferedImage currentTileLayer() {
|
private BufferedImage currentTileLayer() {
|
||||||
int width = Math.max(1, getWidth());
|
int width = Math.max(1, getWidth());
|
||||||
int height = Math.max(1, getHeight());
|
int height = Math.max(1, getHeight());
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
explorerControls.forEach(toolbar::add);
|
explorerControls.forEach(toolbar::add);
|
||||||
|
|
||||||
toolbar.add(Box.createHorizontalGlue());
|
toolbar.add(Box.createHorizontalGlue());
|
||||||
|
toolbar.add(StudioToolbar.themeButton());
|
||||||
JComponent help = StudioToolbar.button("Shortcuts", "Show every mouse and key shortcut (/)", canvas::toggleHelp);
|
JComponent help = StudioToolbar.button("Shortcuts", "Show every mouse and key shortcut (/)", canvas::toggleHelp);
|
||||||
mapControls.add(help);
|
mapControls.add(help);
|
||||||
toolbar.add(help);
|
toolbar.add(help);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.volmit.iris.core.gui.BlockModel.Face;
|
|||||||
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -36,8 +37,7 @@ import java.util.List;
|
|||||||
* what is already drawn.
|
* what is already drawn.
|
||||||
*/
|
*/
|
||||||
final class ObjectRenderer {
|
final class ObjectRenderer {
|
||||||
private static final int BACKGROUND = StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF;
|
private static final Color GRID = new ThemeColor(0xE3E4E8, 0x34373C);
|
||||||
private static final int GRID_COLOR = 0x34373C;
|
|
||||||
private static final int GRID_LINES_PER_SIDE = 25;
|
private static final int GRID_LINES_PER_SIDE = 25;
|
||||||
private static final double GRID_REACH = 0.7D;
|
private static final double GRID_REACH = 0.7D;
|
||||||
private static final double GRID_BELOW_OBJECT = 0.035D;
|
private static final double GRID_BELOW_OBJECT = 0.035D;
|
||||||
@@ -54,6 +54,7 @@ final class ObjectRenderer {
|
|||||||
private final double[] depths;
|
private final double[] depths;
|
||||||
private final boolean[] showsObject;
|
private final boolean[] showsObject;
|
||||||
private final Projection projection;
|
private final Projection projection;
|
||||||
|
private final int gridColor = GRID.getRGB() & 0xFFFFFF;
|
||||||
|
|
||||||
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
||||||
this.width = image.getWidth();
|
this.width = image.getWidth();
|
||||||
@@ -71,7 +72,7 @@ final class ObjectRenderer {
|
|||||||
static BufferedImage render(ObjectMesh mesh, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
static BufferedImage render(ObjectMesh mesh, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||||
BufferedImage image = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
BufferedImage image = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||||
ObjectRenderer renderer = new ObjectRenderer(mesh, camera, image, Math.max(0, Math.min(width - 1, coveredLeftWidth)));
|
ObjectRenderer renderer = new ObjectRenderer(mesh, camera, image, Math.max(0, Math.min(width - 1, coveredLeftWidth)));
|
||||||
Arrays.fill(renderer.pixels, BACKGROUND);
|
Arrays.fill(renderer.pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
|
||||||
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
||||||
renderer.drawGrid(mesh.bounds());
|
renderer.drawGrid(mesh.bounds());
|
||||||
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||||
@@ -131,7 +132,7 @@ final class ObjectRenderer {
|
|||||||
double depth = startDepth + (endDepth - startDepth) * progress;
|
double depth = startDepth + (endDepth - startDepth) * progress;
|
||||||
if (depth > depths[index]) {
|
if (depth > depths[index]) {
|
||||||
depths[index] = depth;
|
depths[index] = depth;
|
||||||
pixels[index] = GRID_COLOR;
|
pixels[index] = gridColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ final class ObjectViewport extends JComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The theme changed, and the last frame was drawn on the old theme's background.
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
super.updateUI();
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void show(ObjectMesh nextMesh) {
|
void show(ObjectMesh nextMesh) {
|
||||||
mesh = nextMesh;
|
mesh = nextMesh;
|
||||||
camera = ObjectCamera.START;
|
camera = ObjectCamera.START;
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ final class PaletteNavigator extends JPanel {
|
|||||||
private static final long serialVersionUID = -6402313718430917284L;
|
private static final long serialVersionUID = -6402313718430917284L;
|
||||||
private static final String HERE_CARD = "here";
|
private static final String HERE_CARD = "here";
|
||||||
private static final String OVERVIEW_CARD = "overview";
|
private static final String OVERVIEW_CARD = "overview";
|
||||||
private static final String DETAIL_COLOR = StudioTheme.SECONDARY_TEXT_HTML;
|
|
||||||
private static final int MESSAGE_WRAP_WIDTH_PIXELS = 190;
|
private static final int MESSAGE_WRAP_WIDTH_PIXELS = 190;
|
||||||
|
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
@@ -438,13 +437,13 @@ final class PaletteNavigator extends JPanel {
|
|||||||
|
|
||||||
private static String biomeText(BiomeEntry biome, String note) {
|
private static String biomeText(BiomeEntry biome, String note) {
|
||||||
String noteText = note == null ? "" : " · " + escape(note);
|
String noteText = note == null ? "" : " · " + escape(note);
|
||||||
return "<html>" + escape(biome.name()) + noteText + " <font color='" + DETAIL_COLOR + "'>" + escape(biome.key())
|
return "<html>" + escape(biome.name()) + noteText + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(biome.key())
|
||||||
+ "</font></html>";
|
+ "</font></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String labeled(String title, String name, String key) {
|
private static String labeled(String title, String name, String key) {
|
||||||
return "<html><font color='" + DETAIL_COLOR + "'>" + title + "</font> " + escape(name)
|
return "<html><font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + title + "</font> " + escape(name)
|
||||||
+ " <font color='" + DETAIL_COLOR + "'>" + escape(key) + "</font></html>";
|
+ " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(key) + "</font></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String escape(String text) {
|
private static String escape(String text) {
|
||||||
@@ -477,7 +476,7 @@ final class PaletteNavigator extends JPanel {
|
|||||||
if (detail == null) {
|
if (detail == null) {
|
||||||
return escape(label);
|
return escape(label);
|
||||||
}
|
}
|
||||||
return "<html>" + escape(label) + " <font color='" + DETAIL_COLOR + "'>" + escape(detail) + "</font></html>";
|
return "<html>" + escape(label) + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(detail) + "</font></html>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import java.util.List;
|
|||||||
final class StickyFolderHeaders extends LayerUI<JScrollPane> {
|
final class StickyFolderHeaders extends LayerUI<JScrollPane> {
|
||||||
private static final long serialVersionUID = -2205474187531254372L;
|
private static final long serialVersionUID = -2205474187531254372L;
|
||||||
private static final int SHADOW_HEIGHT = 4;
|
private static final int SHADOW_HEIGHT = 4;
|
||||||
private static final Color SHADOW = new Color(0, 0, 0, 90);
|
private static final Color SHADOW = new ThemeColor(new Color(0, 0, 0, 28), new Color(0, 0, 0, 90));
|
||||||
private static final Color NO_SHADOW = new Color(0, 0, 0, 0);
|
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.
|
// 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 static final int HANDLE_NUDGE = 1;
|
||||||
|
|||||||
@@ -20,62 +20,64 @@ package com.volmit.iris.core.gui;
|
|||||||
|
|
||||||
import com.formdev.flatlaf.FlatDarkLaf;
|
import com.formdev.flatlaf.FlatDarkLaf;
|
||||||
import com.formdev.flatlaf.FlatLaf;
|
import com.formdev.flatlaf.FlatLaf;
|
||||||
|
import com.formdev.flatlaf.FlatLightLaf;
|
||||||
import com.formdev.flatlaf.util.UIScale;
|
import com.formdev.flatlaf.util.UIScale;
|
||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.IrisSettings;
|
||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
import javax.swing.border.Border;
|
import javax.swing.border.Border;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
|
import java.awt.EventQueue;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The colours of IntelliJ's dark "New UI", shared by FlatLaf's controls and everything the studio windows draw themselves.
|
* The colours of IntelliJ's "New UI", light and dark, shared by FlatLaf's controls and everything the studio windows
|
||||||
|
* draw themselves. The theme follows the system unless the user picked light or dark.
|
||||||
*/
|
*/
|
||||||
final class StudioTheme {
|
final class StudioTheme {
|
||||||
static final Color EDITOR_BACKGROUND = new Color(0x1E1F22);
|
static final Color EDITOR_BACKGROUND = new ThemeColor(0xFFFFFF, 0x1E1F22);
|
||||||
static final Color PANEL_BACKGROUND = new Color(0x2B2D30);
|
static final Color PANEL_BACKGROUND = new ThemeColor(0xF7F8FA, 0x2B2D30);
|
||||||
static final Color HOVER_BACKGROUND = new Color(0x393B40);
|
static final Color HOVER_BACKGROUND = new ThemeColor(0xDFE1E5, 0x393B40);
|
||||||
static final Color INACTIVE_SELECTION = new Color(0x43454A);
|
static final Color INACTIVE_SELECTION = new ThemeColor(0xDFE1E5, 0x43454A);
|
||||||
static final Color SELECTION = new Color(0x2E436E);
|
static final Color SELECTION = new ThemeColor(0xD4E2FF, 0x2E436E);
|
||||||
static final Color CONTROL_BORDER = new Color(0x4E5157);
|
static final Color CONTROL_BORDER = new ThemeColor(0xC9CCD6, 0x4E5157);
|
||||||
static final Color SEPARATOR = new Color(0x1E1F22);
|
static final Color SEPARATOR = new ThemeColor(0xEBECF0, 0x1E1F22);
|
||||||
static final Color TEXT = new Color(0xDFE1E5);
|
static final Color TEXT = new ThemeColor(0x000000, 0xDFE1E5);
|
||||||
static final Color SECONDARY_TEXT = new Color(0x9DA0A8);
|
static final ThemeColor SECONDARY_TEXT = new ThemeColor(0x6C707E, 0x9DA0A8);
|
||||||
static final Color MUTED_TEXT = new Color(0x6F737A);
|
static final Color MUTED_TEXT = new ThemeColor(0x818594, 0x6F737A);
|
||||||
static final Color ACCENT = new Color(0x3574F0);
|
static final Color ACCENT = new Color(0x3574F0);
|
||||||
static final Color LINK = new Color(0x548AF7);
|
static final Color LINK = new ThemeColor(0x315FBD, 0x548AF7);
|
||||||
static final Color ERROR = new Color(0xF75464);
|
static final Color ERROR = new ThemeColor(0xDB3B4B, 0xF75464);
|
||||||
static final Color WARNING = new Color(0xF2C55C);
|
static final Color WARNING = new ThemeColor(0xC27D04, 0xF2C55C);
|
||||||
static final Color SUCCESS = new Color(0x5FB865);
|
static final Color SUCCESS = new ThemeColor(0x208A3C, 0x5FB865);
|
||||||
static final Color OVERLAY_BACKGROUND = new Color(0x2B, 0x2D, 0x30, 236);
|
static final Color OVERLAY_BACKGROUND = new ThemeColor(new Color(0xFF, 0xFF, 0xFF, 240), new Color(0x2B, 0x2D, 0x30, 236));
|
||||||
static final Color OVERLAY_BORDER = new Color(0x43454A);
|
static final Color OVERLAY_BORDER = new ThemeColor(0xDFE1E5, 0x43454A);
|
||||||
static final String SECONDARY_TEXT_HTML = "#9DA0A8";
|
|
||||||
static final int OVERLAY_ARC = 10;
|
static final int OVERLAY_ARC = 10;
|
||||||
|
|
||||||
|
private static final boolean DARK_WITHOUT_SYSTEM_PREFERENCE = true;
|
||||||
private static final AtomicBoolean INSTALLED = new AtomicBoolean();
|
private static final AtomicBoolean INSTALLED = new AtomicBoolean();
|
||||||
|
private static final ExecutorService SYSTEM_CHECK = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Theme Check");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
private static volatile boolean dark = DARK_WITHOUT_SYSTEM_PREFERENCE;
|
||||||
private static volatile float pixelScale = 1F;
|
private static volatile float pixelScale = 1F;
|
||||||
private static final Map<String, String> INTELLIJ_NEW_UI_DEFAULTS = Map.ofEntries(
|
private static final Map<String, String> SHARED_DEFAULTS = Map.ofEntries(
|
||||||
Map.entry("@background", "#2B2D30"),
|
|
||||||
Map.entry("@foreground", "#DFE1E5"),
|
|
||||||
Map.entry("@accentColor", "#3574F0"),
|
|
||||||
Map.entry("@selectionBackground", "#2E436E"),
|
|
||||||
Map.entry("@selectionInactiveBackground", "#43454A"),
|
|
||||||
Map.entry("@disabledForeground", "#6F737A"),
|
|
||||||
Map.entry("Component.borderColor", "#4E5157"),
|
|
||||||
Map.entry("Component.focusedBorderColor", "#3574F0"),
|
|
||||||
Map.entry("Component.focusWidth", "1"),
|
Map.entry("Component.focusWidth", "1"),
|
||||||
Map.entry("Component.innerFocusWidth", "0"),
|
Map.entry("Component.innerFocusWidth", "0"),
|
||||||
Map.entry("Component.arc", "8"),
|
Map.entry("Component.arc", "8"),
|
||||||
Map.entry("Button.arc", "8"),
|
Map.entry("Button.arc", "8"),
|
||||||
Map.entry("TextComponent.arc", "8"),
|
Map.entry("TextComponent.arc", "8"),
|
||||||
Map.entry("Button.toolbar.hoverBackground", "#393B40"),
|
|
||||||
Map.entry("Button.toolbar.pressedBackground", "#43454A"),
|
|
||||||
Map.entry("Button.toolbar.selectedBackground", "#43454A"),
|
|
||||||
Map.entry("ToggleButton.toolbar.selectedBackground", "#43454A"),
|
|
||||||
Map.entry("List.selectionArc", "6"),
|
Map.entry("List.selectionArc", "6"),
|
||||||
Map.entry("List.selectionInsets", "0,6,0,6"),
|
Map.entry("List.selectionInsets", "0,6,0,6"),
|
||||||
Map.entry("Tree.selectionArc", "6"),
|
Map.entry("Tree.selectionArc", "6"),
|
||||||
@@ -85,16 +87,49 @@ final class StudioTheme {
|
|||||||
Map.entry("ScrollBar.thumbArc", "999"),
|
Map.entry("ScrollBar.thumbArc", "999"),
|
||||||
Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),
|
Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),
|
||||||
Map.entry("ScrollBar.showButtons", "false"),
|
Map.entry("ScrollBar.showButtons", "false"),
|
||||||
Map.entry("ScrollBar.track", "#2B2D30"),
|
|
||||||
Map.entry("ScrollPane.border", "0,0,0,0"),
|
Map.entry("ScrollPane.border", "0,0,0,0"),
|
||||||
Map.entry("SplitPane.dividerSize", "1"),
|
Map.entry("SplitPane.dividerSize", "1"),
|
||||||
Map.entry("SplitPaneDivider.style", "plain"),
|
Map.entry("SplitPaneDivider.style", "plain"),
|
||||||
Map.entry("SplitPane.background", "#1E1F22"),
|
|
||||||
Map.entry("Separator.foreground", "#1E1F22"),
|
|
||||||
Map.entry("ToolTip.background", "#393B40"),
|
|
||||||
Map.entry("ProgressBar.arc", "999"),
|
Map.entry("ProgressBar.arc", "999"),
|
||||||
Map.entry("ProgressBar.horizontalSize", "120,4")
|
Map.entry("ProgressBar.horizontalSize", "120,4")
|
||||||
);
|
);
|
||||||
|
private static final Map<String, String> DARK_DEFAULTS = withShared(Map.ofEntries(
|
||||||
|
Map.entry("@background", "#2B2D30"),
|
||||||
|
Map.entry("@foreground", "#DFE1E5"),
|
||||||
|
Map.entry("@accentColor", "#3574F0"),
|
||||||
|
Map.entry("@selectionBackground", "#2E436E"),
|
||||||
|
Map.entry("@selectionInactiveBackground", "#43454A"),
|
||||||
|
Map.entry("@disabledForeground", "#6F737A"),
|
||||||
|
Map.entry("Component.borderColor", "#4E5157"),
|
||||||
|
Map.entry("Component.focusedBorderColor", "#3574F0"),
|
||||||
|
Map.entry("Button.toolbar.hoverBackground", "#393B40"),
|
||||||
|
Map.entry("Button.toolbar.pressedBackground", "#43454A"),
|
||||||
|
Map.entry("Button.toolbar.selectedBackground", "#43454A"),
|
||||||
|
Map.entry("ToggleButton.toolbar.selectedBackground", "#43454A"),
|
||||||
|
Map.entry("ScrollBar.track", "#2B2D30"),
|
||||||
|
Map.entry("SplitPane.background", "#1E1F22"),
|
||||||
|
Map.entry("Separator.foreground", "#1E1F22"),
|
||||||
|
Map.entry("ToolTip.background", "#393B40")
|
||||||
|
));
|
||||||
|
private static final Map<String, String> LIGHT_DEFAULTS = withShared(Map.ofEntries(
|
||||||
|
Map.entry("@background", "#F7F8FA"),
|
||||||
|
Map.entry("@foreground", "#000000"),
|
||||||
|
Map.entry("@accentColor", "#3574F0"),
|
||||||
|
Map.entry("@selectionBackground", "#D4E2FF"),
|
||||||
|
Map.entry("@selectionForeground", "#000000"),
|
||||||
|
Map.entry("@selectionInactiveBackground", "#DFE1E5"),
|
||||||
|
Map.entry("@selectionInactiveForeground", "#000000"),
|
||||||
|
Map.entry("@disabledForeground", "#A8ADBD"),
|
||||||
|
Map.entry("Component.borderColor", "#C9CCD6"),
|
||||||
|
Map.entry("Component.focusedBorderColor", "#3574F0"),
|
||||||
|
Map.entry("Button.toolbar.hoverBackground", "#DFE1E5"),
|
||||||
|
Map.entry("Button.toolbar.pressedBackground", "#C9CCD6"),
|
||||||
|
Map.entry("Button.toolbar.selectedBackground", "#DFE1E5"),
|
||||||
|
Map.entry("ToggleButton.toolbar.selectedBackground", "#DFE1E5"),
|
||||||
|
Map.entry("ScrollBar.track", "#F7F8FA"),
|
||||||
|
Map.entry("SplitPane.background", "#EBECF0"),
|
||||||
|
Map.entry("Separator.foreground", "#EBECF0")
|
||||||
|
));
|
||||||
|
|
||||||
private StudioTheme() {
|
private StudioTheme() {
|
||||||
}
|
}
|
||||||
@@ -104,8 +139,7 @@ final class StudioTheme {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
FlatLaf.setGlobalExtraDefaults(INTELLIJ_NEW_UI_DEFAULTS);
|
applyLookAndFeel(darkFor(mode()));
|
||||||
FlatDarkLaf.setup();
|
|
||||||
pixelScale = UIScale.getUserScaleFactor();
|
pixelScale = UIScale.getUserScaleFactor();
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
Iris.reportError(error);
|
Iris.reportError(error);
|
||||||
@@ -113,6 +147,71 @@ final class StudioTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean isDark() {
|
||||||
|
return dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Mode mode() {
|
||||||
|
return Mode.fromSetting(IrisSettings.get().getGui().getTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers the user's choice and switches every studio window to it.
|
||||||
|
*/
|
||||||
|
static void choose(Mode next) {
|
||||||
|
IrisSettings settings = IrisSettings.get();
|
||||||
|
settings.getGui().setTheme(next.settingName());
|
||||||
|
settings.forceSave();
|
||||||
|
if (next == Mode.AUTO) {
|
||||||
|
followSystem();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switchTo(next == Mode.DARK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Studio windows call this when they come to the front, so a change made in the system's settings shows up.
|
||||||
|
static void followSystem() {
|
||||||
|
if (!INSTALLED.get() || mode() != Mode.AUTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SYSTEM_CHECK.execute(() -> {
|
||||||
|
boolean systemDark = SystemAppearance.prefersDark().orElse(DARK_WITHOUT_SYSTEM_PREFERENCE);
|
||||||
|
EventQueue.invokeLater(() -> switchTo(systemDark));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean darkFor(Mode chosen) {
|
||||||
|
return switch (chosen) {
|
||||||
|
case LIGHT -> false;
|
||||||
|
case DARK -> true;
|
||||||
|
case AUTO -> SystemAppearance.prefersDark().orElse(DARK_WITHOUT_SYSTEM_PREFERENCE);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void switchTo(boolean nextDark) {
|
||||||
|
if (nextDark == dark) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyLookAndFeel(nextDark);
|
||||||
|
FlatLaf.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyLookAndFeel(boolean nextDark) {
|
||||||
|
dark = nextDark;
|
||||||
|
FlatLaf.setGlobalExtraDefaults(nextDark ? DARK_DEFAULTS : LIGHT_DEFAULTS);
|
||||||
|
if (nextDark) {
|
||||||
|
FlatDarkLaf.setup();
|
||||||
|
} else {
|
||||||
|
FlatLightLaf.setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> withShared(Map<String, String> themeDefaults) {
|
||||||
|
Map<String, String> merged = new HashMap<>(SHARED_DEFAULTS);
|
||||||
|
merged.putAll(themeDefaults);
|
||||||
|
return Map.copyOf(merged);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
|
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
|
||||||
*/
|
*/
|
||||||
@@ -160,4 +259,33 @@ final class StudioTheme {
|
|||||||
Font font = UIManager.getFont(key);
|
Font font = UIManager.getFont(key);
|
||||||
return font != null ? font : new Font(Font.SANS_SERIF, fallbackStyle, fallbackSize);
|
return font != null ? font : new Font(Font.SANS_SERIF, fallbackStyle, fallbackSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
AUTO("Match system"),
|
||||||
|
LIGHT("Light"),
|
||||||
|
DARK("Dark");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
Mode(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String settingName() {
|
||||||
|
return name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Mode fromSetting(String setting) {
|
||||||
|
for (Mode mode : values()) {
|
||||||
|
if (mode.settingName().equalsIgnoreCase(setting)) {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AUTO;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,10 +22,13 @@ import com.formdev.flatlaf.FlatClientProperties;
|
|||||||
import com.formdev.flatlaf.icons.FlatSearchIcon;
|
import com.formdev.flatlaf.icons.FlatSearchIcon;
|
||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.ButtonGroup;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JPopupMenu;
|
||||||
|
import javax.swing.JRadioButtonMenuItem;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
import javax.swing.JToggleButton;
|
import javax.swing.JToggleButton;
|
||||||
@@ -63,12 +66,39 @@ final class StudioToolbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static JButton button(String text, String tooltip, Runnable action) {
|
static JButton button(String text, String tooltip, Runnable action) {
|
||||||
|
JButton button = toolbarButton(text, tooltip);
|
||||||
|
button.addActionListener(event -> action.run());
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a menu to pick light, dark or the system's theme.
|
||||||
|
*/
|
||||||
|
static JButton themeButton() {
|
||||||
|
JButton button = toolbarButton("Theme", "Light, dark, or whatever the system uses");
|
||||||
|
button.addActionListener(event -> themeMenu().show(button, 0, button.getHeight()));
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JPopupMenu themeMenu() {
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
ButtonGroup choices = new ButtonGroup();
|
||||||
|
StudioTheme.Mode current = StudioTheme.mode();
|
||||||
|
for (StudioTheme.Mode mode : StudioTheme.Mode.values()) {
|
||||||
|
JRadioButtonMenuItem item = new JRadioButtonMenuItem(mode.label(), mode == current);
|
||||||
|
item.addActionListener(event -> StudioTheme.choose(mode));
|
||||||
|
choices.add(item);
|
||||||
|
menu.add(item);
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JButton toolbarButton(String text, String tooltip) {
|
||||||
JButton button = new JButton(text);
|
JButton button = new JButton(text);
|
||||||
button.setToolTipText(tooltip);
|
button.setToolTipText(tooltip);
|
||||||
button.setFocusable(false);
|
button.setFocusable(false);
|
||||||
button.setMargin(new Insets(4, 8, 4, 8));
|
button.setMargin(new Insets(4, 8, 4, 8));
|
||||||
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
|
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
|
||||||
button.addActionListener(event -> action.run());
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks the operating system whether its user prefers dark or light apps, through the same settings desktop apps
|
||||||
|
* follow: the freedesktop portal or GNOME's setting on Linux, the apps theme on Windows, the appearance on macOS.
|
||||||
|
*/
|
||||||
|
final class SystemAppearance {
|
||||||
|
private static final long COMMAND_TIMEOUT_MILLIS = 2000L;
|
||||||
|
private static final String PORTAL_DARK = "uint32 1";
|
||||||
|
private static final String PORTAL_LIGHT = "uint32 2";
|
||||||
|
|
||||||
|
private SystemAppearance() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty when the system has no preference or could not be asked.
|
||||||
|
*/
|
||||||
|
static Optional<Boolean> prefersDark() {
|
||||||
|
String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
|
||||||
|
if (os.contains("win")) {
|
||||||
|
return windowsPrefersDark();
|
||||||
|
}
|
||||||
|
if (os.contains("mac")) {
|
||||||
|
return macPrefersDark();
|
||||||
|
}
|
||||||
|
return linuxPrefersDark();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<Boolean> linuxPrefersDark() {
|
||||||
|
Optional<String> portal = output(List.of("gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop",
|
||||||
|
"--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne",
|
||||||
|
"org.freedesktop.appearance", "color-scheme"));
|
||||||
|
if (portal.filter(answer -> answer.contains(PORTAL_DARK)).isPresent()) {
|
||||||
|
return Optional.of(true);
|
||||||
|
}
|
||||||
|
if (portal.filter(answer -> answer.contains(PORTAL_LIGHT)).isPresent()) {
|
||||||
|
return Optional.of(false);
|
||||||
|
}
|
||||||
|
Optional<String> gnome = output(List.of("gsettings", "get", "org.gnome.desktop.interface", "color-scheme"));
|
||||||
|
if (gnome.filter(scheme -> scheme.contains("prefer-dark")).isPresent()) {
|
||||||
|
return Optional.of(true);
|
||||||
|
}
|
||||||
|
if (gnome.filter(scheme -> scheme.contains("prefer-light")).isPresent()) {
|
||||||
|
return Optional.of(false);
|
||||||
|
}
|
||||||
|
return output(List.of("gsettings", "get", "org.gnome.desktop.interface", "gtk-theme"))
|
||||||
|
.map(theme -> theme.toLowerCase(Locale.ROOT).contains("dark"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<Boolean> windowsPrefersDark() {
|
||||||
|
return output(List.of("reg", "query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||||
|
"/v", "AppsUseLightTheme"))
|
||||||
|
.filter(answer -> answer.contains("AppsUseLightTheme"))
|
||||||
|
.map(answer -> answer.trim().endsWith("0x0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The setting only exists while macOS is dark, so a failed read means light.
|
||||||
|
private static Optional<Boolean> macPrefersDark() {
|
||||||
|
return Optional.of(output(List.of("defaults", "read", "-g", "AppleInterfaceStyle"))
|
||||||
|
.filter(style -> style.trim().equalsIgnoreCase("dark"))
|
||||||
|
.isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> output(List<String> command) {
|
||||||
|
try {
|
||||||
|
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
|
||||||
|
if (!process.waitFor(COMMAND_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (process.exitValue() != 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try (InputStream stream = process.getInputStream()) {
|
||||||
|
return Optional.of(new String(stream.readAllBytes(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
} catch (IOException missingCommand) {
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (InterruptedException interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.awt.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A colour with a light and a dark version that always answers with the one of the current theme, like IntelliJ's
|
||||||
|
* JBColor. Components keep the same colour object across a theme switch and simply paint in the other version.
|
||||||
|
*/
|
||||||
|
final class ThemeColor extends Color {
|
||||||
|
private static final long serialVersionUID = 2286435620386470207L;
|
||||||
|
private final Color light;
|
||||||
|
private final Color dark;
|
||||||
|
|
||||||
|
ThemeColor(int lightRgb, int darkRgb) {
|
||||||
|
this(new Color(lightRgb), new Color(darkRgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeColor(Color light, Color dark) {
|
||||||
|
super(dark.getRGB(), true);
|
||||||
|
this.light = light;
|
||||||
|
this.dark = dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every other colour query of Color, and Java2D's painting, go through this one.
|
||||||
|
@Override
|
||||||
|
public int getRGB() {
|
||||||
|
return current().getRGB();
|
||||||
|
}
|
||||||
|
|
||||||
|
String toHtml() {
|
||||||
|
return String.format("#%06X", getRGB() & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color current() {
|
||||||
|
return StudioTheme.isDark() ? dark : light;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user