mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-1981 Review fixes
This commit is contained in:
parent
9679752523
commit
a92a27e9f1
90 changed files with 375 additions and 381 deletions
|
@ -1,2 +1,2 @@
|
|||
MODULE FILE LICENSE: lib/flatlaf-2.1.jar Apache License 2.0
|
||||
|
||||
|
||||
|
|
|
@ -26,8 +26,6 @@ eclipse.project.name = 'Framework Docking'
|
|||
dependencies {
|
||||
api project(':Generic')
|
||||
api project(':Help')
|
||||
|
||||
api 'com.formdev:flatlaf:2.2'
|
||||
|
||||
// include code from src/test in Generic
|
||||
testImplementation project(path: ':Generic', configuration: 'testArtifacts')
|
||||
|
|
|
@ -124,8 +124,6 @@ icon.dragon.256 = GhidraIcon256.png
|
|||
icon.help.navigation.aid.disabled.overlay = icon.not.allowed
|
||||
icon.help.navigation.aid.enabled = software-update-available.png
|
||||
|
||||
icon.options.node.leaf = icon.properties
|
||||
|
||||
icon.filechooser.places.my.computer = computer.png
|
||||
icon.filechooser.places.desktop = desktop.png
|
||||
icon.filechooser.places.home = user-home.png
|
||||
|
|
|
@ -28,7 +28,7 @@ body { margin-bottom: 50px; margin-left: 10px; margin-right: 10px; margin-top: 1
|
|||
li { font-family:times new roman; font-size:14pt; }
|
||||
h1 { color:#000080; font-family:times new roman; font-size:36pt; font-style:italic; font-weight:bold; text-align:center; }
|
||||
h2 { margin: 10px; margin-top: 20px; color:#984c4c; font-family:times new roman; font-size:18pt; font-weight:bold; }
|
||||
h3 { margin-left: 10px; margin-top: 20px; color:#0000ff; font-family:times new roman; `font-size:14pt; font-weight:bold; }
|
||||
h3 { margin-left: 10px; margin-top: 20px; color:#0000ff; font-family:times new roman; font-size:14pt; font-weight:bold; }
|
||||
h4 { margin-left: 10px; margin-top: 20px; font-family:times new roman; font-size:14pt; font-style:italic; }
|
||||
|
||||
/*
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
</HEAD>
|
||||
|
||||
<BODY>
|
||||
Theming Development Documentation
|
||||
|
||||
<H1>Ghidra Theming Developer Documentation</H1>
|
||||
|
||||
<P>This document provides everything an application developer should know when developing
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.GeneralPath;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
|
@ -24,34 +25,86 @@ import javax.swing.Icon;
|
|||
*/
|
||||
public class CloseIcon implements Icon {
|
||||
private int size;
|
||||
private int margin;
|
||||
private Color color;
|
||||
private Shape shape;
|
||||
|
||||
/**
|
||||
* Creates a close icon.
|
||||
* @param size the width and height of the icon
|
||||
* @param margin the margin around the "x"
|
||||
* @param color the color of the "x"
|
||||
*/
|
||||
public CloseIcon(int size, int margin, Color color) {
|
||||
this.size = size;
|
||||
this.margin = margin;
|
||||
* Creates a close icon.
|
||||
* @param isSmall false signals to use a 16x16 size; true signals to use an 8x8 size
|
||||
* @param color the color of the "x"
|
||||
*/
|
||||
public CloseIcon(boolean isSmall, Color color) {
|
||||
this.size = isSmall ? 8 : 16;
|
||||
this.color = color;
|
||||
this.shape = buildShape();
|
||||
}
|
||||
|
||||
private Shape buildShape() {
|
||||
GeneralPath path = new GeneralPath();
|
||||
|
||||
/*
|
||||
We use trial and error sizing. This class allows clients to specify the icon size. At
|
||||
the time of writing, there were only 2 sizes in use: 16 and 8 pixels. If more size
|
||||
needs arise, we can revisit how the values below are chosen.
|
||||
*/
|
||||
|
||||
double margin = 2;
|
||||
double shapeSize = 11;
|
||||
double thickness = 1.7;
|
||||
if (size == 8) {
|
||||
margin = 0;
|
||||
shapeSize = 7;
|
||||
thickness = 1;
|
||||
}
|
||||
|
||||
double p1x = margin;
|
||||
double p1y = margin + thickness;
|
||||
double p2x = margin + thickness;
|
||||
double p2y = margin;
|
||||
double p3x = margin + shapeSize;
|
||||
double p3y = margin + shapeSize - thickness;
|
||||
double p4x = margin + shapeSize - thickness;
|
||||
double p4y = margin + shapeSize;
|
||||
|
||||
path.moveTo(p1x, p1y);
|
||||
path.lineTo(p2x, p2y);
|
||||
path.lineTo(p3x, p3y);
|
||||
path.lineTo(p4x, p4y);
|
||||
path.lineTo(p1x, p1y);
|
||||
|
||||
p1x = margin + shapeSize - thickness;
|
||||
p1y = margin;
|
||||
p2x = margin + shapeSize;
|
||||
p2y = margin + thickness;
|
||||
p3x = margin + thickness;
|
||||
p3y = margin + shapeSize;
|
||||
p4x = margin;
|
||||
p4y = margin + shapeSize - thickness;
|
||||
|
||||
path.moveTo(p1x, p1y);
|
||||
path.lineTo(p2x, p2y);
|
||||
path.lineTo(p3x, p3y);
|
||||
path.lineTo(p4x, p4y);
|
||||
path.lineTo(p1x, p1y);
|
||||
|
||||
path.closePath();
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
g.setColor(color);
|
||||
int xStart = x + margin;
|
||||
int yStart = y + margin;
|
||||
int xEnd = x + size - margin;
|
||||
int yEnd = y + size - margin;
|
||||
g.drawLine(xStart, yStart, xEnd, yEnd);
|
||||
g.drawLine(xStart, yEnd, xEnd, yStart);
|
||||
g.drawLine(xStart + 1, yStart, xEnd + 1, yEnd);
|
||||
g.drawLine(xStart + 1, yEnd, xEnd + 1, yStart);
|
||||
g.drawLine(xStart - 1, yStart, xEnd - 1, yEnd);
|
||||
g.drawLine(xStart - 1, yEnd, xEnd - 1, yStart);
|
||||
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
try {
|
||||
g2d.translate(x, y);
|
||||
g2d.setColor(color);
|
||||
g2d.fill(shape);
|
||||
}
|
||||
finally {
|
||||
g2d.translate(-x, -y);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -32,8 +32,8 @@ import ghidra.util.task.SwingUpdateManager;
|
|||
*/
|
||||
class DockableToolBarManager {
|
||||
private static final Color BUTTON_COLOR = new GColor("color.fg.button");
|
||||
private static final Icon CLOSE_ICON = new CloseIcon(16, 4, BUTTON_COLOR);
|
||||
private static final Icon MENU_ICON = new DropDownMenuIcon(16, 4, 4, BUTTON_COLOR);
|
||||
private static final Icon CLOSE_ICON = new CloseIcon(false, BUTTON_COLOR);
|
||||
private Icon MENU_ICON = new DropDownMenuIcon(BUTTON_COLOR);
|
||||
private GenericHeader dockableHeader;
|
||||
private ToolBarManager toolBarManager;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.GeneralPath;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
|
@ -23,49 +24,71 @@ import javax.swing.Icon;
|
|||
* Icon for a drop down menu button (triangle pointing down)
|
||||
*/
|
||||
public class DropDownMenuIcon implements Icon {
|
||||
private int size;
|
||||
private int xMargin;
|
||||
private int yMargin;
|
||||
private static final int ICON_SIZE = 16;
|
||||
|
||||
private Color color;
|
||||
private Shape shape;
|
||||
|
||||
/**
|
||||
* Creates a drop down menu icon.
|
||||
* @param size the width and height of the icon
|
||||
* @param xMargin the margin around triangle base
|
||||
* @param yMargin the margin around triangle height
|
||||
* @param color the color of the triangle
|
||||
*/
|
||||
public DropDownMenuIcon(int size, int xMargin, int yMargin, Color color) {
|
||||
this.size = size;
|
||||
this.xMargin = xMargin;
|
||||
this.yMargin = yMargin;
|
||||
public DropDownMenuIcon(Color color) {
|
||||
this.color = color;
|
||||
this.shape = buildShape();
|
||||
}
|
||||
|
||||
private Shape buildShape() {
|
||||
|
||||
GeneralPath path = new GeneralPath();
|
||||
|
||||
double iconSize = 16;
|
||||
double height = 6;
|
||||
double width = 10;
|
||||
double leftMargin = (iconSize - width) / 2;
|
||||
double topMargin = (iconSize - height) / 2;
|
||||
|
||||
// draw a triangle pointing down; p1 is the bottom; p2 is the left
|
||||
double p1x = leftMargin + (width / 2);
|
||||
double p1y = topMargin + height;
|
||||
double p2x = leftMargin;
|
||||
double p2y = topMargin;
|
||||
double p3x = leftMargin + width;
|
||||
double p3y = topMargin;
|
||||
|
||||
path.moveTo(p1x, p1y);
|
||||
path.lineTo(p2x, p2y);
|
||||
path.lineTo(p3x, p3y);
|
||||
path.lineTo(p1x, p1y);
|
||||
path.closePath();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
g.setColor(color);
|
||||
|
||||
// draw a triangle pointing down
|
||||
int p1x = x + size / 2;
|
||||
int p1y = y + size - yMargin;
|
||||
int p2x = x + xMargin;
|
||||
int p2y = y + yMargin;
|
||||
int p3x = x + size - xMargin + 1;
|
||||
int p3y = y + yMargin;
|
||||
int xPoints[] = { p1x, p2x, p3x };
|
||||
int yPoints[] = { p1y, p2y, p3y };
|
||||
g.fillPolygon(xPoints, yPoints, 3);
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
try {
|
||||
g2d.translate(x, y);
|
||||
g2d.setColor(color);
|
||||
g2d.fill(shape);
|
||||
}
|
||||
finally {
|
||||
g2d.translate(-x, -y);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return size;
|
||||
return ICON_SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return size;
|
||||
return ICON_SIZE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ public class DockingApplicationConfiguration extends ApplicationConfiguration {
|
|||
super.initializeApplication();
|
||||
|
||||
Gui.initialize();
|
||||
LookAndFeelUtils.platformSpecificFixups();
|
||||
LookAndFeelUtils.performPlatformSpecificFixups();
|
||||
|
||||
if (showSplashScreen) {
|
||||
SplashScreen.showSplashScreen();
|
||||
|
|
|
@ -133,12 +133,6 @@ public class IconPropertyEditor extends PropertyEditorSupport {
|
|||
return panel;
|
||||
}
|
||||
|
||||
// private void iconChanged() {
|
||||
// Icon icon = dropDown.getSelectedValue();
|
||||
// Msg.debug(this, "action listener: icon changed " + icon);
|
||||
// dropDown.getSelectedValue();
|
||||
// }
|
||||
|
||||
private void iconChanged(Icon icon) {
|
||||
boolean isDropDownWindowShowing = dropDown.isMatchingListShowing();
|
||||
if (!isDropDownWindowShowing) {
|
||||
|
@ -216,10 +210,8 @@ public class IconPropertyEditor extends PropertyEditorSupport {
|
|||
|
||||
previewLabel = new GDLabel("");
|
||||
previewLabel.setIcon(ResourceManager.getDefaultIcon());
|
||||
// previewLabel.setPreferredSize(new Dimension(350, 50));
|
||||
previewLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
previewLabel.setVerticalAlignment(SwingConstants.CENTER);
|
||||
// previewLabel.setMinimumSize(new Dimension(300, 50));
|
||||
panel.add(previewLabel, BorderLayout.CENTER);
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
return panel;
|
||||
|
|
|
@ -23,11 +23,12 @@ import docking.widgets.tree.GTreeLazyNode;
|
|||
import docking.widgets.tree.GTreeNode;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.options.Options;
|
||||
import resources.Icons;
|
||||
|
||||
class OptionsTreeNode extends GTreeLazyNode {
|
||||
private final static Icon OPEN_FOLDER_ICON = new GIcon("icon.folder.open");
|
||||
private final static Icon CLOSED_FOLDER_ICON = new GIcon("icon.folder.closed");
|
||||
private final static Icon PROPERTIES_ICON = new GIcon("icon.options.node.leaf");
|
||||
private final static Icon OPEN_FOLDER_ICON = Icons.OPEN_FOLDER_ICON;
|
||||
private final static Icon CLOSED_FOLDER_ICON = Icons.CLOSED_FOLDER_ICON;
|
||||
private final static Icon PROPERTIES_ICON = new GIcon("icon.properties");
|
||||
|
||||
private final Options options;
|
||||
private final String name;
|
||||
|
|
|
@ -107,7 +107,7 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider {
|
|||
|
||||
@Override
|
||||
public ActionContext getActionContext(MouseEvent e) {
|
||||
if (e.getSource() == table) {
|
||||
if (e != null && e.getSource() == table) {
|
||||
ColorValue currentValue = filterTable.getSelectedRowObject();
|
||||
if (currentValue == null) {
|
||||
return null;
|
||||
|
|
|
@ -79,7 +79,7 @@ public class ThemeDialog extends DialogComponentProvider {
|
|||
.enabledWhen(c -> c.isChanged())
|
||||
.popupWhen(c -> true)
|
||||
.helpLocation(new HelpLocation("Theming", "Restore_Value"))
|
||||
.onAction(c -> c.getThemeValue().makeCurrentValue())
|
||||
.onAction(c -> c.getThemeValue().installValue())
|
||||
.build();
|
||||
addAction(resetValueAction);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import docking.DockingWindowManager;
|
|||
import generic.theme.ThemeValue;
|
||||
|
||||
/**
|
||||
* Base class for Theme properties (Colors, Fonts, and Icons)
|
||||
* Base class for Theme property Editors (Colors, Fonts, and Icons)
|
||||
*
|
||||
* @param <T> the base property (Color, Font, or Icon)
|
||||
*/
|
||||
|
|
|
@ -1152,6 +1152,7 @@ public class FieldPanel extends JPanel
|
|||
}
|
||||
|
||||
private Color blend(Color primary, Color secondary) {
|
||||
|
||||
return ColorUtils.blend(primary, secondary, 0.75);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,9 +32,7 @@ import generic.theme.GColor;
|
|||
public class DockingTabRenderer extends JPanel {
|
||||
|
||||
private static final int MAX_TITLE_LENGTH = 25;
|
||||
private Icon CLOSE_ICON = new CloseIcon(8, 1, new GColor("color.fg.button"));
|
||||
private Icon EMPTY_ICON = CLOSE_ICON;
|
||||
//TODO if we don't want the close icon to come and go, then this class can be greatly simplified
|
||||
private Icon CLOSE_ICON = new CloseIcon(true, new GColor("color.fg.button"));
|
||||
|
||||
private JLabel titleLabel;
|
||||
private JLabel iconLabel;
|
||||
|
@ -56,9 +54,9 @@ public class DockingTabRenderer extends JPanel {
|
|||
|
||||
setTitle(tabTitle, fullTitle);
|
||||
closeButton.setToolTipText("Close " + tabTitle);
|
||||
closeButton.setIcon(EMPTY_ICON); // no icon until we rollover the tab
|
||||
closeButton.setFocusable(false);
|
||||
closeButton.addActionListener(closeListener);
|
||||
closeButton.setIcon(CLOSE_ICON);
|
||||
closeButton.setRolloverIcon(CLOSE_ICON);
|
||||
|
||||
JPanel container = new JPanel();
|
||||
|
@ -84,20 +82,10 @@ public class DockingTabRenderer extends JPanel {
|
|||
titleLabel.addMouseListener(eventForwardingListener);
|
||||
titleLabel.addMouseMotionListener(eventForwardingListener);
|
||||
|
||||
// listeners to know when to hide our close button (from the tabbed pane)
|
||||
final ButtonIconSwapperMouseListener iconListener =
|
||||
new ButtonIconSwapperMouseListener(tabbedPane);
|
||||
installIconListener(container, tabbedPane, iconListener);
|
||||
|
||||
// listeners to know when to hide our close button (from this panel)
|
||||
addMouseListener(iconListener);
|
||||
addMouseMotionListener(iconListener);
|
||||
|
||||
installMouseForwardingListenerWorkaround(tabbedPane, iconListener);
|
||||
installMouseForwardingListenerWorkaround(tabbedPane);
|
||||
}
|
||||
|
||||
private void installMouseForwardingListenerWorkaround(final JTabbedPane tabbedPane,
|
||||
final ButtonIconSwapperMouseListener iconListener) {
|
||||
private void installMouseForwardingListenerWorkaround(final JTabbedPane tabbedPane) {
|
||||
|
||||
forwardingListener = new TabContainerForwardingMouseListener(tabbedPane);
|
||||
|
||||
|
@ -114,20 +102,14 @@ public class DockingTabRenderer extends JPanel {
|
|||
boolean isDisplayable = isDisplayable();
|
||||
if (isDisplayable) {
|
||||
// remove and add in order to prevent duplicate adding
|
||||
myParent.removeMouseListener(iconListener);
|
||||
myParent.removeMouseMotionListener(iconListener);
|
||||
myParent.removeMouseListener(forwardingListener);
|
||||
myParent.removeMouseMotionListener(forwardingListener);
|
||||
myParent.addMouseListener(iconListener);
|
||||
myParent.addMouseMotionListener(iconListener);
|
||||
myParent.addMouseListener(forwardingListener);
|
||||
myParent.addMouseMotionListener(forwardingListener);
|
||||
|
||||
hierarchyListener = this;
|
||||
}
|
||||
else if (hierarchyListener != null) {
|
||||
myParent.removeMouseListener(iconListener);
|
||||
myParent.removeMouseMotionListener(iconListener);
|
||||
myParent.removeMouseListener(forwardingListener);
|
||||
myParent.removeMouseMotionListener(forwardingListener);
|
||||
|
||||
|
@ -145,20 +127,6 @@ public class DockingTabRenderer extends JPanel {
|
|||
return title;
|
||||
}
|
||||
|
||||
// add listeners to the tabbed pane so that the icon for closing appears (and disappears) as
|
||||
// needed
|
||||
private void installIconListener(Container container, JTabbedPane tabbedPane,
|
||||
ButtonIconSwapperMouseListener iconListener) {
|
||||
Component[] components = container.getComponents();
|
||||
for (Component component : components) {
|
||||
component.addMouseListener(iconListener);
|
||||
}
|
||||
|
||||
tabbedPane.addMouseListener(iconListener);
|
||||
// we need this due to the gaps between the renderer and the tabbed pane
|
||||
tabbedPane.addMouseMotionListener(iconListener);
|
||||
}
|
||||
|
||||
public void installRenameAction(MouseListener listener) {
|
||||
this.renameListener = listener;
|
||||
}
|
||||
|
@ -193,57 +161,6 @@ public class DockingTabRenderer extends JPanel {
|
|||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
/**
|
||||
* A class to hide/show the close button's icon to make it appear that the button is hidden
|
||||
* and removed.
|
||||
*/
|
||||
private class ButtonIconSwapperMouseListener extends MouseAdapter {
|
||||
|
||||
private final JTabbedPane tabbedPane;
|
||||
|
||||
ButtonIconSwapperMouseListener(JTabbedPane tabbedPane) {
|
||||
this.tabbedPane = tabbedPane;
|
||||
}
|
||||
|
||||
private void updateButton(MouseEvent e) {
|
||||
Point point = e.getPoint();
|
||||
Point tabbedPaneRelativePoint =
|
||||
SwingUtilities.convertPoint(e.getComponent(), point, tabbedPane);
|
||||
int x = tabbedPaneRelativePoint.x;
|
||||
int y = tabbedPaneRelativePoint.y;
|
||||
int tabIndex = tabbedPane.indexAtLocation(x, y);
|
||||
if (tabIndex < 0) {
|
||||
// no tab for the given point (can happen when over the tabbed pane, but not
|
||||
// over any tab)
|
||||
closeButton.setIcon(EMPTY_ICON);
|
||||
return;
|
||||
}
|
||||
|
||||
Component tabComponent = tabbedPane.getTabComponentAt(tabIndex);
|
||||
if (SwingUtilities.isDescendingFrom(closeButton, tabComponent)) {
|
||||
closeButton.setIcon(CLOSE_ICON);
|
||||
}
|
||||
else {
|
||||
closeButton.setIcon(EMPTY_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
updateButton(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
updateButton(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
closeButton.setIcon(EMPTY_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class designed to listen for mouse events on this renderer component which it will then
|
||||
* forward on to the given component.
|
||||
|
|
|
@ -72,7 +72,7 @@ public class LookAndFeelUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void platformSpecificFixups() {
|
||||
public static void performPlatformSpecificFixups() {
|
||||
// Set the dock icon for macOS
|
||||
if (Taskbar.isTaskbarSupported()) {
|
||||
Taskbar taskbar = Taskbar.getTaskbar();
|
||||
|
|
|
@ -204,8 +204,8 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testParseGroupingsParseError() {
|
||||
String source = "(ab (cd))(ef)( gh))";
|
||||
public void testParseGroupingsWithUnbalancedGroups() {
|
||||
String source = "(ab (cd))(ef)( gh))"; // note the groupings are unbalanced
|
||||
try {
|
||||
ThemeValueUtils.parseGroupings(source, '(', ')');
|
||||
fail("Expected parse Exception");
|
||||
|
@ -216,8 +216,8 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testParseGroupingsParseError2() {
|
||||
String source = " xx";
|
||||
public void testParseGroupingsWhenNoGroupingsExist() {
|
||||
String source = "xx yy"; // note there are no grouping chars
|
||||
try {
|
||||
ThemeValueUtils.parseGroupings(source, '(', ')');
|
||||
fail("Expected parse Exception");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue