Skip to content

Commit a9ac285

Browse files
committed
GROOVY-11932: Allow theme switching light/dark/system in groovyConsole (tweaks for context menus)
1 parent 0bc137e commit a9ac285

7 files changed

Lines changed: 121 additions & 37 deletions

File tree

subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstBrowser.groovy

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class AstBrowser {
7777
def prefs = new AstBrowserUiPreferences()
7878
Action refreshAction
7979
private CompilerConfiguration config
80+
private Runnable themeChangeListener
8081

8182
AstBrowser(inputArea, rootElement, classLoader, config = null) {
8283
this.inputArea = inputArea
@@ -127,7 +128,10 @@ class AstBrowser {
127128
size: prefs.frameSize,
128129
iconImage: swing.imageIcon(Console.ICON_PATH).image,
129130
defaultCloseOperation: WindowConstants.DISPOSE_ON_CLOSE,
130-
windowClosing: { event -> prefs.save(frame, splitterPane, mainSplitter, showScriptFreeForm, showScriptClass, showClosureClasses, phasePicker.selectedItem, showTreeView) }) {
131+
windowClosing: { event ->
132+
if (themeChangeListener) ThemeManager.removeThemeChangeListener(themeChangeListener)
133+
prefs.save(frame, splitterPane, mainSplitter, showScriptFreeForm, showScriptClass, showClosureClasses, phasePicker.selectedItem, showTreeView)
134+
}) {
131135

132136
menuBar {
133137
menu(text: 'Show Script', mnemonic: 'S') {
@@ -154,14 +158,14 @@ class AstBrowser {
154158
name: 'Larger Font',
155159
closure: this.&largerFont,
156160
mnemonic: 'L',
157-
smallIcon: Icons.load('text_increase'),
161+
smallIcon: Icons.menu('text_increase'),
158162
accelerator: shortcut('shift L'))
159163
}
160164
menuItem {
161165
action(name: 'Smaller Font',
162166
closure: this.&smallerFont,
163167
mnemonic: 'S',
164-
smallIcon: Icons.load('text_decrease'),
168+
smallIcon: Icons.menu('text_decrease'),
165169
accelerator: shortcut('shift S'))
166170
}
167171
menuItem {
@@ -173,15 +177,15 @@ class AstBrowser {
173177
initAuxViews()
174178
},
175179
mnemonic: 'R',
176-
smallIcon: Icons.load('refresh'),
180+
smallIcon: Icons.menu('refresh'),
177181
accelerator: KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0))
178182
}
179183
}
180184
menu(text: 'Help', mnemonic: 'H') {
181185
menuItem { action(
182186
name: 'About',
183187
closure: this.&aboutAction,
184-
smallIcon: Icons.load('info'),
188+
smallIcon: Icons.menu('info'),
185189
mnemonic: 'A')
186190
}
187191
}
@@ -346,6 +350,31 @@ class AstBrowser {
346350
jTree.rootVisible = false
347351
jTree.showsRootHandles = true // some OS's require this as a step to show nodes
348352

353+
themeChangeListener = { applyCurrentThemeToSelf() } as Runnable
354+
ThemeManager.addThemeChangeListener(themeChangeListener)
355+
}
356+
357+
private void applyCurrentThemeToSelf() {
358+
def fg = ThemeManager.isDark() ? new java.awt.Color(204, 204, 204) : java.awt.Color.BLACK
359+
def bg = ThemeManager.inputBackground
360+
[decompiledSource, bytecodeView, asmifierView].each { editor ->
361+
if (!editor) return
362+
editor.textEditor.background = bg
363+
editor.textEditor.foreground = fg
364+
editor.reapplyHighlighting()
365+
}
366+
Icons.refreshAll()
367+
// DefaultTreeCellRenderer.updateUI() — triggered by FlatLaf.updateUI's
368+
// cascade — resets leaf/closed/open icons to LaF defaults, wiping our
369+
// custom green circle. Defer re-apply onto the EDT so it runs after
370+
// the cascade settles.
371+
javax.swing.SwingUtilities.invokeLater {
372+
if (jTree?.cellRenderer) {
373+
try { jTree.cellRenderer.leafIcon = Console.nodeIcon } catch (ignored) {}
374+
jTree.repaint()
375+
}
376+
frame?.repaint()
377+
}
349378
}
350379

351380
private static final int INITIAL_CAPACITY = 64 * 1024 // 64K

subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import groovy.transform.EqualsAndHashCode
3434
import groovy.transform.ThreadInterrupt
3535
import groovy.transform.TupleConstructor
3636
import groovy.ui.GroovyMain
37-
import org.antlr.v4.gui.TestRig
38-
import org.antlr.v4.runtime.CharStream
37+
import org.antlr.v4.gui.TreeViewer
38+
import org.antlr.v4.gui.Trees
3939
import org.antlr.v4.runtime.CharStreams
4040
import org.antlr.v4.runtime.CommonTokenStream
4141
import org.apache.groovy.antlr.LexerFrame
@@ -766,6 +766,8 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
766766
ThemeManager.applyTheme(mode)
767767
// reapply custom styles for all open console windows
768768
consoleControllers.each { Console console -> console.reapplyStyles() }
769+
// let auxiliary windows (AstBrowser, ObjectBrowser) retint themselves
770+
ThemeManager.notifyThemeChanged()
769771
}
770772

771773
private void reapplyStyles() {
@@ -1212,27 +1214,55 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
12121214
new AstBrowser(inputArea, rootElement, shell.getClassLoader(), config).run({ inputArea.getText() })
12131215
}
12141216

1215-
@CompileStatic
1216-
private static class CstInspector extends TestRig {
1217-
CstInspector() throws Exception {
1218-
super(new String[] { 'Groovy', 'compilationUnit', '-gui' })
1219-
}
1220-
1221-
void inspectParseTree(String text) {
1222-
CharStream charStream = CharStreams.fromReader(new StringReader(text))
1223-
GroovyLangLexer lexer = new GroovyLangLexer(charStream)
1224-
CommonTokenStream tokens = new CommonTokenStream(lexer)
1225-
GroovyLangParser parser = new GroovyLangParser(tokens)
1226-
process(lexer, GroovyLangParser.class, parser, charStream)
1227-
}
1228-
}
1229-
1230-
@CompileStatic
12311217
void inspectCst(EventObject evt = null) {
12321218
String text = this.inputEditor.textEditor.text
1219+
def charStream = CharStreams.fromReader(new StringReader(text))
1220+
def lexer = new GroovyLangLexer(charStream)
1221+
def tokens = new CommonTokenStream(lexer)
1222+
def parser = new GroovyLangParser(tokens)
1223+
def tree = parser.compilationUnit()
1224+
// Trees.inspect spawns its own dialog on the EDT; we wait off-EDT then
1225+
// hop back on to theme it — ANTLR's TreeViewer defaults to black text
1226+
// on white/transparent boxes, which ignores the app theme otherwise.
1227+
def future = Trees.inspect(tree, parser)
1228+
Thread.start {
1229+
def dialog = future.get()
1230+
javax.swing.SwingUtilities.invokeLater { themeCstDialog(dialog) }
1231+
}
1232+
}
1233+
1234+
private void themeCstDialog(javax.swing.JDialog dialog) {
1235+
TreeViewer viewer = findTreeViewer(dialog)
1236+
if (!viewer) return
1237+
applyCstColors(viewer)
1238+
// track theme switches so an already-open CST dialog keeps pace
1239+
def listener = { javax.swing.SwingUtilities.invokeLater { applyCstColors(viewer); viewer.repaint() } } as Runnable
1240+
ThemeManager.addThemeChangeListener(listener)
1241+
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
1242+
@Override void windowClosed(java.awt.event.WindowEvent e) {
1243+
ThemeManager.removeThemeChangeListener(listener)
1244+
}
1245+
})
1246+
}
12331247

1234-
def gtr = new CstInspector()
1235-
gtr.inspectParseTree(text)
1248+
private static void applyCstColors(TreeViewer viewer) {
1249+
boolean dark = ThemeManager.isDark()
1250+
viewer.textColor = dark ? new java.awt.Color(210, 210, 210) : java.awt.Color.BLACK
1251+
viewer.boxColor = dark ? new java.awt.Color(50, 55, 65) : java.awt.Color.WHITE
1252+
viewer.borderColor = null // null = no box outline around each label (TreeViewer default)
1253+
viewer.highlightedBoxColor = dark ? new java.awt.Color(80, 120, 80) : new java.awt.Color(200, 255, 200)
1254+
viewer.background = dark ? new java.awt.Color(43, 43, 43) : java.awt.Color.WHITE
1255+
}
1256+
1257+
private static TreeViewer findTreeViewer(java.awt.Container container) {
1258+
for (Component comp : container.components) {
1259+
if (comp instanceof TreeViewer) return (TreeViewer) comp
1260+
if (comp instanceof java.awt.Container) {
1261+
TreeViewer found = findTreeViewer((java.awt.Container) comp)
1262+
if (found) return found
1263+
}
1264+
}
1265+
null
12361266
}
12371267

12381268
void inspectTokens(EventObject evt = null) {

subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleTextEditor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public TextUndoManager getUndoManager() {
162162
* because UndoableEdits restore attributes captured at edit time,
163163
* which may no longer match the active theme.
164164
*/
165-
private void reapplyHighlighting() {
165+
public void reapplyHighlighting() {
166166
Document doc = textEditor.getDocument();
167167
if (!(doc instanceof DefaultStyledDocument)) {
168168
return;

subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleView.groovy

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,27 @@ container(consoleFrame) {
7171
build(statusBarClass)
7272
}
7373

74+
// Popup menus are FlatLaf-drawn even on macOS (the screen menu bar only
75+
// captures JMenuBar), so popup menu-item icons must track the app theme
76+
// — override each action's smallIcon (which is OS-themed for the screen
77+
// menu bar) with an app-themed instance here.
7478
inputEditor.textEditor.componentPopupMenu = popupMenu {
75-
menuItem(cutAction)
76-
menuItem(copyAction)
77-
menuItem(pasteAction)
78-
menuItem(selectAllAction)
79+
menuItem(cutAction, icon: Icons.load('content_cut'))
80+
menuItem(copyAction, icon: Icons.load('content_copy'))
81+
menuItem(pasteAction, icon: Icons.load('content_paste'))
82+
menuItem(selectAllAction, icon: Icons.load('select_all'))
7983
separator()
80-
menuItem(undoAction)
81-
menuItem(redoAction)
84+
menuItem(undoAction, icon: Icons.load('undo'))
85+
menuItem(redoAction, icon: Icons.load('redo'))
8286
separator()
83-
menuItem(runAction)
87+
menuItem(runAction, icon: Icons.green('play_arrow'))
8488
menuItem(runSelectionAction)
8589
}
8690

8791
outputArea.componentPopupMenu = popupMenu {
88-
menuItem(copyAction)
89-
menuItem(selectAllAction)
90-
menuItem(clearOutputAction)
92+
menuItem(copyAction, icon: Icons.load('content_copy'))
93+
menuItem(selectAllAction, icon: Icons.load('select_all'))
94+
menuItem(clearOutputAction, icon: Icons.load('delete_sweep'))
9195
}
9296

9397
controller.promptStyle = promptStyle

subprojects/groovy-console/src/main/groovy/groovy/console/ui/Icons.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ class Icons {
158158
}
159159

160160
void refreshColors() {
161+
// rebuild the FlatSVGIcon entirely — setColorFilter alone leaves the
162+
// internal raster cache in a state where some contexts (notably the
163+
// macOS screen menu bar) keep painting blank icons after a theme switch
164+
this.delegate = new FlatSVGIcon(path, size, size)
161165
delegate.setColorFilter(new FlatSVGIcon.ColorFilter(colorMapper as Function<Color, Color>))
162166
}
163167

subprojects/groovy-console/src/main/groovy/groovy/console/ui/ObjectBrowser.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class ObjectBrowser {
102102
menuItem { action(name: 'Usage', closure: this.&usageAction) }
103103
menuItem { action(
104104
name: 'About',
105-
smallIcon: Icons.load('info'),
105+
smallIcon: Icons.menu('info'),
106106
closure: this.&aboutAction)
107107
}
108108
}

subprojects/groovy-console/src/main/groovy/groovy/console/ui/ThemeManager.groovy

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import javax.swing.UIManager
2626
import javax.swing.text.StyleConstants
2727
import javax.swing.text.StyleContext
2828
import java.awt.Color
29+
import java.util.concurrent.CopyOnWriteArrayList
2930
import java.util.prefs.Preferences
3031

3132
/**
@@ -43,6 +44,22 @@ class ThemeManager {
4344
// (icon color filters), so we avoid shelling out per call
4445
private static volatile Boolean cachedSystemDark = null
4546

47+
// theme-change listeners — invoked after the LaF is installed so auxiliary
48+
// frames (AstBrowser, ObjectBrowser) can retint their own text panes/icons
49+
private static final List<Runnable> themeChangeListeners = new CopyOnWriteArrayList<>()
50+
51+
static void addThemeChangeListener(Runnable listener) {
52+
themeChangeListeners << listener
53+
}
54+
55+
static void removeThemeChangeListener(Runnable listener) {
56+
themeChangeListeners.remove(listener)
57+
}
58+
59+
static void notifyThemeChanged() {
60+
themeChangeListeners.each { it.run() }
61+
}
62+
4663
static ThemeMode getCurrentMode() {
4764
try {
4865
ThemeMode.valueOf(prefs.get('theme', 'SYSTEM').toUpperCase())

0 commit comments

Comments
 (0)