weisJ / darklaf

Darklaf - A themeable swing Look and Feel based on Darcula-Laf
MIT License
440 stars 40 forks source link

[Help] How to configure window decorations #316

Closed bric3 closed 2 years ago

bric3 commented 2 years ago

Hi sorry this is not really a feature request but a usage question.

I would like to be able to control the color of the main window title bar color. And I'm not quite sure how to do it, as I have to dived in the code yet.

For reference the application is using LaF from other project (so it doesn't rely on LafManager). At this time the application is relying on ThemePreferencesHandler.getSharedInstance() to synchronize with OS dark mode. But I'd like to allow the user to control the appearance if they wish so.

The thing is that I can switch the LaF, but the window decoration keeps the current system appearance, while I would like to assign a color to this window title bar to match the current LaF.

I have noticed the LafManager.setDecorationsEnabled(enabled); API but as I said I avoid LafManager as it initializes a lot of stuff the application don't need.

I've noticed that LafManager is actually invoking

DecorationsHandler.getSharedInstance().setDecorationsEnabled()

Bit from there I'm not quite sure how to proceed. Thank you in advance for the bits.

By the way in https://github.com/weisJ/darklaf/issues/310 we've discussed about version 3 snapshot, but I'm not yet relying on it yet as it's a snapshot.

bric3 commented 2 years ago

I'm totally experimenting with it. So I noticed the CustomTitlePane in darklaf-platform-base, although I'm not quite sure it works as expected.

var frame = new JFrame("FirePlace");
final JRootPane rootPane = frame.getRootPane();
if (SystemInfo.isMacOS) {
    UIManager.put("MacOS.TitlePane.background", Color.PINK);
    UIManager.put("MacOS.TitlePane.foreground", Color.GRAY);
    UIManager.put("MacOS.TitlePane.inactiveBackground", Color.PINK.darker());
    UIManager.put("MacOS.TitlePane.inactiveForeground", Color.GRAY.darker());
    UIManager.put("MacOS.TitlePane.borderColor", Color.PINK);
    CustomTitlePane titlePane = DecorationsHandler.getSharedInstance().createTitlePane(rootPane, JRootPane.FRAME, frame);
    var layeredPane = rootPane.getLayeredPane();
    layeredPane.add(titlePane, JLayeredPane.FRAME_CONTENT_LAYER);
    titlePane.setVisible(true);
}
bric3 commented 2 years ago

Adding doesn't seem to change a thing (I thought that the custom title bar might have been hidden by the system one.)

// allows to place swing components on the whole window
rootPane.putClientProperty("apple.awt.fullWindowContent", true);
// makes the title bar transparent
rootPane.putClientProperty("apple.awt.transparentTitleBar", true);
weisJ commented 2 years ago

In comparison to the system theme detection mechanism it isn't quite as straight forward to get the custom decorations to work without actually using the laf itself. More specifically I don't think that it is possible at all in the current state. This is because DarkRootPaneUI has to be the ui of the frames JRootPane. I'll have to think about whether it is feasible to abstract it aways to the point where only setting the frames rootpane ui is enough.

weisJ commented 2 years ago

I have somewhat extracted the necessary code for native decorations to work. Using the following version:

repositories {
    maven {
        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
    }
}

dependencies {
    // The double decorations isn't a typo. It has this name because the artifact is named "platform-decorations" and
    // the branch "decorations" ;)
    implementation("com.github.weisj:darklaf-platform-decorations-decorations:latest.integration")
}

You should be able to do the following then:

import com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator;
...
// Everything here should happen on the swing UI thread:
ExternalLafDecorator.instance().install();
// Install you LaF.

I can't be sure that it will work as expected with every LaF e.g. any LaF that needs to have it's own implementation of RootPaneUI present won't work (though most lafs should be fine). I haven't had the chance to test this on macOS yet, so it might not work fully for you.

Now if you wan't to change color of the titlebar you can do the following: After installing the LaF set the appropriate properties in the UIManager. They property names can be found here:

bric3 commented 2 years ago

Cool il check it out. thanks a lot. Can these properties be adjusted on Leaf change?

weisJ commented 2 years ago

As long as you change the properties before you call updatesUI on the window hierarchy you should be fine.

bric3 commented 2 years ago

Hi sorry for the delay, I got caught in various things.

So I have tried the ExternalLafDecorator, but I'm unsure how to use it.

var frame = new JFrame("FirePlace");
frame.getRootPane().getUI(); // =>BasicNativeDecorationsRootPaneUI

When changing the colors myself the title bar don't get updated, after an updateUI, I'm using FlatLaf from formdev.

  switch (appearanceModeButton.getClientProperty(TO_APPEARANCE).toString()) {
      case TO_DARK_LAF:
          FlatDarculaLaf.setup();
          Colors.setDarkMode(true);
          appearanceModeButton.putClientProperty(TO_APPEARANCE, TO_LIGHT_LAF);
          appearanceModeButton.setIcon(toLightMode);
          break;
      case TO_LIGHT_LAF:
          FlatIntelliJLaf.setup();
          Colors.setDarkMode(false);
          appearanceModeButton.putClientProperty(TO_APPEARANCE, TO_DARK_LAF);
          appearanceModeButton.setIcon(toDarkMode);
          break;
  }
+ UIManager.put("MacOS.TitlePane.borderColor", UIManager.get("TitlePane.borderColor"));
+ UIManager.put("MacOS.TitlePane.background", UIManager.get("TitlePane.background"));
+ UIManager.put("MacOS.TitlePane.foreground", UIManager.get("TitlePane.foreground"));
+ UIManager.put("MacOS.TitlePane.inactiveBackground", UIManager.get("TitlePane.inactiveBackground"));
+ UIManager.put("MacOS.TitlePane.inactiveForeground", UIManager.get(" TitlePane.inactiveForeground"));
  FlatLaf.updateUI();
  FlatAnimatedLafChange.hideSnapshotWithAnimation();

Also as a convenience I wonder if there's a default color that matches the light/dark system.


The following happens right in ExternalLafDecorator.instance().install();

Exception in thread "main" java.lang.IncompatibleClassChangeError: Class com.github.weisj.darklaf.platform.macos.MacOSDecorationsProvider does not implement the requested interface com.github.weisj.darklaf.platform.DecorationsProvider
    at com.github.weisj.darklaf.platform.decorations.NativeDecorationsManager.initialize(NativeDecorationsManager.java:94)
    at com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator.install(ExternalLafDecorator.java:61)
    at io.github.bric3.fireplace.FirePlaceMain$AppearanceControl.install(FirePlaceMain.java:257)
    at io.github.bric3.fireplace.FirePlaceMain.setupLaF(FirePlaceMain.java:224)
    at io.github.bric3.fireplace.FirePlaceMain.initUI(FirePlaceMain.java:73)
    at io.github.bric3.fireplace.FirePlaceMain.main(FirePlaceMain.java:66)

I just had to do that.

    implementation("com.github.weisj:darklaf-platform-preferences:latest.integration") {
        exclude("com.github.weisj", "darklaf-platform-decorations")
    }
    implementation("com.github.weisj:darklaf-platform-decorations-decorations:latest.integration")
bric3 commented 2 years ago

The current draft PR is here : https://github.com/bric3/fireplace/pull/17

weisJ commented 2 years ago

The following works perfectly fine for me:

public class Test {

    static boolean systemLaf = true;

    private static void setLaf(String name, boolean updateUI) {
        try {
            UIManager.setLookAndFeel(name);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (systemLaf) {
            UIManager.put("Windows.TitlePane.borderColor", Color.YELLOW);
            UIManager.put("Windows.TitlePane.background", Color.CYAN);
            UIManager.put("Windows.TitlePane.foreground", Color.RED);
            UIManager.put("Windows.TitlePane.inactiveBackground", Color.BLUE);
            UIManager.put("Windows.TitlePane.inactiveForeground", Color.ORANGE);
        } else {
            UIManager.put("Windows.TitlePane.borderColor", Color.YELLOW);
            UIManager.put("Windows.TitlePane.background", Color.RED);
            UIManager.put("Windows.TitlePane.foreground", Color.CYAN);
            UIManager.put("Windows.TitlePane.inactiveBackground", Color.ORANGE);
            UIManager.put("Windows.TitlePane.inactiveForeground", Color.BLUE);
        }
        if (updateUI) updateLaf();
    }

    static void updateLaf() {
        for (final Window w : Window.getWindows()) {
            updateLafRecursively(w);
        }
    }

    private static void updateLafRecursively(final Window window) {
        for (final Window childWindow : window.getOwnedWindows()) {
            updateLafRecursively(childWindow);
        }
        SwingUtilities.updateComponentTreeUI(window);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            ExternalLafDecorator.instance().install();
            setLaf(UIManager.getSystemLookAndFeelClassName(), false);
            systemLaf = false;

            JFrame frame = new JFrame("Test Frame");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().setLayout(new GridBagLayout());
            frame.getContentPane().setPreferredSize(new Dimension(400, 400));
            JButton button = (JButton) frame.getContentPane().add(new JButton("I am a button!"));

            button.addActionListener(e -> {
                setLaf(systemLaf
                    ? UIManager.getSystemLookAndFeelClassName()
                    : UIManager.getCrossPlatformLookAndFeelClassName(), true);
                systemLaf = !systemLaf;
                button.setText(systemLaf ? "Change to System Laf" : "Change to cross platform Laf");
                button.getParent().doLayout();
            });

            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Let me quickly check whether it also works on macOS (with adjusted prooerty names of course). It also works on macOS.

Also as a convenience I wonder if there's a default color that matches the light/dark system.

Currently it tries to "match" the colors of installed LaF (at least as good as possible without making assumptions about the LaF). Based on the colors one could try to guess whether the colors are supposed to be light or dark, but that isn't really reliable and the resulting colors will have to be adjusted to be compatible with the LaF in question to not look out of place. What did you have in mind specifically here?

bric3 commented 2 years ago

What did you have in mind specifically here?

Oh your description is actually better that what I had in mind (I was suggesting tha default "color set" matching either the light / dark modes of the OS.

I don't have any windows installation, nor Linux but I'll also try to adapt your example on macOs. Thanks for sharing this!

weisJ commented 2 years ago

I have added a more convenient way to customize the titlebar colors:

ExternalLafDecorator.instance().setColorProvider(new DecorationsColorProvider() {

    @Override
    public Color backgroundColor() {
        return Color.CYAN;
    }

    @Override
    public Color activeForegroundColor() {
        return Color.ORANGE;
    }

    @Override
    public Color inactiveForegroundColor() {
        return Color.RED;
    }
});

There are more colors you can customize, but these three should suffice for the most use cases.

weisJ commented 2 years ago

Does the current state of the implementation work for you suffiently? If yes I would cut the next release.

bric3 commented 2 years ago

Ah sorry I'm between two "fires" and forgot to report back.

So I'm not sure if I'm doing this right but I got a NPE, because there's no value for the key borderSecondary.

Apr 05, 2022 11:33:37 AM com.formdev.flatlaf.util.LoggingFacadeImpl logSevere
SEVERE: FlatLaf: Failed to setup look and feel 'com.formdev.flatlaf.FlatIntelliJLaf'.
java.lang.NullPointerException
    at java.base/java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
    at java.base/java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
    at java.base/java.util.Properties.put(Properties.java:1301)
    at com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator.putOrCopy(ExternalLafDecorator.java:89)
    at com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator.installExtraProperties(ExternalLafDecorator.java:99)
    at com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator.lambda$new$0(ExternalLafDecorator.java:47)
    at java.desktop/java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:343)
    at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:335)
    at java.desktop/javax.swing.event.SwingPropertyChangeSupport.firePropertyChange(SwingPropertyChangeSupport.java:93)
    at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:268)
    at java.desktop/javax.swing.UIManager.setLookAndFeel(UIManager.java:602)
    at com.formdev.flatlaf.FlatLaf.setup(FlatLaf.java:117)
    at com.formdev.flatlaf.FlatIntelliJLaf.setup(FlatIntelliJLaf.java:39)
    at io.github.bric3.fireplace.FirePlaceMain$AppearanceControl.lambda$new$0(FirePlaceMain.java:225)
    at io.github.bric3.fireplace.FirePlaceMain$AppearanceControl.lambda$new$1(FirePlaceMain.java:240)
    at com.github.weisj.darklaf.platform.preferences.SystemPreferencesManager.lambda$onPreferenceChange$0(SystemPreferencesManager.java:55)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at com.github.weisj.darklaf.platform.preferences.SystemPreferencesManager.onPreferenceChange(SystemPreferencesManager.java:54)
    at com.github.weisj.darklaf.platform.preferences.SystemPreferencesManager.enableReporting(SystemPreferencesManager.java:100)
    at io.github.bric3.fireplace.FirePlaceMain$AppearanceControl.install(FirePlaceMain.java:262)
    at io.github.bric3.fireplace.FirePlaceMain.setupLaF(FirePlaceMain.java:203)
    at io.github.bric3.fireplace.FirePlaceMain.initUI(FirePlaceMain.java:74)
    at io.github.bric3.fireplace.FirePlaceMain.main(FirePlaceMain.java:67)
ExternalLafDecorator.instance().install();
ExternalLafDecorator.instance().setColorProvider(new DecorationsColorProvider() {
    @Override
    public Color backgroundColor() {
        return UIManager.getColor("TitlePane.background");
    }
    @Override
    public Color activeForegroundColor() {
        return UIManager.getColor("TitlePane.foreground");
    }
    @Override
    public Color inactiveForegroundColor() {
        return UIManager.getColor(" TitlePane.inactiveForeground");
    }
});

So I think the code should protect itself against null values, returned by the color provider.

Thank you very much for your patience !

bric3 commented 2 years ago

Also I'm not sure about how to change the window title color. (It is set like this new JFrame("FirePlace"))

image image

Shouldn't be orange with this code ?

    @Override
    public Color activeForegroundColor() {
        return Color.ORANGE;
    }
weisJ commented 2 years ago

So I think the code should protect itself against null values, returned by the color provider.

I can fallback to the default colors if null is returned. However it probably will look somewhat out of place.

Also I'm not sure about how to change the window title color. (It is set like this new JFrame("FirePlace"))

On macOS there actually isn't any way to change the color of the window title as it is painted by the OS. I have added to option to switch between light/dark title text by implementing DecorationsColorProvider::isDark.

bric3 commented 2 years ago

This looks way better, thanks. I noted two things

  1. I have my own color theme setting, however the setting has to be inverted, I think the isDark name could be renamed to something more speaking when reading the code isSystemWindowTitleDark

    @Override
    public boolean isDark() {
        return !Colors.isDarkMode();
    }
  2. The Window title is not the right color when I first start the application, once the LaF changes it works as expected though. I'll investigate.

    video showcasing the issue with system wide color change https://user-images.githubusercontent.com/803621/161760921-cee6875c-cbe7-4e81-b3dc-3c78e67ab57a.mov
    video showcasing the issue with interface toggle https://user-images.githubusercontent.com/803621/161761709-49f50d90-64a5-422a-ad02-aab125665175.mov
  3. It's a nitpick, but why not propose a default implementation for this method.

bric3 commented 2 years ago

On point 1 and 2

After looking at the code, I noticed this line

https://github.com/weisJ/darklaf/blob/cc3c62c375fffaa78d5f07118708d8a45c3d5370/platform-decorations/src/main/java/com/github/weisj/darklaf/platform/decorations/ExternalLafDecorator.java#L130

Actually affects this one.

https://github.com/weisJ/darklaf/blob/d7d08139ab6d56a11d0c9a3c61ed73e0d1564510/macos/src/main/java/com/github/weisj/darklaf/platform/macos/ui/MacOSDecorationsUtil.java#L64-L67

So when my implementation

@Override
public boolean isDark() {
    return !Colors.isDarkMode();
}

returns true from isDark, I actually get a dark text, and vice versa when returning false.

bric3 commented 2 years ago

Also I noticed these logs (running JDK 17), not sure it is related though.

2022-04-05 15:04:04.733 java[69308:1421889] Bad JNI lookup accessibilityHitTest
2022-04-05 15:04:04.734 java[69308:1421889] (
    0   libawt_lwawt.dylib                  0x0000000102ed720d -[CommonComponentAccessibility accessibilityHitTest:] + 173
    1   libawt_lwawt.dylib                  0x0000000102e920a0 -[AWTView accessibilityHitTest:] + 176
    2   AppKit                              0x00007ff81e780eca -[NSWindow(NSWindowAccessibility) accessibilityHitTest:] + 302
    3   AppKit                              0x00007ff81e31531b -[NSApplication(NSApplicationAccessibility) accessibilityHitTest:] + 285
    4   AppKit                              0x00007ff81e2e4cec CopyElementAtPosition + 136
    5   HIServices                          0x00007ff820f3979e _AXXMIGCopyElementAtPosition + 393
    6   HIServices                          0x00007ff820f5b084 _XCopyElementAtPosition + 355
    7   HIServices                          0x00007ff820f189b9 mshMIGPerform + 182
    8   CoreFoundation                      0x00007ff81b4c0294 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41
    9   CoreFoundation                      0x00007ff81b4c0174 __CFRunLoopDoSource1 + 619
    10  CoreFoundation                      0x00007ff81b4be7db __CFRunLoopRun + 2415
    11  CoreFoundation                      0x00007ff81b4bd7ac CFRunLoopRunSpecific + 562
    12  HIToolbox                           0x00007ff824144ce6 RunCurrentEventLoopInMode + 292
    13  HIToolbox                           0x00007ff824144913 ReceiveNextEventCommon + 283
    14  HIToolbox                           0x00007ff8241447e5 _BlockUntilNextEventMatchingListInModeWithFilter + 70
    15  AppKit                              0x00007ff81dee453d _DPSNextEvent + 927
    16  AppKit                              0x00007ff81dee2bfa -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1394
    17  libosxapp.dylib                     0x00000001023234aa -[NSApplicationAWT nextEventMatchingMask:untilDate:inMode:dequeue:] + 122
    18  AppKit                              0x00007ff81ded52a9 -[NSApplication run] + 586
    19  libosxapp.dylib                     0x0000000102323275 +[NSApplicationAWT runAWTLoopWithApp:] + 165
    20  libawt_lwawt.dylib                  0x0000000102ef7140 +[AWTStarter starter:headless:] + 496
    21  libosxapp.dylib                     0x0000000102324f5f +[ThreadUtilities invokeBlockCopy:] + 15
    22  Foundation                          0x00007ff81c333857 __NSThreadPerformPerform + 179
    23  CoreFoundation                      0x00007ff81b4bfaeb __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    24  CoreFoundation                      0x00007ff81b4bfa53 __CFRunLoopDoSource0 + 180
    25  CoreFoundation                      0x00007ff81b4bf7cd __CFRunLoopDoSources0 + 242
    26  CoreFoundation                      0x00007ff81b4be1e8 __CFRunLoopRun + 892
    27  CoreFoundation                      0x00007ff81b4bd7ac CFRunLoopRunSpecific + 562
    28  libjli.dylib                        0x0000000101ef0132 CreateExecutionEnvironment + 386
    29  libjli.dylib                        0x0000000101eebc96 JLI_Launch + 1366
    30  java                                0x0000000101dc3c0a main + 394
    31  dyld                                0x000000011196351e start + 462
)
Exception in thread "AppKit Thread" java.lang.NoSuchMethodError: accessibilityHitTest
weisJ commented 2 years ago

Also I noticed these logs (running JDK 17), not sure it is related though. ...

I don't think this is related. At least I don't see anything which might cause it as the native portion of the implementation doesn't interface with accessibilityHitTest.

  1. I have my own color theme setting, however the setting has to be inverted, I think the isDark name could be renamed to something more speaking when reading the code isSystemWindowTitleDark

I have changed the api to make it clearer on how to change the title color. You can now specify windowTitleColor as either LIGHT, DARK or CUSTOM, wheras CUSTOM will force the color returned by ::activeForegroundColor to be used and LIGHT/DARK repsectively use the native colors (or use sane defaults based on ::activeForegroundColor on Windows). Let me know if this API is easier to reason about.

weisJ commented 2 years ago

As for point 2: The beginning of the first video is directly after application startup right? In the second video it breaks after checking "Sync appearance". I suspect there might be some bad interplay between system theme detection and the decorations. Is https://github.com/bric3/fireplace/pull/17 an up-to date version reproducing this behaviour?

bric3 commented 2 years ago

On each video I restarted the app. But I agree there's something fishy, when the sync is re-enabled.

Is https://github.com/bric3/fireplace/pull/17 an up-to date version reproducing this behaviour?

Yes, however it is based on the snapshot earlier today : https://oss.sonatype.org/content/repositories/snapshots/com/github/weisj/darklaf-platform-decorations/3.0.0-SNAPSHOT/darklaf-platform-decorations-3.0.0-20220405.121934-75.jar

bric3 commented 2 years ago

The API is nicer with the latest change, however the title color is still weird. Wether it syncs with the OS or from settings.

I'll push the latest commit.

bric3 commented 2 years ago

Hi there,

With the very latest snapshots (3.0.0-20220406.014333-77), I tried to install/uninstall the ExternalLafDecorator yet the title bar changes are is jumpy.

That made be realize there's a thin black border when the ExternalLafDecorator is active, in some occasions, and toggling the to dark theme, then light theme again makes this border disappear.

image image

Also I'm not sure about the buttons when inactive, they are not visible, or not even there. This seems to happen when os synchronization is active new SystemPreferencesManager().enableReporting(true)

And even for the out of color title, this seems related to when system reporting is active.

image
bric3 commented 2 years ago

Also I noticed these logs (running JDK 17), not sure it is related though. ...

I don't think this is related. At least I don't see anything which might cause it as the native portion of the implementation doesn't interface with accessibilityHitTest.

This seem to be related to these issues (running corretto 17.0.2.8.1)

weisJ commented 2 years ago

Hi there,

With the very latest snapshots (3.0.0-20220406.014333-77), I tried to install/uninstall the ExternalLafDecorator yet the title bar changes are is jumpy.

Could you elaborate what you mean by "jumpy".

That made be realize there's a thin black border when the ExternalLafDecorator is active, in some occasions, and toggling the to dark theme, then light theme again makes this border disappear.

I suppose the dark border is coupled to the color of the window title. If it is light (which is achieved by switching the window to dark mode) the border also becomes dark. If this is bothering you, you can use TitleColor.CUSTOM to force the window to be in light theme (i.e. it has a light border).

Also I'm not sure about the buttons when inactive, they are not visible, or not even there. This seems to happen when os synchronization is active new SystemPreferencesManager().enableReporting(true)

What do you mean by inactive? Inactive as in ExternalLafDecorator is uninstalled or inactive as in the window isn't the active window?

And even for the out of color title, this seems related to when system reporting is active.

image

Does it behave correctly without system reporting enabled (except maybe the first time)?

bric3 commented 2 years ago

With the very latest snapshots (3.0.0-20220406.014333-77), I tried to install/uninstall the ExternalLafDecorator yet the title bar changes are is jumpy.

Could you elaborate what you mean by "jumpy".

Sorry I should have dropped a god example and a video.

https://user-images.githubusercontent.com/803621/162060035-9776464f-d923-4e1b-9424-d71164adc321.mov

The sync checkbox action is basically calling this method, and in this experience I tried to install custom decorations when os reporting is off and vice versa.

        void toggleSync(boolean sync) {
            try {
                manager.enableReporting(sync);
+               if (sync) {
+                   ExternalLafDecorator.instance().uninstall();
+               } else {
+                   ExternalLafDecorator.instance().install();
+               }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

Note this code change is only for this experiment. All other remarks are based on code like this

https://github.com/bric3/fireplace/pull/17/files#diff-926a8863ccf9519490252d2b47c70a1cdcaf401e59b66c977ce13cbe74a26db4R237-R265


Ok after having the time to dig it a bit I noticed an issue in my preference detector, it seems that in some specific circumstances my darkMode boolean got inverted, I've changed that and this fixes the three issues mentioned above (the darker border, the inactive buttons, and the window title.

Thanks for your "insistance" on this, I was able to nail the thing.

So for me everything looks good now, thank you very much !!!

bric3 commented 2 years ago

Maybe one thing that might be worth documenting is that installing the platform decorations make these properties ignored, it is expected since it is no the same UI component.

final JRootPane rootPane = frame.getRootPane();
if (SystemInfo.isMacOS) {
    // allows to place swing components on the whole window
    rootPane.putClientProperty("apple.awt.fullWindowContent", true);
    // makes the title bar transparent
    rootPane.putClientProperty("apple.awt.transparentTitleBar", true);
}

In a previous experiment I used these to have some UI components in the title bar.

https://user-images.githubusercontent.com/803621/162071344-5f84128f-2f90-4b3b-bf88-ba8d4ca45d4e.mov

But here as you see the color of window title is not updated

weisJ commented 2 years ago

Could you elaborate what you mean by "jumpy".

Sorry I should have dropped a god example and a video.

Screen.Recording.2022-04-06.at.21.54.40.mov The sync checkbox action is basically calling this method, and in this experience I tried to install custom decorations when os reporting is off and vice versa.

        void toggleSync(boolean sync) {
            try {
                manager.enableReporting(sync);
+               if (sync) {
+                   ExternalLafDecorator.instance().uninstall();
+               } else {
+                   ExternalLafDecorator.instance().install();
+               }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

This is expected behaviour when installing/uninstalling the decorations. Due to the nature that it changes the underlying window the "jumps" are somewhat inevitable.

Ok after having the time to dig it a bit I noticed an issue in my preference detector, it seems that in some specific circumstances my darkMode boolean got inverted, I've changed that and this fixes the three issues mentioned above (the darker border, the inactive buttons, and the window title. ... So for me everything looks good now, thank you very much !!!

Thats great to hear :)

Maybe one thing that might be worth documenting is that installing the platform decorations make these properties ignored, it is expected since it is no the same UI component.

I think I can reasonably replicate the behaviour of those two properties.

bric3 commented 2 years ago

Maybe one thing that might be worth documenting is that installing the platform decorations make these properties ignored, it is expected since it is no the same UI component.

I think I can reasonably replicate the behaviour of those two properties.

That would be nice to have a proper API for that. So it's possible to benefit from both worlds.

weisJ commented 2 years ago

With the latest snapshot using the properties should be possible (I hope).

Edit: Due to failing CI I suppose there is a regression and it won’t work as expected at all.

bric3 commented 2 years ago

That's curious the Linux and Windows failures seem unrelated.

weisJ commented 2 years ago

Should be fixed now

bric3 commented 2 years ago

Excellent ! it works really well !

I just noted a simple issue upon first launch, the orange border does not seem to be correctly rendered / visible at the beginning of the app, however when toggling stuff it comes back. That's not a big issue though.

https://user-images.githubusercontent.com/803621/162460575-6e0a4f3e-1365-4534-b365-ab2e2ceef8db.mov

Really awesome work !

weisJ commented 2 years ago

Does it appear if you force the whole window to be repainted? This might be a situation where the titlebar doesn't yet know that it is visible when painting (i.e. try to call SwingUtilities.invokeLater(() -> frame.repaint()) after installing the laf).

bric3 commented 2 years ago

Actually I'm installing the laf and decorations before creating the JFrame, and before frame.setVisible(true).

I'll investigate.

bric3 commented 2 years ago

So I just discovered the DecorationsColorProvider isn't called if UIManager::setLookAndFeel() or if the look and feel is com.apple.laf.AquaLookAndFeel. I was expecting this wasn't called if updateUI isn't called.

So once I got this using nimbus this gave this, it seems the border is painted over.

image

As for my app I still don't quite get what going on. It seems that on bootstrap the border is painted first, then the other component are painted over. And once I click on the buttons, the border is painted on the top.

https://user-images.githubusercontent.com/803621/162544196-e0d781ec-db90-4624-b40c-19949213b310.mov

I'm not sure this a bug per se but a behavior I'm not familiar with.

bric3 commented 2 years ago

I've cooked have simple reproducer. Basically when the components are added to the frame they cover the border, but when refreshing the LaF, the border stays on top.

Single class reproducer code ```java import com.github.weisj.darklaf.platform.decorations.DecorationsColorProvider; import com.github.weisj.darklaf.platform.decorations.ExternalLafDecorator; import com.github.weisj.darklaf.platform.preferences.SystemPreferencesManager; import com.github.weisj.darklaf.theme.spec.ColorToneRule; import javax.swing.*; import java.awt.*; public class BorderMain { private static final SystemPreferencesManager manager = new SystemPreferencesManager(); public static final Runnable SYNC_THEME_CHANGER = () -> { dark = manager.getPreferredThemeStyle().getColorToneRule() == ColorToneRule.DARK; setLaF(); updateUI(); }; private static void setLaF() { try { UIManager.setLookAndFeel(new javax.swing.plaf.metal.MetalLookAndFeel()); } catch (UnsupportedLookAndFeelException e) { throw new RuntimeException(e); } } static { manager.addListener(style -> SYNC_THEME_CHANGER.run()); } private static boolean dark = false; public static void main(String[] args) { System.setProperty("apple.awt.application.name", "Border"); System.setProperty("apple.awt.application.appearance", "system"); SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("Border"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(new Dimension(1000, 600)); frame.getContentPane().add(getComp()); var rootPane = frame.getRootPane(); rootPane.putClientProperty("apple.awt.fullWindowContent", true); rootPane.putClientProperty("apple.awt.transparentTitleBar", true); ExternalLafDecorator.instance().install(); ExternalLafDecorator.instance().setColorProvider(new DecorationsColorProvider() { @Override public Color backgroundColor() { return dark ? Color.BLACK : Color.WHITE; } @Override public Color activeForegroundColor() { return UIManager.getColor("TitlePane.foreground"); } @Override public Color inactiveForegroundColor() { return Color.RED; } @Override public TitleColor windowTitleColor() { return dark ? TitleColor.LIGHT : TitleColor.DARK; } }); SYNC_THEME_CHANGER.run(); manager.enableReporting(true); frame.setVisible(true); }); } private static JComponent getComp() { var topPanel = new JPanel(new BorderLayout()); var textField = new JTextField(""); textField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(Color.BLUE, 1), BorderFactory.createEmptyBorder(30, 5, 5, 5) )); textField.setEditable(false); textField.setBackground(Color.PINK); topPanel.add(textField, BorderLayout.CENTER); topPanel.setBackground(Color.DARK_GRAY); var control = getAppearanceControls(); control.setBorder(BorderFactory.createLineBorder(Color.PINK, 1)); control.setBackground(Color.GRAY); topPanel.add(control, BorderLayout.EAST); var content = new JPanel(new BorderLayout()); content.setBackground(Color.GRAY); var panel = new JPanel(new BorderLayout()); panel.add(topPanel, BorderLayout.NORTH); panel.add(content, BorderLayout.CENTER); return panel; } static JComponent getAppearanceControls() { var appearanceModeButton = new JCheckBox("L/D"); { appearanceModeButton.addActionListener(e -> { dark = !dark; setLaF(); updateUI(); }); appearanceModeButton.setEnabled(false); appearanceModeButton.setVisible(true); } var syncAppearanceButton = new JCheckBox("Sync appearance"); { syncAppearanceButton.addActionListener(e -> { manager.enableReporting(syncAppearanceButton.isSelected()); appearanceModeButton.setEnabled(!syncAppearanceButton.isSelected()); SYNC_THEME_CHANGER.run(); }); syncAppearanceButton.setSelected(true); syncAppearanceButton.setVisible(true); } var appearanceControlsPanel = new JPanel(new FlowLayout()); appearanceControlsPanel.add(appearanceModeButton); appearanceControlsPanel.add(syncAppearanceButton); return appearanceControlsPanel; } public static void updateUI() { for (var window : Window.getWindows()) { SwingUtilities.updateComponentTreeUI(window); } } } ```
weisJ commented 2 years ago

I realised that the correct behaviour should be that no border is painted at all in this case (specifying that the titlebar ist transparent should make it non-visible). Latest snapshot resolve the issue.

bric3 commented 2 years ago

Works for me, on my side there's nothing against publishing v3 :)

Quick question, if this is possible to get the height of the title bar or the button location. If I want to place a menu on the left to the titlebar buttons (close, minimize, expand), or if I want to put something under. At this time I'm settings my borders with magic numbers that I found by guess.

weisJ commented 2 years ago

I think I can expose the bounds of the buttons somehow. I’ll probably also add an easy way to hide the title as well (without setting the title to "", which would cripple accessibility), as otherwise one has to respect that one too which might be difficult.

bric3 commented 2 years ago

Nice, although don't block the V3 for that it can wait. I mean if you're keen on releasing V3 sooner rather than later.

weisJ commented 2 years ago

I scheduled the release of V3 for this weekend so I had some time on my hands. You can check the bound of the title buttons using ExternalLafDecorator.instance().decorationsManager().titlePaneLayout(frame).windowButtonRect(). Also you can hide the title of the window by setting rootPane.putClientProperty(DecorationsConstants.KEY_HIDE_TITLE, true)

bric3 commented 2 years ago

Cool!

I just gave these a try

weisJ commented 2 years ago
  • Hiding window title, it does work ✅ But the property is inverted DecorationsConstants.KEY_HIDE_TITLE to false will hide the title.

Fixed

After setting the frame visible there's no issue. But this happesn really fast so that seem ok. Maybe throwing a IllegalStateEx could suggest a dev to use that API after the frame is visible.

An exception is now thrown with a more descriptive error message. You can actually force it to succeed by calling frame.addNotify() before calling it if you really need it before the window is visible. But I would go for something like this

class TitleBarSpace extends JComponent {
    public Dimension getPreferredSize() {
        ExternalLafDecorator.instance().decorationsManager().titlePaneLayout(frame).windowButtonRect().getSize();
    }

    public Dimension getMinimumSize() {
        return getPreferredSize();
    }

    public Dimension getPreferredSize() {
        return getPreferredSize();
    }
}

and using it as a spacer in e.g. a `BorderLayout or BoxLayout.

bric3 commented 2 years ago

Interesting. I'll give it a try.

bric3 commented 2 years ago

That works, however the location (x in particular for my case) of the rectangle must taken into account. I suppose on Linux or Windows the button rectangle is on the other side, which require a different handling.

weisJ commented 2 years ago

You are right. I overlooked that fact :D I‘ll release the next version later today