Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
110 changes: 106 additions & 4 deletions vcell-client/src/main/java/cbit/vcell/client/VCellLookAndFeel.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@

import java.awt.Font;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;

import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.plaf.FontUIResource;

import cbit.vcell.resource.ResourceUtil;
import org.apache.logging.log4j.LogManager;
Expand All @@ -24,7 +28,101 @@

public class VCellLookAndFeel {
private final static Logger lg = LogManager.getLogger(VCellLookAndFeel.class);


/**
* Multiplies every font the look and feel supplies. 1.0 (the default) leaves the UI exactly
* as it was.
* <p>
* This only reaches fonts obtained <em>implicitly</em>, from the look and feel's defaults. A
* component whose font was set explicitly - {@code setFont(new Font("Dialog", PLAIN, 12))} -
* keeps that font, because a plain {@code Font} is not a {@link javax.swing.plaf.UIResource}
* and the look and feel will not replace it. Those sites have to be converted separately.
*/
public static final String PROPERTY_FONT_SCALE = "vcell.ui.fontScale";

private static final float MIN_FONT_SCALE = 0.5f;
private static final float MAX_FONT_SCALE = 4.0f;

/** Resolved once: the property cannot change while the client is running. */
private static volatile Float cachedFontScale = null;

/**
* @return the requested font scale, or 1.0 if unset, unparseable or out of range - a bad value
* here must never stop the client from starting.
*/
public static float getFontScale() {
Float scale = cachedFontScale;
if (scale == null) {
scale = computeFontScale();
cachedFontScale = scale;
}
return scale;
}

/**
* Scales a hard-coded pixel dimension that exists to fit text - a split-pane divider, a
* minimum size, a column width. Such a constant was chosen against the default font, so it
* has to move with the font or the text it was sized for no longer fits.
* <p>
* This is for dimensions that <em>bound text</em>. Do not use it for icon sizes, insets or
* borders, which should stay where they are.
*/
public static int scaleTextPixels(int pixels) {
return Math.round(pixels * getFontScale());
}

private static float computeFontScale() {
final String raw = System.getProperty(PROPERTY_FONT_SCALE);
if (raw == null || raw.trim().isEmpty()) {
return 1.0f;
}
final float scale;
try {
scale = Float.parseFloat(raw.trim());
} catch (NumberFormatException e) {
lg.warn("ignoring " + PROPERTY_FONT_SCALE + "='" + raw + "': not a number");
return 1.0f;
}
if (scale < MIN_FONT_SCALE || scale > MAX_FONT_SCALE) {
lg.warn("ignoring " + PROPERTY_FONT_SCALE + "=" + scale + ": outside ["
+ MIN_FONT_SCALE + ", " + MAX_FONT_SCALE + "]");
return 1.0f;
}
return scale;
}

/**
* Scales every {@link Font} in the look and feel's defaults, in place, before any window is
* built. Must run after {@code setLookAndFeel} (which replaces the whole defaults table) and
* before anything reads a font, because Swing components resolve their font once, at
* construction.
*/
private static void applyFontScale(float scale) {
if (scale == 1.0f) {
return;
}
final UIDefaults defaults = UIManager.getLookAndFeelDefaults();
// snapshot the keys: resolving a lazy value can add entries, and we are writing as we go
final List<Object> keys = new ArrayList<>(defaults.keySet());
int count = 0;
for (Object key : keys) {
final Object value;
try {
value = defaults.get(key); // resolves LazyValue / ActiveValue
} catch (Exception e) {
continue; // a defaults entry that cannot be resolved is not ours to fix
}
if (value instanceof Font) {
final Font font = (Font) value;
// FontUIResource, not Font: a plain Font would be treated as a user-set override
UIManager.put(key, new FontUIResource(font.deriveFont(font.getSize2D() * scale)));
count++;
}
}
lg.info("scaled " + count + " look-and-feel fonts by " + scale
+ " (" + PROPERTY_FONT_SCALE + ")");
}

public static Font defaultFont = null;
public static void setVCellLookAndFeel() {
OperatingSystemInfo osi = OperatingSystemInfo.getInstance();
Expand All @@ -40,10 +138,14 @@ public static void setVCellLookAndFeel() {
lg.warn("Error while setting look and feel:", e);
}
// }
// before anything reads a font: setLookAndFeel above replaced the defaults table, and the
// Mac block below derives from Label.font, so it inherits the scale rather than undoing it.
applyFontScale(getFontScale());

final boolean isMac = osi.isMac();
if (defaultFont == null) {
defaultFont = UIManager.getFont("Label.font");

if (defaultFont == null) {
defaultFont = UIManager.getFont("Label.font");
if (isMac) {
defaultFont = defaultFont.deriveFont(defaultFont.getSize2D() - 2);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.vcell.util.gui.VCellIcons;

import cbit.vcell.biomodel.BioModel;
import cbit.vcell.client.VCellLookAndFeel;
import cbit.vcell.client.constants.GuiConstants;
import cbit.vcell.client.desktop.DatabaseWindowPanel;
import cbit.vcell.client.desktop.biomodel.DocumentEditorTreeModel.DocumentEditorTreeFolderClass;
Expand Down Expand Up @@ -511,16 +512,17 @@ public boolean isPathEditable(TreePath path) {

JScrollPane treePanel = new javax.swing.JScrollPane(documentEditorTree);
leftSplitPane.setTopComponent(treePanel);
leftBottomTabbedPane.setMinimumSize(new java.awt.Dimension(198, 148));
leftBottomTabbedPane.setMinimumSize(new java.awt.Dimension(
VCellLookAndFeel.scaleTextPixels(198), VCellLookAndFeel.scaleTextPixels(148)));
leftSplitPane.setBottomComponent(leftBottomTabbedPane);
leftSplitPane.setResizeWeight(0.5);
leftSplitPane.setDividerLocation(300);
leftSplitPane.setDividerLocation(VCellLookAndFeel.scaleTextPixels(300));
leftSplitPane.setDividerSize(8);
leftSplitPane.setOneTouchExpandable(true);

rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
rightSplitPane.setResizeWeight(0.7);
rightSplitPane.setDividerLocation(400);
rightSplitPane.setDividerLocation(VCellLookAndFeel.scaleTextPixels(400));
rightSplitPane.setDividerSize(8);
rightSplitPane.setOneTouchExpandable(true);

Expand Down Expand Up @@ -562,11 +564,12 @@ public boolean isPathEditable(TreePath path) {
rightBottomTabbedPane.addTab(TAB_TITLE_OBJECT_PROPERTIES, rightBottomEmptyPanel);
rightBottomTabbedPane.addTab(TAB_TITLE_ANNOTATIONS, rightBottomEmptyAnnotationsPanel);
rightBottomTabbedPane.addTab(TAB_TITLE_PROBLEMS, issuePanel);
rightBottomTabbedPane.setMinimumSize(new java.awt.Dimension(198, 148));
rightBottomTabbedPane.setMinimumSize(new java.awt.Dimension(
VCellLookAndFeel.scaleTextPixels(198), VCellLookAndFeel.scaleTextPixels(148)));
rightSplitPane.setBottomComponent(rightBottomTabbedPane);

JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setDividerLocation(270);
splitPane.setDividerLocation(VCellLookAndFeel.scaleTextPixels(270));
splitPane.setOneTouchExpandable(true);
splitPane.setResizeWeight(0.3);
splitPane.setDividerSize(8);
Expand Down
Loading