mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +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 maximumPregenGuiFPS = false;
|
||||
public boolean colorMode = true;
|
||||
public String theme = "auto";
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -73,6 +73,11 @@ public final class GuiHost {
|
||||
public void windowClosed(WindowEvent event) {
|
||||
MANAGED_FRAMES.remove(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent event) {
|
||||
StudioTheme.followSystem();
|
||||
}
|
||||
});
|
||||
installMacQuitGuard();
|
||||
return frame;
|
||||
|
||||
@@ -66,8 +66,8 @@ final class MapCanvas extends JPanel {
|
||||
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_MAJOR = new Color(255, 255, 255, 38);
|
||||
private static final Color TILE_EMPTY_A = new Color(0x232427);
|
||||
private static final Color TILE_EMPTY_B = new Color(0x26272B);
|
||||
private static final Color TILE_EMPTY_A = new ThemeColor(0xF0F1F3, 0x232427);
|
||||
private static final Color TILE_EMPTY_B = new ThemeColor(0xEBECEF, 0x26272B);
|
||||
private static final int CARD_PADDING = 12;
|
||||
private static final int NOTIFICATION_MILLIS = 2_500;
|
||||
private static final double DEFAULT_BLOCKS_PER_PIXEL = 4D;
|
||||
@@ -585,6 +585,13 @@ final class MapCanvas extends JPanel {
|
||||
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() {
|
||||
int width = Math.max(1, getWidth());
|
||||
int height = Math.max(1, getHeight());
|
||||
|
||||
@@ -178,6 +178,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
||||
explorerControls.forEach(toolbar::add);
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue());
|
||||
toolbar.add(StudioToolbar.themeButton());
|
||||
JComponent help = StudioToolbar.button("Shortcuts", "Show every mouse and key shortcut (/)", canvas::toggleHelp);
|
||||
mapControls.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.BlockTextures.BlockTexture;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.ArrayList;
|
||||
@@ -36,8 +37,7 @@ import java.util.List;
|
||||
* what is already drawn.
|
||||
*/
|
||||
final class ObjectRenderer {
|
||||
private static final int BACKGROUND = StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF;
|
||||
private static final int GRID_COLOR = 0x34373C;
|
||||
private static final Color GRID = new ThemeColor(0xE3E4E8, 0x34373C);
|
||||
private static final int GRID_LINES_PER_SIDE = 25;
|
||||
private static final double GRID_REACH = 0.7D;
|
||||
private static final double GRID_BELOW_OBJECT = 0.035D;
|
||||
@@ -54,6 +54,7 @@ final class ObjectRenderer {
|
||||
private final double[] depths;
|
||||
private final boolean[] showsObject;
|
||||
private final Projection projection;
|
||||
private final int gridColor = GRID.getRGB() & 0xFFFFFF;
|
||||
|
||||
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
||||
this.width = image.getWidth();
|
||||
@@ -71,7 +72,7 @@ final class ObjectRenderer {
|
||||
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);
|
||||
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);
|
||||
renderer.drawGrid(mesh.bounds());
|
||||
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||
@@ -131,7 +132,7 @@ final class ObjectRenderer {
|
||||
double depth = startDepth + (endDepth - startDepth) * progress;
|
||||
if (depth > depths[index]) {
|
||||
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) {
|
||||
mesh = nextMesh;
|
||||
camera = ObjectCamera.START;
|
||||
|
||||
@@ -57,7 +57,6 @@ final class PaletteNavigator extends JPanel {
|
||||
private static final long serialVersionUID = -6402313718430917284L;
|
||||
private static final String HERE_CARD = "here";
|
||||
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 final Listener listener;
|
||||
@@ -438,13 +437,13 @@ final class PaletteNavigator extends JPanel {
|
||||
|
||||
private static String biomeText(BiomeEntry biome, String 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>";
|
||||
}
|
||||
|
||||
private static String labeled(String title, String name, String key) {
|
||||
return "<html><font color='" + DETAIL_COLOR + "'>" + title + "</font> " + escape(name)
|
||||
+ " <font color='" + DETAIL_COLOR + "'>" + escape(key) + "</font></html>";
|
||||
return "<html><font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + title + "</font> " + escape(name)
|
||||
+ " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(key) + "</font></html>";
|
||||
}
|
||||
|
||||
private static String escape(String text) {
|
||||
@@ -477,7 +476,7 @@ final class PaletteNavigator extends JPanel {
|
||||
if (detail == null) {
|
||||
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> {
|
||||
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 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);
|
||||
// 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;
|
||||
|
||||
@@ -20,62 +20,64 @@ package com.volmit.iris.core.gui;
|
||||
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import com.formdev.flatlaf.FlatLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.border.Border;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Font;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
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 {
|
||||
static final Color EDITOR_BACKGROUND = new Color(0x1E1F22);
|
||||
static final Color PANEL_BACKGROUND = new Color(0x2B2D30);
|
||||
static final Color HOVER_BACKGROUND = new Color(0x393B40);
|
||||
static final Color INACTIVE_SELECTION = new Color(0x43454A);
|
||||
static final Color SELECTION = new Color(0x2E436E);
|
||||
static final Color CONTROL_BORDER = new Color(0x4E5157);
|
||||
static final Color SEPARATOR = new Color(0x1E1F22);
|
||||
static final Color TEXT = new Color(0xDFE1E5);
|
||||
static final Color SECONDARY_TEXT = new Color(0x9DA0A8);
|
||||
static final Color MUTED_TEXT = new Color(0x6F737A);
|
||||
static final Color EDITOR_BACKGROUND = new ThemeColor(0xFFFFFF, 0x1E1F22);
|
||||
static final Color PANEL_BACKGROUND = new ThemeColor(0xF7F8FA, 0x2B2D30);
|
||||
static final Color HOVER_BACKGROUND = new ThemeColor(0xDFE1E5, 0x393B40);
|
||||
static final Color INACTIVE_SELECTION = new ThemeColor(0xDFE1E5, 0x43454A);
|
||||
static final Color SELECTION = new ThemeColor(0xD4E2FF, 0x2E436E);
|
||||
static final Color CONTROL_BORDER = new ThemeColor(0xC9CCD6, 0x4E5157);
|
||||
static final Color SEPARATOR = new ThemeColor(0xEBECF0, 0x1E1F22);
|
||||
static final Color TEXT = new ThemeColor(0x000000, 0xDFE1E5);
|
||||
static final ThemeColor SECONDARY_TEXT = new ThemeColor(0x6C707E, 0x9DA0A8);
|
||||
static final Color MUTED_TEXT = new ThemeColor(0x818594, 0x6F737A);
|
||||
static final Color ACCENT = new Color(0x3574F0);
|
||||
static final Color LINK = new Color(0x548AF7);
|
||||
static final Color ERROR = new Color(0xF75464);
|
||||
static final Color WARNING = new Color(0xF2C55C);
|
||||
static final Color SUCCESS = new Color(0x5FB865);
|
||||
static final Color OVERLAY_BACKGROUND = new Color(0x2B, 0x2D, 0x30, 236);
|
||||
static final Color OVERLAY_BORDER = new Color(0x43454A);
|
||||
static final String SECONDARY_TEXT_HTML = "#9DA0A8";
|
||||
static final Color LINK = new ThemeColor(0x315FBD, 0x548AF7);
|
||||
static final Color ERROR = new ThemeColor(0xDB3B4B, 0xF75464);
|
||||
static final Color WARNING = new ThemeColor(0xC27D04, 0xF2C55C);
|
||||
static final Color SUCCESS = new ThemeColor(0x208A3C, 0x5FB865);
|
||||
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 ThemeColor(0xDFE1E5, 0x43454A);
|
||||
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 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 final Map<String, String> INTELLIJ_NEW_UI_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"),
|
||||
private static final Map<String, String> SHARED_DEFAULTS = Map.ofEntries(
|
||||
Map.entry("Component.focusWidth", "1"),
|
||||
Map.entry("Component.innerFocusWidth", "0"),
|
||||
Map.entry("Component.arc", "8"),
|
||||
Map.entry("Button.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.selectionInsets", "0,6,0,6"),
|
||||
Map.entry("Tree.selectionArc", "6"),
|
||||
@@ -85,16 +87,49 @@ final class StudioTheme {
|
||||
Map.entry("ScrollBar.thumbArc", "999"),
|
||||
Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),
|
||||
Map.entry("ScrollBar.showButtons", "false"),
|
||||
Map.entry("ScrollBar.track", "#2B2D30"),
|
||||
Map.entry("ScrollPane.border", "0,0,0,0"),
|
||||
Map.entry("SplitPane.dividerSize", "1"),
|
||||
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.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() {
|
||||
}
|
||||
@@ -104,8 +139,7 @@ final class StudioTheme {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FlatLaf.setGlobalExtraDefaults(INTELLIJ_NEW_UI_DEFAULTS);
|
||||
FlatDarkLaf.setup();
|
||||
applyLookAndFeel(darkFor(mode()));
|
||||
pixelScale = UIScale.getUserScaleFactor();
|
||||
} catch (Throwable 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.
|
||||
*/
|
||||
@@ -160,4 +259,33 @@ final class StudioTheme {
|
||||
Font font = UIManager.getFont(key);
|
||||
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 javax.swing.BorderFactory;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JRadioButtonMenuItem;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JToggleButton;
|
||||
@@ -63,12 +66,39 @@ final class StudioToolbar {
|
||||
}
|
||||
|
||||
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);
|
||||
button.setToolTipText(tooltip);
|
||||
button.setFocusable(false);
|
||||
button.setMargin(new Insets(4, 8, 4, 8));
|
||||
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
|
||||
button.addActionListener(event -> action.run());
|
||||
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