From 0884486e9151a4d0c048630f3e61efeafdf92c45 Mon Sep 17 00:00:00 2001 From: hstyi Date: Wed, 15 Jan 2025 22:24:19 +0800 Subject: [PATCH] feat: theme sync with OS (#82) --- src/main/java/app/termora/SwingUtils.java | 433 ++++++++++++++++++ .../kotlin/app/termora/ApplicationRunner.kt | 12 +- src/main/kotlin/app/termora/Database.kt | 2 + src/main/kotlin/app/termora/DialogWrapper.kt | 30 +- src/main/kotlin/app/termora/Laf.kt | 58 +-- .../kotlin/app/termora/SettingsOptionsPane.kt | 82 +++- src/main/kotlin/app/termora/ThemeManager.kt | 17 +- 7 files changed, 577 insertions(+), 57 deletions(-) create mode 100644 src/main/java/app/termora/SwingUtils.java diff --git a/src/main/java/app/termora/SwingUtils.java b/src/main/java/app/termora/SwingUtils.java new file mode 100644 index 0000000..eef1f14 --- /dev/null +++ b/src/main/java/app/termora/SwingUtils.java @@ -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 container in the + * component hierarchy and return nested components that are instances of + * class clazz it finds. Returns an empty list if no such + * components exist in the container. + *

+ * Invoking this method with a class parameter of JComponent.class + * will return all nested components. + *

+ * 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 List getDescendantsOfType( + Class clazz, Container container) { + return getDescendantsOfType(clazz, container, true); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components that are instances of + * class clazz it finds. Returns an empty list if no such + * components exist in the container. + *

+ * 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 List getDescendantsOfType( + Class clazz, Container container, boolean nested) { + List tList = new ArrayList(); + for (Component component : container.getComponents()) { + if (clazz.isAssignableFrom(component.getClass())) { + tList.add(clazz.cast(component)); + } + if (nested || !clazz.isAssignableFrom(component.getClass())) { + tList.addAll(SwingUtils.getDescendantsOfType(clazz, + (Container) component, nested)); + } + } + return tList; + } + + /** + * Convenience method that searches below container in the + * component hierarchy and returns the first found component that is an + * instance of class clazz having the bound property value. + * Returns {@code null} if such component cannot be found. + *

+ * 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 getDescendantOfType( + Class clazz, Container container, String property, Object value) + throws IllegalArgumentException { + return getDescendantOfType(clazz, container, property, value, true); + } + + /** + * Convenience method that searches below container in the + * component hierarchy and returns the first found component that is an + * instance of class clazz 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 clazz, 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 getDescendantOfType(Class clazz, + Container container, String property, Object value, boolean nested) + throws IllegalArgumentException { + List list = getDescendantsOfType(clazz, container, nested); + return getComponentFromList(clazz, list, property, value); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components of class + * clazz it finds. Returns an empty list if no such + * components exist in the container. + *

+ * 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 List getDescendantsOfClass( + Class clazz, Container container) { + return getDescendantsOfClass(clazz, container, true); + } + + /** + * Convenience method for searching below container in the + * component hierarchy and return nested components of class + * clazz 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 List getDescendantsOfClass( + Class clazz, Container container, boolean nested) { + List tList = new ArrayList(); + for (Component component : container.getComponents()) { + if (clazz.equals(component.getClass())) { + tList.add(clazz.cast(component)); + } + if (nested || !clazz.equals(component.getClass())) { + tList.addAll(SwingUtils.getDescendantsOfClass(clazz, + (Container) component, nested)); + } + } + return tList; + } + + /** + * Convenience method that searches below container in the + * component hierarchy in a depth first manner and returns the first + * found component of class clazz having the bound property + * value. + *

+ * Returns {@code null} if such component cannot be found. + *

+ * 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 getDescendantOfClass(Class clazz, + Container container, String property, Object value) + throws IllegalArgumentException { + return getDescendantOfClass(clazz, container, property, value, true); + } + + /** + * Convenience method that searches below container in the + * component hierarchy in a depth first manner and returns the first + * found component of class clazz having the bound property + * value. + *

+ * 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 getDescendantOfClass(Class clazz, + Container container, String property, Object value, boolean nested) + throws IllegalArgumentException { + List list = getDescendantsOfClass(clazz, container, nested); + return getComponentFromList(clazz, list, property, value); + } + + private static T getComponentFromList(Class clazz, + List 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. + *

+ * 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> getComponentMap( + JComponent container, boolean nested) { + HashMap> retVal = + new HashMap>(); + for (JComponent component : getDescendantsOfType(JComponent.class, + container, false)) { + if (!retVal.containsKey(container)) { + retVal.put(container, + new ArrayList()); + } + 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 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 setExclude = new HashSet(); + 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. + *

+ * 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 getProperties(JComponent component) { + Map retVal = new HashMap(); + 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 Class getJClass(T component) { + Class clazz = component.getClass(); + while (!clazz.getName().matches("javax.swing.J[^.]*$")) { + clazz = clazz.getSuperclass(); + } + return clazz; + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 1e71420..8a9696c 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -129,14 +129,14 @@ class ApplicationRunner { } val themeManager = ThemeManager.getInstance() - val settings = Database.getDatabase() - var theme = settings.appearance.theme - // 如果是跟随系统或者不存在样式,那么使用默认的 - if (settings.appearance.followSystem || !themeManager.themes.containsKey(theme)) { + val appearance = Database.getDatabase().appearance + var theme = appearance.theme + // 如果是跟随系统 + if (appearance.followSystem) { theme = if (OsThemeDetector.getDetector().isDark) { - "Dark" + appearance.darkTheme } else { - "Light" + appearance.lightTheme } } diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index d78f0cd..89e434a 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -551,6 +551,8 @@ class Database private constructor(private val env: Environment) : Disposable { * 跟随系统 */ var followSystem by BooleanPropertyDelegate(true) + var darkTheme by StringPropertyDelegate("Dark") + var lightTheme by StringPropertyDelegate("Light") /** * 语言 diff --git a/src/main/kotlin/app/termora/DialogWrapper.kt b/src/main/kotlin/app/termora/DialogWrapper.kt index 73f1b47..e54b1d2 100644 --- a/src/main/kotlin/app/termora/DialogWrapper.kt +++ b/src/main/kotlin/app/termora/DialogWrapper.kt @@ -6,14 +6,13 @@ import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.util.SystemInfo import com.jetbrains.JBR -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Window +import java.awt.* import java.awt.event.KeyEvent import java.awt.event.WindowAdapter import java.awt.event.WindowEvent import javax.swing.* + abstract class DialogWrapper(owner: Window?) : JDialog(owner) { private val rootPanel = JPanel(BorderLayout()) private val titleLabel = JLabel() @@ -147,6 +146,31 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { rootPane.actionMap.put("close", object : AnAction() { override fun actionPerformed(evt: AnActionEvent) { + val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner + val popups: List = 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() } }) diff --git a/src/main/kotlin/app/termora/Laf.kt b/src/main/kotlin/app/termora/Laf.kt index c534571..3564753 100644 --- a/src/main/kotlin/app/termora/Laf.kt +++ b/src/main/kotlin/app/termora/Laf.kt @@ -8,6 +8,11 @@ import com.formdev.flatlaf.FlatPropertiesLaf import com.formdev.flatlaf.util.SystemInfo import java.util.* + +interface LafTag +interface LightLafTag : LafTag +interface DarkLafTag : LafTag + class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply { putAll( mapOf( @@ -16,7 +21,7 @@ class DraculaLaf : FlatPropertiesLaf("Dracula", Properties().apply { "@windowText" to "#eaeaea", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { 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 { return when (color) { 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 { return when (color) { 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 { return when (color) { @@ -158,7 +164,7 @@ class TermiusLightLaf : FlatPropertiesLaf("Termius Light", Properties().apply { "@windowText" to "#32364a", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { @@ -201,7 +207,7 @@ class TermiusDarkLaf : FlatPropertiesLaf("Termius Dark", Properties().apply { "@windowText" to "#21b568", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { @@ -243,7 +249,7 @@ class NovelLaf : FlatPropertiesLaf("Novel", Properties().apply { "@windowText" to "#3b2322", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -282,7 +288,7 @@ class AtomOneDarkLaf : FlatPropertiesLaf("Atom One Dark", Properties().apply { "@windowText" to "#abb2bf", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -320,7 +326,7 @@ class AtomOneLightLaf : FlatPropertiesLaf("Atom One Light", Properties().apply { "@windowText" to "#383a42", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -358,7 +364,7 @@ class EverforestDarkLaf : FlatPropertiesLaf("Everforest Dark", Properties().appl "@windowText" to "#d3c6aa", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x42494e @@ -395,7 +401,7 @@ class EverforestLightLaf : FlatPropertiesLaf("Everforest Light", Properties().ap "@windowText" to "#5c6a72", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x42494e @@ -432,7 +438,7 @@ class NightOwlLaf : FlatPropertiesLaf("Night Owl", Properties().apply { "@windowText" to "#d6deeb", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x072945 @@ -469,7 +475,7 @@ class LightOwlLaf : FlatPropertiesLaf("Light Owl", Properties().apply { "@windowText" to "#403f53", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x403f53 @@ -506,7 +512,7 @@ class AuraLaf : FlatPropertiesLaf("Aura", Properties().apply { "@windowText" to "#edecee", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x1c1b22 @@ -543,7 +549,7 @@ class Cobalt2Laf : FlatPropertiesLaf("Cobalt2", Properties().apply { "@windowText" to "#ffffff", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -580,7 +586,7 @@ class OctocatDarkLaf : FlatPropertiesLaf("Octocat Dark", Properties().apply { "@windowText" to "#8b949e", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -617,7 +623,7 @@ class OctocatLightLaf : FlatPropertiesLaf("Octocat Light", Properties().apply { "@windowText" to "#3e3e3e", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -654,7 +660,7 @@ class AyuDarkLaf : FlatPropertiesLaf("Ayu Dark", Properties().apply { "@windowText" to "#e6e1cf", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -691,7 +697,7 @@ class AyuLightLaf : FlatPropertiesLaf("Ayu Light", Properties().apply { "@windowText" to "#5c6773", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -728,7 +734,7 @@ class HomebrewLaf : FlatPropertiesLaf("Homebrew", Properties().apply { "@windowText" to "#00ff00", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x2e2e2e @@ -767,7 +773,7 @@ class ProLaf : FlatPropertiesLaf("Pro", Properties().apply { "@windowText" to "#f2f2f2", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x2e2e2e @@ -806,7 +812,7 @@ class NordLightLaf : FlatPropertiesLaf("Nord Light", Properties().apply { "@windowText" to "#414858", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x2c3344 @@ -845,7 +851,7 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply { "@windowText" to "#d8dee9", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x3b4252 @@ -885,7 +891,7 @@ class GitHubLightLaf : FlatPropertiesLaf("GitHub Light", Properties().apply { "@windowText" to "#3e3e3e", ) ) -}), ColorTheme { +}), ColorTheme, LightLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x3e3e3e @@ -924,7 +930,7 @@ class GitHubDarkLaf : FlatPropertiesLaf("GitHub Dark", Properties().apply { "@windowText" to "#8b949e", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x000000 @@ -964,7 +970,7 @@ class ChalkLaf : FlatPropertiesLaf("Chalk", Properties().apply { "@windowText" to "#d2d8d9", ) ) -}), ColorTheme { +}), ColorTheme, DarkLafTag { override fun getColor(color: TerminalColor): Int { return when (color) { TerminalColor.Normal.BLACK -> 0x7d8b8f diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index 8d9103c..b621d7d 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -17,11 +17,8 @@ import app.termora.terminal.CursorStyle import app.termora.terminal.DataKey import app.termora.terminal.panel.TerminalPanel import cash.z.ecc.android.bip39.Mnemonics -import com.formdev.flatlaf.FlatLaf import com.formdev.flatlaf.extras.FlatSVGIcon -import com.formdev.flatlaf.extras.components.FlatButton -import com.formdev.flatlaf.extras.components.FlatComboBox -import com.formdev.flatlaf.extras.components.FlatLabel +import com.formdev.flatlaf.extras.components.* import com.formdev.flatlaf.util.SystemInfo import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.layout.FormLayout @@ -49,6 +46,8 @@ import java.nio.charset.StandardCharsets import java.util.* import javax.swing.* import javax.swing.event.DocumentEvent +import javax.swing.event.PopupMenuEvent +import javax.swing.event.PopupMenuListener import kotlin.time.Duration.Companion.milliseconds @@ -109,6 +108,7 @@ class SettingsOptionsPane : OptionsPane() { val themeComboBox = FlatComboBox() val languageComboBox = FlatComboBox() val followSystemCheckBox = JCheckBox(I18n.getString("termora.settings.appearance.follow-system")) + val preferredThemeBtn = JButton(Icons.settings) private val appearance get() = database.appearance init { @@ -119,6 +119,7 @@ class SettingsOptionsPane : OptionsPane() { private fun initView() { followSystemCheckBox.isSelected = appearance.followSystem + preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected themeManager.themes.keys.forEach { themeComboBox.addItem(it) } @@ -158,19 +159,17 @@ class SettingsOptionsPane : OptionsPane() { followSystemCheckBox.addActionListener { appearance.followSystem = followSystemCheckBox.isSelected themeComboBox.isEnabled = !followSystemCheckBox.isSelected + preferredThemeBtn.isEnabled = followSystemCheckBox.isSelected + appearance.theme = themeComboBox.selectedItem as String if (followSystemCheckBox.isSelected) { SwingUtilities.invokeLater { if (OsThemeDetector.getDetector().isDark) { - if (!FlatLaf.isLafDark()) { - themeManager.change("Dark") - themeComboBox.selectedItem = "Dark" - } + themeManager.change(appearance.darkTheme) + themeComboBox.selectedItem = appearance.darkTheme } else { - if (FlatLaf.isLafDark()) { - themeManager.change("Light") - themeComboBox.selectedItem = "Light" - } + themeManager.change(appearance.lightTheme) + themeComboBox.selectedItem = appearance.lightTheme } } } @@ -189,6 +188,8 @@ class SettingsOptionsPane : OptionsPane() { } } } + + preferredThemeBtn.addActionListener { showPreferredThemeContextmenu() } } override fun getIcon(isSelected: Boolean): Icon { @@ -203,19 +204,74 @@ class SettingsOptionsPane : OptionsPane() { 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 { val layout = FormLayout( "left:pref, $formMargin, default:grow, $formMargin, default, default:grow", "pref, $formMargin, pref, $formMargin" ) + val box = FlatToolBar() + box.add(followSystemCheckBox) + box.add(Box.createHorizontalStrut(2)) + box.add(preferredThemeBtn) var rows = 1 val step = 2 return FormBuilder.create().layout(layout) .add("${I18n.getString("termora.settings.appearance.theme")}:").xy(1, 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(languageComboBox).xy(3, rows) .add(Hyperlink(object : AnAction(I18n.getString("termora.settings.appearance.i-want-to-translate")) { diff --git a/src/main/kotlin/app/termora/ThemeManager.kt b/src/main/kotlin/app/termora/ThemeManager.kt index 8de2f5c..9e7def8 100644 --- a/src/main/kotlin/app/termora/ThemeManager.kt +++ b/src/main/kotlin/app/termora/ThemeManager.kt @@ -28,6 +28,7 @@ class ThemeManager private constructor() { } } + val appearance by lazy { Database.getDatabase().appearance } val themes = mapOf( "Light" to LightLaf::class.java.name, "Dark" to DarkLaf::class.java.name, @@ -79,18 +80,16 @@ class ThemeManager private constructor() { GlobalScope.launch(Dispatchers.IO) { OsThemeDetector.getDetector().registerListener(object : Consumer { override fun accept(isDark: Boolean) { - if (!Database.getDatabase().appearance.followSystem) { + if (!appearance.followSystem) { return } - if (FlatLaf.isLafDark() && isDark) { - return - } - - if (isDark) { - SwingUtilities.invokeLater { change("Dark") } - } else { - SwingUtilities.invokeLater { change("Light") } + SwingUtilities.invokeLater { + if (isDark) { + change(appearance.darkTheme) + } else { + change(appearance.lightTheme) + } } } })