All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.formdev.flatlaf.extras.FlatInspector Maven / Gradle / Ivy

There is a newer version: 3.5.2
Show newest version
/*
 * Copyright 2019 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
 *
 *     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 com.formdev.flatlaf.extras;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.LayoutManager;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.swing.AbstractButton;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JMenuBar;
import javax.swing.JRootPane;
import javax.swing.JToolBar;
import javax.swing.JToolTip;
import javax.swing.KeyStroke;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.plaf.UIResource;
import javax.swing.text.JTextComponent;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.ui.MigLayoutVisualPadding;
import com.formdev.flatlaf.util.SystemInfo;
import com.formdev.flatlaf.util.UIScale;

/**
 * A simple UI inspector that shows information about UI component at mouse location
 * in a tooltip.
 * 

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

 * FlatInspector.install( "ctrl shift alt X" );
 * 
* This can be done e.g. in the main() method and allows enabling (and disabling) * the UI inspector for the active window with the given keystroke. *

* When the UI inspector is active some additional keys are available: *

    *
  • press {@code Esc} key to disable UI inspector
  • *
  • press {@code Ctrl} key to increase inspection level, which shows * information about parent of UI component at mouse location
  • *
  • press {@code Shift} key to decrease inspection level
  • *
* * @author Karl Tauber */ public class FlatInspector { private static final Integer HIGHLIGHT_LAYER = JLayeredPane.POPUP_LAYER - 1; private static final int KEY_MODIFIERS_MASK = InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.META_DOWN_MASK; private final JRootPane rootPane; private final MouseMotionListener mouseMotionListener; private final AWTEventListener keyListener; private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport( this ); private final WindowListener windowListener; private Window window; private boolean enabled; private Object oldGlassPaneFullHeight; private Component lastComponent; private int lastX; private int lastY; private int inspectParentLevel; private boolean wasModifierKeyPressed; private boolean showClassHierarchy; private long lastWhen; private JComponent highlightFigure; private Popup popup; /** * Installs a key listener into the application that allows enabling and disabling * the UI inspector with the given keystroke (e.g. "ctrl shift alt X"). * * @param activationKeys a keystroke (e.g. "ctrl shift alt X") */ 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) ) { Window activeWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); RootPaneContainer rootPaneContainer = null; if( activeWindow instanceof RootPaneContainer ) rootPaneContainer = (RootPaneContainer) activeWindow; else { // search for root pain container in children // (e.g. for Swing embedded into SWT) for( Component child : activeWindow.getComponents() ) { if( child instanceof RootPaneContainer ) { rootPaneContainer = (RootPaneContainer) child; break; } } } if( rootPaneContainer != null ) { JRootPane rootPane = rootPaneContainer.getRootPane(); FlatInspector inspector = (FlatInspector) rootPane.getClientProperty( FlatInspector.class ); if( inspector == null ) { inspector = new FlatInspector( rootPane ); rootPane.putClientProperty( FlatInspector.class, inspector ); inspector.setEnabled( true ); } else { inspector.setEnabled( false ); rootPane.putClientProperty( FlatInspector.class, null ); } } } }, AWTEvent.KEY_EVENT_MASK ); } public FlatInspector( JRootPane rootPane ) { this.rootPane = rootPane; mouseMotionListener = new MouseMotionAdapter() { @Override public void mouseMoved( MouseEvent e ) { lastX = e.getX(); lastY = e.getY(); inspect( lastX, lastY ); } }; keyListener = e -> { KeyEvent keyEvent = (KeyEvent) e; int keyCode = keyEvent.getKeyCode(); int id = e.getID(); if( id == KeyEvent.KEY_PRESSED ) { // this avoids that the inspection level is changed when UI inspector // is enabled with keyboard shortcut (e.g. Ctrl+Shift+Alt+X) if( keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_ALT ) wasModifierKeyPressed = true; } else if( id == KeyEvent.KEY_RELEASED && wasModifierKeyPressed ) { // ignore duplicate events (for Swing embedded into SWT) if( (keyEvent.getWhen() - lastWhen) <= 5 ) return; lastWhen = keyEvent.getWhen(); if( keyCode == KeyEvent.VK_CONTROL ) { inspectParentLevel++; int parentLevel = inspect( lastX, lastY ); // limit level if( inspectParentLevel > parentLevel ) inspectParentLevel = parentLevel; } else if( keyCode == KeyEvent.VK_SHIFT && inspectParentLevel > 0 ) { inspectParentLevel--; int parentLevel = inspect( lastX, lastY ); // decrease level if( inspectParentLevel > parentLevel ) { inspectParentLevel = Math.max( parentLevel - 1, 0 ); inspect( lastX, lastY ); } } else if( keyCode == KeyEvent.VK_ALT && lastComponent != null) { showClassHierarchy = !showClassHierarchy; showToolTip( lastComponent, lastX, lastY, inspectParentLevel ); } } if( keyCode == KeyEvent.VK_ESCAPE ) { // consume pressed and released ESC key events to e.g. avoid that dialog is closed keyEvent.consume(); if( id == KeyEvent.KEY_PRESSED ) { setEnabled( false ); FlatInspector inspector = (FlatInspector) rootPane.getClientProperty( FlatInspector.class ); if( inspector == FlatInspector.this ) rootPane.putClientProperty( FlatInspector.class, null ); } } }; windowListener = new WindowAdapter() { @Override public void windowActivated( WindowEvent e ) { update(); } @Override public void windowDeactivated( WindowEvent e ) { hidePopup(); } }; } public void addPropertyChangeListener( PropertyChangeListener l ) { propertyChangeSupport.addPropertyChangeListener( l ); } public void removePropertyChangeListener( PropertyChangeListener l ) { propertyChangeSupport.removePropertyChangeListener( l ); } public boolean isEnabled() { return enabled; } public void setEnabled( boolean enabled ) { if( this.enabled == enabled ) return; this.enabled = enabled; // make sure that glass pane has full height if enabled if( enabled ) { oldGlassPaneFullHeight = rootPane.getClientProperty( FlatClientProperties.GLASS_PANE_FULL_HEIGHT ); rootPane.putClientProperty( FlatClientProperties.GLASS_PANE_FULL_HEIGHT, true ); rootPane.validate(); } else rootPane.putClientProperty( FlatClientProperties.GLASS_PANE_FULL_HEIGHT, oldGlassPaneFullHeight ); // make sure that glass pane is not opaque, which is not the case in WebLaF ((JComponent)rootPane.getGlassPane()).setOpaque( false ); rootPane.getGlassPane().setVisible( enabled ); // add/remove key listener Toolkit toolkit = Toolkit.getDefaultToolkit(); if( enabled ) toolkit.addAWTEventListener( keyListener, AWTEvent.KEY_EVENT_MASK ); else toolkit.removeAWTEventListener( keyListener ); // add/remove mouse listener if( enabled ) rootPane.getGlassPane().addMouseMotionListener( mouseMotionListener ); else rootPane.getGlassPane().removeMouseMotionListener( mouseMotionListener ); // add/remove window listener if( enabled ) { window = SwingUtilities.windowForComponent( rootPane ); if( window != null ) window.addWindowListener( windowListener ); } else { if( window != null ) { window.removeWindowListener( windowListener ); window = null; } } // show/hide popup if( enabled ) { Point pt = new Point( MouseInfo.getPointerInfo().getLocation() ); SwingUtilities.convertPointFromScreen( pt, rootPane ); lastX = pt.x; lastY = pt.y; inspect( lastX, lastY ); } else { lastComponent = null; inspectParentLevel = 0; if( highlightFigure != null ) highlightFigure.getParent().remove( highlightFigure ); highlightFigure = null; hidePopup(); } propertyChangeSupport.firePropertyChange( "enabled", !enabled, enabled ); } private void hidePopup() { if( popup != null ) { popup.hide(); popup = null; } } public void update() { if( !rootPane.getGlassPane().isVisible() ) return; EventQueue.invokeLater( () -> { setEnabled( false ); setEnabled( true ); inspect( lastX, lastY ); } ); } private int inspect( int x, int y ) { Point pt = SwingUtilities.convertPoint( rootPane.getGlassPane(), x, y, rootPane ); Component c = getDeepestComponentAt( rootPane, pt.x, pt.y ); int parentLevel = 0; for( int i = 0; i < inspectParentLevel && c != null; i++ ) { Container parent = c.getParent(); if( parent == null ) break; c = parent; parentLevel++; } if( c == lastComponent ) return parentLevel; lastComponent = c; highlight( c ); showToolTip( c, x, y, parentLevel ); return parentLevel; } private Component getDeepestComponentAt( Component parent, int x, int y ) { if( !parent.contains( x, y ) ) return null; if( parent instanceof Container ) { for( Component child : ((Container)parent).getComponents() ) { if( child == null || !child.isVisible() ) continue; int cx = x - child.getX(); int cy = y - child.getY(); Component c = (child instanceof Container) ? getDeepestComponentAt( child, cx, cy ) : child.getComponentAt( cx, cy ); if( c == null || !c.isVisible() ) continue; // ignore highlight figure and tooltip if( c == highlightFigure ) continue; // ignore glass pane if( c.getParent() instanceof JRootPane && c == ((JRootPane)c.getParent()).getGlassPane() ) continue; if( "com.formdev.flatlaf.ui.FlatWindowResizer".equals( c.getClass().getName() ) ) continue; return c; } } return parent; } private void highlight( Component c ) { if( highlightFigure == null ) { highlightFigure = createHighlightFigure(); rootPane.getLayeredPane().add( highlightFigure, HIGHLIGHT_LAYER ); } highlightFigure.setVisible( c != null ); if( c != null ) { Insets insets = rootPane.getInsets(); highlightFigure.setBounds( new Rectangle( SwingUtilities.convertPoint( c, -insets.left, -insets.top, rootPane ), c.getSize() ) ); } } private JComponent createHighlightFigure() { JComponent c = new JComponent() { @Override protected void paintComponent( Graphics g ) { g.setColor( getBackground() ); g.fillRect( 0, 0, getWidth(), getHeight() ); } @Override protected void paintBorder( Graphics g ) { Object[] oldRenderingHints = FlatUIUtils.setRenderingHints( g ); super.paintBorder( g ); FlatUIUtils.resetRenderingHints( g, oldRenderingHints ); } }; c.setBackground( new Color( 255, 0, 0, 32 ) ); c.setBorder( new LineBorder( Color.red ) ); return c; } private void showToolTip( Component c, int x, int y, int parentLevel ) { hidePopup(); if( c == null || (window != null && !window.isActive()) ) return; JToolTip tip = new JToolTip(); tip.setTipText( buildToolTipText( c, parentLevel, showClassHierarchy ) ); tip.putClientProperty( FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true ); Point pt = new Point( x, y ); SwingUtilities.convertPointToScreen( pt, rootPane.getGlassPane() ); int tx = pt.x + UIScale.scale( 8 ); int ty = pt.y + UIScale.scale( 16 ); Dimension size = tip.getPreferredSize(); // position the tip in the visible area Rectangle visibleRect = rootPane.getGraphicsConfiguration().getBounds(); if( tx + size.width > visibleRect.x + visibleRect.width ) tx -= size.width + UIScale.scale( 16 ); if( ty + size.height > visibleRect.y + visibleRect.height ) ty -= size.height + UIScale.scale( 32 ); if( tx < visibleRect.x ) tx = visibleRect.x; if( ty < visibleRect.y ) ty = visibleRect.y; PopupFactory popupFactory = PopupFactory.getSharedInstance(); popup = popupFactory.getPopup( c, tip, tx, ty ); popup.show(); } private static String buildToolTipText( Component c, int parentLevel, boolean classHierarchy ) { StringBuilder buf = new StringBuilder( 1500 ); buf.append( "" ); appendRow( buf, "Class", toString( c.getClass(), classHierarchy ) ); appendRow( buf, "Size", c.getWidth() + ", " + c.getHeight() + "   @ " + c.getX() + ", " + c.getY() ); if( c instanceof Container ) appendRow( buf, "Insets", toString( ((Container)c).getInsets() ) ); Insets margin = null; if( c instanceof AbstractButton ) margin = ((AbstractButton) c).getMargin(); else if( c instanceof JTextComponent ) margin = ((JTextComponent) c).getMargin(); else if( c instanceof JMenuBar ) margin = ((JMenuBar) c).getMargin(); else if( c instanceof JToolBar ) margin = ((JToolBar) c).getMargin(); if( margin != null ) appendRow( buf, "Margin", toString( margin ) ); if( c instanceof JComponent ) { Object value = ((JComponent)c).getClientProperty( MigLayoutVisualPadding.VISUAL_PADDING_PROPERTY ); Insets visualPadding = (value instanceof int[]) ? new Insets( ((int[])value)[0], ((int[])value)[1], ((int[])value)[2], ((int[])value)[3] ) : (value instanceof Insets ? (Insets) value : null); if( visualPadding != null ) appendRow( buf, "Mig visual padding", toString( visualPadding ) ); } Dimension prefSize = c.getPreferredSize(); Dimension minSize = c.getMinimumSize(); Dimension maxSize = c.getMaximumSize(); appendRow( buf, "Pref size", prefSize.width + ", " + prefSize.height ); appendRow( buf, "Min size", minSize.width + ", " + minSize.height ); appendRow( buf, "Max size", maxSize.width + ", " + maxSize.height ); if( c instanceof JComponent ) appendRow( buf, "Border", toString( ((JComponent)c).getBorder(), classHierarchy ) ); appendRow( buf, "Background", toString( c.getBackground() ) + (c.isBackgroundSet() ? "" : " NOT SET") ); appendRow( buf, "Foreground", toString( c.getForeground() ) + (c.isForegroundSet() ? "" : " NOT SET") ); appendRow( buf, "Font", toString( c.getFont() ) + (c.isFontSet() ? "" : " NOT SET") ); if( c instanceof JComponent ) { try { Object ui; if( SystemInfo.isJava_9_orLater ) { // Java 9+: use public method JComponent.getUI() Method m = JComponent.class.getMethod( "getUI" ); ui = m.invoke( c ); } else { // Java 8: read protected field 'ui' Field f = JComponent.class.getDeclaredField( "ui" ); f.setAccessible( true ); ui = f.get( c ); } appendRow( buf, "UI", (ui != null ? toString( ui.getClass(), classHierarchy ) : "null") ); } catch( Exception ex ) { // ignore } } if( c instanceof Container ) { LayoutManager layout = ((Container)c).getLayout(); if( layout != null ) appendRow( buf, "Layout", toString( layout.getClass(), classHierarchy ) ); } appendRow( buf, "Enabled", String.valueOf( c.isEnabled() ) ); appendRow( buf, "Opaque", String.valueOf( c.isOpaque() ) + (c instanceof JComponent && FlatUIUtils.hasOpaqueBeenExplicitlySet( (JComponent) c ) ? " EXPLICIT" : "") ); if( c instanceof AbstractButton ) appendRow( buf, "ContentAreaFilled", String.valueOf( ((AbstractButton)c).isContentAreaFilled() ) ); appendRow( buf, "Focusable", String.valueOf( c.isFocusable() ) ); appendRow( buf, "Left-to-right", String.valueOf( c.getComponentOrientation().isLeftToRight() ) ); appendRow( buf, "Parent", (c.getParent() != null ? toString( c.getParent().getClass(), classHierarchy ) : "null") ); if( c instanceof JComponent ) { Object style = ((JComponent)c).getClientProperty( FlatClientProperties.STYLE ); if( style != null ) appendRow( buf, "FlatLaf Style", style.toString() ); } // append parent level buf.append( "" ); buf.append( "
" ); if( parentLevel > 0 ) buf.append( "
Parent level: " + parentLevel ); // append modifier keys hint buf.append( "
(" ) .append( (parentLevel > 0) ? "press Ctrl/Shift to increase/decrease level" : "press Ctrl key to inspect parent" ) .append( ";  " ) .append( classHierarchy ? "press Alt key to hide class hierarchy" : "press Alt key to show class hierarchy" ) .append( ')' ); buf.append( "
" ); return buf.toString(); } private static void appendRow( StringBuilder buf, String key, String value ) { buf.append( "" ) .append( key ) .append( ":" ) .append( value ) .append( "" ); } private static String toString( Class cls, boolean classHierarchy ) { StringBuilder buf = new StringBuilder( 100 ); int level = 0; while( cls != null ) { if( level > 0 ) { if( cls == Object.class ) break; buf.append( "
 " ); for( int i = 1; i < level; i++ ) buf.append( "    " ); buf.append( "\u2570 " ); } level++; String name = cls.getName(); int dot = name.lastIndexOf( '.' ); String pkg = (dot >= 0) ? name.substring( 0, dot ) : "-"; String simpleName = (dot >= 0) ? name.substring( dot + 1 ) : name; buf.append( simpleName ).append( ' ' ).append( toDimmedText( "(" + pkg + ")" ) ); if( UIResource.class.isAssignableFrom( cls ) ) buf.append( " UI" ); if( !classHierarchy ) break; cls = cls.getSuperclass(); } return buf.toString(); } private static String toString( Insets insets ) { if( insets == null ) return "null"; return insets.top + ", " + insets.left + ", " + insets.bottom + ", " + insets.right + (insets instanceof UIResource ? " UI" : ""); } private static String toString( Color c ) { if( c == null ) return "null"; StringBuilder buf = new StringBuilder( 150 ); buf.append( "" ); // is similar to , but uses same font size as body buf.append( (c.getAlpha() != 255) ? String.format( "#%06x%02x", c.getRGB() & 0xffffff, (c.getRGB() >> 24) & 0xff ) : String.format( "#%06x", c.getRGB() & 0xffffff ) ); buf.append( "" ); if( c instanceof UIResource ) buf.append( " UI" ); // color preview buf.append( "   " ) .append( "" ) .append( "                 " ) .append( "" ); if( c.getAlpha() != 255 ) buf.append( " " ).append( Math.round( c.getAlpha() / 2.55f ) ).append( '%' ); return buf.toString(); } private static String toString( Font f ) { if( f == null ) return "null"; return f.getFamily() + " " + f.getSize() + " " + f.getStyle() + (f instanceof UIResource ? " UI" : ""); } private static String toString( Border b, boolean classHierarchy ) { if( b == null ) return "null"; String s = toString( b.getClass(), classHierarchy ); if( b instanceof EmptyBorder ) { String borderInsets = " (" + toString( ((EmptyBorder)b).getBorderInsets() ) + ')'; int brIndex = s.indexOf( "
" ); if( brIndex >= 0 ) s = s.substring( 0, brIndex ) + borderInsets + s.substring( brIndex ); else s += borderInsets; } return s; } private static String toDimmedText( String text ) { Color color = UIManager.getColor( "Label.disabledForeground" ); if( color == null ) color = UIManager.getColor( "Label.disabledText" ); if( color == null ) color = Color.GRAY; return String.format( "%s", color.getRGB() & 0xffffff, text ); } }