kirill-grouchnikov / radiance

Building modern, elegant and fast Swing applications
BSD 3-Clause "New" or "Revised" License
806 stars 89 forks source link

Scaled Fonts and Icons should be supported for Ribbons #368

Closed dpolivaev closed 2 years ago

dpolivaev commented 3 years ago

Version of Radiance (latest release is 5.0.0)

5.0.0

Sub-project (Common, Animation, Theming, Component, ...)

Component

Version of Java (current minimum is 9)

11

Version of OS

Linux Mint 20.1

The issue you're experiencing (expected vs actual, screenshot, stack trace etc)

I develop Open Source Mind Map Editor Freeplane, https://sourceforge.net/projects/freeplane/ https://github.com/freeplane/freeplane

Currently, we are evaluating if we can improve its usability by replacing old school menus and toolbars with ribbon UI.

To support HiDPI monitors Freeplane lets user to set its monitor size and replaces all fonts by fonts with bigger sizes. The sizes are calculated from monitor size entered by a user and monitor resolution available from graphics environment. This solution works on all OS (Linux, MacOS, Windows) and with all Java versions. Java's own HiDpi support introduced with Java 9 for Linux and Windows seems to support only integer number scaling factors (1, 2 or 3). The solution used in Freeplane calculates all font sizes so that effectively it allows more steps in between.

So I tried to check how the latest Radiance Ribbon works with changed UI fonts by modifying BasicCheckRibbon from the component demo used as a starting point.

Unscaled fonts looked very small:

UnscaledFonts

At first I fixed all Java UI fonts similar to Freeplane as follows:

    private static void scaleDefaultUIFonts(double scalingFactor) {
        Set<Object> keySet = UIManager.getLookAndFeelDefaults().keySet();
        Set<Font> scaledFonts = new HashSet<>();
        Object[] keys = keySet.toArray(new Object[keySet.size()]);
        final UIDefaults uiDefaults = UIManager.getDefaults();
        final UIDefaults lookAndFeelDefaults = UIManager.getLookAndFeel().getDefaults();
        for (Object key : keys) {
            if (isFontKey(key)) {
                Font font = uiDefaults.getFont(key);
                if (font != null && ! scaledFonts.contains(font)) {
                    font = scaleFontInt(font, scalingFactor);
                    UIManager.put(key, font);
                    lookAndFeelDefaults.put(key, font);
                    scaledFonts.add(font);
                }
            }
        }
    }
    private static boolean isFontKey(Object key) {
        return key != null && key.toString().toLowerCase().endsWith("font");
    }
    public static Font scaleFontInt(Font font, double additionalFactor) {
        return font.deriveFont(font.getStyle(), Math.round(font.getSize2D() * additionalFactor));
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            scaleDefaultUIFonts(1.6);
            JFrame.setDefaultLookAndFeelDecorated(true);
            RadianceThemingCortex.GlobalScope.setSkin(new GeminiSkin());

Some fonts became scaled, but not all of them: SomeFontsScaled

I discovered that font sizes of the other fonts are controlled by font policies and tried to add

      RadianceThemingCortex.GlobalScope.setFontPolicy(RadianceCommonCortex.getScaledFontPolicy(1.6f));

at the end of the last code fragment.

In this configuration, Radiance threw an exception

Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Inconsistent preferred widths
Ribbon band 'Document' has the following resize policies
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$Mirror with preferred width 586
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$Mid2Low with preferred width 298
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Mid with preferred width 183
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Low with preferred width 104
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$IconRibbonBandResizePolicy with preferred width 111
org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Low with pref width 104 is followed by resize policy org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$IconRibbonBandResizePolicy with larger pref width

I tried to analyze the code and found that Command Button Preferred Sizes are calculated using hardcoded numbers e.g.

    @Override
    public int getPreferredIconSize(JCommandButton commandButton) {
        return ComponentUtilities.getScaledSize(32, commandButton.getFont().getSize(), 2.0f, 4);
    }

It means that currently icon and font sizes can only be scaled by factor of 2 under Linux using -Dsun.java2d.uiScale=2, but fractional scaling is not supported for Linux and MacOS, only for Windows.

Are there chances it could be improved on the radiance side? Could we work on this issue together?

Best regards, Dimitry

kirill-grouchnikov commented 3 years ago

Development of version 6.0 starts tomorrow. Here's what I'm going to start with on this issue.

I'll add a slider to some of the component demos (command buttons, command button strips, and the main ribbon demo). The slider will change the font policy with RadianceCommonCortex.getScaledFontPolicy on a sliding scale from 1.0 to let's say 3.0, with stops at every 0.1. Whatever layout bugs get found along the way, will be addressed starting from the lowest level - the command buttons, and then going "up the stack" so to speak, to command button strips, command button panels and the various parts of the ribbon.

kirill-grouchnikov commented 3 years ago

Going to start by addressing this part:

    @Override
    public int getPreferredIconSize(JCommandButton commandButton) {
        return ComponentUtilities.getScaledSize(32, commandButton.getFont().getSize(), 2.0f, 4);
    }

It means that currently icon and font sizes can only be scaled by factor of 2 under Linux using -Dsun.java2d.uiScale=2, but fractional scaling is not supported for Linux and MacOS, only for Windows.

This is not the logic of ComponentUtilities.getScaledSize. Here is a small demo loop to illustrate the intended logic:

    public static void main(String[] args) {
        for (int fontSize = 13; fontSize < 32; fontSize++) {
            System.out.println(fontSize + " --> " +
                    ComponentUtilities.getScaledSize(32, fontSize, 2.0f, 4));
        }
    }

and it prints:

13 --> 32
14 --> 32
15 --> 32
16 --> 32
17 --> 32
18 --> 32
19 --> 32
20 --> 40
21 --> 40
22 --> 40
23 --> 40
24 --> 48
25 --> 48
26 --> 48
27 --> 48
28 --> 56
29 --> 56
30 --> 56
31 --> 56

The meaning of the last two parameters - scaleFactor and stepQuantizationSize - is that for every stepQuantizationSize increases in the base font size, the value increases by scaleFactor. So in this particular case, for every increase of 4 units in font size, the result will grow by 8 units (2 times 4).

There is also a built-in "magical" constant of 16 units of font size, which is considered to be the base font size from which the quantized scaling kicks in - which is why you see value of 32 returned for all font sizes under 16.

Now, the intent here is to scale the icons along with the font sizes, but not for every single increase in the base font size, and that is very much an intentional decision on my side. It aims to address two specific points:

  1. Slight differences between default font sizes on different OSes. I don't have the full list, but it can go from 11 on older versions of Windows, to 13 on macOS. I do not want the default icon sizes to be slightly different for these slight differences.
  2. Most design systems, icons included, do have a certain grid under which they may look better. In this particular case, I choose the grid of 8 units as the quantized points for scaling the icons as the fonts get bigger.

If your expectation from Radiance component content is that every icon grows slightly for every point increase in font sizes, I do not see making this change on my side.

kirill-grouchnikov commented 3 years ago

The higher-level exception with this message:

Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Inconsistent preferred widths
Ribbon band 'Document' has the following resize policies
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$Mirror with preferred width 586
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$Mid2Low with preferred width 298
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Mid with preferred width 183
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Low with preferred width 104
    org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$IconRibbonBandResizePolicy with preferred width 111
org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$High2Low with pref width 104 is followed by resize policy org.pushingpixels.radiance.component.api.ribbon.resize.CoreRibbonResizePolicies$IconRibbonBandResizePolicy with larger pref width

is an interesting case. It is an indicator that Radiance should limit how wide an iconified ribbon band representation should be. So when it goes to the iconified state and is represented by a single popup button, that button can get too wide - depending on the band title and the font size. I'll add some logic in there to limit that width, even at the expense of cutting off the band title.

kirill-grouchnikov commented 3 years ago

The crash in checkResizePoliciesConsistency has been addressed

dpolivaev commented 3 years ago

Now, the intent here is to scale the icons along with the font sizes, but not for every single increase in the base font size, and that is very much an intentional decision on my side. If your expectation from Radiance component content is that every icon grows slightly for every point increase in font sizes, I do not see making this change on my side.

I do not understand why icons provided with the app as vector graphics can not scale with fonts and why a step of 4 or a step of 8 is needed. I think you could easily make the step configurable. However, I think it is not as important as the scalable font sizes.

dpolivaev commented 3 years ago

The crash in checkResizePoliciesConsistency has been addressed

I confirm that the modified test ribbon works as expected with scales 1.6 and 2 now

dpolivaev commented 3 years ago

I have also checked the slider. It does not affect fonts in the drop-down box so that I need to change also Fonts of Java's own components to have consistent font sizes. It is not a bug, rather a consequence of the current component mix.

kirill-grouchnikov commented 3 years ago

Which drop-down box?

dpolivaev commented 3 years ago

It happens only if I call scaleDefaultUIFonts from https://github.com/kirill-grouchnikov/radiance/issues/368#issue-1046737488 with an argument different from 1, and I do not understand why it changes the behavior when scaling is applied. But it looks like this step is not needed any longer because Radiance updates all UI fonts consistently.

kirill-grouchnikov commented 3 years ago

In the last few years I keep moving further and further away from UIManager as the global configuration manager. It has outlived its usefulness in terms of letting application code configuring some aspects of the look-and-feel without an explicit "tie" to that look-and-feel as a dependency.