Browse Source

Theme preference support on macOS. (#127)

Theme preference support on macOS.
pull/139/head
Jannis Weis 5 years ago committed by GitHub
parent
commit
6eeb813562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      core/build.gradle.kts
  2. 40
      core/gradle.properties
  3. 24
      core/src/main/java/com/github/weisj/darklaf/LafManager.java
  4. 4
      core/src/main/java/com/github/weisj/darklaf/color/DarkColorModelHSB.java
  5. 10
      core/src/main/java/com/github/weisj/darklaf/platform/ThemePreferencesHandler.java
  6. 133
      core/src/main/java/com/github/weisj/darklaf/task/AccentColorAdjustmentTask.java
  7. 10
      core/src/main/java/com/github/weisj/darklaf/task/ThemeDefaultsInitTask.java
  8. 12
      core/src/main/java/com/github/weisj/darklaf/util/ColorWrapper.java
  9. 112
      core/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java
  10. 2
      core/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java
  11. 1
      core/src/main/resources/com/github/weisj/darklaf/properties/globals.properties
  12. 2
      core/src/main/resources/com/github/weisj/darklaf/properties/platform/mac.properties
  13. 51
      core/src/test/java/PreferenceChangeDemo.java
  14. 13
      core/src/test/java/ui/ComponentDemo.java
  15. 14
      core/src/test/java/ui/DemoPanel.java
  16. 3
      core/src/test/java/ui/button/ButtonDemo.java
  17. 6
      macos/build.gradle.kts
  18. 46
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/JNIDecorationsMacOS.java
  19. 113
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/JNIThemeInfoMacOS.java
  20. 4
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSDecorationsProvider.java
  21. 73
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSLibrary.java
  22. 102
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSPreferenceMonitor.java
  23. 87
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSThemePreferenceProvider.java
  24. 46
      macos/src/main/java/com/github/weisj/darklaf/platform/macos/theme/MacOSColors.java
  25. 14
      macos/src/main/objectiveCpp/Decorations.mm
  26. 184
      macos/src/main/objectiveCpp/ThemeInfo.mm
  27. 1
      property-loader/build.gradle.kts
  28. 125
      property-loader/src/main/java/com/github/weisj/darklaf/PropertyLoader.java
  29. 9
      property-loader/src/main/java/com/github/weisj/darklaf/icons/SolidColorIcon.java
  30. 5
      property-loader/src/main/java/com/github/weisj/darklaf/icons/StateIcon.java
  31. 106
      theme/src/main/java/com/github/weisj/darklaf/theme/Theme.java
  32. 155
      theme/src/main/java/com/github/weisj/darklaf/theme/ThemeDelegate.java
  33. 70
      theme/src/main/java/com/github/weisj/darklaf/theme/info/AccentColorRule.java
  34. 5
      theme/src/main/java/com/github/weisj/darklaf/theme/info/DefaultThemeProvider.java
  35. 38
      theme/src/main/java/com/github/weisj/darklaf/theme/info/PreferredThemeStyle.java
  36. 41
      theme/src/main/resources/com/github/weisj/darklaf/theme/darcula/darcula_accents.properties
  37. 4
      theme/src/main/resources/com/github/weisj/darklaf/theme/darcula/darcula_defaults.properties
  38. 41
      theme/src/main/resources/com/github/weisj/darklaf/theme/intellij/intellij_accents.properties
  39. 5
      theme/src/main/resources/com/github/weisj/darklaf/theme/intellij/intellij_defaults.properties
  40. 2
      windows/libraries/windows-x86-64/library.md
  41. 2
      windows/libraries/windows-x86/library.md
  42. 103
      windows/src/main/cpp/ThemeInfo.cpp
  43. 23
      windows/src/main/java/com/github/weisj/darklaf/platform/windows/JNIThemeInfoWindows.java
  44. 41
      windows/src/main/java/com/github/weisj/darklaf/platform/windows/WindowsPreferenceMonitor.java
  45. 19
      windows/src/main/java/com/github/weisj/darklaf/platform/windows/WindowsThemePreferenceProvider.java

16
core/build.gradle.kts

@ -35,6 +35,22 @@ tasks.jar {
}
}
project.properties
.filter { it.key.startsWith("demoClass") }
.forEach {
val name = it.key.split(".")[1]
rootProject.tasks.register(name, JavaExec::class) {
group = "Demo"
description = "Run the demo '$name'."
dependsOn(tasks.testClasses)
workingDir = File(project.rootDir, "build")
workingDir.mkdirs()
main = it.value?.toString() ?: throw GradleException("Main class for demo ``$name` not specified.")
classpath(sourceSets.main.get().runtimeClasspath, sourceSets.test.get().runtimeClasspath)
}
}
val makeDocumentation by tasks.registering(JavaExec::class) {
group = "Development"
description = "Builds the documentation"

40
core/gradle.properties

@ -1 +1,41 @@
generatePomFile = true
demoClass.UIManagerDefaults = defaults.UIManagerDefaults
demoClass.AllIcons = icon.AllIcons
demoClass.IconDemo = icon.IconDemo
demoClass.RotatableIconDemo = icon.RotatableIconDemo
demoClass.ButtonDemo = ui.button.ButtonDemo
demoClass.GroupedButtonDemo = ui.button.GroupedButtonDemo
demoClass.ToggleButtonDemo = ui.button.ToggleButtonDemo
demoClass.CheckBoxDemo = ui.checkBox.CheckBoxDemo
demoClass.TriCheckBoxDemo = ui.checkBox.TriCheckBoxDemo
demoClass.ColorChooserDemo = ui.colorChooser.ColorChooserDemo
demoClass.ComboBoxDemo = ui.comboBox.ComboBoxDemo
demoClass.DialogDemo = ui.dialog.DialogDemo
demoClass.FileChooserDemo = ui.fileChooser.FileChooserDemo
demoClass.InternalFrameDemo = ui.internalFrame.InternalFrameDemo
demoClass.LabelDemo = ui.label.LabelDemo
demoClass.ListDemo = ui.list.ListDemo
demoClass.PopupMenuDemo = ui.popupMenu.PopupMenuDemo
demoClass.ProgressBarDemo = ui.progressBar.ProgressBarDemo
demoClass.RadioButtonDemo = ui.radioButton.RadioButtonDemo
demoClass.ScrollPaneDemo = ui.scrollPane.ScrollPaneDemo
demoClass.OverlayScrollPaneDemo = ui.scrollPane.OverlayScrollPaneDemo
demoClass.SliderDemo = ui.slider.SliderDemo
demoClass.SpinnerDemo = ui.spinner.SpinnerDemo
demoClass.SplitPaneDemo = ui.splitPane.SplitPaneDemo
demoClass.TabbedPaneDemo = ui.tabbedPane.TabbedPaneDemo
demoClass.TabbedPaneKeyboardShortcutDemo = ui.tabbedPane.TabbedPaneKeyboardShortcut
demoClass.ClosableTabbedPaneDemo = ui.tabbedPane.ClosableTabbedPaneDemo
demoClass.TabFrameDemo = ui.tabFrame.TabFrameDemo
demoClass.TableDemo = ui.table.TableDemo
demoClass.EditorPaneDemo = ui.text.EditorPaneDemo
demoClass.FormattedTextFieldDemo = ui.text.FormattedTextFieldDemo
demoClass.PasswordFieldDemo = ui.text.PasswordFieldDemo
demoClass.TextAreaDemo = ui.text.TextAreaDemo
demoClass.TextFieldDemo = ui.text.TextFieldDemo
demoClass.TextPaneDemo = ui.text.TextPaneDemo
demoClass.ToolBarDemo = ui.toolBar.ToolBarDemo
demoClass.ToolTipDemo = ui.toolTip.ToolTipDemo
demoClass.TreeDemo = ui.tree.TreeDemo
demoClass.PreferenceChangeDemo = PreferenceChangeDemo

24
core/src/main/java/com/github/weisj/darklaf/LafManager.java

@ -27,6 +27,7 @@ import com.github.weisj.darklaf.platform.DecorationsHandler;
import com.github.weisj.darklaf.platform.ThemePreferencesHandler;
import com.github.weisj.darklaf.task.DefaultsAdjustmentTask;
import com.github.weisj.darklaf.theme.*;
import com.github.weisj.darklaf.theme.event.ThemePreferenceChangeEvent;
import com.github.weisj.darklaf.theme.event.ThemePreferenceListener;
import com.github.weisj.darklaf.theme.info.DefaultThemeProvider;
import com.github.weisj.darklaf.theme.info.PreferredThemeStyle;
@ -36,7 +37,6 @@ import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.*;
import java.util.logging.Level;
@ -119,6 +119,9 @@ public final class LafManager {
/**
* Enabled whether changes in the preferred theme style should be reported to {@link ThemePreferenceListener}s. On
* some platforms this setting may do nothing.
* <p>
* Warning: If preference reporting is enabled it <b>needs</b> to be disabled before closing the program. Not doing
* so can result in memory leaks and prevent the classloader from being garbage collected.
*
* @param enabled true if changes should be reported.
*/
@ -278,6 +281,16 @@ public final class LafManager {
LafManager.theme = theme;
}
/**
* Install the theme reported by the {@link ThemePreferenceChangeEvent}.
*
* @param changeEvent the change event.
*/
public static void installTheme(final ThemePreferenceChangeEvent changeEvent) {
setTheme(changeEvent.getPreferredThemeStyle());
install();
}
/**
* Install the theme suited for the given preferred theme style.
*
@ -313,14 +326,7 @@ public final class LafManager {
* Reloads the theme. Forces all properties to be reloaded.
*/
public static void reloadTheme() {
try {
setTheme(getTheme().getClass().getDeclaredConstructor().newInstance());
} catch (InstantiationException
| IllegalAccessException
| NoSuchMethodException
| InvocationTargetException e) {
e.printStackTrace();
}
setTheme(getTheme().copy());
}
/**

4
core/src/main/java/com/github/weisj/darklaf/color/DarkColorModelHSB.java

@ -88,7 +88,9 @@ public class DarkColorModelHSB extends DarkColorModel {
}
public static Color getColorFromHSBValues(final double h, final double s, final double b) {
return Color.getHSBColor((float) h, (float) s, (float) b);
return Color.getHSBColor((float) Math.max(Math.min(h, 1), 0),
(float) Math.max(Math.min(s, 1), 0),
(float) Math.max(Math.min(b, 1), 0));
}
@Override

10
core/src/main/java/com/github/weisj/darklaf/platform/ThemePreferencesHandler.java

@ -24,6 +24,7 @@
package com.github.weisj.darklaf.platform;
import com.github.weisj.darklaf.DarkLaf;
import com.github.weisj.darklaf.platform.macos.MacOSThemePreferenceProvider;
import com.github.weisj.darklaf.platform.windows.WindowsThemePreferenceProvider;
import com.github.weisj.darklaf.theme.event.ThemePreferenceChangeEvent;
import com.github.weisj.darklaf.theme.event.ThemePreferenceChangeSupport;
@ -33,6 +34,8 @@ import com.github.weisj.darklaf.theme.info.ThemePreferenceProvider;
import com.github.weisj.darklaf.util.PropertyValue;
import com.github.weisj.darklaf.util.SystemInfo;
import javax.swing.*;
public class ThemePreferencesHandler {
public static final String PREFERENCE_REPORTING_FLAG = DarkLaf.SYSTEM_PROPERTY_PREFIX + "enableNativePreferences";
@ -56,13 +59,14 @@ public class ThemePreferencesHandler {
// Extend for different platforms.
boolean enableNativePreferences = isNativePreferencesEnabled();
if (SystemInfo.isWindows10 && enableNativePreferences) {
// Decorations are in the Windows10 visuals. Disable for older version.
preferenceProvider = new WindowsThemePreferenceProvider();
} else if (SystemInfo.isMac && enableNativePreferences) {
preferenceProvider = new MacOSThemePreferenceProvider();
} else {
preferenceProvider = new DefaultThemePreferenceProvider();
}
} catch (Throwable e) {
// If decorations modules are not available disable them.
// If native modules are not available disable them.
preferenceProvider = new DefaultThemePreferenceProvider();
}
preferenceProvider.initialize();
@ -70,7 +74,9 @@ public class ThemePreferencesHandler {
}
private void onChange(final PreferredThemeStyle style) {
SwingUtilities.invokeLater(() -> {
changeSupport.notifyPreferenceChange(new ThemePreferenceChangeEvent(style));
});
}
public void addThemePreferenceChangeListener(final ThemePreferenceListener listener) {

133
core/src/main/java/com/github/weisj/darklaf/task/AccentColorAdjustmentTask.java

@ -0,0 +1,133 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.task;
import com.github.weisj.darklaf.PropertyLoader;
import com.github.weisj.darklaf.color.DarkColorModelHSB;
import com.github.weisj.darklaf.icons.IconLoader;
import com.github.weisj.darklaf.theme.Theme;
import com.github.weisj.darklaf.util.Pair;
import javax.swing.*;
import javax.swing.plaf.ColorUIResource;
import java.awt.*;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;
public class AccentColorAdjustmentTask implements DefaultsAdjustmentTask {
private static final Logger LOGGER = Logger.getLogger(AccentColorAdjustmentTask.class.getName());
private static final String MAIN_ACCENT_LIST_KEY = "mainAccent.propertyList";
private static final String SELECTION_ACCENT_LIST_KEY = "selectionAccent.propertyList";
private static final UIDefaults DEFAULTS = new UIDefaults();
@Override
public void run(final Theme currentTheme, final Properties properties) {
Properties props = currentTheme.loadPropertyFile("accents", true);
Color accentColor = currentTheme.getAccentColorRule().getAccentColor();
Color selectionColor = currentTheme.getAccentColorRule().getSelectionColor();
adjust(MAIN_ACCENT_LIST_KEY, accentColor, props, properties);
adjust(SELECTION_ACCENT_LIST_KEY, selectionColor, props, properties);
}
private void adjust(final String listKey, final Color c,
final Properties listProperties, final Properties properties) {
if (c == null) return;
Object obj = PropertyLoader.parseValue(listKey,
listProperties.getProperty(listKey),
listProperties, DEFAULTS, IconLoader.get());
DEFAULTS.clear();
if (obj instanceof List<?>) {
double[] hsb = DarkColorModelHSB.RGBtoHSBValues(c.getRed(), c.getGreen(), c.getBlue());
adjustList((List<?>) obj, hsb, properties);
}
}
private void adjustList(final List<?> list, final double[] hsb,
final Properties properties) {
ColorInfo info = new ColorInfo();
for (Object o : list) {
info = getColorInfo(o, info);
if (info.key == null) continue;
Object c = mapColor(info, hsb, properties);
if (c instanceof Color) {
properties.put(info.key, c);
} else {
LOGGER.warning("Color with key '" + info.key
+ "' could not be adjusted because the value '" + c + "' is not a color");
}
}
}
private ColorInfo getColorInfo(final Object o, final ColorInfo info) {
info.set(null, 0, 0, 0);
if (o instanceof String) {
info.set(o.toString(), 100, 100, 100);
return info;
}
if (o instanceof Pair<?, ?>) {
Object first = ((Pair<?, ?>) o).getFirst();
Object second = ((Pair<?, ?>) o).getSecond();
if (!(first instanceof String)) return null;
if (!(second instanceof List<?>)) return null;
String key = first.toString();
List<?> list = (List<?>) second;
if (list.size() != 3
|| !(list.get(0) instanceof Integer)
|| !(list.get(1) instanceof Integer)
|| !(list.get(2) instanceof Integer)) {
return info;
}
info.set(key, (Integer) list.get(0), (Integer) list.get(1), (Integer) list.get(2));
return info;
}
return info;
}
private Object mapColor(final ColorInfo info, final double[] hsbMatch, final Properties properties) {
Object obj = properties.get(info.key);
if (obj instanceof Color) {
Color color = DarkColorModelHSB.getColorFromHSBValues(hsbMatch[0] * (info.hAdj / 100.0),
hsbMatch[1] * (info.sAdj / 100.0),
hsbMatch[2] * (info.bAdj / 100.0));
return new ColorUIResource(color);
}
return obj;
}
private static class ColorInfo {
private String key;
private int hAdj;
private int sAdj;
private int bAdj;
private void set(final String key, final int hAdj, final int sAdj, final int bAdj) {
this.key = key;
this.hAdj = hAdj;
this.sAdj = sAdj;
this.bAdj = bAdj;
}
}
}

10
core/src/main/java/com/github/weisj/darklaf/task/ThemeDefaultsInitTask.java

@ -48,7 +48,8 @@ public class ThemeDefaultsInitTask implements DefaultsInitTask {
private static final String[] ICON_PROPERTIES = new String[]{
"control", "dialog", "files", "frame", "indicator", "menu", "misc", "navigation"
};
private final DefaultsAdjustmentTask userPreferenceInitTask = new UserPreferenceAdjustmentTask();
private final DefaultsAdjustmentTask userPreferenceAdjustment = new UserPreferenceAdjustmentTask();
private final DefaultsAdjustmentTask accentColorAdjustment = new AccentColorAdjustmentTask();
@Override
public void run(final Theme currentTheme, final UIDefaults defaults) {
@ -63,7 +64,12 @@ public class ThemeDefaultsInitTask implements DefaultsInitTask {
* User preferences need to be applied here so changes are applied to all
* components that use the property.
*/
userPreferenceInitTask.run(currentTheme, uiProps);
userPreferenceAdjustment.run(currentTheme, uiProps);
/*
* Adjust the accent/selection colors.
*/
accentColorAdjustment.run(currentTheme, uiProps);
initGlobals(currentTheme, defaults, uiProps);
initUIProperties(currentTheme, defaults, uiProps);

12
core/src/main/java/com/github/weisj/darklaf/color/ColorWrapper.java → core/src/main/java/com/github/weisj/darklaf/util/ColorWrapper.java

@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.color;
package com.github.weisj.darklaf.util;
import java.awt.*;
import java.awt.color.ColorSpace;
@ -75,12 +75,14 @@ public class ColorWrapper extends Color {
return color.getRGB();
}
public ColorWrapper brighter() {
return new ColorWrapper(color.brighter());
@Override
public Color brighter() {
return color.brighter();
}
public ColorWrapper darker() {
return new ColorWrapper(color.darker());
@Override
public Color darker() {
return color.darker();
}
@Override

112
core/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java

@ -23,7 +23,6 @@
*/
package com.github.weisj.darklaf.util;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
@ -59,11 +58,33 @@ public final class ImageUtil {
* @param bounds the bounds inside the component to capture.
* @return image containing the captured area.
*/
public static BufferedImage imageFromComponent(final Component c, final Rectangle bounds) {
return scaledImageFromComponent(c, bounds, 1.0, 1.0, true);
}
public static Image scaledImageFromComponent(final Component c, final Rectangle bounds) {
return scaledImageFromComponent(c, bounds, Scale.SCALE_X, Scale.SCALE_Y);
/**
* Create image from component.
*
* @param c the component.
* @param bounds the bounds inside the component to capture.
* @return image containing the captured area.
*/
public static BufferedImage imageFromComponent(final Component c, final Rectangle bounds, final boolean print) {
return scaledImageFromComponent(c, bounds, 1.0, 1.0, print);
}
/**
* Create image from component.
*
* @param c the component.
* @param bounds the bounds inside the component to capture.
* @return image containing the captured area.
*/
public static BufferedImage scaledImageFromComponent(final Component c, final Rectangle bounds) {
return scaledImageFromComponent(c, bounds, Scale.SCALE_X, Scale.SCALE_Y, true);
}
/**
* Create image from component.
*
@ -73,9 +94,9 @@ public final class ImageUtil {
* @param scaley the y scale
* @return image containing the captured area.
*/
public static Image scaledImageFromComponent(final Component c, final Rectangle bounds,
final double scalex, final double scaley) {
public static BufferedImage scaledImageFromComponent(final Component c, final Rectangle bounds,
final double scalex, final double scaley,
final boolean print) {
BufferedImage image;
boolean scale = scalex != 1.0 || scaley != 1.0;
if (scale) {
@ -89,87 +110,16 @@ public final class ImageUtil {
g2d.scale(scalex, scaley);
}
g2d.translate(-bounds.x, -bounds.y);
if (print) {
c.printAll(g2d);
g2d.dispose();
return image;
}
/**
* Create image from component.
*
* @param c the component.
* @param bounds the bounds inside the component to capture.
* @return image containing the captured area.
*/
public static Image imageFromComponent(final Component c, final Rectangle bounds) {
return scaledImageFromComponent(c, bounds, 1.0, 1.0);
}
public static Icon cropIcon(final Icon icon, int maxWidth, int maxHeight) {
if (icon.getIconHeight() <= maxHeight && icon.getIconWidth() <= maxWidth) {
return icon;
}
Image image = toImage(icon);
if (image == null) return icon;
double scale = 1f;
BufferedImage bi = ImageUtil.toBufferedImage(image);
final Graphics2D g = bi.createGraphics();
int imageWidth = image.getWidth(null);
int imageHeight = image.getHeight(null);
maxWidth = maxWidth == Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.round(maxWidth * scale);
maxHeight = maxHeight == Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.round(maxHeight * scale);
final int w = Math.min(imageWidth, maxWidth);
final int h = Math.min(imageHeight, maxHeight);
final BufferedImage img = new BufferedImage(w, h, Transparency.TRANSLUCENT);
final int offX = imageWidth > maxWidth ? (imageWidth - maxWidth) / 2 : 0;
final int offY = imageHeight > maxHeight ? (imageHeight - maxHeight) / 2 : 0;
for (int col = 0; col < w; col++) {
for (int row = 0; row < h; row++) {
img.setRGB(col, row, bi.getRGB(col + offX, row + offY));
}
}
g.dispose();
return new ImageIcon(img);
}
public static Image toImage(final Icon icon) {
if (icon instanceof ImageIcon) {
return ((ImageIcon) icon).getImage();
} else {
BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(),
BufferedImage.TYPE_INT_RGB);
icon.paintIcon(null, image.getGraphics(), 0, 0);
return image;
}
c.paintAll(g2d);
}
public static BufferedImage toBufferedImage(final Image image) {
if (image == null) {
throw new NullPointerException("Can't covert null image");
}
if (image instanceof BufferedImage) {
return (BufferedImage) image;
} else {
BufferedImage bufferedImage = new BufferedImage(image.getWidth(null),
image.getHeight(null),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bufferedImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return bufferedImage;
}
g2d.dispose();
return image;
}
public static BufferedImage createCompatibleTranslucentImage(final int width,
final int height) {
return isHeadless() ?

2
core/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java

@ -23,9 +23,9 @@
*/
package javax.swing.text.DefaultHighlighterDark;
import com.github.weisj.darklaf.color.ColorWrapper;
import com.github.weisj.darklaf.ui.text.DarkTextUI;
import com.github.weisj.darklaf.ui.text.StyleConstantsEx;
import com.github.weisj.darklaf.util.ColorWrapper;
import com.github.weisj.darklaf.util.GraphicsContext;
import com.github.weisj.darklaf.util.GraphicsUtil;
import sun.swing.SwingUtilities2;

1
core/src/main/resources/com/github/weisj/darklaf/properties/globals.properties

@ -31,6 +31,7 @@ global.textForeground = %textForeground
global.foreground = %textForeground
global.inactiveForeground = %textForegroundInactive
global.selectionInactiveForeground = %textForegroundInactive
global.inactiveForeground = %textForegroundInactive
global.disabledText = %textForegroundInactive
global.selectionForeground = %textSelectionForeground
global.selectionForegroundInactive = %textSelectionForegroundInactive

2
core/src/main/resources/com/github/weisj/darklaf/properties/platform/mac.properties

@ -18,7 +18,5 @@ Table.alternateRowColor = true
Tree.alternateRowColor = true
List.alternateRowColor = true
FileChooser.listViewWindowsStyle = false
CheckBox.borderInsets = 2,2,2,2
RadioButton.borderInsets = 2,2,2,2
PopupMenu.defaultLightWeightPopups = false
ToolTip.paintShadow = false

51
core/src/test/java/PreferenceChangeDemo.java

@ -1,9 +1,14 @@
import com.github.weisj.darklaf.LafManager;
import com.github.weisj.darklaf.icons.SolidColorIcon;
import com.github.weisj.darklaf.theme.Theme;
import ui.ComponentDemo;
import ui.DemoPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
/*
* MIT License
@ -36,15 +41,53 @@ public class PreferenceChangeDemo implements ComponentDemo {
@Override
public JComponent createComponent() {
LafManager.addThemePreferenceChangeListener(e -> LafManager.installTheme(e.getPreferredThemeStyle()));
return new JPanel(new GridBagLayout()) {{
add(new JToggleButton("Start") {{
LafManager.addThemePreferenceChangeListener(LafManager::installTheme);
DemoPanel panel = new DemoPanel(new JToggleButton("Start") {{
addActionListener(e -> {
setText(isSelected() ? "Stop" : "Start");
LafManager.enabledPreferenceChangeReporting(isSelected());
});
}});
}};
Icon accentColorIcon = new SolidColorIcon() {
@Override
public Color getColor() {
return LafManager.getTheme().getAccentColorRule().getAccentColor();
}
};
Icon selectionColorIcon = new SolidColorIcon() {
@Override
public Color getColor() {
return LafManager.getTheme().getAccentColorRule().getSelectionColor();
}
};
JPanel controlPanel = panel.addControls();
controlPanel.add(new JLabel("Accent Color", accentColorIcon, JLabel.LEFT));
controlPanel.add(new JLabel("Selection Color", selectionColorIcon, JLabel.LEFT));
controlPanel = panel.addControls();
controlPanel.add(new JTextArea() {{
setMargin(new Insets(5, 5, 5, 5));
setEditable(false);
setText("Press start/stop to enable/disable preference monitoring.\n"
+ "Then do one of the following\n"
+ " - switch between dark/light theme (Windows/macOS)\n"
+ " - toggle high contrast mode (Windows/macOS)\n"
+ " - change accent color (Windows/macOS)\n"
+ " - change selection color (macOS)\n"
+ " - change font scaling (Windows)\n"
+ "The theme should then adjust automatically (if monitoring is started).\n");
}});
return panel;
}
@Override
public WindowListener createWindowListener() {
return new WindowAdapter() {
@Override
public void windowClosing(final WindowEvent e) {
LafManager.enabledPreferenceChangeReporting(false);
}
};
}
@Override

13
core/src/test/java/ui/ComponentDemo.java

@ -32,6 +32,8 @@ import com.github.weisj.darklaf.theme.info.PreferredThemeStyle;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowListener;
public interface ComponentDemo {
@ -50,7 +52,8 @@ public interface ComponentDemo {
SwingUtilities.invokeLater(() -> {
LafManager.install(demo.createTheme());
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.addWindowListener(demo.createWindowListener());
frame.setTitle(demo.getTitle());
frame.setContentPane(demo.createComponent());
frame.setJMenuBar(demo.createMenuBar());
@ -71,6 +74,11 @@ public interface ComponentDemo {
});
}
default WindowListener createWindowListener() {
return new WindowAdapter() {
};
}
static JMenu createThemeMenu() {
String currentThemeName = LafManager.getTheme().getClass().getSimpleName();
JMenu menu = new JMenu("Theme");
@ -87,7 +95,8 @@ public interface ComponentDemo {
final Action action = new AbstractAction(name) {
@Override
public void actionPerformed(final ActionEvent e) {
LafManager.install(theme);
Theme current = LafManager.getTheme();
LafManager.install(theme.derive(current.getFontSizeRule(), current.getAccentColorRule()));
}
};
final JRadioButtonMenuItem mi = new JRadioButtonMenuItem(action);

14
core/src/test/java/ui/DemoPanel.java

@ -60,16 +60,20 @@ public class DemoPanel extends JPanel {
add(controls, BorderLayout.SOUTH);
}
public JPanel addControls(final int columns) {
public JPanel addControls(final LayoutManager layoutManager) {
JPanel control = new JPanel();
control.setLayout(layoutManager);
control.setBorder(DarkBorders.createLineBorder(1, 0, 0, 0));
controls.add(control);
return control;
}
public JPanel addControls(final int columns) {
String constraints = "fillx";
if (columns > 0) {
constraints += ", wrap" + columns;
}
control.setLayout(new MigLayout(constraints, "[][grow]"));
control.setBorder(DarkBorders.createLineBorder(1, 0, 0, 0));
controls.add(control);
return control;
return addControls(new MigLayout(constraints, "[][grow]"));
}
public JPanel addControls() {

3
core/src/test/java/ui/button/ButtonDemo.java

@ -55,8 +55,7 @@ public class ButtonDemo implements ComponentDemo {
}});
controlPanel.add(new JCheckBox("default") {{
setSelected(button.isDefaultButton());
addActionListener(e -> SwingUtilities.getRootPane(button).setDefaultButton(isSelected() ? button
: null));
addActionListener(e -> SwingUtilities.getRootPane(button).setDefaultButton(isSelected() ? button : null));
}});
controlPanel.add(new JCheckBox("LeftToRight") {{
setSelected(button.getComponentOrientation().isLeftToRight());

6
macos/build.gradle.kts

@ -8,6 +8,7 @@ fun DependencyHandlerScope.javaImplementation(dep: Any) {
}
dependencies {
javaImplementation(project(":darklaf-theme"))
javaImplementation(project(":darklaf-native-utils"))
javaImplementation(project(":darklaf-utils"))
javaImplementation(project(":darklaf-platform-base"))
@ -34,7 +35,10 @@ library {
compilerArgs.addAll("-x", "objective-c++")
compilerArgs.addAll("-mmacosx-version-min=10.10")
compilerArgs.addJavaFrameworks()
source.from(file("src/main/objectiveCpp/JNIDecorations.mm"))
source.from(
file("src/main/objectiveCpp/Decorations.mm"),
file("src/main/objectiveCpp/ThemeInfo.mm")
)
}
}
binaries.whenElementFinalized(CppSharedLibrary::class) {

46
macos/src/main/java/com/github/weisj/darklaf/platform/macos/JNIDecorationsMacOS.java

@ -23,19 +23,10 @@
*/
package com.github.weisj.darklaf.platform.macos;
import com.github.weisj.darklaf.platform.NativeUtil;
import com.github.weisj.darklaf.util.SystemInfo;
import java.awt.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JNIDecorationsMacOS {
private static final Logger LOGGER = Logger.getLogger(JNIDecorationsMacOS.class.getName());
private static boolean loaded;
private static boolean attemptedLoad;
public static native long getComponentPointer(final Window window);
public static native void retainWindow(final long hwnd);
@ -55,41 +46,4 @@ public class JNIDecorationsMacOS {
public static native boolean isFullscreen(final long hwnd);
public static native double getTitleFontSize(final long hwnd);
/**
* Load the decorations-library if necessary.
*/
public static void updateLibrary() {
if (!loaded && !attemptedLoad) {
loadLibrary();
}
}
private static void loadLibrary() {
attemptedLoad = true;
if (!SystemInfo.isMac || loaded) {
return;
}
try {
if (SystemInfo.isX64) {
NativeUtil.loadLibraryFromJar(
"/com/github/weisj/darklaf/platform/darklaf-macos/macos-x86-64/libdarklaf-macos.dylib");
loaded = true;
LOGGER.info("Loaded libdarklaf-macos.dylib. Decorations are enabled.");
} else {
LOGGER.warning("JRE model '"
+ SystemInfo.jreArchitecture
+ "' not supported. Decorations will be disabled");
}
} catch (Throwable e) {
//Library not found, SecurityManager prevents library loading etc.
LOGGER.log(Level.SEVERE, "Could not load decorations library libdarklaf-macos.dylib." +
" Decorations will be disabled", e);
}
}
public static boolean isCustomDecorationSupported() {
return loaded;
}
}

113
macos/src/main/java/com/github/weisj/darklaf/platform/macos/JNIThemeInfoMacOS.java

@ -0,0 +1,113 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.platform.macos;
import com.github.weisj.darklaf.platform.macos.theme.MacOSColors;
import java.awt.*;
public class JNIThemeInfoMacOS {
/**
* Returns whether dark mode is enabled.
*
* @return true if dark mode is enabled.
*/
public static native boolean isDarkThemeEnabled();
/**
* Returns whether high contrast mode is enabled.
*
* @return true if high contrast mode is enabled.
*/
public static native boolean isHighContrastEnabled();
/*
* Get the index of the accent color.
*/
private static native int nativeGetAccentColor();
/**
* Returns the current accent color.
*
* @return the accent color.
*/
public static Color getAccentColor() {
int index = nativeGetAccentColor();
switch (index) {
case -2:
return MacOSColors.BLUE;
case -1:
return MacOSColors.GRAY;
case 0:
return MacOSColors.RED;
case 1:
return MacOSColors.ORANGE;
case 2:
return MacOSColors.YELLOW;
case 3:
return MacOSColors.GREEN;
case 5:
return MacOSColors.LILAC;
case 6:
return MacOSColors.ROSE;
default:
return null;
}
}
/*
* Returns the selection color as an AARRGGBB integer.
*/
private static native int nativeGetSelectionColor();
/**
* Returns the current selection color.
*
* @return the current selection color.
*/
public static Color getSelectionColor() {
int rgba = nativeGetSelectionColor();
// If rgba == 0 then it has an alpha channel != 255, so it is invalid.
if (rgba == 0) return null;
return new Color(rgba);
}
/**
* Create an preference change listener.
*
* @param callback the event callback.
* @return the pointer to the listener.
*/
public static native long createPreferenceChangeListener(final Runnable callback);
/**
* Delete the preference change listener.
*
* @param listenerPtr pointer to the listener.
*/
public static native void deletePreferenceChangeListener(final long listenerPtr);
}

4
macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSDecorationsProvider.java

@ -42,12 +42,12 @@ public class MacOSDecorationsProvider implements DecorationsProvider {
@Override
public boolean isCustomDecorationSupported() {
return JNIDecorationsMacOS.isCustomDecorationSupported();
return MacOSLibrary.isLoaded();
}
@Override
public void initialize() {
JNIDecorationsMacOS.updateLibrary();
MacOSLibrary.updateLibrary();
}
@Override

73
macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSLibrary.java

@ -0,0 +1,73 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.platform.macos;
import com.github.weisj.darklaf.platform.NativeUtil;
import com.github.weisj.darklaf.util.SystemInfo;
import java.util.logging.Level;
import java.util.logging.Logger;
public class MacOSLibrary {
private static final Logger LOGGER = Logger.getLogger(MacOSLibrary.class.getName());
private static boolean loaded;
private static boolean attemptedLoad;
/**
* Load the decorations-library if necessary.
*/
public static void updateLibrary() {
if (!loaded && !attemptedLoad) {
loadLibrary();
}
}
private static void loadLibrary() {
attemptedLoad = true;
if (!SystemInfo.isMac || loaded) {
return;
}
try {
if (SystemInfo.isX64) {
NativeUtil.loadLibraryFromJar(
"/com/github/weisj/darklaf/platform/darklaf-macos/macos-x86-64/libdarklaf-macos.dylib");
loaded = true;
LOGGER.info("Loaded libdarklaf-macos.dylib. Native features are enabled.");
} else {
LOGGER.warning("JRE model '"
+ SystemInfo.jreArchitecture
+ "' not supported. Native features will be disabled");
}
} catch (Throwable e) {
//Library not found, SecurityManager prevents library loading etc.
LOGGER.log(Level.SEVERE, "Could not load decorations library libdarklaf-macos.dylib." +
" Native features will be disabled", e);
}
}
public static boolean isLoaded() {
return loaded;
}
}

102
macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSPreferenceMonitor.java

@ -0,0 +1,102 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.platform.macos;
import java.awt.*;
import java.util.Objects;
import java.util.logging.Logger;
public class MacOSPreferenceMonitor {
private static final Logger LOGGER = Logger.getLogger(MacOSThemePreferenceProvider.class.getName());
private final MacOSThemePreferenceProvider preferenceProvider;
private boolean darkMode;
private boolean highContrast;
private Color accentColor;
private Color selectionColor;
private long listenerHandle;
private boolean running;
public MacOSPreferenceMonitor(final MacOSThemePreferenceProvider preferenceProvider) {
this.preferenceProvider = preferenceProvider;
}
private void onNotification() {
boolean newDark = JNIThemeInfoMacOS.isDarkThemeEnabled();
boolean newHighContrast = JNIThemeInfoMacOS.isHighContrastEnabled();
Color newAccentColor = JNIThemeInfoMacOS.getAccentColor();
Color newSelectionColor = JNIThemeInfoMacOS.getSelectionColor();
LOGGER.info("Event received:");
LOGGER.info(" Dark Mode: [old: " + darkMode + " new: " + newDark + "]");
LOGGER.info(" High Contrast: [old: " + highContrast + " new: " + newHighContrast + "]");
LOGGER.info(" Accent Color: [old: " + accentColor + " new: " + newAccentColor + "]");
LOGGER.info(" Selection Color: [old: " + selectionColor + " new: " + newSelectionColor + "]");
if (darkMode != newDark
|| highContrast != newHighContrast
|| !Objects.equals(accentColor, newAccentColor)
|| !Objects.equals(selectionColor, newSelectionColor)) {
darkMode = newDark;
accentColor = newAccentColor;
selectionColor = newSelectionColor;
highContrast = newHighContrast;
preferenceProvider.reportPreferenceChange(highContrast, darkMode, accentColor, selectionColor);
}
}
private void start() {
darkMode = JNIThemeInfoMacOS.isDarkThemeEnabled();
highContrast = JNIThemeInfoMacOS.isHighContrastEnabled();
accentColor = JNIThemeInfoMacOS.getAccentColor();
selectionColor = JNIThemeInfoMacOS.getSelectionColor();
listenerHandle = JNIThemeInfoMacOS.createPreferenceChangeListener(this::onNotification);
if (listenerHandle == 0) {
LOGGER.severe("Could not create notification listener. Monitoring will not be started");
return;
}
running = true;
LOGGER.info("Started preference monitoring.");
}
private void stop() {
if (!running) return;
running = false;
LOGGER.info("Stopped preference monitoring.");
JNIThemeInfoMacOS.deletePreferenceChangeListener(listenerHandle);
}
public void setRunning(final boolean running) {
if (running == isRunning()) return;
if (running) {
start();
} else {
stop();
}
}
public boolean isRunning() {
return running;
}
}

87
macos/src/main/java/com/github/weisj/darklaf/platform/macos/MacOSThemePreferenceProvider.java

@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.platform.macos;
import com.github.weisj.darklaf.theme.info.*;
import java.awt.*;
import java.util.function.Consumer;
public class MacOSThemePreferenceProvider implements ThemePreferenceProvider {
private final PreferredThemeStyle fallbackStyle = new PreferredThemeStyle(ContrastRule.STANDARD,
ColorToneRule.LIGHT);
private final MacOSPreferenceMonitor monitor = new MacOSPreferenceMonitor(this);
private Consumer<PreferredThemeStyle> callback;
@Override
public PreferredThemeStyle getPreference() {
if (!MacOSLibrary.isLoaded()) return fallbackStyle;
boolean darkMode = JNIThemeInfoMacOS.isDarkThemeEnabled();
boolean highContrast = JNIThemeInfoMacOS.isHighContrastEnabled();
Color accentColor = JNIThemeInfoMacOS.getAccentColor();
Color selectionColor = JNIThemeInfoMacOS.getSelectionColor();
return create(highContrast, darkMode, accentColor, selectionColor);
}
private PreferredThemeStyle create(final boolean highContrast, final boolean darkMode,
final Color accentColor, final Color selectionColor) {
ContrastRule contrastRule = highContrast ? ContrastRule.HIGH_CONTRAST : ContrastRule.STANDARD;
ColorToneRule toneRule = darkMode ? ColorToneRule.DARK : ColorToneRule.LIGHT;
AccentColorRule accentColorRule = AccentColorRule.fromColor(accentColor, selectionColor);
return new PreferredThemeStyle(contrastRule, toneRule, accentColorRule);
}
void reportPreferenceChange(final boolean dark, final boolean highContrast,
final Color accentColor, final Color selectionColor) {
if (callback != null) {
PreferredThemeStyle style = create(dark, highContrast, accentColor, selectionColor);
callback.accept(style);
}
}
@Override
public void setReporting(final boolean reporting) {
if (reporting && !MacOSLibrary.isLoaded()) MacOSLibrary.updateLibrary();
synchronized (monitor) {
monitor.setRunning(reporting);
}
}
@Override
public boolean isReporting() {
return monitor.isRunning();
}
@Override
public void initialize() {
MacOSLibrary.updateLibrary();
}
@Override
public void setCallback(final Consumer<PreferredThemeStyle> callback) {
this.callback = callback;
}
}

46
macos/src/main/java/com/github/weisj/darklaf/platform/macos/theme/MacOSColors.java

@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.platform.macos.theme;
import java.awt.*;
public class MacOSColors {
// 0.000000 0.478431 1.000000
public static final Color BLUE = new Color(0, 122, 255);
// 0.584314 0.239216 0.588235
public static final Color LILAC = new Color(149, 61, 149);
// 0.968627 0.309804 0.619608
public static final Color ROSE = new Color(247, 79, 158);
// 0.878431 0.219608 0.243137
public static final Color RED = new Color(224, 56, 62);
// 0.968627 0.509804 0.105882
public static final Color ORANGE = new Color(247, 130, 27);
// 0.988235 0.721569 0.152941
public static final Color YELLOW = new Color(252, 184, 39);
// 0.384314 0.729412 0.27451
public static final Color GREEN = new Color(98, 186, 70);
// 0.596078 0.596078 0.596078
public static final Color GRAY = new Color(152, 152, 152);
}

14
macos/src/main/objectiveCpp/JNIDecorations.mm → macos/src/main/objectiveCpp/Decorations.mm

@ -87,7 +87,7 @@ JNF_COCOA_ENTER(env);
NSWindow *nsWindow = OBJC(hwnd);
return (jboolean)(([nsWindow styleMask] & NSWindowStyleMaskFullScreen) != 0);
JNF_COCOA_EXIT(env);
return false;
return NO;
}
JNIEXPORT jdouble JNICALL
@ -109,7 +109,7 @@ JNF_COCOA_ENTER(env);
} else {
nsWindow.titleVisibility = NSWindowTitleHidden;
}
[nsWindow contentView].needsDisplay = true;
[nsWindow contentView].needsDisplay = TRUE;
}];
JNF_COCOA_EXIT(env);
}
@ -126,7 +126,7 @@ JNF_COCOA_ENTER(env);
} else {
nsWindow.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
}
[nsWindow contentView].needsDisplay = true;
[nsWindow contentView].needsDisplay = YES;
}];
}
JNF_COCOA_EXIT(env);
@ -138,8 +138,8 @@ JNF_COCOA_ENTER(env);
NSWindow *nsWindow = OBJC(hwnd);
[JNFRunLoop performOnMainThreadWaiting:YES withBlock:^{
nsWindow.styleMask |= NSWindowStyleMaskFullSizeContentView;
nsWindow.titlebarAppearsTransparent = true;
[nsWindow contentView].needsDisplay = true;
nsWindow.titlebarAppearsTransparent = YES;
[nsWindow contentView].needsDisplay = YES;
}];
JNF_COCOA_EXIT(env);
}
@ -150,8 +150,8 @@ JNF_COCOA_ENTER(env);
NSWindow *nsWindow = OBJC(hwnd);
[JNFRunLoop performOnMainThreadWaiting:YES withBlock:^{
nsWindow.styleMask &= ~NSWindowStyleMaskFullSizeContentView;
nsWindow.titlebarAppearsTransparent = false;
[nsWindow contentView].needsDisplay = true;
nsWindow.titlebarAppearsTransparent = NO;
[nsWindow contentView].needsDisplay = YES;
}];
JNF_COCOA_EXIT(env);
}

184
macos/src/main/objectiveCpp/ThemeInfo.mm

@ -0,0 +1,184 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#import "com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS.h"
#import <JavaNativeFoundation/JavaNativeFoundation.h>
#import <AppKit/AppKit.h>
#define OBJC(jl) ((id)jlong_to_ptr(jl))
#define KEY_APPLE_INTERFACE_STYLE @"AppleInterfaceStyle"
#define KEY_SWITCHES_AUTOMATICALLY @"AppleInterfaceStyleSwitchesAutomatically"
#define KEY_ACCENT_COLOR @"AppleAccentColor"
#define KEY_SELECTION_COLOR @"selectedTextBackgroundColor"
#define KEY_SYSTEM_COLOR_LIST @"System"
#define EVENT_ACCENT_COLOR @"AppleColorPreferencesChangedNotification"
#define EVENT_AQUA_CHANGE @"AppleAquaColorVariantChanged"
#define EVENT_THEME_CHANGE @"AppleInterfaceThemeChangedNotification"
#define EVENT_HIGH_CONTRAST @"AXInterfaceIncreaseContrastStatusDidChange"
#define EVENT_COLOR_CHANGE NSSystemColorsDidChangeNotification
#define VALUE_DARK @"Dark"
#define VALUE_DEFAULT_ACCENT_COLOR (-2)
#define VALUE_NO_ACCENT_COLOR (-100)
#define VALUE_NO_SELECTION_COLOR (-1)
@interface PreferenceChangeListener:NSObject {
@public JavaVM *jvm;
@public jobject callback;
}
@end
@implementation PreferenceChangeListener
- (id)initWithJVM:(JavaVM *)jvm_ andCallBack:(jobject)callback_ {
self = [super init];
self->jvm = jvm_;
self->callback = callback_;
NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];
[self listenToKey:EVENT_ACCENT_COLOR onCenter:center];
[self listenToKey:EVENT_AQUA_CHANGE onCenter:center];
[self listenToKey:EVENT_THEME_CHANGE onCenter:center];
[self listenToKey:EVENT_HIGH_CONTRAST onCenter:center];
[self listenToKey:EVENT_COLOR_CHANGE onCenter:center];
return self;
}
- (void)dealloc {
NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];
[center removeObserver:self]; // Removes all registered notifications.
[super dealloc];
}
- (void)listenToKey:(NSString *)key onCenter:(NSDistributedNotificationCenter *)center {
[center addObserver:self
selector:@selector(notificationEvent:)
name:key
object:nil];
}
- (void)runCallback {
if (!jvm) return;
JNIEnv *env;
BOOL detach = NO;
int getEnvStat = jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
detach = YES;
if (jvm->AttachCurrentThread((void **) &env, NULL) != 0) return;
} else if (getEnvStat == JNI_EVERSION) {
return;
}
jclass runnableClass = env->GetObjectClass(callback);
jmethodID runMethodId = env->GetMethodID(runnableClass, "run", "()V");
if (runMethodId) {
env->CallVoidMethod(callback, runMethodId);
}
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
}
if (detach) jvm->DetachCurrentThread();
}
- (void)notificationEvent:(NSNotification *)notification {
[self runCallback];
}
@end
JNIEXPORT jboolean JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_isDarkThemeEnabled(JNIEnv *env, jclass obj) {
JNF_COCOA_ENTER(env);
if(@available(macOS 10.14, *)) {
NSString *interfaceStyle = [[NSUserDefaults standardUserDefaults] stringForKey:KEY_APPLE_INTERFACE_STYLE];
// interfaceStyle can be nil (light mode) or "Dark" (dark mode).
BOOL isDark = [VALUE_DARK caseInsensitiveCompare:interfaceStyle] == NSOrderedSame;
return (jboolean) isDark;
} else {
return (jboolean) NO;
}
JNF_COCOA_EXIT(env);
return NO;
}
JNIEXPORT jboolean JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_isHighContrastEnabled(JNIEnv *env, jclass obj) {
JNF_COCOA_ENTER(env);
return (jboolean) NSWorkspace.sharedWorkspace.accessibilityDisplayShouldIncreaseContrast;
JNF_COCOA_EXIT(env);
return (jboolean) NO;
}
JNIEXPORT jint JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_nativeGetAccentColor(JNIEnv *env, jclass obj) {
JNF_COCOA_ENTER(env);
if (@available(macOS 10.14, *)) {
BOOL hasAccentSet = ([[NSUserDefaults standardUserDefaults] objectForKey:KEY_ACCENT_COLOR] != nil);
if (hasAccentSet) {
return (jint) ([[NSUserDefaults standardUserDefaults] integerForKey:KEY_ACCENT_COLOR]);
}
}
return (jint) VALUE_DEFAULT_ACCENT_COLOR;
JNF_COCOA_EXIT(env);
return (jint) VALUE_NO_ACCENT_COLOR;
}
JNIEXPORT jint JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_nativeGetSelectionColor(JNIEnv *env, jclass obj) {
JNF_COCOA_ENTER(env);
NSColorSpace *rgbSpace = [NSColorSpace genericRGBColorSpace];
NSColor *accentColor = [[[NSColorList colorListNamed: KEY_SYSTEM_COLOR_LIST] colorWithKey:KEY_SELECTION_COLOR] colorUsingColorSpace:rgbSpace];
NSInteger r = (NSInteger) (255 * [accentColor redComponent]);
NSInteger g = (NSInteger) (255 * [accentColor greenComponent]);
NSInteger b = (NSInteger) (255 * [accentColor blueComponent]);
return (jint) (0xff000000 | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | ((b & 0xFF) << 0));
JNF_COCOA_EXIT(env);
return (jint) VALUE_NO_SELECTION_COLOR;
}
JNIEXPORT jlong JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_createPreferenceChangeListener(JNIEnv *env, jclass obj, jobject callback) {
JNF_COCOA_DURING(env); // We dont want an auto release pool.
JavaVM *jvm;
if (env->GetJavaVM(&jvm) == 0) {
jobject callbackRef = env->NewGlobalRef(callback);
PreferenceChangeListener *listener = [[PreferenceChangeListener alloc] initWithJVM:jvm andCallBack: callbackRef];
[listener retain];
return reinterpret_cast<jlong>(listener);
}
return (jlong) 0;
JNF_COCOA_HANDLE(env);
return (jlong) 0;
}
JNIEXPORT void JNICALL
Java_com_github_weisj_darklaf_platform_macos_JNIThemeInfoMacOS_deletePreferenceChangeListener(JNIEnv *env, jclass obj, jlong listenerPtr) {
JNF_COCOA_ENTER(env);
PreferenceChangeListener *listener = OBJC(listenerPtr);
if (listener) {
env->DeleteGlobalRef(listener->callback);
[listener release];
[listener dealloc];
}
JNF_COCOA_EXIT(env);
}

1
property-loader/build.gradle.kts

@ -6,4 +6,3 @@ dependencies {
implementation(project(":darklaf-utils"))
implementation("com.formdev:svgSalamander")
}

125
property-loader/src/main/java/com/github/weisj/darklaf/PropertyLoader.java

@ -30,7 +30,6 @@ import com.github.weisj.darklaf.icons.StateIcon;
import com.github.weisj.darklaf.util.ColorUtil;
import com.github.weisj.darklaf.util.Pair;
import com.github.weisj.darklaf.util.PropertyValue;
import com.github.weisj.darklaf.util.StringUtil;
import javax.swing.*;
import javax.swing.plaf.ColorUIResource;
@ -48,6 +47,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* @author Konstantin Bulenkov
@ -56,11 +56,11 @@ import java.util.logging.Logger;
public final class PropertyLoader {
private static final Logger LOGGER = Logger.getLogger(PropertyLoader.class.getName());
private static final IconLoader ICON_LOADER = IconLoader.get();
private static final char ICON_KEY_START = '[';
private static final char ICON_KEY_END = ']';
private static final String DUAL_KEY = ICON_KEY_START + "dual" + ICON_KEY_END;
private static final String AWARE_KEY = ICON_KEY_START + "aware" + ICON_KEY_END;
private static final String THEMED_KEY = ICON_KEY_START + "themed" + ICON_KEY_END;
private static final char INT_LIST_START = '[';
private static final char INT_LIST_END = ']';
private static final String DUAL_KEY = "[dual]";
private static final String AWARE_KEY = "[aware]";
private static final String THEMED_KEY = "[themed]";
private static final String ICON_EMPTY = "empty";
private static final char REFERENCE_PREFIX = '%';
@ -76,6 +76,7 @@ public final class PropertyLoader {
private static final char ARG_END = ')';
private static final char SEPARATOR = ',';
private static final char LIST_SEPARATOR = ';';
private static final char PAIR_SEPARATOR = ':';
private static boolean addReferenceInfo;
@ -167,10 +168,10 @@ public final class PropertyLoader {
}
private static Object parseValue(final String propertyKey, final String value,
public static Object parseValue(final String propertyKey, final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
if (PropertyValue.NULL.equals(value)) {
if (value == null || PropertyValue.NULL.equals(value)) {
return null;
}
String key = propertyKey;
@ -182,7 +183,7 @@ public final class PropertyLoader {
Object returnVal = new LoadError();
if (key.endsWith("Insets") || key.endsWith(".insets")) {
returnVal = parseInsets(value);
returnVal = parseInsets(value, accumulator, currentDefaults, iconLoader);
} else if (!skipObjects
&& (key.endsWith("Border")
|| key.endsWith(".border")
@ -193,9 +194,20 @@ public final class PropertyLoader {
} else if (key.toLowerCase().endsWith("font")) {
returnVal = parseFont(key, value, accumulator, currentDefaults);
} else if (key.endsWith(".icon") || key.endsWith("Icon")) {
returnVal = parseIcon(value, iconLoader);
returnVal = parseIcon(value, accumulator, currentDefaults, iconLoader);
} else if (key.endsWith("Size") || key.endsWith(".size")) {
returnVal = parseSize(value);
} else if (value.startsWith(String.valueOf(LIST_START))) {
returnVal = parseList(
(v, acc, defs, iconL) -> PropertyLoader.parseValue("", v, acc, defs, iconL),
value, accumulator, currentDefaults, iconLoader);
} else if (value.startsWith(String.valueOf(INT_LIST_START))) {
returnVal = parseList((SimpleValueMapper<Integer>) Integer::parseInt, value, accumulator, currentDefaults,
iconLoader, INT_LIST_START, INT_LIST_END, SEPARATOR);
} else if (value.contains(String.valueOf(PAIR_SEPARATOR))) {
returnVal = parsePair(
(v, acc, defs, iconL) -> PropertyLoader.parseValue("", v, acc, defs, iconL),
value, accumulator, currentDefaults, iconLoader);
} else if (PropertyValue.NULL.equalsIgnoreCase(value)) {
returnVal = null;
} else if (value.startsWith(String.valueOf(REFERENCE_PREFIX))) {
@ -220,6 +232,20 @@ public final class PropertyLoader {
return value;
}
private static <T> Pair<T, T> parsePair(final ParseFunction<T> mapper,
final String value, final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
return parsePair(mapper, mapper, value, accumulator, currentDefaults, iconLoader);
}
private static <T, K> Pair<T, K> parsePair(final ParseFunction<T> firstMapper, final ParseFunction<K> secondMapper,
final String value, final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
String[] pairVals = value.split(String.valueOf(PAIR_SEPARATOR), 2);
return new Pair<>(firstMapper.parseValue(pairVals[0], accumulator, currentDefaults, iconLoader),
secondMapper.parseValue(pairVals[1], accumulator, currentDefaults, iconLoader));
}
private static Object parseReference(final String key, final String value,
final Map<Object, Object> accumulator) {
String val = parseKey(value);
@ -239,13 +265,13 @@ public final class PropertyLoader {
}
private static Object parseInsets(final String value) {
final List<String> numbers = StringUtil.split(value, String.valueOf(SEPARATOR));
return new InsetsUIResource(
Integer.parseInt(numbers.get(0)),
Integer.parseInt(numbers.get(1)),
Integer.parseInt(numbers.get(2)),
Integer.parseInt(numbers.get(3)));
private static Object parseInsets(final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
List<Integer> insets = parseList((SimpleValueMapper<Integer>) Integer::parseInt, value, accumulator,
currentDefaults, iconLoader, Character.MIN_VALUE, Character.MIN_VALUE,
SEPARATOR);
return new InsetsUIResource(insets.get(0), insets.get(1), insets.get(2), insets.get(3));
}
@SuppressWarnings("MagicConstant")
@ -331,9 +357,37 @@ public final class PropertyLoader {
return new Pair<>(font, rest);
}
private static Icon parseIcon(final String value, final IconLoader iconLoader) {
private static <T> List<T> parseList(final ParseFunction<T> mapper,
final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
return parseList(mapper, value, accumulator, currentDefaults, iconLoader, LIST_START, LIST_END,
LIST_SEPARATOR);
}
private static <T> List<T> parseList(final ParseFunction<T> mapper,
final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader,
final char start, final char end,
final char delimiter) {
if (value == null || value.isEmpty()) return new ArrayList<>();
String val = value;
if (val.charAt(0) == start) {
val = value.substring(1, value.length() - 1);
}
String[] values = val.split(String.valueOf(delimiter));
return Arrays.stream(values)
.map(k -> mapper.parseValue(k, accumulator, currentDefaults, iconLoader))
.collect(Collectors.toList());
}
private static Icon parseIcon(final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults,
final IconLoader iconLoader) {
if (value.startsWith(String.valueOf(LIST_START))) {
return parseStateIcon(value, iconLoader);
return parseStateIcon(value, accumulator, currentDefaults, iconLoader);
}
String path = value;
Dimension dim = new Dimension(16, 16);
@ -346,7 +400,7 @@ public final class PropertyLoader {
dim.height = values[1];
path = path.substring(0, i);
}
if (path.charAt(path.length() - 1) == ICON_KEY_END) {
if (path.charAt(path.length() - 1) == INT_LIST_END) {
String tag = null;
if (path.endsWith(DUAL_KEY)) {
tag = DUAL_KEY;
@ -376,16 +430,13 @@ public final class PropertyLoader {
return iconLoader.getIcon(path, dim.width, dim.height);
}
private static Icon parseStateIcon(final String value, final IconLoader iconLoader) {
String[] keys = value.substring(1, value.length() - 1).split(String.valueOf(LIST_SEPARATOR));
Icon[] icons = new Icon[keys.length];
for (int i = 0; i < icons.length; i++) {
icons[i] = parseIcon(keys[i], iconLoader);
}
return new StateIcon(icons);
private static Icon parseStateIcon(final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults,
final IconLoader iconLoader) {
return new StateIcon(parseList(PropertyLoader::parseIcon, value, accumulator, currentDefaults, iconLoader));
}
private static Object parseSize(final String value) {
try {
int[] dim = Arrays.stream(value.split(String.valueOf(SEPARATOR), 2)).mapToInt(Integer::parseInt).toArray();
@ -424,4 +475,22 @@ public final class PropertyLoader {
private static final class LoadError {
}
private interface ParseFunction<T> {
T parseValue(final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader);
}
private interface SimpleValueMapper<T> extends ParseFunction<T> {
T map(final String value);
@Override
default T parseValue(final String value,
final Map<Object, Object> accumulator,
final UIDefaults currentDefaults, final IconLoader iconLoader) {
return map(value);
}
}
}

9
property-loader/src/main/java/com/github/weisj/darklaf/icons/SolidColorIcon.java

@ -31,6 +31,10 @@ public class SolidColorIcon implements Icon {
private final int height;
private Color color;
public SolidColorIcon() {
this(null);
}
public SolidColorIcon(final Color color) {
this(color, 16, 16);
}
@ -43,9 +47,12 @@ public class SolidColorIcon implements Icon {
@Override
public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
g.setColor(color);
Color col = getColor();
if (col != null) {
g.setColor(col);
g.fillRect(x, y, getIconWidth(), getIconHeight());
}
}
@Override
public int getIconWidth() {

5
property-loader/src/main/java/com/github/weisj/darklaf/icons/StateIcon.java

@ -25,6 +25,7 @@ package com.github.weisj.darklaf.icons;
import javax.swing.*;
import java.awt.*;
import java.util.List;
/**
* Icon that should be used with JCheckBox or JRadioButton. All icons should have the same size.
@ -38,6 +39,10 @@ public class StateIcon implements Icon {
private final Icon selectedDisabledIcon;
private final Icon selectedFocusedIcon;
public StateIcon(final List<Icon> icons) {
this(icons.toArray(new Icon[0]));
}
public StateIcon(final Icon[] icons) {
if (icons == null || icons.length < 6) {
throw new IllegalArgumentException("Not enough icons given: count="

106
theme/src/main/java/com/github/weisj/darklaf/theme/Theme.java

@ -24,10 +24,7 @@
package com.github.weisj.darklaf.theme;
import com.github.weisj.darklaf.PropertyLoader;
import com.github.weisj.darklaf.theme.info.ColorToneRule;
import com.github.weisj.darklaf.theme.info.ContrastRule;
import com.github.weisj.darklaf.theme.info.FontSizeRule;
import com.github.weisj.darklaf.theme.info.PresetIconRule;
import com.github.weisj.darklaf.theme.info.*;
import javax.swing.*;
import javax.swing.text.html.StyleSheet;
@ -46,8 +43,40 @@ import java.util.logging.Logger;
*/
public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
private static final Logger LOGGER = Logger.getLogger(Theme.class.getName());
private FontSizeRule fontSizeRule;
private final FontSizeRule fontSizeRule;
private final AccentColorRule accentColorRule;
public Theme() {
this(null, null);
}
public Theme(final FontSizeRule fontSizeRule, final AccentColorRule accentColorRule) {
this.fontSizeRule = fontSizeRule != null ? fontSizeRule : FontSizeRule.getDefault();
this.accentColorRule = accentColorRule != null ? accentColorRule : AccentColorRule.getDefault();
}
/**
* Create a derived theme with the given {@link FontSizeRule} and {@link AccentColorRule}.
*
* @param fontSizeRule the font size rule.
* @param accentColorRule the accent color rule.
* @return the derived theme.
*/
public Theme derive(final FontSizeRule fontSizeRule, final AccentColorRule accentColorRule) {
return new ThemeDelegate(this, fontSizeRule, accentColorRule);
}
/**
* Creates a copy of this theme. This is not equivalent to {@link #clone()} in the sense that
* <code>clone().getClass() == this.getClass()</code> and <code>copy().getClass() != this.getClass()</code>.
* Nonetheless the copy theme behaves exactly the same as the original.
*
* @return a copy of the theme.
*/
public Theme copy() {
return new ThemeDelegate(this);
}
/**
* Returns whether the theme is a dark theme. This is used to determine the default mode for [aware] icons.
@ -82,8 +111,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @param currentDefaults the current ui defaults.
*/
public void loadDefaults(final Properties properties, final UIDefaults currentDefaults) {
String name = getResourcePath() + getPrefix() + "_defaults.properties";
PropertyLoader.putProperties(load(name), properties, currentDefaults);
PropertyLoader.putProperties(loadPropertyFile("defaults"), properties, currentDefaults);
}
/**
@ -134,7 +162,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
break;
case NONE:
default:
props = load(getResourcePath() + getPrefix() + "_icons.properties");
props = loadPropertyFile("icons");
}
PropertyLoader.putProperties(props, properties, currentDefaults);
}
@ -181,10 +209,9 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @param properties the properties to load into.
* @param currentDefaults the current ui defaults.
*/
protected void loadCustomProperties(final String propertySuffix, final Properties properties,
protected final void loadCustomProperties(final String propertySuffix, final Properties properties,
final UIDefaults currentDefaults) {
String name = getResourcePath() + getPrefix() + "_" + propertySuffix + ".properties";
PropertyLoader.putProperties(load(name), properties, currentDefaults);
PropertyLoader.putProperties(loadPropertyFile(propertySuffix), properties, currentDefaults);
}
/**
@ -195,7 +222,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @param name the properties file to load.
* @return the properties.
*/
protected Properties load(final String name) {
protected final Properties load(final String name) {
return loadWithClass(name, getLoaderClass());
}
@ -206,7 +233,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @param loaderClass the class to resolve the file location from.
* @return the properties.
*/
protected Properties loadWithClass(final String name, final Class<?> loaderClass) {
protected final Properties loadWithClass(final String name, final Class<?> loaderClass) {
final Properties properties = new Properties();
try (InputStream stream = loaderClass.getResourceAsStream(name)) {
properties.load(stream);
@ -223,7 +250,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
*
* @return the {@link StyleSheet}.
*/
public StyleSheet loadStyleSheet() {
public final StyleSheet loadStyleSheet() {
return loadStyleSheetWithClass(getLoaderClass());
}
@ -234,7 +261,7 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @param loaderClass the class to resolve the location of the style sheet.
* @return the {@link StyleSheet}.
*/
public StyleSheet loadStyleSheetWithClass(final Class<?> loaderClass) {
public final StyleSheet loadStyleSheetWithClass(final Class<?> loaderClass) {
StyleSheet styleSheet = new StyleSheet();
try (InputStream in = loaderClass.getResourceAsStream(getResourcePath() + getPrefix() + "_styleSheet.css");
InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8);
@ -283,6 +310,46 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
*/
protected abstract Class<? extends Theme> getLoaderClass();
/**
* Get the path for the file [prefix]_[name].properties in the themes resource location.
*
* @param name the of the file.
* @return the path relative to the location of {@link #getLoaderClass()}.
*/
protected String getPropertyFilePath(final String name) {
return getResourcePath() + getPrefix() + "_" + name + ".properties";
}
/**
* Load the theme property file with the specified name. The name gets resolved to the resource location of the
* theme adds the theme property prefix and appends ".properties" e.g. "test" ->
* [resource_location]/[prefix_of_theme]_test.properties.
*
* @param name the properties name.
* @return the properties.
*/
public final Properties loadPropertyFile(final String name) {
return loadPropertyFile(name, false);
}
/**
* Load the theme property file with the specified name. The name gets resolved to the resource location of the
* theme adds the theme property prefix and appends ".properties" e.g. "test" ->
* [resource_location]/[prefix_of_theme]_test.properties.
*
* @param name the properties name.
* @param silent if true no warnings are issues if the file is not present. Instead, an empty property instance is
* returned.
* @return the properties.
*/
public final Properties loadPropertyFile(final String name, final boolean silent) {
Level level = LOGGER.getLevel();
if (silent) LOGGER.setLevel(Level.OFF);
Properties properties = load(getPropertyFilePath(name));
LOGGER.setLevel(level);
return properties;
}
/**
* Returns whether this theme should use custom decorations if available.
*
@ -314,17 +381,16 @@ public abstract class Theme implements Comparable<Theme>, Comparator<Theme> {
* @return the font size rule.
*/
public FontSizeRule getFontSizeRule() {
if (fontSizeRule == null) fontSizeRule = FontSizeRule.getDefault();
return fontSizeRule;
}
/**
* Set the font size rule.
* Get the accent color rule.
*
* @param fontSizeRule the font size rule.
* @return the accent color rule.
*/
public void setFontSizeRule(final FontSizeRule fontSizeRule) {
this.fontSizeRule = fontSizeRule;
public AccentColorRule getAccentColorRule() {
return accentColorRule;
}
@Override

155
theme/src/main/java/com/github/weisj/darklaf/theme/ThemeDelegate.java

@ -0,0 +1,155 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.theme;
import com.github.weisj.darklaf.theme.info.*;
import javax.swing.*;
import java.util.Properties;
public class ThemeDelegate extends Theme {
private final Theme delegate;
private final boolean overwriteFontSize;
private final boolean overWriteAccentColor;
public ThemeDelegate(final Theme delegate) {
this(delegate, null, null);
}
public ThemeDelegate(final Theme delegate, final FontSizeRule fontSizeRule, final AccentColorRule accentColorRule) {
super(fontSizeRule, accentColorRule);
if (delegate == null) {
throw new IllegalArgumentException("Theme delegate cannot be null");
}
this.delegate = delegate;
this.overwriteFontSize = fontSizeRule != null;
this.overWriteAccentColor = accentColorRule != null;
}
public Theme getDelegate() {
return delegate;
}
@Override
public Theme derive(final FontSizeRule fontSizeRule,
final AccentColorRule accentColorRule) {
return getDelegate().derive(fontSizeRule, accentColorRule);
}
@Override
public Theme copy() {
return getDelegate().copy();
}
@Override
public FontSizeRule getFontSizeRule() {
return overwriteFontSize ? super.getFontSizeRule() : delegate.getFontSizeRule();
}
@Override
public AccentColorRule getAccentColorRule() {
return overWriteAccentColor ? super.getAccentColorRule() : delegate.getAccentColorRule();
}
@Override
public ColorToneRule getColorToneRule() {
return delegate.getColorToneRule();
}
@Override
public ContrastRule getContrastRule() {
return delegate.getContrastRule();
}
@Override
protected PresetIconRule getPresetIconRule() {
return delegate.getPresetIconRule();
}
@Override
public void loadDefaults(final Properties properties, final UIDefaults currentDefaults) {
delegate.loadDefaults(properties, currentDefaults);
}
@Override
public void customizeGlobals(final Properties properties, final UIDefaults currentDefaults) {
delegate.customizeGlobals(properties, currentDefaults);
}
@Override
public void customizeIconTheme(final Properties properties, final UIDefaults currentDefaults) {
delegate.customizeIconTheme(properties, currentDefaults);
}
@Override
public void loadIconTheme(final Properties properties, final UIDefaults currentDefaults) {
delegate.loadIconTheme(properties, currentDefaults);
}
@Override
public void customizePlatformProperties(final Properties properties, final UIDefaults currentDefaults) {
delegate.customizePlatformProperties(properties, currentDefaults);
}
@Override
public void customizeUIProperties(final Properties properties, final UIDefaults currentDefaults) {
delegate.customizeUIProperties(properties, currentDefaults);
}
@Override
public String toString() {
return delegate.getName();
}
@Override
protected String getResourcePath() {
return delegate.getResourcePath();
}
@Override
public String getPrefix() {
return delegate.getPrefix();
}
@Override
public String getName() {
return delegate.getName();
}
@Override
protected Class<? extends Theme> getLoaderClass() {
return delegate.getClass();
}
@Override
protected String getPropertyFilePath(final String name) {
return delegate.getPropertyFilePath(name);
}
@Override
public boolean useCustomDecorations() {
return delegate.useCustomDecorations();
}
}

70
theme/src/main/java/com/github/weisj/darklaf/theme/info/AccentColorRule.java

@ -0,0 +1,70 @@
/*
* MIT License
*
* Copyright (c) 2020 Jannis Weis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.weisj.darklaf.theme.info;
import java.awt.*;
public class AccentColorRule {
private static final AccentColorRule DEFAULT = new AccentColorRule(null, null);
private final Color accentColor;
private final Color selectionColor;
public AccentColorRule(final Color accentColor) {
this(accentColor, null);
}
protected AccentColorRule(final Color accentColor, final Color selectionColor) {
this.accentColor = accentColor;
this.selectionColor = selectionColor;
}
public static AccentColorRule getDefault() {
return DEFAULT;
}
public static AccentColorRule fromColor(final Color accentColor, final Color selectionColor) {
return new AccentColorRule(accentColor, selectionColor);
}
public static AccentColorRule fromColor(final Color accentColor) {
return fromColor(accentColor, null);
}
public Color getAccentColor() {
return accentColor;
}
public Color getSelectionColor() {
return selectionColor;
}
@Override
public String toString() {
return "AccentColorRule{" +
"accentColor=" + accentColor +
", selectionColor=" + selectionColor +
'}';
}
}

5
theme/src/main/java/com/github/weisj/darklaf/theme/info/DefaultThemeProvider.java

@ -86,8 +86,7 @@ public class DefaultThemeProvider implements ThemeProvider {
boolean highContrast = themeStyle.getContrastRule() == ContrastRule.HIGH_CONTRAST;
Theme theme = dark ? highContrast ? darkHighContrastTheme : darkTheme
: highContrast ? lightHighContrastTheme : lightTheme;
// Apply the font size.
theme.setFontSizeRule(themeStyle.getFontSizeRule());
return theme;
return theme.derive(themeStyle.getFontSizeRule(), themeStyle.getAccentColorRule());
}
}

38
theme/src/main/java/com/github/weisj/darklaf/theme/info/PreferredThemeStyle.java

@ -28,6 +28,11 @@ public class PreferredThemeStyle {
private final ContrastRule contrastRule;
private final ColorToneRule colorToneRule;
private final FontSizeRule fontSizeRule;
private final AccentColorRule accentColorRule;
public PreferredThemeStyle() {
this(ContrastRule.STANDARD, ColorToneRule.LIGHT);
}
public PreferredThemeStyle(final ContrastRule contrastRule,
final ColorToneRule colorToneRule) {
@ -36,17 +41,45 @@ public class PreferredThemeStyle {
this.contrastRule = contrastRule;
this.colorToneRule = colorToneRule;
this.fontSizeRule = FontSizeRule.getDefault();
this.accentColorRule = AccentColorRule.getDefault();
}
public PreferredThemeStyle(final ContrastRule contrastRule,
final ColorToneRule colorToneRule,
final AccentColorRule accentColorRule) {
if (contrastRule == null) throw new IllegalArgumentException("null is not a valid contrast rule");
if (colorToneRule == null) throw new IllegalArgumentException("null is not a valid style rule");
if (accentColorRule == null) throw new IllegalArgumentException("null is not a valid accent color rule");
this.contrastRule = contrastRule;
this.colorToneRule = colorToneRule;
this.fontSizeRule = FontSizeRule.getDefault();
this.accentColorRule = accentColorRule;
}
public PreferredThemeStyle(final ContrastRule contrastRule,
final ColorToneRule colorToneRule,
final FontSizeRule fontSizeRule) {
if (contrastRule == null) throw new IllegalArgumentException("null is not a valid contrast rule");
if (colorToneRule == null) throw new IllegalArgumentException("null is not a valid style rule");
if (fontSizeRule == null) throw new IllegalArgumentException("null is not a valid font size rule");
this.contrastRule = contrastRule;
this.colorToneRule = colorToneRule;
this.fontSizeRule = fontSizeRule;
this.accentColorRule = AccentColorRule.getDefault();
}
public PreferredThemeStyle(final ContrastRule contrastRule,
final ColorToneRule colorToneRule,
final AccentColorRule accentColorRule,
final FontSizeRule fontSizeRule) {
if (contrastRule == null) throw new IllegalArgumentException("null is not a valid contrast rule");
if (colorToneRule == null) throw new IllegalArgumentException("null is not a valid style rule");
if (fontSizeRule == null) throw new IllegalArgumentException("null is not a valid font size rule");
if (accentColorRule == null) throw new IllegalArgumentException("null is not a valid accent color rule");
this.contrastRule = contrastRule;
this.colorToneRule = colorToneRule;
this.fontSizeRule = fontSizeRule;
this.accentColorRule = accentColorRule;
}
public ContrastRule getContrastRule() {
@ -61,11 +94,16 @@ public class PreferredThemeStyle {
return fontSizeRule;
}
public AccentColorRule getAccentColorRule() {
return accentColorRule;
}
@Override
public String toString() {
return "PreferredThemeStyle{" +
"contrastRule=" + contrastRule +
", colorToneRule=" + colorToneRule +
", accentColorRule=" + accentColorRule +
", fontSizeRule=" + fontSizeRule +
'}';
}

41
theme/src/main/resources/com/github/weisj/darklaf/theme/darcula/darcula_accents.properties

@ -0,0 +1,41 @@
#
# MIT License
#
# Copyright (c) 2020 Jannis Weis
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
mainAccent.propertyList = {widgetFillDefault:[100,100,100];\
hoverHighlightDefault:[100,80,80];\
clickHighlightDefault:[100,90,85];\
widgetBorderDefault:[100,80,110];\
controlBorderFocus:[100,90,116];\
controlBorderFocusSelected:[100,90,116];\
glowFocusLine:[100,90,116];\
glowFocus:[100,120,155];\
borderFocus:[100,76,122];\
highlightFill:[100,80,45];\
highlightFillFocus:[100,100,80];\
highlightFillFocusSecondary:[100,110,100];\
highlightFillMono:[100,25,50];\
controlFillHighlight:[100,100,100];\
controlFillHighlightDisabled:[100,77,50];\
hyperlink:[100,100,100]}
selectionAccent.propertyList = {textSelectionBackground}

4
theme/src/main/resources/com/github/weisj/darklaf/theme/darcula/darcula_defaults.properties

@ -107,14 +107,14 @@ Theme.highContrast = false
####Text####
%caret = bbbbbb
%textForeground = bbbbbb
%textForegroundDefault = bbbbbb
%textForegroundDefault = DDDDDD
%textForegroundHighlight = DDDDDD
%textForegroundInactive = 777777
%textForegroundSecondary = 919191
%acceleratorForeground = eeeeee
%textContrastForeground = 000000
%textSelectionForeground = bbbbbb
%textSelectionForeground = DDDDDD
%textSelectionForegroundInactive = bbbbbb
%textSelectionForegroundDisabled = 929292
%textSelectionBackground = 214283

41
theme/src/main/resources/com/github/weisj/darklaf/theme/intellij/intellij_accents.properties

@ -0,0 +1,41 @@
#
# MIT License
#
# Copyright (c) 2020 Jannis Weis
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
mainAccent.propertyList = {widgetFillDefault:[100,100,100];\
hoverHighlightDefault:[100,80,100];\
clickHighlightDefault:[100,90,100];\
widgetBorderDefault:[100,100,90];\
controlBorderFocus:[100,60,100];\
controlBorderFocusSelected:[100,50,122];\
glowFocusLine:[100,60,100];\
glowFocus:[100,120,120];\
borderFocus:[100,76,122];\
highlightFillFocus:[100,130,95];\
highlightFillFocusSecondary:[100,110,100];\
highlightFillMono:[100,25,90];\
controlBorderSelected:[100,100,100];\
widgetFillSelected:[100,100,100];\
controlFillHighlight:[100,100,100];\
hyperlink:[100,100,100]}
selectionAccent.propertyList = {textSelectionBackground}

5
theme/src/main/resources/com/github/weisj/darklaf/theme/intellij/intellij_defaults.properties

@ -4,6 +4,7 @@
# Copyright (c) 2020 Jannis Weis
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
@ -82,7 +83,7 @@ Theme.highContrast = false
%widgetFill = FFFFFF
%widgetFillSelected = 4D89C9
%widgetFillInactive = F2F2F2
%widgetFillDefault = 4C8ACA
%widgetFillDefault = 4D89C9
####Controls####
%controlBorder = 878787
@ -97,7 +98,7 @@ Theme.highContrast = false
%controlFillFocus = FFFFFF
%controlFillDisabled = ABABAB
%controlFillHighlight = 4D89C9
%controlFillHighlightDisabled = b0b3b6
%controlFillHighlightDisabled = ABABAB
%controlBackground = C4C4C4
%controlFadeStart = C4C4C4
%controlFadeEnd = 808080

2
windows/libraries/windows-x86-64/library.md

@ -1,3 +1,3 @@
If your machine can't build the macos library download the latest
If your machine can't build the library download the latest
'darklaf-windows_x86-64.dll' from the repository and place it in this folder
renamed as 'darklaf-windows.dll'.

2
windows/libraries/windows-x86/library.md

@ -1,3 +1,3 @@
If your machine can't build the macos library download the latest
If your machine can't build the library download the latest
'darklaf-windows_x86.dll' from the repository and place it in this folder
renamed as 'darklaf-windows.dll'.

103
windows/src/main/cpp/ThemeInfo.cpp

@ -25,6 +25,8 @@
#include <iostream>
#include <string>
#include <thread>
#include <atomic>
#include <windows.h>
#include <winreg.h>
#include <winuser.h>
@ -180,25 +182,96 @@ Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_getAccentColo
return (jint) GetAccentColor();
}
struct EventHandler {
JavaVM *jvm;
JNIEnv *env;
jobject callback;
HANDLE eventHandle;
std::thread notificationLoop;
std::atomic<bool> running = FALSE;
void runCallBack()
{
jclass runnableClass = env->GetObjectClass(callback);
jmethodID runMethodId = env->GetMethodID(runnableClass, "run", "()V");
if (runMethodId) {
env->CallVoidMethod(callback, runMethodId);
}
}
bool awaitPreferenceChange()
{
if (!RegisterRegistryEvent(FONT_SCALE_PATH, eventHandle)) return FALSE;
if (!RegisterRegistryEvent(DARK_MODE_PATH, eventHandle)) return FALSE;
if (!RegisterRegistryEvent(HIGH_CONTRAST_PATH, eventHandle)) return FALSE;
if (!RegisterRegistryEvent(ACCENT_COLOR_PATH, eventHandle)) return FALSE;
return WaitForSingleObject(eventHandle, INFINITE) != WAIT_FAILED;
}
void run()
{
int getEnvStat = jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED)
{
if (jvm->AttachCurrentThread((void **) &env, NULL) != 0) return;
}
else if (getEnvStat == JNI_EVERSION)
{
return;
}
while(running && awaitPreferenceChange())
{
if (running)
{
runCallBack();
if (env->ExceptionCheck())
{
env->ExceptionDescribe();
break;
}
}
}
jvm->DetachCurrentThread();
}
void stop()
{
running = FALSE;
SetEvent(eventHandle);
notificationLoop.join();
}
EventHandler(JavaVM *jvm_, jobject callback_, HANDLE eventHandle_)
{
jvm = jvm_;
callback = callback_;
eventHandle = eventHandle_;
running = TRUE;
notificationLoop = std::thread(&EventHandler::run, this);
}
};
JNIEXPORT jlong JNICALL
Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_createEventHandle(JNIEnv *env, jclass obj)
Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_createEventHandler(JNIEnv *env, jclass obj, jobject callback)
{
return (jlong) CreateEvent(NULL, FALSE, FALSE, NULL);
JavaVM *jvm;
if (env->GetJavaVM(&jvm) == 0) {
jobject callbackRef = env->NewGlobalRef(callback);
HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL);
EventHandler* eventHandler = new EventHandler(jvm, callbackRef, event);
return reinterpret_cast<jlong>(eventHandler);
}
return (jlong) 0;
}
JNIEXPORT void JNICALL
Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_notifyEventHandle(JNIEnv *env, jclass obj, jlong eventHandle)
{
SetEvent(reinterpret_cast<HANDLE>(eventHandle));
}
JNIEXPORT jboolean JNICALL
Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_waitPreferenceChange(JNIEnv *env, jclass obj, jlong eventHandle)
Java_com_github_weisj_darklaf_platform_windows_JNIThemeInfoWindows_deleteEventHandler(JNIEnv *env, jclass obj, jlong eventHandler)
{
HANDLE event = reinterpret_cast<HANDLE>(eventHandle);
if (!RegisterRegistryEvent(FONT_SCALE_PATH, event)) return (jboolean) false;
if (!RegisterRegistryEvent(DARK_MODE_PATH, event)) return (jboolean) false;
if (!RegisterRegistryEvent(HIGH_CONTRAST_PATH, event)) return (jboolean) false;
if (!RegisterRegistryEvent(ACCENT_COLOR_PATH, event)) return (jboolean) false;
return (jboolean) (WaitForSingleObject(event, INFINITE) != WAIT_FAILED);
EventHandler *handler = reinterpret_cast<EventHandler *>(eventHandler);
if (handler) {
env->DeleteGlobalRef(handler->callback);
handler->stop();
delete handler;
}
}

23
windows/src/main/java/com/github/weisj/darklaf/platform/windows/JNIThemeInfoWindows.java

@ -54,26 +54,17 @@ public class JNIThemeInfoWindows {
public static native int getAccentColor();
/**
* Create a monitor event handle.
* Create a monitor event handler.
*
* @return the event handle.
* @param callback the event callback.
* @return the event handler pointer.
*/
public static native long createEventHandle();
public static native long createEventHandler(final Runnable callback);
/**
* Sets the event status to notify.
* Deletes the event handler.
*
* @param handle the event handle.
* @param handle the event handler pointer.
*/
public static native void notifyEventHandle(final long handle);
/**
* Wait for any possible changes to the preferences. This does not guarantee an actual change. This method will
* block until a change has occurred.
*
* @param eventHandle the handle to the monitor event.
* @return the success status. false means an error happened.
*/
public static native boolean waitPreferenceChange(final long eventHandle);
public static native void deleteEventHandler(final long handle);
}

41
windows/src/main/java/com/github/weisj/darklaf/platform/windows/WindowsPreferenceMonitor.java

@ -23,7 +23,6 @@
*/
package com.github.weisj.darklaf.platform.windows;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
public class WindowsPreferenceMonitor {
@ -35,17 +34,15 @@ public class WindowsPreferenceMonitor {
private boolean darkMode;
private boolean highContrast;
private long fontScaleFactor;
private long eventHandle;
private long eventHandler;
private int color;
private AtomicBoolean running = new AtomicBoolean(false);
private boolean running;
public WindowsPreferenceMonitor(final WindowsThemePreferenceProvider preferenceProvider) {
this.preferenceProvider = preferenceProvider;
}
public void run() {
LOGGER.info("Started preference monitoring.");
while (running.get() && JNIThemeInfoWindows.waitPreferenceChange(eventHandle)) {
private void onNotification() {
boolean newDark = JNIThemeInfoWindows.isDarkThemeEnabled();
boolean newHighContrast = JNIThemeInfoWindows.isHighContrastEnabled();
long newFotScale = JNIThemeInfoWindows.getFontScaleFactor();
@ -61,36 +58,30 @@ public class WindowsPreferenceMonitor {
preferenceProvider.reportPreferenceChange(highContrast, darkMode, fontScaleFactor, color);
}
}
if (running.get()) {
LOGGER.severe("Monitor encountered an error. Stopping preference monitoring.");
running.set(false);
} else {
LOGGER.info("Stopped preference monitoring.");
}
}
private void start() {
darkMode = JNIThemeInfoWindows.isDarkThemeEnabled();
highContrast = JNIThemeInfoWindows.isHighContrastEnabled();
fontScaleFactor = JNIThemeInfoWindows.getFontScaleFactor();
eventHandle = JNIThemeInfoWindows.createEventHandle();
color = JNIThemeInfoWindows.getAccentColor();
/*
* In theory this shouldn't be necessary, but
* it ensures that the registry listeners are actually unregistered.
*/
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
this.running.set(true);
new Thread(this::run).start();
eventHandler = JNIThemeInfoWindows.createEventHandler(this::onNotification);
if (eventHandler == 0) {
LOGGER.severe("Could not create notification listener. Monitoring will not be started");
return;
}
running = true;
LOGGER.info("Started preference monitoring.");
}
private void stop() {
this.running.set(false);
JNIThemeInfoWindows.notifyEventHandle(eventHandle);
if (!running) return;
LOGGER.info("Stopped preference monitoring.");
running = false;
JNIThemeInfoWindows.deleteEventHandler(eventHandler);
}
public void setRunning(final boolean running) {
if (running == this.running.get()) return;
if (running == isRunning()) return;
if (running) {
start();
} else {
@ -99,6 +90,6 @@ public class WindowsPreferenceMonitor {
}
public boolean isRunning() {
return running.get();
return running;
}
}

19
windows/src/main/java/com/github/weisj/darklaf/platform/windows/WindowsThemePreferenceProvider.java

@ -50,7 +50,8 @@ public class WindowsThemePreferenceProvider implements ThemePreferenceProvider {
ContrastRule contrastRule = highContrast ? ContrastRule.HIGH_CONTRAST : ContrastRule.STANDARD;
ColorToneRule toneRule = darkMode ? ColorToneRule.DARK : ColorToneRule.LIGHT;
FontSizeRule fontSizeRule = FontSizeRule.relativeAdjustment(fontScaling / 100f);
return new PreferredThemeStyle(contrastRule, toneRule, fontSizeRule);
AccentColorRule accentColorRule = AccentColorRule.fromColor(createColorFromRGB(accentColorRGB));
return new PreferredThemeStyle(contrastRule, toneRule, accentColorRule, fontSizeRule);
}
private Color createColorFromRGB(final int rgb) {
@ -58,6 +59,14 @@ public class WindowsThemePreferenceProvider implements ThemePreferenceProvider {
return new Color(rgb);
}
void reportPreferenceChange(final boolean highContrast, final boolean darkMode,
final long fontScaleFactor, final int accentColorRGB) {
if (callback != null) {
PreferredThemeStyle style = create(highContrast, darkMode, fontScaleFactor, accentColorRGB);
callback.accept(style);
}
}
@Override
public void setReporting(final boolean reporting) {
if (reporting && !WindowsLibrary.isLoaded()) WindowsLibrary.updateLibrary();
@ -71,14 +80,6 @@ public class WindowsThemePreferenceProvider implements ThemePreferenceProvider {
return monitor.isRunning();
}
void reportPreferenceChange(final boolean highContrast, final boolean darkMode,
final long fontScaleFactor, final int accentColorRGB) {
if (callback != null) {
PreferredThemeStyle style = create(highContrast, darkMode, fontScaleFactor, accentColorRGB);
callback.accept(style);
}
}
@Override
public void initialize() {
WindowsLibrary.updateLibrary();

Loading…
Cancel
Save