From 015b04a29a576d4dd632ad734e72e1d5bdbcf189 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 14 Sep 2020 12:27:52 +0200 Subject: [PATCH] UI defaults inspector: initial commit with basic functionality --- CHANGELOG.md | 3 + .../flatlaf/util/ScaledEmptyBorder.java | 53 +++ .../com/formdev/flatlaf/demo/DemoFrame.java | 15 +- .../com/formdev/flatlaf/demo/DemoFrame.jfd | 7 +- .../com/formdev/flatlaf/demo/FlatLafDemo.java | 4 +- flatlaf-extras/README.md | 3 + .../extras/FlatUIDefaultsInspector.java | 373 ++++++++++++++++++ .../extras/FlatUIDefaultsInspector.jfd | 33 ++ 8 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledEmptyBorder.java create mode 100644 flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.java create mode 100644 flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.jfd diff --git a/CHANGELOG.md b/CHANGELOG.md index f90ca5e5..6f478494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ FlatLaf Change Log - Menu items "File > Open" and "File > Save As" now show file choosers. - InternalFrame: Support draggable border for resizing frame inside of the visible frame border. (issue #121) +- `FlatUIDefaultsInspector` added (see [FlatLaf Extras](flatlaf-extras)). A + simple UI defaults inspector that shows a window with all UI defaults used in + current theme (look and feel). #### Fixed bugs diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledEmptyBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledEmptyBorder.java new file mode 100644 index 00000000..4d40e61e --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledEmptyBorder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * 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 + * + * https://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 com.formdev.flatlaf.util; + +import static com.formdev.flatlaf.util.UIScale.scale; +import java.awt.Component; +import java.awt.Insets; +import javax.swing.border.EmptyBorder; + +/** + * Empty border that scales insets. + * + * @author Karl Tauber + */ +public class ScaledEmptyBorder + extends EmptyBorder +{ + public ScaledEmptyBorder( int top, int left, int bottom, int right ) { + super( top, left, bottom, right ); + } + + public ScaledEmptyBorder( Insets insets ) { + super( insets ); + } + + @Override + public Insets getBorderInsets() { + return new Insets( scale( top ), scale( left ), scale( bottom ), scale( right ) ); + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + insets.left = scale( left ); + insets.top = scale( top ); + insets.right = scale( right ); + insets.bottom = scale( bottom ); + return insets; + } +} diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 70fc1748..8100f140 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -31,6 +31,7 @@ import com.formdev.flatlaf.demo.extras.*; import com.formdev.flatlaf.demo.intellijthemes.*; import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import com.formdev.flatlaf.extras.FlatSVGIcon; +import com.formdev.flatlaf.extras.FlatUIDefaultsInspector; import com.formdev.flatlaf.extras.SVGUtils; import com.formdev.flatlaf.ui.JBRCustomDecorations; import net.miginfocom.layout.ConstraintParser; @@ -93,6 +94,10 @@ class DemoFrame state.remove( "hint.themesPanel" ); } + private void showUIDefaultsInspector() { + FlatUIDefaultsInspector.show(); + } + private void openActionPerformed() { JFileChooser chooser = new JFileChooser(); chooser.showOpenDialog( this ); @@ -104,7 +109,7 @@ class DemoFrame } private void exitActionPerformed() { - dispose(); + System.exit( 0 ); } private void aboutActionPerformed() { @@ -308,6 +313,7 @@ class DemoFrame alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem(); animatedLafChangeMenuItem = new JCheckBoxMenuItem(); JMenuItem showHintsMenuItem = new JMenuItem(); + JMenuItem showUIDefaultsInspectorMenuItem = new JMenuItem(); JMenu helpMenu = new JMenu(); JMenuItem aboutMenuItem = new JMenuItem(); JToolBar toolBar1 = new JToolBar(); @@ -331,7 +337,7 @@ class DemoFrame //======== this ======== setTitle("FlatLaf Demo"); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); @@ -579,6 +585,11 @@ class DemoFrame showHintsMenuItem.setText("Show hints"); showHintsMenuItem.addActionListener(e -> showHintsChanged()); optionsMenu.add(showHintsMenuItem); + + //---- showUIDefaultsInspectorMenuItem ---- + showUIDefaultsInspectorMenuItem.setText("Show UIDefaults Inspector"); + showUIDefaultsInspectorMenuItem.addActionListener(e -> showUIDefaultsInspector()); + optionsMenu.add(showUIDefaultsInspectorMenuItem); } menuBar1.add(optionsMenu); diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 13895915..f7cb04af 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -9,7 +9,7 @@ new FormModel { add( new FormWindow( "javax.swing.JFrame", new FormLayoutManager( class java.awt.BorderLayout ) ) { name: "this" "title": "FlatLaf Demo" - "defaultCloseOperation": 2 + "defaultCloseOperation": 3 "$locationPolicy": 2 "$sizePolicy": 2 add( new FormContainer( "javax.swing.JToolBar", new FormLayoutManager( class javax.swing.JToolBar ) ) { @@ -383,6 +383,11 @@ new FormModel { "text": "Show hints" addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showHintsChanged", false ) ) } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "showUIDefaultsInspectorMenuItem" + "text": "Show UIDefaults Inspector" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showUIDefaultsInspector", false ) ) + } ) } ) add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { name: "helpMenu" diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java index 38d0ef11..3e24a316 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java @@ -22,6 +22,7 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.FlatInspector; +import com.formdev.flatlaf.extras.FlatUIDefaultsInspector; import com.formdev.flatlaf.util.SystemInfo; /** @@ -52,8 +53,9 @@ public class FlatLafDemo // set look and feel DemoPrefs.initLaf( args ); - // install inspector + // install inspectors FlatInspector.install( "ctrl shift alt X" ); + FlatUIDefaultsInspector.install( "ctrl shift alt Y" ); // create frame DemoFrame frame = new DemoFrame(); diff --git a/flatlaf-extras/README.md b/flatlaf-extras/README.md index 42b0499e..c7bf8b80 100644 --- a/flatlaf-extras/README.md +++ b/flatlaf-extras/README.md @@ -11,6 +11,9 @@ This sub-project provides some additional components and classes: - [FlatSVGIcon](src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java): An icon that displays SVG using [svgSalamander](https://github.com/JFormDesigner/svgSalamander). +- [FlatUIDefaultsInspector](src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.java): + A simple UI defaults inspector that shows a window with all UI defaults used + in current theme (look and feel). - [TriStateCheckBox](src/main/java/com/formdev/flatlaf/extras/TriStateCheckBox.java): A tri-state check box. diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.java new file mode 100644 index 00000000..eff21202 --- /dev/null +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.java @@ -0,0 +1,373 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * 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 + * + * https://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 com.formdev.flatlaf.extras; + +import java.awt.*; +import java.awt.event.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.prefs.Preferences; +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableColumnModel; +import com.formdev.flatlaf.util.HSLColor; +import com.formdev.flatlaf.util.ScaledEmptyBorder; +import com.formdev.flatlaf.util.UIScale; + +/** + * A simple UI defaults inspector that shows a window with all UI defaults used + * in current look and feel. + *

+ * To use it in an application install it with: + *

+ * FlatUIDefaultsInspector.install( "ctrl shift alt Y" );
+ * 
+ * This can be done e.g. in the main() method and allows enabling (and disabling) + * the UI defaults inspector with the given keystroke. + * + * @author Karl Tauber + */ +public class FlatUIDefaultsInspector +{ + private static final int KEY_MODIFIERS_MASK = InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.META_DOWN_MASK; + + private static FlatUIDefaultsInspector inspector; + + /** + * Installs a key listener into the application that allows enabling and disabling + * the UI inspector with the given keystroke (e.g. "ctrl shift alt Y"). + */ + public static void install( String activationKeys ) { + KeyStroke keyStroke = KeyStroke.getKeyStroke( activationKeys ); + Toolkit.getDefaultToolkit().addAWTEventListener( e -> { + if( e.getID() == KeyEvent.KEY_RELEASED && + ((KeyEvent)e).getKeyCode() == keyStroke.getKeyCode() && + (((KeyEvent)e).getModifiersEx() & KEY_MODIFIERS_MASK) == (keyStroke.getModifiers() & KEY_MODIFIERS_MASK) ) + { + show(); + } + }, AWTEvent.KEY_EVENT_MASK ); + } + + public static void show() { + if( inspector != null ) { + inspector.frame.toFront(); + return; + } + + inspector = new FlatUIDefaultsInspector(); + inspector.frame.setVisible( true ); + } + + public static void hide() { + if( inspector != null ) + inspector.frame.dispose(); + } + + private FlatUIDefaultsInspector() { + initComponents(); + + panel.setBorder( new ScaledEmptyBorder( 10, 10, 10, 10 ) ); + + // initialize table + Item[] items = getUIDefaultsItems(); + table.setModel( new ItemsTableModel( items ) ); + table.setDefaultRenderer( Item.class, new ValueRenderer() ); + + // restore window bounds + Preferences prefs = getPrefs(); + int x = prefs.getInt( "x", -1 ); + int y = prefs.getInt( "y", -1 ); + int width = prefs.getInt( "width", UIScale.scale( 600 ) ); + int height = prefs.getInt( "height", UIScale.scale( 800 ) ); + frame.setSize( width, height ); + if( x >= 0 && y >= 0 ) + frame.setLocation( x, y ); + else + frame.setLocationRelativeTo( null ); + + // restore column widths + TableColumnModel columnModel = table.getColumnModel(); + columnModel.getColumn( 0 ).setPreferredWidth( prefs.getInt( "column1width", 100 ) ); + columnModel.getColumn( 1 ).setPreferredWidth( prefs.getInt( "column2width", 100 ) ); + } + + private Item[] getUIDefaultsItems() { + UIDefaults defaults = UIManager.getDefaults(); + + ArrayList items = new ArrayList<>( defaults.size() ); + Enumeration e = defaults.keys(); + while( e.hasMoreElements() ) { + Object key = e.nextElement(); + + // ignore non-string keys + if( !(key instanceof String) ) + continue; + + // ignore values of type Class + Object value = defaults.get( key ); + if( value instanceof Class ) + continue; + + Item item = new Item(); + item.key = String.valueOf( key ); + item.value = value; + items.add( item ); + } + + items.sort( (item1, item2) -> item1.key.compareToIgnoreCase( item2.key ) ); + return items.toArray( new Item[items.size()] ); + } + + private void saveWindowBounds() { + Preferences prefs = getPrefs(); + prefs.putInt( "x", frame.getX() ); + prefs.putInt( "y", frame.getY() ); + prefs.putInt( "width", frame.getWidth() ); + prefs.putInt( "height", frame.getHeight() ); + + TableColumnModel columnModel = table.getColumnModel(); + prefs.putInt( "column1width", columnModel.getColumn( 0 ).getWidth() ); + prefs.putInt( "column2width", columnModel.getColumn( 1 ).getWidth() ); + } + + private Preferences getPrefs() { + return Preferences.userRoot().node( "flatlaf-uidefaults-inspector" ); + } + + private void windowClosed() { + inspector = null; + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + frame = new JFrame(); + panel = new JPanel(); + scrollPane = new JScrollPane(); + table = new JTable(); + + //======== frame ======== + { + frame.setTitle("UI Defaults"); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + FlatUIDefaultsInspector.this.windowClosed(); + } + @Override + public void windowClosing(WindowEvent e) { + saveWindowBounds(); + } + @Override + public void windowDeactivated(WindowEvent e) { + saveWindowBounds(); + } + }); + Container frameContentPane = frame.getContentPane(); + frameContentPane.setLayout(new BorderLayout()); + + //======== panel ======== + { + panel.setLayout(new BorderLayout()); + + //======== scrollPane ======== + { + scrollPane.setViewportView(table); + } + panel.add(scrollPane, BorderLayout.CENTER); + } + frameContentPane.add(panel, BorderLayout.CENTER); + } + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JFrame frame; + private JPanel panel; + private JScrollPane scrollPane; + private JTable table; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class Item --------------------------------------------------------- + + private static class Item { + String key; + Object value; + } + + //---- class ItemsTableModel ---------------------------------------------- + + private static class ItemsTableModel + extends AbstractTableModel + { + private final Item[] items; + + ItemsTableModel( Item[] items ) { + this.items = items; + } + + @Override + public int getRowCount() { + return items.length; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public String getColumnName( int columnIndex ) { + switch( columnIndex ) { + case 0: return "Name"; + case 1: return "Value"; + } + return super.getColumnName( columnIndex ); + } + + @Override + public Class getColumnClass( int columnIndex ) { + switch( columnIndex ) { + case 0: return String.class; + case 1: return Item.class; + } + return super.getColumnClass( columnIndex ); + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) { + Item item = items[rowIndex]; + switch( columnIndex ) { + case 0: return item.key; + case 1: return item; + } + return null; + } + } + + //---- class ValueRenderer ------------------------------------------------ + + private static class ValueRenderer + extends DefaultTableCellRenderer + { + @Override + public Component getTableCellRendererComponent( JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column ) + { + Item item = (Item) value; + + // reset background, foreground and icon + if( !(item.value instanceof Color) ) { + setBackground( null ); + setForeground( null ); + } + if( !(item.value instanceof Icon) ) + setIcon( null ); + + // value to string + if( item.value instanceof Color ) { + Color color = (Color) item.value; + HSLColor hslColor = new HSLColor( color ); + if( color.getAlpha() == 255 ) { + value = String.format( "#%06x rgb(%d, %d, %d) hsl(%d, %d, %d)", + color.getRGB() & 0xffffff, + color.getRed(), color.getGreen(), color.getBlue(), + (int) hslColor.getHue(), (int) hslColor.getSaturation(), + (int) hslColor.getLuminance() ); + } else { + value = String.format( "#%06x%02x rgba(%d, %d, %d, %d) hsla(%d, %d, %d, %d)", + color.getRGB() & 0xffffff, color.getAlpha(), + color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha(), + (int) hslColor.getHue(), (int) hslColor.getSaturation(), + (int) hslColor.getLuminance(), (int) (hslColor.getAlpha() * 100) ); + } + } else if( item.value instanceof Insets ) { + Insets insets = (Insets) item.value; + value = insets.top + "," + insets.left + "," + insets.bottom + "," + insets.right; + } else if( item.value instanceof Dimension ) { + Dimension dim = (Dimension) item.value; + value = dim.width + "," + dim.height; + } else if( item.value instanceof Font ) { + Font font = (Font) item.value; + value = font.getFamily() + " " + font.getSize(); + if( font.isBold() ) + value += " bold"; + if( font.isItalic() ) + value += " italic"; + } else if( item.value instanceof Icon ) { + Icon icon = (Icon) item.value; + value = icon.getIconWidth() + "x" + icon.getIconHeight() + " " + icon.getClass().getName(); + setIcon( new SafeIcon( icon ) ); + } else if( item.value instanceof ActionMap ) { + ActionMap actionMap = (ActionMap) item.value; + value = "ActionMap (" + actionMap.size() + ")"; + } else if( item.value instanceof InputMap ) { + InputMap inputMap = (InputMap) item.value; + value = "InputMap (" + inputMap.size() + ")"; + } else + value = item.value; + + super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); + + if( item.value instanceof Color ) { + Color color = (Color) item.value; + boolean isDark = new HSLColor( color ).getLuminance() < 70; + setBackground( color ); + setForeground( isDark ? Color.white : Color.black ); + } + + setToolTipText( String.valueOf( item.value ) ); + return this; + } + } + + //---- class SafeIcon ----------------------------------------------------- + + private static class SafeIcon + implements Icon + { + private final Icon icon; + + SafeIcon( Icon icon ) { + this.icon = icon; + } + + @Override + public void paintIcon( Component c, Graphics g, int x, int y ) { + try { + icon.paintIcon( c, g, x, y ); + } catch( Exception ex ) { + g.setColor( Color.red ); + g.drawRect( x, y, getIconWidth() - 1, getIconHeight() - 1 ); + } + } + + @Override + public int getIconWidth() { + return icon.getIconWidth(); + } + + @Override + public int getIconHeight() { + return icon.getIconHeight(); + } + } +} diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.jfd b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.jfd new file mode 100644 index 00000000..2eb0ad87 --- /dev/null +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatUIDefaultsInspector.jfd @@ -0,0 +1,33 @@ +JFDML JFormDesigner: "7.0.2.0.298" Java: "14" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormWindow( "javax.swing.JFrame", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "frame" + "title": "UI Defaults" + "defaultCloseOperation": 2 + "$sizePolicy": 2 + "$locationPolicy": 2 + addEvent( new FormEvent( "java.awt.event.WindowListener", "windowClosed", "windowClosed", false ) ) + addEvent( new FormEvent( "java.awt.event.WindowListener", "windowClosing", "saveWindowBounds", false ) ) + addEvent( new FormEvent( "java.awt.event.WindowListener", "windowDeactivated", "saveWindowBounds", false ) ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "panel" + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane" + add( new FormComponent( "javax.swing.JTable" ) { + name: "table" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 400, 300 ) + } ) + } +}