feat: theme sync with OS (#82)

This commit is contained in:
hstyi
2025-01-15 22:24:19 +08:00
committed by GitHub
parent e30316eab3
commit 0884486e91
7 changed files with 577 additions and 57 deletions

View File

@@ -0,0 +1,433 @@
package app.termora;/*
* @(#)SwingUtils.java 1.02 11/15/08
*
*/
//package darrylbu.util;
import javax.swing.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.*;
/**
* A collection of utility methods for Swing.
*
* @author Darryl Burke
*/
public final class SwingUtils {
private SwingUtils() {
throw new Error("SwingUtils is just a container for static methods");
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components that are instances of
* class <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* Invoking this method with a class parameter of JComponent.class
* will return all nested components.
* <P>
* This method invokes getDescendantsOfType(clazz, container, true)
*
* @param clazz the class of components whose instances are to be found.
* @param container the container at which to begin the search
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfType(
Class<T> clazz, Container container) {
return getDescendantsOfType(clazz, container, true);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components that are instances of
* class <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* Invoking this method with a class parameter of JComponent.class
* will return all nested components.
*
* @param clazz the class of components whose instances are to be found.
* @param container the container at which to begin the search
* @param nested true to list components nested within another listed
* component, false otherwise
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfType(
Class<T> clazz, Container container, boolean nested) {
List<T> tList = new ArrayList<T>();
for (Component component : container.getComponents()) {
if (clazz.isAssignableFrom(component.getClass())) {
tList.add(clazz.cast(component));
}
if (nested || !clazz.isAssignableFrom(component.getClass())) {
tList.addAll(SwingUtils.<T>getDescendantsOfType(clazz,
(Container) component, nested));
}
}
return tList;
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy and returns the first found component that is an
* instance of class <code>clazz</code> having the bound property value.
* Returns {@code null} if such component cannot be found.
* <P>
* This method invokes getDescendantOfType(clazz, container, property, value,
* true)
*
* @param clazz the class of component whose instance is to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* @param value the value of the bound property
* @return the component, or null if no such component exists in the
* container
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfType(
Class<T> clazz, Container container, String property, Object value)
throws IllegalArgumentException {
return getDescendantOfType(clazz, container, property, value, true);
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy and returns the first found component that is an
* instance of class <code>clazz</code> and has the bound property value.
* Returns {@code null} if such component cannot be found.
*
* @param clazz the class of component whose instance to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* @param value the value of the bound property
* @param nested true to list components nested within another component
* which is also an instance of <code>clazz</code>, false otherwise
* @return the component, or null if no such component exists in the
* container
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
Container container, String property, Object value, boolean nested)
throws IllegalArgumentException {
List<T> list = getDescendantsOfType(clazz, container, nested);
return getComponentFromList(clazz, list, property, value);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components of class
* <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
* <P>
* This method invokes getDescendantsOfClass(clazz, container, true)
*
* @param clazz the class of components to be found.
* @param container the container at which to begin the search
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfClass(
Class<T> clazz, Container container) {
return getDescendantsOfClass(clazz, container, true);
}
/**
* Convenience method for searching below <code>container</code> in the
* component hierarchy and return nested components of class
* <code>clazz</code> it finds. Returns an empty list if no such
* components exist in the container.
*
* @param clazz the class of components to be found.
* @param container the container at which to begin the search
* @param nested true to list components nested within another listed
* component, false otherwise
* @return the List of components
*/
public static <T extends JComponent> List<T> getDescendantsOfClass(
Class<T> clazz, Container container, boolean nested) {
List<T> tList = new ArrayList<T>();
for (Component component : container.getComponents()) {
if (clazz.equals(component.getClass())) {
tList.add(clazz.cast(component));
}
if (nested || !clazz.equals(component.getClass())) {
tList.addAll(SwingUtils.<T>getDescendantsOfClass(clazz,
(Container) component, nested));
}
}
return tList;
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy in a depth first manner and returns the first
* found component of class <code>clazz</code> having the bound property
* value.
* <P>
* Returns {@code null} if such component cannot be found.
* <P>
* This method invokes getDescendantOfClass(clazz, container, property,
* value, true)
*
* @param clazz the class of component to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed in
* the accessor e.g. "Text" for getText(), "Value" for getValue().
* This parameter is case sensitive.
* @param value the value of the bound property
* @return the component, or null if no such component exists in the
* container's hierarchy.
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
Container container, String property, Object value)
throws IllegalArgumentException {
return getDescendantOfClass(clazz, container, property, value, true);
}
/**
* Convenience method that searches below <code>container</code> in the
* component hierarchy in a depth first manner and returns the first
* found component of class <code>clazz</code> having the bound property
* value.
* <P>
* Returns {@code null} if such component cannot be found.
*
* @param clazz the class of component to be found.
* @param container the container at which to begin the search
* @param property the className of the bound property, exactly as expressed
* in the accessor e.g. "Text" for getText(), "Value" for getValue().
* This parameter is case sensitive.
* @param value the value of the bound property
* @param nested true to include components nested within another listed
* component, false otherwise
* @return the component, or null if no such component exists in the
* container's hierarchy
* @throws java.lang.IllegalArgumentException if the bound property does
* not exist for the class or cannot be accessed
*/
public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
Container container, String property, Object value, boolean nested)
throws IllegalArgumentException {
List<T> list = getDescendantsOfClass(clazz, container, nested);
return getComponentFromList(clazz, list, property, value);
}
private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
List<T> list, String property, Object value)
throws IllegalArgumentException {
T retVal = null;
Method method = null;
try {
method = clazz.getMethod("get" + property);
} catch (NoSuchMethodException ex) {
try {
method = clazz.getMethod("is" + property);
} catch (NoSuchMethodException ex1) {
throw new IllegalArgumentException("Property " + property +
" not found in class " + clazz.getName());
}
}
try {
for (T t : list) {
Object testVal = method.invoke(t);
if (equals(value, testVal)) {
return t;
}
}
} catch (InvocationTargetException ex) {
throw new IllegalArgumentException(
"Error accessing property " + property +
" in class " + clazz.getName());
} catch (IllegalAccessException ex) {
throw new IllegalArgumentException(
"Property " + property +
" cannot be accessed in class " + clazz.getName());
} catch (SecurityException ex) {
throw new IllegalArgumentException(
"Property " + property +
" cannot be accessed in class " + clazz.getName());
}
return retVal;
}
/**
* Convenience method for determining whether two objects are either
* equal or both null.
*
* @param obj1 the first reference object to compare.
* @param obj2 the second reference object to compare.
* @return true if obj1 and obj2 are equal or if both are null,
* false otherwise
*/
public static boolean equals(Object obj1, Object obj2) {
return obj1 == null ? obj2 == null : obj1.equals(obj2);
}
/**
* Convenience method for mapping a container in the hierarchy to its
* contained components. The keys are the containers, and the values
* are lists of contained components.
* <P>
* Implementation note: The returned value is a HashMap and the values
* are of type ArrayList. This is subject to change, so callers should
* code against the interfaces Map and List.
*
* @param container The JComponent to be mapped
* @param nested true to drill down to nested containers, false otherwise
* @return the Map of the UI
*/
public static Map<JComponent, List<JComponent>> getComponentMap(
JComponent container, boolean nested) {
HashMap<JComponent, List<JComponent>> retVal =
new HashMap<JComponent, List<JComponent>>();
for (JComponent component : getDescendantsOfType(JComponent.class,
container, false)) {
if (!retVal.containsKey(container)) {
retVal.put(container,
new ArrayList<JComponent>());
}
retVal.get(container).add(component);
if (nested) {
retVal.putAll(getComponentMap(component, nested));
}
}
return retVal;
}
/**
* Convenience method for retrieving a subset of the UIDefaults pertaining
* to a particular class.
*
* @param clazz the class of interest
* @return the UIDefaults of the class
*/
public static UIDefaults getUIDefaultsOfClass(Class clazz) {
String name = clazz.getName();
name = name.substring(name.lastIndexOf(".") + 2);
return getUIDefaultsOfClass(name);
}
/**
* Convenience method for retrieving a subset of the UIDefaults pertaining
* to a particular class.
*
* @param className fully qualified name of the class of interest
* @return the UIDefaults of the class named
*/
public static UIDefaults getUIDefaultsOfClass(String className) {
UIDefaults retVal = new UIDefaults();
UIDefaults defaults = UIManager.getLookAndFeelDefaults();
List<?> listKeys = Collections.list(defaults.keys());
for (Object key : listKeys) {
if (key instanceof String && ((String) key).startsWith(className)) {
String stringKey = (String) key;
String property = stringKey;
if (stringKey.contains(".")) {
property = stringKey.substring(stringKey.indexOf(".") + 1);
}
retVal.put(property, defaults.get(key));
}
}
return retVal;
}
/**
* Convenience method for retrieving the UIDefault for a single property
* of a particular class.
*
* @param clazz the class of interest
* @param property the property to query
* @return the UIDefault property, or null if not found
*/
public static Object getUIDefaultOfClass(Class clazz, String property) {
Object retVal = null;
UIDefaults defaults = getUIDefaultsOfClass(clazz);
List<Object> listKeys = Collections.list(defaults.keys());
for (Object key : listKeys) {
if (key.equals(property)) {
return defaults.get(key);
}
if (key.toString().equalsIgnoreCase(property)) {
retVal = defaults.get(key);
}
}
return retVal;
}
/**
* Exclude methods that return values that are meaningless to the user
*/
static Set<String> setExclude = new HashSet<String>();
static {
setExclude.add("getFocusCycleRootAncestor");
setExclude.add("getAccessibleContext");
setExclude.add("getColorModel");
setExclude.add("getGraphics");
setExclude.add("getGraphicsConfiguration");
}
/**
* Convenience method for obtaining most non-null human readable properties
* of a JComponent. Array properties are not included.
* <P>
* Implementation note: The returned value is a HashMap. This is subject
* to change, so callers should code against the interface Map.
*
* @param component the component whose proerties are to be determined
* @return the class and value of the properties
*/
public static Map<Object, Object> getProperties(JComponent component) {
Map<Object, Object> retVal = new HashMap<Object, Object>();
Class<?> clazz = component.getClass();
Method[] methods = clazz.getMethods();
Object value = null;
for (Method method : methods) {
if (method.getName().matches("^(is|get).*") &&
method.getParameterTypes().length == 0) {
try {
Class returnType = method.getReturnType();
if (returnType != void.class &&
!returnType.getName().startsWith("[") &&
!setExclude.contains(method.getName())) {
String key = method.getName();
value = method.invoke(component);
if (value != null && !(value instanceof Component)) {
retVal.put(key, value);
}
}
// ignore exceptions that arise if the property could not be accessed
} catch (IllegalAccessException ex) {
} catch (IllegalArgumentException ex) {
} catch (InvocationTargetException ex) {
}
}
}
return retVal;
}
/**
* Convenience method to obtain the Swing class from which this
* component was directly or indirectly derived.
*
* @param component The component whose Swing superclass is to be
* determined
* @return The nearest Swing class in the inheritance tree
*/
public static <T extends JComponent> Class getJClass(T component) {
Class<?> clazz = component.getClass();
while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
clazz = clazz.getSuperclass();
}
return clazz;
}
}

View File

@@ -129,14 +129,14 @@ class ApplicationRunner {
} }
val themeManager = ThemeManager.getInstance() val themeManager = ThemeManager.getInstance()
val settings = Database.getDatabase() val appearance = Database.getDatabase().appearance
var theme = settings.appearance.theme var theme = appearance.theme
// 如果是跟随系统或者不存在样式,那么使用默认的 // 如果是跟随系统
if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) { if (appearance.followSystem) {
theme = if (OsThemeDetector.getDetector().isDark) { theme = if (OsThemeDetector.getDetector().isDark) {
"Dark" appearance.darkTheme
} else { } else {
"Light" appearance.lightTheme
} }
} }

View File

@@ -551,6 +551,8 @@ class Database private constructor(private val env: Environment) : Disposable {
* 跟随系统 * 跟随系统
*/ */
var followSystem by BooleanPropertyDelegate(true) var followSystem by BooleanPropertyDelegate(true)
var darkTheme by StringPropertyDelegate("Dark")
var lightTheme by StringPropertyDelegate("Light")
/** /**
* 语言 * 语言

View File

@@ -6,14 +6,13 @@ import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jetbrains.JBR import com.jetbrains.JBR
import java.awt.BorderLayout import java.awt.*
import java.awt.Dimension
import java.awt.Window
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import javax.swing.* import javax.swing.*
abstract class DialogWrapper(owner: Window?) : JDialog(owner) { abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
private val rootPanel = JPanel(BorderLayout()) private val rootPanel = JPanel(BorderLayout())
private val titleLabel = JLabel() private val titleLabel = JLabel()
@@ -147,6 +146,31 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) {
rootPane.actionMap.put("close", object : AnAction() { rootPane.actionMap.put("close", object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
JPopupMenu::class.java,
c as Container, true
)
var openPopup = false
for (p in popups) {
p.isVisible = false
openPopup = true
}
val window = SwingUtilities.windowForComponent(c)
val windows = window.ownedWindows
for (w in windows) {
if (w.isVisible && w.javaClass.getName().endsWith("HeavyWeightWindow")) {
openPopup = true
w.dispose()
}
}
if (openPopup) {
return
}
doCancelAction() doCancelAction()
} }
}) })

View File

@@ -8,6 +8,11 @@ import com.formdev.flatlaf.FlatPropertiesLaf
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import java.util.* import java.util.*
interface LafTag
interface LightLafTag : LafTag
interface DarkLafTag : LafTag
class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply { class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
putAll( putAll(
mapOf( mapOf(
@@ -16,7 +21,7 @@ class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
"@windowText" to "#eaeaea", "@windowText" to "#eaeaea",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Basic.BACKGROUND -> 0x282935 TerminalColor.Basic.BACKGROUND -> 0x282935
@@ -54,7 +59,8 @@ class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply {
} }
class LightLaf : FlatLightLaf(), ColorTheme {
class LightLaf : FlatLightLaf(), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0 TerminalColor.Normal.BLACK -> 0
@@ -81,7 +87,7 @@ class LightLaf : FlatLightLaf(), ColorTheme {
} }
class DarkLaf : FlatDarkLaf(), ColorTheme { class DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0 TerminalColor.Normal.BLACK -> 0
@@ -110,7 +116,7 @@ class DarkLaf : FlatDarkLaf(), ColorTheme {
} }
} }
class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme { class iTerm2DarkLaf : FlatDarkLaf(), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
@@ -158,7 +164,7 @@ class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply {
"@windowText" to "#32364a", "@windowText" to "#32364a",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
@@ -201,7 +207,7 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply {
"@windowText" to "#21b568", "@windowText" to "#21b568",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
@@ -243,7 +249,7 @@ class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply {
"@windowText" to "#3b2322", "@windowText" to "#3b2322",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -282,7 +288,7 @@ class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply {
"@windowText" to "#abb2bf", "@windowText" to "#abb2bf",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -320,7 +326,7 @@ class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply {
"@windowText" to "#383a42", "@windowText" to "#383a42",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -358,7 +364,7 @@ class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().appl
"@windowText" to "#d3c6aa", "@windowText" to "#d3c6aa",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e TerminalColor.Normal.BLACK -> 0x42494e
@@ -395,7 +401,7 @@ class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().ap
"@windowText" to "#5c6a72", "@windowText" to "#5c6a72",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x42494e TerminalColor.Normal.BLACK -> 0x42494e
@@ -432,7 +438,7 @@ class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply {
"@windowText" to "#d6deeb", "@windowText" to "#d6deeb",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x072945 TerminalColor.Normal.BLACK -> 0x072945
@@ -469,7 +475,7 @@ class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply {
"@windowText" to "#403f53", "@windowText" to "#403f53",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x403f53 TerminalColor.Normal.BLACK -> 0x403f53
@@ -506,7 +512,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply {
"@windowText" to "#edecee", "@windowText" to "#edecee",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x1c1b22 TerminalColor.Normal.BLACK -> 0x1c1b22
@@ -543,7 +549,7 @@ class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply {
"@windowText" to "#ffffff", "@windowText" to "#ffffff",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -580,7 +586,7 @@ class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply {
"@windowText" to "#8b949e", "@windowText" to "#8b949e",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -617,7 +623,7 @@ class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply {
"@windowText" to "#3e3e3e", "@windowText" to "#3e3e3e",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -654,7 +660,7 @@ class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply {
"@windowText" to "#e6e1cf", "@windowText" to "#e6e1cf",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -691,7 +697,7 @@ class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply {
"@windowText" to "#5c6773", "@windowText" to "#5c6773",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -728,7 +734,7 @@ class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply {
"@windowText" to "#00ff00", "@windowText" to "#00ff00",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -767,7 +773,7 @@ class ProLaf : FlatPropertiesLaf("Pro", Properties().apply {
"@windowText" to "#f2f2f2", "@windowText" to "#f2f2f2",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2e2e2e TerminalColor.Normal.BLACK -> 0x2e2e2e
@@ -806,7 +812,7 @@ class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply {
"@windowText" to "#414858", "@windowText" to "#414858",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x2c3344 TerminalColor.Normal.BLACK -> 0x2c3344
@@ -845,7 +851,7 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
"@windowText" to "#d8dee9", "@windowText" to "#d8dee9",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x3b4252 TerminalColor.Normal.BLACK -> 0x3b4252
@@ -885,7 +891,7 @@ class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply {
"@windowText" to "#3e3e3e", "@windowText" to "#3e3e3e",
) )
) )
}), ColorTheme { }), ColorTheme, LightLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x3e3e3e TerminalColor.Normal.BLACK -> 0x3e3e3e
@@ -924,7 +930,7 @@ class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply {
"@windowText" to "#8b949e", "@windowText" to "#8b949e",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x000000 TerminalColor.Normal.BLACK -> 0x000000
@@ -964,7 +970,7 @@ class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply {
"@windowText" to "#d2d8d9", "@windowText" to "#d2d8d9",
) )
) )
}), ColorTheme { }), ColorTheme, DarkLafTag {
override fun getColor(color: TerminalColor): Int { override fun getColor(color: TerminalColor): Int {
return when (color) { return when (color) {
TerminalColor.Normal.BLACK -> 0x7d8b8f TerminalColor.Normal.BLACK -> 0x7d8b8f

View File

@@ -17,11 +17,8 @@ import app.termora.terminal.CursorStyle
import app.termora.terminal.DataKey import app.termora.terminal.DataKey
import app.termora.terminal.panel.TerminalPanel import app.termora.terminal.panel.TerminalPanel
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatButton import com.formdev.flatlaf.extras.components.*
import com.formdev.flatlaf.extras.components.FlatComboBox
import com.formdev.flatlaf.extras.components.FlatLabel
import com.formdev.flatlaf.util.SystemInfo import com.formdev.flatlaf.util.SystemInfo
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
@@ -49,6 +46,8 @@ import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -109,6 +108,7 @@ class SettingsOptionsPane : OptionsPane() {
val themeComboBox = FlatComboBox<String>() val themeComboBox = FlatComboBox<String>()
val languageComboBox = FlatComboBox<String>() val languageComboBox = FlatComboBox<String>()
val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system"))
val preferredThemeBtn = JButton(Icons.settings)
private val appearance get() = database.appearance private val appearance get() = database.appearance
init { init {
@@ -119,6 +119,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun initView() { private fun initView() {
followSystemCheckBox.isSelected = appearance.followSystem followSystemCheckBox.isSelected = appearance.followSystem
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
themeManager.themes.keys.forEach { themeComboBox.addItem(it) } themeManager.themes.keys.forEach { themeComboBox.addItem(it) }
@@ -158,19 +159,17 @@ class SettingsOptionsPane : OptionsPane() {
followSystemCheckBox.addActionListener { followSystemCheckBox.addActionListener {
appearance.followSystem = followSystemCheckBox.isSelected appearance.followSystem = followSystemCheckBox.isSelected
themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected
preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected
appearance.theme = themeComboBox.selectedItem as String
if (followSystemCheckBox.isSelected) { if (followSystemCheckBox.isSelected) {
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
if (OsThemeDetector.getDetector().isDark) { if (OsThemeDetector.getDetector().isDark) {
if (!FlatLaf.isLafDark()) { themeManager.change(appearance.darkTheme)
themeManager.change("Dark") themeComboBox.selectedItem = appearance.darkTheme
themeComboBox.selectedItem = "Dark"
}
} else { } else {
if (FlatLaf.isLafDark()) { themeManager.change(appearance.lightTheme)
themeManager.change("Light") themeComboBox.selectedItem = appearance.lightTheme
themeComboBox.selectedItem = "Light"
}
} }
} }
} }
@@ -189,6 +188,8 @@ class SettingsOptionsPane : OptionsPane() {
} }
} }
} }
preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() }
} }
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
@@ -203,19 +204,74 @@ class SettingsOptionsPane : OptionsPane() {
return this return this
} }
private fun showPreferredThemeContextmenu() {
val popupMenu = FlatPopupMenu()
val dark = JMenu("For Dark OS")
val light = JMenu("For Light OS")
val darkTheme = appearance.darkTheme
val lightTheme = appearance.lightTheme
for (e in themeManager.themes) {
val clazz = Class.forName(e.value)
val item = JCheckBoxMenuItem(e.key)
item.isSelected = e.key == lightTheme || e.key == darkTheme
if (clazz.interfaces.contains(DarkLafTag::class.java)) {
dark.add(item).addActionListener {
if (e.key != darkTheme) {
appearance.darkTheme = e.key
if (OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
} else if (clazz.interfaces.contains(LightLafTag::class.java)) {
light.add(item).addActionListener {
if (e.key != lightTheme) {
appearance.lightTheme = e.key
if (!OsThemeDetector.getDetector().isDark) {
themeComboBox.selectedItem = e.key
}
}
}
}
}
popupMenu.add(dark)
popupMenu.addSeparator()
popupMenu.add(light)
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {
}
override fun popupMenuCanceled(e: PopupMenuEvent) {
}
})
popupMenu.show(preferredThemeBtn, 0, preferredThemeBtn.height + 2)
}
private fun getFormPanel(): JPanel { private fun getFormPanel(): JPanel {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, default, default:grow", "left:pref, $formMargin, default:grow, $formMargin, default, default:grow",
"pref, $formMargin, pref, $formMargin" "pref, $formMargin, pref, $formMargin"
) )
val box = FlatToolBar()
box.add(followSystemCheckBox)
box.add(Box.createHorizontalStrut(2))
box.add(preferredThemeBtn)
var rows = 1 var rows = 1
val step = 2 val step = 2
return FormBuilder.create().layout(layout) return FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows) .add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, rows)
.add(themeComboBox).xy(3, rows) .add(themeComboBox).xy(3, rows)
.add(followSystemCheckBox).xy(5, rows).apply { rows += step } .add(box).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows) .add("${I18n.getString("termora.settings.appearance.language")}:").xy(1, rows)
.add(languageComboBox).xy(3, rows) .add(languageComboBox).xy(3, rows)
.add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) { .add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) {

View File

@@ -28,6 +28,7 @@ class ThemeManager private constructor() {
} }
} }
val appearance by lazy { Database.getDatabase().appearance }
val themes = mapOf( val themes = mapOf(
"Light" to LightLaf::class.java.name, "Light" to LightLaf::class.java.name,
"Dark" to DarkLaf::class.java.name, "Dark" to DarkLaf::class.java.name,
@@ -79,18 +80,16 @@ class ThemeManager private constructor() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> { OsThemeDetector.getDetector().registerListener(object : Consumer<Boolean> {
override fun accept(isDark: Boolean) { override fun accept(isDark: Boolean) {
if (!Database.getDatabase().appearance.followSystem) { if (!appearance.followSystem) {
return
}
if (FlatLaf.isLafDark() && isDark) {
return return
} }
SwingUtilities.invokeLater {
if (isDark) { if (isDark) {
SwingUtilities.invokeLater { change("Dark") } change(appearance.darkTheme)
} else { } else {
SwingUtilities.invokeLater { change("Light") } change(appearance.lightTheme)
}
} }
} }
}) })