GP-4154 - Theming - Fixed font issues; updated font usage with attributes

This commit is contained in:
dragonmacher 2024-02-23 13:13:06 -05:00
parent c5bad0a88f
commit b586d65a3b
91 changed files with 1309 additions and 1191 deletions

View file

@ -34,7 +34,8 @@ color.fg.tree.selected = [color]system.color.fg.selected.view
// Fonts
font.standard = [font]system.font.control
font.monospaced = monospaced-PLAIN-12
font.standard.bold = font.standard[bold]
font.monospaced = monospaced-plain-12
//

View file

@ -298,6 +298,11 @@ public class ApplicationThemeManager extends ThemeManager {
lookAndFeelManager.registerFont(component, fontId);
}
@Override
public void registerFont(Component component, String fontId, int fontStyle) {
lookAndFeelManager.registerFont(component, fontId, fontStyle);
}
private void installFlatLookAndFeels() {
UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName());
UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName());

View file

@ -68,7 +68,7 @@ public class FontModifier {
}
/**
* Sets the font stle modifier. This can be called multiple times to bold and italicize.
* Sets the font style modifier. This can be called multiple times to bold and italicize.
* @param newStyle the style to use for the font.
*/
public void addStyleModifier(int newStyle) {

View file

@ -0,0 +1,57 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package generic.theme;
import java.awt.Font;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import ghidra.util.HTMLUtilities;
/**
* A drop-in replacement for clients using {@link SimpleAttributeSet}s. This class will apply a
* default set of font attributes based on the given font and optional color.
*/
public class GAttributes extends SimpleAttributeSet {
public GAttributes(Font f) {
this(f, null);
}
public GAttributes(Font f, GColor c) {
addAttribute(StyleConstants.FontFamily, f.getFamily());
addAttribute(StyleConstants.FontSize, f.getSize());
addAttribute(StyleConstants.Bold, f.isBold());
addAttribute(StyleConstants.Italic, f.isItalic());
if (c != null) {
addAttribute(StyleConstants.Foreground, c);
}
}
/**
* A convenience method to style the given text in HTML using the font and color attributes
* defined in this attribute set. The text will be HTML escaped.
*
* @param content the content
* @return the styled content
* @see HTMLUtilities#styleText(SimpleAttributeSet, String)
*/
public String toStyledHtml(String content) {
return HTMLUtilities.styleText(this, content);
}
}

View file

@ -17,8 +17,9 @@ package generic.theme;
import java.awt.*;
import javax.swing.Icon;
import javax.swing.LookAndFeel;
import javax.swing.*;
import ghidra.util.Msg;
/**
* Provides a static set of methods for globally managing application themes and their values.
@ -36,6 +37,8 @@ import javax.swing.LookAndFeel;
*
*/
public class Gui {
private static final String FONT_SUFFIX = ".font";
// Start with an StubThemeManager so that simple tests can operate without having
// to initialize the theme system. Applications and integration tests will
// called ThemeManager.initialize() which will replace this with a fully initialized version.
@ -146,6 +149,9 @@ public class Gui {
/**
* Binds the component to the font identified by the given font id. Whenever the font for
* the font id changes, the component will updated with the new font.
* <p>
* Calling this method will trigger a call to {@link JComponent#setFont(Font)}.
*
* @param component the component to set/update the font
* @param fontId the id of the font to register with the given component
*/
@ -153,6 +159,37 @@ public class Gui {
themeManager.registerFont(component, fontId);
}
/**
* Registers the given component with the given font style. This method allows clients to not
* define a font id in the theme system, but instead to signal that they want the default font
* for the given component, modified with the given style. As the underlying font is changed,
* the client will be updated with that new font with the given style applied.
* <P>
* Most clients should <b>not</b> be using this method. Instead, use
* {@link #registerFont(JComponent, int)}.
* <P>
* The downside of using this method is that the end user cannot modify the style of the font.
* By using the standard theming mechanism for registering fonts, the end user has full control.
*
* @param component the component to set/update the font
* @param fontStyle the font style, one of Font.BOLD, Font.ITALIC,
*/
public static void registerFont(JComponent component, int fontStyle) {
if (fontStyle == Font.PLAIN) {
Msg.warn(Gui.class,
"Gui.registerFont(Component, int) may only be used for a non-plain font style. " +
"Use registerFont(Component, String) instead.");
return;
}
String id = component.getUIClassID(); // e.g., ButtonUI
String name = id.substring(0, id.length() - 2); // strip off "UI"
String fontId = FontValue.LAF_ID_PREFIX + name + FONT_SUFFIX; // e.g., laf.font.Button.font
themeManager.registerFont(component, fontId, fontStyle);
}
/**
* Returns true if the active theme is using dark defaults
* @return true if the active theme is using dark defaults

View file

@ -575,6 +575,21 @@ public abstract class ThemeManager {
// do nothing
}
/**
* Binds the component to the font identified by the given font id. Whenever the font for
* the font id changes, the component will updated with the new font.
* <p>
* This method is fairly niche and should not be called by most clients. Instead, call
* {@link #registerFont(Component, String)}.
*
* @param component the component to set/update the font
* @param fontId the id of the font to register with the given component
* @param fontStyle the font style
*/
public void registerFont(Component component, String fontId, int fontStyle) {
// do nothing
}
/**
* Returns true if the current theme use dark default values.
* @return true if the current theme use dark default values.

View file

@ -28,7 +28,8 @@ import ghidra.util.datastruct.WeakSet;
* for the font id, this class will update the component's font to the new value.
*/
public class ComponentFontRegistry {
private WeakSet<Component> components = WeakDataStructureFactory.createCopyOnReadWeakSet();
private WeakSet<StyledComponent> components =
WeakDataStructureFactory.createCopyOnReadWeakSet();
private String fontId;
/**
@ -45,8 +46,18 @@ public class ComponentFontRegistry {
* @param component the component to add
*/
public void addComponent(Component component) {
component.setFont(Gui.getFont(fontId));
components.add(component);
addComponent(component, Font.PLAIN);
}
/**
* Allows clients to update the default font being used for a component to use the given style.
* @param component the component
* @param fontStyle the font style (e.g., {@link Font#BOLD})
*/
public void addComponent(Component component, int fontStyle) {
StyledComponent sc = new StyledComponent(component, fontStyle);
sc.setFont(Gui.getFont(fontId));
components.add(sc);
}
/**
@ -54,10 +65,26 @@ public class ComponentFontRegistry {
*/
public void updateComponentFonts() {
Font font = Gui.getFont(fontId);
for (Component component : components) {
for (StyledComponent c : components) {
c.setFont(font);
}
}
private record StyledComponent(Component component, int fontStyle) {
void setFont(Font font) {
Font existingFont = component.getFont();
if (!Objects.equals(existingFont, font)) {
component.setFont(font);
Font styledFont = font;
int style = fontStyle();
if (style != Font.PLAIN) {
// Only style the font when it is not plain. Doing this means that clients cannot
// override a non-plain font to be plain. If clients need that behavior, they must
// create their own custom font id and register their component with Gui.
styledFont = font.deriveFont(style);
}
if (!Objects.equals(existingFont, styledFont)) {
component.setFont(styledFont);
}
}
}

View file

@ -30,6 +30,7 @@ import generic.theme.*;
import generic.util.action.*;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import utilities.util.reflection.ReflectionUtilities;
/**
* Manages installing and updating a {@link LookAndFeel}
@ -38,6 +39,7 @@ public abstract class LookAndFeelManager {
private LafType laf;
private Map<String, ComponentFontRegistry> fontRegistryMap = new HashMap<>();
private Map<Component, String> componentToIdMap = new WeakHashMap<>();
protected ApplicationThemeManager themeManager;
protected Map<String, String> normalizedIdToLafIdMap;
@ -158,7 +160,7 @@ public abstract class LookAndFeelManager {
* Called when one or more fonts have changed.
* <p>
* This will update the Java {@link UIManager} and trigger a reload of the UIs.
*
*
* @param changedFontIds the set of Java Font ids that are affected by this change; these are
* the normalized ids
*/
@ -202,12 +204,49 @@ public abstract class LookAndFeelManager {
* @param fontId the id of the font to register with the given component
*/
public void registerFont(Component component, String fontId) {
checkForAlreadyRegistered(component, fontId);
componentToIdMap.put(component, fontId);
ComponentFontRegistry register =
fontRegistryMap.computeIfAbsent(fontId, id -> new ComponentFontRegistry(id));
register.addComponent(component);
}
/**
* Binds the component to the font identified by the given font id. Whenever the font for
* the font id changes, the component will be updated with the new font.
* <p>
* This method is fairly niche and should not be called by most clients. Instead, call
* {@link #registerFont(Component, String)}.
*
* @param component the component to set/update the font
* @param fontId the id of the font to register with the given component
* @param fontStyle the font style
*/
public void registerFont(Component component, String fontId, int fontStyle) {
checkForAlreadyRegistered(component, fontId);
componentToIdMap.put(component, fontId);
ComponentFontRegistry register =
fontRegistryMap.computeIfAbsent(fontId, id -> new ComponentFontRegistry(id));
register.addComponent(component, fontStyle);
}
private void checkForAlreadyRegistered(Component component, String newFontId) {
String existingFontId = componentToIdMap.get(component);
if (existingFontId != null) {
Msg.warn(this, """
Component has a Font ID registered more than once. \
Previously registered ID: '%s'. Newly registered ID: '%s'.
""".formatted(existingFontId, newFontId),
ReflectionUtilities.createJavaFilteredThrowable());
}
}
private Font toUiResource(Font font) {
if (!(font instanceof UIResource)) {
return new FontUIResource(font);
@ -292,8 +331,7 @@ public abstract class LookAndFeelManager {
return false;
}
protected void setKeyBinding(String existingKsText, String newKsText,
String[] prefixValues) {
protected void setKeyBinding(String existingKsText, String newKsText, String[] prefixValues) {
KeyStroke existingKs = KeyStroke.getKeyStroke(existingKsText);
KeyStroke newKs = KeyStroke.getKeyStroke(newKsText);

View file

@ -153,8 +153,8 @@ public class UiDefaultsMapper {
* the user changeable values for affecting the Java LookAndFeel colors, fonts, and icons.
* <p>
* The keys in the returned map have been normalized and all start with 'laf.'
*
*
*
*
* @return a map of changeable values that affect java LookAndFeel values
*/
public GThemeValueMap getNormalizedJavaDefaults() {
@ -184,7 +184,7 @@ public class UiDefaultsMapper {
* Returns a mapping of normalized LaF Ids so that when fonts and icons get changed using the
* normalized ids that are presented to the user, we know which LaF ids need to be updated in
* the UiDefaults so that the LookAndFeel will pick up and use the changes.
*
*
* @return a mapping of normalized LaF ids to original LaF ids.
*/
public Map<String, String> getNormalizedIdToLafIdMap() {
@ -281,7 +281,7 @@ public class UiDefaultsMapper {
/**
* This allows clients to hard-code a chosen color for a group
*
*
* @param group the system color id to assign the given color
* @param color the color to be assigned to the system color id
*/
@ -291,7 +291,7 @@ public class UiDefaultsMapper {
/**
* This allows clients to hard-code a chosen font for a group
*
*
* @param group the system font id to assign the given font
* @param font the font to be assigned to the system font id
*/
@ -693,7 +693,7 @@ public class UiDefaultsMapper {
* Groups allow us to use the same group id for many components that by default have the same
* value (Color or Font). This grouper allows us to specify the precedence to use when
* searching for the best group.
*
*
* @param <T> The theme value type (Color or Font)
*/
private abstract class ValueGrouper<T> {

View file

@ -22,9 +22,10 @@ import java.util.regex.Pattern;
import javax.swing.JLabel;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.View;
import javax.swing.text.*;
import generic.text.TextLayoutGraphics;
import generic.theme.GAttributes;
import ghidra.util.html.HtmlLineSplitter;
import utilities.util.reflection.ReflectionUtilities;
@ -343,6 +344,67 @@ public class HTMLUtilities {
return buffy.toString();
}
/**
* Escapes and wraps the given text in {@code SPAN} tag with font attributes specified in the
* given attributes. Specifically, these attributes are used:
*
* <UL>
* <LI>{@link StyleConstants#Foreground} - {@link Color} object</LI>
* <LI>{@link StyleConstants#FontFamily} - font name</LI>
* <LI>{@link StyleConstants#FontSize} - size in pixels</LI>
* <LI>{@link StyleConstants#Italic} - true if italic</LI>
* <LI>{@link StyleConstants#Bold} - true if bold</LI>
* </UL>
* <P>
* See {@link GAttributes} for a convenient way to create the correct attributes for a font and
* color.
*
* @param attributes the attributes
* @param text the content to style
* @return the styled content
* @see GAttributes
*/
public static String styleText(SimpleAttributeSet attributes, String text) {
// StyleConstants.Foreground color: #00FF00;
// StyleConstants.FontFamily font-family: "Tahoma";
// StyleConstants.FontSize font-size: 40px;
// StyleConstants.Italic font-style: italic;
// StyleConstants.Bold font-weight: bold;
String family = attributes.getAttribute(StyleConstants.FontFamily).toString();
String size = attributes.getAttribute(StyleConstants.FontSize).toString();
String style = "plain";
String weight = "plain";
Boolean isItalic = (Boolean) attributes.getAttribute(StyleConstants.Italic);
Boolean isBold = (Boolean) attributes.getAttribute(StyleConstants.Bold);
if (Boolean.TRUE.equals(isItalic)) {
style = "italic";
}
if (Boolean.TRUE.equals(isBold)) {
weight = "bold";
}
// color is optional and defaults to the containing component's color
String color = "";
Object colorAttribute = attributes.getAttribute(StyleConstants.Foreground);
if (colorAttribute instanceof Color fgColor) {
String hexColor = HTMLUtilities.toHexString(fgColor);
color = "color: % s;".formatted(hexColor);
}
String escaped = escapeHTML(text);
//@formatter:off
return """
<SPAN STYLE=\"%s font-family: '%s'; font-size: %spx; font-style: %s; font-weight: %s;\">\
%s\
</SPAN>
""".formatted(color, family, size, style, weight, escaped);
//@formatter:on
}
/**
* Returns the given text wrapped in {@link #LINK_PLACEHOLDER_OPEN} and close tags.
* If <code>foo</code> is passed for the HTML text, with a content value of <code>123456</code>, then
@ -510,7 +572,7 @@ public class HTMLUtilities {
/**
* See {@link #friendlyEncodeHTML(String)}
*
*
* @param text string to be encoded
* @param skipLeadingWhitespace true signals to ignore any leading whitespace characters.
* This is useful when line wrapping to force wrapped lines to the left
@ -593,8 +655,7 @@ public class HTMLUtilities {
* Calling this twice will result in text being double-escaped, which will not display correctly.
* <p>
* See also <code>StringEscapeUtils#escapeHtml3(String)</code> if you need quote-safe html encoding.
* <p>
*
*
* @param text plain-text that might have some characters that should NOT be interpreted as HTML
* @param makeSpacesNonBreaking true to convert spaces into {@value #HTML_SPACE}
* @return string with any html characters replaced with equivalents
@ -634,7 +695,7 @@ public class HTMLUtilities {
/**
* Escapes any HTML special characters in the specified text.
*
*
* @param text plain-text that might have some characters that should NOT be interpreted as HTML
* @return string with any html characters replaced with equivalents
* @see #escapeHTML(String, boolean)
@ -647,7 +708,7 @@ public class HTMLUtilities {
* Tests a unicode code point (i.e., 32 bit character) to see if it needs to be escaped before
* being added to a HTML document because it is non-printable or a non-standard control
* character
*
*
* @param codePoint character to test
* @return boolean true if character should be escaped
*/