Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NETBEANS-3592] Workarounds for JDK Windows LAF bugs on HiDPI screens #1777

Merged
merged 3 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ public static void initCustomFontSize (int uiFontSize) {
switchFont("windowTitleFont", fontTranslation, uiFontSize, nbDialogBold); // NOI18N
switchFont("Spinner.font", fontTranslation, uiFontSize, nbDialogPlain); // NOI18N
switchFont("FormattedTextField.font", fontTranslation, uiFontSize, nbDialogPlain); // NOI18N
switchFont("Slider.font", fontTranslation, uiFontSize, nbDialogPlain); // NOI18N
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Should add // NOI18Ns to strings of the other files that are not expected translation as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

}

//#144402
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.awt.Insets;
import java.awt.geom.AffineTransform;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.border.MatteBorder;

/**
Expand All @@ -34,8 +35,7 @@
* @author Eirik Bakke ([email protected])
*/
final class DPISafeBorder implements Border {
private final Insets insets;
private final Color color;
private final Border delegate;

/**
* Create a new instance with the same semantics as that produced by
Expand All @@ -44,16 +44,26 @@ final class DPISafeBorder implements Border {
* @param color may not be null
*/
public static Border matte(int top, int left, int bottom, int right, Color color) {
return new DPISafeBorder(new Insets(top, left, bottom, right), color);
return fromDelegate(new MatteBorder(new Insets(top, left, bottom, right), color));
}

private DPISafeBorder(Insets insets, Color color) {
if (insets == null)
public static Border fromDelegate(Border delegate) {
if (delegate == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use curly braces({}) although existing code doesn't use them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my humble opinion is that we shall avoid throwing NullPointerExceptions directly from the code. It would be better to do an assert <whatever> != null; instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add curly braces... I keep forgetting the code style. (The previous code was mine, too.)

Assertions can be disabled, which make them more or less useless for enforcing invariants--no way to know down the line whether they were enabled or not (e.g. when reading a log file while trying to debug an intermittent problem down the road).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - I tried to run netbeans with substance laf (or one of its siblings) and the author decided to enforce the "things interacting with GUI have to run on the EDT" rule. I ended badly (netbeans tries to do to much work off the EDT) and if Swing would have enforced the rule, netbeans could not have gone down the path in the first place. Now it is difficult to get rid of this.

The Code can be made better readable by using java.util.Objects#requireNonNull, then the missing braces will also not be a problem :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, we can also use @NonNull to make "non-null" clear ? e.g. public static Border fromDelegate(@NonNull Border delegate)

Copy link
Contributor

@matthiasblaesing matthiasblaesing Dec 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the annotations we'd need a dependency on Common Annotations. That module causes headaches: #1764. DPISafeBorder#fromDelegate is a method, that is only called from within the module, so I would not put to much API thoughts into it.

throw new NullPointerException();
if (color == null)
if (delegate instanceof MatteBorder) {
return new DPISafeBorder(delegate);
} else if (delegate instanceof LineBorder && !((LineBorder) delegate).getRoundedCorners()) {
return new DPISafeBorder(delegate);
} else {
return delegate;
}
}

private DPISafeBorder(Border delegate) {
if (delegate == null) {
throw new NullPointerException();
this.insets = new Insets(insets.top, insets.left, insets.bottom, insets.right);
this.color = color;
}
this.delegate = delegate;
}

@Override
Expand All @@ -78,11 +88,15 @@ are identical, and always integral (whole number) values. */
{
// HiDPI scaling is active.
scale = tx.getScaleX();
/* Round the starting (top-left) position up and the end (bottom-right) position down,
to ensure we are painting the border in an area that will not be painted over by an
adjacent component. */
int deviceX = (int) Math.ceil(tx.getTranslateX());
int deviceY = (int) Math.ceil(tx.getTranslateY());
/* To be completely safe from overpainting the previous adjacent component, we would
probably need to round up here. But for borders to work properly on JTextField, we
must round down. And it seems to work fine in the
EDITOR_TAB_CONTENT_BORDER/VIEW_TAB_CONTENT_BORDER cases as well. */
int deviceX = (int) tx.getTranslateX();
int deviceY = (int) tx.getTranslateY();
/* Rounding down here should guarantee that we do not paint in an area that will be
painted over by the next adjacent component. Rounding up, or to the nearest integer,
is confirmed to cause problems. */
int deviceXend = (int) (tx.getTranslateX() + width * scale);
int deviceYend = (int) (tx.getTranslateY() + height * scale);
deviceWidth = deviceXend - deviceX;
Expand All @@ -95,21 +109,38 @@ are identical, and always integral (whole number) values. */
deviceWidth = width;
deviceHeight = height;
}
final int deviceLeft = deviceBorderWidth(scale, insets.left);
final int deviceRight = deviceBorderWidth(scale, insets.right);
final int deviceTop = deviceBorderWidth(scale, insets.top);
final int deviceBottom = deviceBorderWidth(scale, insets.bottom);
final Insets insets = getBorderInsets(c);
// Thicknesses of respective borders, in device pixels.
final int dtLeft, dtRight, dtTop, dtBottom;

final Color color;
if (delegate instanceof MatteBorder) {
color = ((MatteBorder) delegate).getMatteColor();
dtLeft = deviceBorderWidth(scale, insets.left);
dtRight = deviceBorderWidth(scale, insets.right);
dtTop = deviceBorderWidth(scale, insets.top);
dtBottom = deviceBorderWidth(scale, insets.bottom);
} else if (delegate instanceof LineBorder) {
LineBorder lineBorder = (LineBorder) delegate;
color = lineBorder.getLineColor();
final int dt = deviceBorderWidth(scale, lineBorder.getThickness());
dtLeft = dt;
dtRight = dt;
dtTop = dt;
dtBottom = dt;
} else {
throw new RuntimeException("Expected either a MatteBorder or LineBorder delegate");
}
g.setColor(color);

// Top border.
g.fillRect(0, 0, deviceWidth - deviceRight, deviceTop);
g.fillRect(0, 0, deviceWidth - dtRight, dtTop);
// Left border.
g.fillRect(0, deviceTop, deviceLeft, deviceHeight - deviceTop);
g.fillRect(0, dtTop, dtLeft, deviceHeight - dtTop);
// Bottom border.
g.fillRect(deviceLeft, deviceHeight - deviceBottom, deviceWidth - deviceLeft, deviceBottom);
g.fillRect(dtLeft, deviceHeight - dtBottom, deviceWidth - dtLeft, dtBottom);
// Right border.
g.fillRect(deviceWidth - deviceRight, 0, deviceRight, deviceHeight - deviceBottom);
g.fillRect(deviceWidth - dtRight, 0, dtRight, deviceHeight - dtBottom);

g.setTransform(oldTransform);
g.setColor(oldColor);
Expand All @@ -123,7 +154,7 @@ private int deviceBorderWidth(double scale, int logical) {

@Override
public Insets getBorderInsets(Component c) {
return new Insets(insets.top, insets.left, insets.bottom, insets.right);
return delegate.getBorderInsets(c);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@

package org.netbeans.swing.plaf.windows8;

import java.awt.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.ColorUIResource;
import org.netbeans.swing.plaf.LFCustoms;
import org.netbeans.swing.plaf.util.GuaranteedValue;
import org.netbeans.swing.plaf.util.UIBootstrapValue;
import org.netbeans.swing.plaf.util.UIUtils;

Expand Down Expand Up @@ -54,18 +63,37 @@ public final class Windows8LFCustoms extends LFCustoms {

static final String SCROLLPANE_BORDER_COLOR = "scrollpane_border"; //NOI18N

private static final String[] DEFAULT_GUI_FONT_PROPERTIES = new String[] {
"TitledBorder.font", "Slider.font", "PasswordField.font", "TableHeader.font", "TextPane.font",
"ProgressBar.font", "Viewport.font", "TabbedPane.font", "List.font", "CheckBox.font",
"Table.font", "ScrollPane.font", "ToggleButton.font", "Panel.font", "RadioButton.font",
"FormattedTextField.font", "TextField.font", "Spinner.font", "Button.font", "EditorPane.font",
"Label.font", "ComboBox.font", "Tree.font", "TextArea.font"};

private static final String[] ADJUST_HIDPI_BORDERS = new String[] {
"TextField.border", "PasswordField.border", "FormattedTextField.border",
"ScrollPane.border"};

@Override
public Object[] createLookAndFeelCustomizationKeysAndValues() {
/* Don't try to fetch this font size from the LAF; it won't work reliably for HiDPI
configurations (see a related workaround below). */
int fontsize = 11;
Integer in = (Integer) UIManager.get(CUSTOM_FONT_SIZE); //NOI18N
if (in != null) {
fontsize = in.intValue();
}

Object[] result = new Object[] {
//Work around a bug in windows which sets the text area font to
//"MonoSpaced", causing all accessible dialogs to have monospaced text
"TextArea.font", new GuaranteedValue ("Label.font", new Font("Dialog", Font.PLAIN, fontsize)),
//Work around a bug in windows which sets the text area font to
//"MonoSpaced", causing all accessible dialogs to have monospaced text
Font textAreaFont = UIManager.getFont("Label.font");
if (textAreaFont == null) {
textAreaFont = new Font("Dialog", Font.PLAIN, fontsize);
} else {
textAreaFont = textAreaFont.deriveFont((float) fontsize);
}

Object[] constants = new Object[] {
"TextArea.font", textAreaFont,

EDITOR_ERRORSTRIPE_SCROLLBAR_INSETS, new Insets(17, 0, 17, 0),

Expand All @@ -84,7 +112,82 @@ EDITOR_ERRORSTRIPE_SCROLLBAR_INSETS, new Insets(17, 0, 17, 0),
apps. Fixing that would be a bigger job, though (replacing WindowsPopupMenuSeparatorUI
to override getPreferredSize). */
};
return result;
List<Object> result = new ArrayList<>();
result.addAll(Arrays.asList(constants));

/* Workaround for Windows LAF JDK bug that causes fonts to appear at the wrong size on
certain configurations involving HiDPI monitors. Currently, WindowsLookAndFeel derive
font properties such as Label.font from the Windows API call
GetStockObject(DEFAULT_GUI_FONT), which appears to be unreliable when HiDPI display
configurations are changed without logging out of Windows and back in again (as may
frequently happen, for instance, when an external monitor is connected or
disconnected). See the "win.defaultGUI.font" property in WindowsLookAndFeel and
java.desktop/windows/native/libawt/windows/awt_DesktopProperties.cpp . */
/* If a custom font size is set, the font sizes have already been set in
AllLFCustoms.initCustomFontSize. */
if (UIManager.get(CUSTOM_FONT_SIZE) == null) {
/* Use the same logic as in AllLFCustoms.switchFont, since it seems to take some special
precautions (e.g. using deriveFont instead of FontUIResource for the Windows case). */
Map<Font,Font> fontTranslation = new HashMap<>();
for (String uiKey : DEFAULT_GUI_FONT_PROPERTIES) {
if (uiKey.equals("TextArea.font")) {
// Skip this one; it was set earlier as part of an unrelated workaround.
continue;
}
Font oldFont = UIManager.getFont(uiKey);
if (oldFont == null) {
continue;
}
Font newFont = fontTranslation.get(oldFont);
if (newFont == null) {
newFont = oldFont.deriveFont((float) fontsize);
fontTranslation.put(oldFont, newFont);
}
result.add(uiKey);
result.add(newFont);
}
}

if (WindowsDPIWorkaroundIcon.isWorkaroundRequired()) {
// Use entrySet rather than keySet to actually get all values.
for (Map.Entry<Object,Object> entry : UIManager.getDefaults().entrySet()) {
Object key = entry.getKey();
/* Force loading of lazily loaded values, so we can see if the actual implementation
type is of the kind that needs to be patched. All currently known icon properties
are suffixed "icon" or "Icon". All but one is of the kind that needs to be
patched. */
Object value = key.toString().toLowerCase(Locale.ROOT).endsWith("icon")
? UIManager.getDefaults().get(key) : null;
if (value == null) {
continue;
}
String valueCN = value.getClass().getName();
if (value instanceof Icon &&
(valueCN.startsWith("com.sun.java.swing.plaf.windows.WindowsIconFactory$") ||
valueCN.startsWith("com.sun.java.swing.plaf.windows.WindowsTreeUI$")) &&
/* This particular one can't be used as a delegate, as it intentionally behaves
differently when the application has overridden UIDefaults. */
!valueCN.contains("VistaMenuItemCheckIcon"))
{
result.add(key);
result.add(new WindowsDPIWorkaroundIcon((Icon) value));
}
}
}
/* Workaround for ugly borders on fractional HiDPI scalings (e.g. 150%), including
NETBEANS-338. It would have been nice to fix this for JComboBox as well, but that one does
not use the borders from UIManager. */
for (String key : ADJUST_HIDPI_BORDERS) {
Object value = UIManager.getDefaults().get(key);
if (value instanceof Border) {
Border adjustedBorder = DPISafeBorder.fromDelegate((Border) value);
if (adjustedBorder != value) {
result.add(key);
result.add(adjustedBorder);
}
}
}
return result.toArray();
}

@Override
Expand Down
Loading