GP-4873 - Updated tables to respond to font changes in real time; Added support for changing the monospaced font used by tables; Fixed tooltip in theme font table

This commit is contained in:
dragonmacher 2024-09-20 13:29:39 -04:00
parent 7148590e5c
commit 9a0cc8d547
9 changed files with 220 additions and 41 deletions

View file

@ -165,6 +165,7 @@ font.splash.status = serif-bold-12
// default table renderer uses the JLabel font, which is mapped to system.font.control
font.table.base = [font]system.font.control
font.table.fixed.width = font.table.base[monospaced]
font.table.header.number = arial-bold-12
font.task.monitor.label.message = sansserif-plain-10

View file

@ -4,9 +4,9 @@
* 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.
@ -141,7 +141,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
return "<No Value>";
}
if (resolvedFont.refId() != null) {
return "[" + resolvedFont.refId() + "]";
return resolvedFont.getValueText();
}
Font font = resolvedFont.font();
@ -193,7 +193,8 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
if (fontValue == null) {
return null;
}
return new ResolvedFont(id, fontValue.getReferenceId(), valueMap.getResolvedFont(id));
return new ResolvedFont(fontValue, id, fontValue.getReferenceId(),
valueMap.getResolvedFont(id));
}
@Override
@ -237,7 +238,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
Font font = resolvedFont.font();
if (font != null) {
setToolTipText(FontValue.fontToString(font));
setToolTipText(resolvedFont.getFullValueText());
}
return label;
@ -250,6 +251,41 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
}
private record ResolvedFont(String id, String refId, Font font) {
/**/}
// This class is used as a column cell value. It holds the info that defines the font in the
// theme files, as long as the final font that was loaded by the system.
private record ResolvedFont(FontValue fontValue, String id, String refId, Font font) {
String getValueText() {
if (refId == null) {
return FontValue.fontToString(font);
}
FontModifier modifier = fontValue.getModifier();
String modifierText = "";
if (modifier != null) {
modifierText = "*";
}
// ex: [font.foo]
// [font.foo]*
return "[" + refId + "]" + modifierText;
}
String getFullValueText() {
if (refId == null) {
return FontValue.fontToString(font);
}
FontModifier modifier = fontValue.getModifier();
String modifierText = "";
if (modifier != null) {
modifierText = " [%s]%s".formatted(refId, modifier.toString());
}
// ex: monospaced-PLAIN-13
// monospaced-PLAIN-13 [font.foo][monospaced]
return FontValue.fontToString(font) + modifierText;
}
}
}

View file

@ -16,10 +16,12 @@
package docking.widgets;
import java.awt.*;
import java.util.Objects;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.border.Border;
import javax.swing.plaf.LabelUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.table.DefaultTableCellRenderer;
@ -28,6 +30,7 @@ import docking.widgets.label.GDHtmlLabel;
import generic.theme.*;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.GThemeDefaults.Colors.Tables;
import generic.theme.laf.FontChangeListener;
import ghidra.util.Msg;
import util.CollectionUtils;
import utilities.util.reflection.ReflectionUtilities;
@ -41,15 +44,19 @@ import utilities.util.reflection.ReflectionUtilities;
* The preferred method to change the font used by this renderer is {@link #setBaseFontId(String)}.
* If you would like this renderer to use a monospaced font, then, as an alternative to creating a
* font ID, you can instead override {@link #getDefaultFont()} to return this
* class's {@link #fixedWidthFont}. Also, the fixed width font of this class is based on the
* class's {@link #fixedWidthFont}.
*
* Also, the fixed width font of this class is based on the
* default font set when calling {@link #setBaseFontId(String)}, so it stays up-to-date with theme
* changes.
*/
public abstract class AbstractGCellRenderer extends GDHtmlLabel {
public abstract class AbstractGCellRenderer extends GDHtmlLabel implements FontChangeListener {
private static final Color BACKGROUND_COLOR = new GColor("color.bg.table.row");
private static final Color ALT_BACKGROUND_COLOR = new GColor("color.bg.table.row.alt");
private static final String BASE_FONT_ID = "font.table.base";
private static final String DEFAULT_BASE_FONT_ID = "font.table.base";
private static final String MONOSPACED_FONT_ID = "font.table.fixed.width";
/** Allows the user to disable alternating row colors on JLists and JTables */
private static final String DISABLE_ALTERNATING_ROW_COLORS_PROPERTY =
@ -63,6 +70,8 @@ public abstract class AbstractGCellRenderer extends GDHtmlLabel {
protected Border focusBorder;
protected Border noFocusBorder;
protected String baseFontId;
protected String fixedWidthFontId;
protected Font defaultFont;
protected Font fixedWidthFont;
protected Font boldFont;
@ -73,7 +82,8 @@ public abstract class AbstractGCellRenderer extends GDHtmlLabel {
public AbstractGCellRenderer() {
setBaseFontId(BASE_FONT_ID);
setBaseFontId(DEFAULT_BASE_FONT_ID);
setFixedWidthFontId(MONOSPACED_FONT_ID);
noFocusBorder = BorderFactory.createEmptyBorder(0, 5, 0, 5);
Border innerBorder = BorderFactory.createEmptyBorder(0, 4, 0, 4);
@ -135,21 +145,73 @@ public abstract class AbstractGCellRenderer extends GDHtmlLabel {
* @see Gui#registerFont(Component, String)
*/
public void setBaseFontId(String fontId) {
if (baseFontId != null) {
Gui.unRegisterFont(this, baseFontId);
}
Font f = Gui.getFont(fontId);
updateDefaultFont(f);
baseFontId = fontId;
Gui.registerFont(this, baseFontId);
}
/**
* Sets this renderer's fixed width theme font id.
* @param fontId the font id
* @see Gui#registerFont(Component, String)
*/
public void setFixedWidthFontId(String fontId) {
if (fixedWidthFontId != null) {
Gui.unRegisterFont(this, fixedWidthFontId);
}
Font f = Gui.getFont(fontId);
fixedWidthFont = f;
fixedWidthFontId = fontId;
Gui.registerFont(this, fixedWidthFontId);
}
private void updateDefaultFont(Font f) {
if (Objects.equals(f, defaultFont)) {
return;
}
defaultFont = f;
fixedWidthFont = new Font("monospaced", f.getStyle(), f.getSize());
boldFont = f.deriveFont(Font.BOLD);
italicFont = f.deriveFont(Font.ITALIC);
}
// Gui does not allow registering the same component multiple times, so unregister first
Gui.unRegisterFont(this, fontId);
Gui.registerFont(this, fontId);
@Override
public void setUI(LabelUI ui) {
super.setUI(ui);
if (baseFontId == null) {
return; // initializing
}
Font font = Gui.getFont(baseFontId);
updateDefaultFont(font);
font = Gui.getFont(fixedWidthFontId);
fixedWidthFont = font;
}
@Override
public void fontChanged(String fontId, Font f) {
if (fontId.equals(baseFontId)) {
updateDefaultFont(f);
}
else if (fontId.equals(fixedWidthFontId)) {
fixedWidthFont = f;
}
}
@Override
public void setFont(Font f) {
super.setFont(f);
checkForInvalidSetFont(f);
}

View file

@ -4,9 +4,9 @@
* 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.
@ -60,6 +60,7 @@ public class DefaultTableCellRendererWrapper extends GTableCellRenderer {
rendererComponent.setForeground(thisRenderer.getForeground());
rendererComponent.setBackground(thisRenderer.getBackground());
rendererComponent.setFont(thisRenderer.getFont());
if (rendererComponent instanceof JComponent) {
((JComponent) rendererComponent).setBorder(thisRenderer.getBorder());

View file

@ -4,9 +4,9 @@
* 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.
@ -134,6 +134,11 @@ public class FontModifier {
return builder.toString();
}
@Override
public String toString() {
return getSerializationString();
}
/**
* Parses the given string as one or more font modifiers
* @param value the string to parse as modifiers

View file

@ -4,9 +4,9 @@
* 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.
@ -162,12 +162,15 @@ public class FontValue extends ThemeValue<Font> {
@Override
protected Font getUnresolvedReferenceValue(String primaryId, String unresolvedId) {
Msg.warn(this,
"Could not resolve indirect font path for \"" + unresolvedId +
"\" for primary id \"" + primaryId + "\", using last resort default");
Msg.warn(this, "Could not resolve indirect font path for \"" + unresolvedId +
"\" for primary id \"" + primaryId + "\", using last resort default");
return LAST_RESORT_DEFAULT;
}
public FontModifier getModifier() {
return modifier;
}
private static String toExternalId(String internalId) {
if (internalId.startsWith(FONT_ID_PREFIX)) {
return internalId;

View file

@ -17,20 +17,20 @@ package generic.theme.laf;
import java.awt.Component;
import java.awt.Font;
import java.util.Iterator;
import java.util.Objects;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import generic.theme.Gui;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
/**
* Maintains a weak set of components associated with a given font id. Whenever the font changes
* for the font id, this class will update the component's font to the new value.
*/
public class ComponentFontRegistry {
private WeakSet<StyledComponent> components =
WeakDataStructureFactory.createCopyOnReadWeakSet();
// use a thread-safe set that allows for changes while iterating
private Set<StyledComponent> components = ConcurrentHashMap.newKeySet();
private String fontId;
/**
@ -69,7 +69,7 @@ public class ComponentFontRegistry {
Iterator<StyledComponent> it = components.iterator();
while (it.hasNext()) {
StyledComponent sc = it.next();
if (component == sc.component) {
if (sc.matches(component)) {
it.remove();
break;
}
@ -81,17 +81,51 @@ public class ComponentFontRegistry {
*/
public void updateComponentFonts() {
Font font = Gui.getFont(fontId);
for (StyledComponent c : components) {
c.setFont(font);
List<StyledComponent> copy = new ArrayList<>(components);
for (StyledComponent c : copy) {
if (!c.isValid()) {
// the component has been garbage collected
components.remove(c);
}
else {
c.setFont(font);
}
}
}
private record StyledComponent(Component component, int fontStyle) {
/**
* A simple container that holds a font style and the component that uses that style. The
* component is managed using a weak reference to help prevent memory leaks.
*/
private class StyledComponent {
private int fontStyle;
private WeakReference<Component> componentReference;
StyledComponent(Component component, int fontStyle) {
this.fontStyle = fontStyle;
this.componentReference = new WeakReference<>(component);
}
boolean matches(Component c) {
return componentReference.refersTo(c);
}
boolean isValid() {
return !componentReference.refersTo(null);
}
void setFont(Font font) {
Component component = componentReference.get();
if (component == null) {
return; // garbage collected
}
Font existingFont = component.getFont();
Font styledFont = font;
int style = fontStyle();
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
@ -99,7 +133,14 @@ public class ComponentFontRegistry {
styledFont = font.deriveFont(style);
}
if (!Objects.equals(existingFont, styledFont)) {
if (Objects.equals(existingFont, styledFont)) {
return;
}
if (component instanceof FontChangeListener fontListener) {
fontListener.fontChanged(fontId, styledFont);
}
else {
component.setFont(styledFont);
}
}

View file

@ -0,0 +1,32 @@
/* ###
* 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.laf;
import java.awt.Font;
/**
* A simple interface that signals the client has a font that should be updated when the theme is
* updated.
*/
public interface FontChangeListener {
/**
* Called when the client should update its font to the given font.
* @param fontId the theme font id being updated
* @param f the font
*/
public void fontChanged(String fontId, Font f);
}

View file

@ -4,9 +4,9 @@
* 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.
@ -230,8 +230,6 @@ public class PluginInstallerDialog extends DialogComponentProvider {
private class NameCellRenderer extends GTableCellRenderer {
NameCellRenderer() {
defaultFont = getFont();
boldFont = defaultFont.deriveFont(defaultFont.getStyle() | Font.BOLD);
setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
}