/* * Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Substance Kirill Grouchnikov nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.pushingpixels.substance.internal.utils; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.ComponentOrientation; import java.awt.Container; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.LayoutManager; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Formatter; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.UIResource; import org.pushingpixels.lafwidget.animation.effects.GhostPaintingUtils; import org.pushingpixels.lafwidget.utils.RenderingUtils; import org.pushingpixels.lafwidget.utils.TrackableThread; import org.pushingpixels.substance.api.DecorationAreaType; import org.pushingpixels.substance.api.SubstanceColorScheme; import org.pushingpixels.substance.api.SubstanceConstants.SubstanceWidgetType; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.SubstanceSkin; import org.pushingpixels.substance.api.skin.SkinInfo; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.ui.SubstanceButtonUI; import org.pushingpixels.substance.internal.ui.SubstanceRootPaneUI; import org.pushingpixels.substance.internal.utils.icon.SubstanceIconFactory; import org.pushingpixels.substance.internal.utils.icon.TransitionAwareIcon; /** * Title pane for <b>Substance</b> look and feel. * * @author Kirill Grouchnikov */ public class SubstanceTitlePane extends JComponent { /** * PropertyChangeListener added to the JRootPane. */ private PropertyChangeListener propertyChangeListener; /** * JMenuBar, typically renders the system menu items. */ protected JMenuBar menuBar; /** * Action used to close the Window. */ private Action closeAction; /** * Action used to iconify the Frame. */ private Action iconifyAction; /** * Action to restore the Frame size. */ private Action restoreAction; /** * Action to restore the Frame size. */ private Action maximizeAction; /** * Button used to maximize or restore the frame. */ protected JButton toggleButton; /** * Button used to minimize the frame */ protected JButton minimizeButton; /** * Button used to close the frame. */ protected JButton closeButton; /** * Listens for changes in the state of the Window listener to update the * state of the widgets. */ private WindowListener windowListener; /** * Window we're currently in. */ protected Window window; /** * JRootPane rendering for. */ protected JRootPane rootPane; /** * Buffered Frame.state property. As state isn't bound, this is kept to * determine when to avoid updating widgets. */ private int state; /** * SubstanceRootPaneUI that created us. */ private SubstanceRootPaneUI rootPaneUI; /** * The logfile name for the heap status panel. Can be <code>null</code> - in * this case the {@link HeapStatusThread} will not write heap information. */ private static String heapStatusLogfileName; /** * The heap status panel of <code>this</code> title pane. */ protected HeapStatusPanel heapStatusPanel; /** * The heap status toggle menu item of <code>this</code> title pane. */ protected JCheckBoxMenuItem heapStatusMenuItem; /** * Listens on changes to <code>componentOrientation</code> and * {@link SubstanceLookAndFeel#WINDOW_MODIFIED} properties. */ protected PropertyChangeListener propertyListener; /** * Client property to mark every child to be either leading or trailing. The * value must be one of {@link ExtraComponentKind}. * * @see #markExtraComponent(JComponent, ExtraComponentKind) * @see #getTitleTextRectangle() */ protected static final String EXTRA_COMPONENT_KIND = "substancelaf.internal.titlePane.extraComponentKind"; /** * The application icon to be displayed. */ protected Image appIcon; /** * Enumerates the types of children components. * * @author Kirill Grouchnikov */ protected enum ExtraComponentKind { /** * Leading child components (left on LTR and right on RTL). */ LEADING, /** * Trailing child components (right on LTR and left on RTL). */ TRAILING } /** * Panel that shows heap status and allows running the garbage collector. * * @author Kirill Grouchnikov */ public static class HeapStatusPanel extends JPanel { /** * The current heap size in kilobytes. */ private int currHeapSizeKB; /** * The current used portion of heap in kilobytes. */ private int currTakenHeapSizeKB; /** * History of used heap portion (in percents). Each value is in 0.0-1.0 * range. */ private LinkedList<Double> graphValues; /** * Creates new heap status panel. */ public HeapStatusPanel() { this.graphValues = new LinkedList<Double>(); HeapStatusThread.getInstance(); } /** * Updates the values for <code>this</code> heap status panel. * * @param currHeapSizeKB * The current heap size in kilobytes. * @param currTakenHeapSizeKB * The current used portion of heap in kilobytes. */ public synchronized void updateStatus(int currHeapSizeKB, int currTakenHeapSizeKB) { this.currHeapSizeKB = currHeapSizeKB; this.currTakenHeapSizeKB = currTakenHeapSizeKB; double newGraphValue = (double) currTakenHeapSizeKB / (double) currHeapSizeKB; this.graphValues.addLast(newGraphValue); this.repaint(); } /* * (non-Javadoc) * * @see javax.swing.JComponent#paint(java.awt.Graphics) */ @Override public synchronized void paint(Graphics g) { Graphics2D graphics = (Graphics2D) g.create(); SubstanceColorScheme scheme = SubstanceCoreUtilities.getSkin(this) .getActiveColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE); graphics.setColor(scheme.getDarkColor()); int w = this.getWidth(); int h = this.getHeight(); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); float borderThickness = SubstanceSizeUtils.getBorderStrokeWidth(); graphics.setStroke(new BasicStroke(borderThickness, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); graphics.draw(new Rectangle2D.Float(borderThickness / 2.0f, borderThickness / 2.0f, w - borderThickness, h - borderThickness)); graphics.setColor(scheme.getExtraLightColor()); graphics.draw(new Rectangle2D.Float(borderThickness, borderThickness, w - 2 * borderThickness, h - 2 * borderThickness)); graphics.setStroke(new BasicStroke(1.0f)); while (this.graphValues.size() > (w - 2)) this.graphValues.removeFirst(); int xOff = w - this.graphValues.size() - 1; graphics.setColor(scheme.getMidColor()); int count = 0; for (double value : this.graphValues) { int valueH = (int) (value * (h - 2)); graphics.drawLine(xOff + count, h - 1 - valueH, xOff + count, h - 2); count++; } graphics.setFont(UIManager.getFont("Panel.font")); FontMetrics fm = graphics.getFontMetrics(); StringBuffer longFormat = new StringBuffer(); Formatter longFormatter = new Formatter(longFormat); longFormatter.format("%.1fMB / %.1fMB", this.currTakenHeapSizeKB / 1024.f, this.currHeapSizeKB / 1024.f); longFormatter.close(); int strW = fm.stringWidth(longFormat.toString()); int strH = fm.getAscent() + fm.getDescent(); graphics.setColor(scheme.getForegroundColor()); RenderingUtils.installDesktopHints(graphics, this); if (strW < (w - 5)) { graphics.drawString(longFormat.toString(), (w - strW) / 2, (h + strH) / 2 - 2); } else { String shortFormat = (this.currTakenHeapSizeKB / 1024) + "MB / " + (this.currHeapSizeKB / 1024) + "MB"; strW = fm.stringWidth(shortFormat); graphics.drawString(shortFormat, (w - strW) / 2, (h + strH) / 2 - 2); } graphics.dispose(); } /** * Returns the preferred width of this panel. * * @return Preferred width of this panel. */ public int getPreferredWidth() { BufferedImage dummy = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = dummy.createGraphics(); RenderingUtils.installDesktopHints(g2d, this); g2d.setFont(UIManager.getFont("Panel.font")); FontMetrics fm = g2d.getFontMetrics(); int result = fm.stringWidth("100.9MB / 200.9MB"); g2d.dispose(); return result; } } /** * Thread for heap status panel. */ public static class HeapStatusThread extends TrackableThread { /** * Current heap size in kilobytes. */ private int heapSizeKB; /** * Current used portion of heap in kilobytes. */ private int takenHeapSizeKB; /** * All heap status panels. */ private static Set<WeakReference<HeapStatusPanel>> panels = new HashSet<WeakReference<HeapStatusPanel>>(); /** * Single instance of <code>this</code> thread. */ private static HeapStatusThread instance; /** * Formatter object (for logfile). */ private SimpleDateFormat format; /** * Signifies whether a stop request has been issued on <code>this</code> * thread using the {@link #requestStop()} call. */ private boolean isStopRequested; /** * Simple constructor. Defined private for singleton. * * @see #getInstance() */ private HeapStatusThread() { this.format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.SSS"); this.isStopRequested = false; this.setName("Substance heap status"); } /** * Gets singleton instance of <code>this</code> thread. * * @return Singleton instance of <code>this</code> thread. */ public synchronized static HeapStatusThread getInstance() { if (HeapStatusThread.instance == null) { HeapStatusThread.instance = new HeapStatusThread(); HeapStatusThread.instance.start(); } return HeapStatusThread.instance; } /** * Registers new heap status panel with <code>this</code> thread. * * @param panel * Heap statuc panel. */ public static synchronized void registerPanel(HeapStatusPanel panel) { panels.add(new WeakReference<HeapStatusPanel>(panel)); } /** * Unregisters new heap status panel from <code>this</code> thread. * * @param panel * Heap statuc panel. */ public static synchronized void unregisterPanel(HeapStatusPanel panel) { for (Iterator<WeakReference<HeapStatusPanel>> it = panels.iterator(); it.hasNext();) { WeakReference<HeapStatusPanel> ref = it.next(); HeapStatusPanel currPanel = ref.get(); if (panel == currPanel) { it.remove(); return; } } } /** * Updates the values of heap status. */ private synchronized void updateHeapCounts() { long heapSize = Runtime.getRuntime().totalMemory(); long heapFreeSize = Runtime.getRuntime().freeMemory(); this.heapSizeKB = (int) (heapSize / 1024); this.takenHeapSizeKB = (int) ((heapSize - heapFreeSize) / 1024); } /* * (non-Javadoc) * * @see java.lang.Thread#run() */ @Override public void run() { while (!this.isStopRequested) { try { // update every 0.5 seconds Thread.sleep(500); } catch (InterruptedException ie) { } if (!SubstanceWidgetManager.getInstance() .isAllowedAnywhere(SubstanceWidgetType.TITLE_PANE_HEAP_STATUS)) continue; this.updateHeapCounts(); for (Iterator<WeakReference<HeapStatusPanel>> it = panels.iterator(); it .hasNext();) { WeakReference<HeapStatusPanel> refPanel = it.next(); HeapStatusPanel panel = refPanel.get(); if (panel == null) { // prune it.remove(); continue; } panel.updateStatus(this.heapSizeKB, this.takenHeapSizeKB); } // see if need to put info in log file if (SubstanceTitlePane.heapStatusLogfileName != null) { try (PrintWriter pw = new PrintWriter( new FileWriter(SubstanceTitlePane.heapStatusLogfileName, true))) { pw.println(this.format.format(new Date()) + " " + this.takenHeapSizeKB + "KB / " + this.heapSizeKB + "KB"); } catch (IOException ioe) { } } } } @Override protected void requestStop() { this.isStopRequested = true; HeapStatusThread.instance = null; } } /** * Creates a new title pane. * * @param root * Root pane. * @param ui * Root pane UI. */ public SubstanceTitlePane(JRootPane root, SubstanceRootPaneUI ui) { this.rootPane = root; this.rootPaneUI = ui; this.state = -1; this.installSubcomponents(); this.installDefaults(); this.setLayout(this.createLayout()); this.setToolTipText(this.getTitle()); SubstanceLookAndFeel.setDecorationType(this, DecorationAreaType.PRIMARY_TITLE_PANE); this.setForeground(SubstanceColorUtilities.getForegroundColor(SubstanceCoreUtilities .getSkin(this).getBackgroundColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE))); // SubstanceColorSchemeUtilities // .getColorScheme(this, ComponentState.ACTIVE))); } /** * Uninstalls the necessary state. */ public void uninstall() { this.uninstallListeners(); this.window = null; HeapStatusThread.unregisterPanel(this.heapStatusPanel); // Swing bug (?) - the updateComponentTree never gets to the // system menu (and in our case we have radio menu items with // rollover listeners). Fix for defect 109 - memory leak on skin // switch if ((this.menuBar != null) && (this.menuBar.getMenuCount() > 0)) { this.menuBar.getUI().uninstallUI(this.menuBar); SubstanceCoreUtilities.uninstallMenu(this.menuBar.getMenu(0)); } if (this.heapStatusPanel != null) { for (MouseListener listener : this.heapStatusPanel.getMouseListeners()) this.heapStatusPanel.removeMouseListener(listener); HeapStatusThread.unregisterPanel(this.heapStatusPanel); this.remove(this.heapStatusPanel); } if (this.menuBar != null) this.menuBar.removeAll(); this.removeAll(); } /** * Installs the necessary listeners. */ private void installListeners() { if (this.window != null) { this.windowListener = new WindowHandler(); this.window.addWindowListener(this.windowListener); this.propertyChangeListener = new PropertyChangeHandler(); this.window.addPropertyChangeListener(this.propertyChangeListener); } // Property change listener for pulsating close button // when window has been marked as changed. // Fix for defect 109 - memory leak on skin change. this.propertyListener = (final PropertyChangeEvent evt) -> { if (SubstanceLookAndFeel.WINDOW_MODIFIED.equals(evt.getPropertyName())) { syncCloseButtonTooltip(); } if ("componentOrientation".equals(evt.getPropertyName())) { SwingUtilities.invokeLater(() -> { if (SubstanceTitlePane.this.menuBar != null) { SubstanceTitlePane.this.menuBar.applyComponentOrientation( (ComponentOrientation) evt.getNewValue()); } }); } }; // Wire it on the frame itself and its root pane. this.rootPane.addPropertyChangeListener(this.propertyListener); if (this.getFrame() != null) this.getFrame().addPropertyChangeListener(this.propertyListener); } /** * Uninstalls the necessary listeners. */ private void uninstallListeners() { if (this.window != null) { this.window.removeWindowListener(this.windowListener); this.windowListener = null; this.window.removePropertyChangeListener(this.propertyChangeListener); this.propertyChangeListener = null; } // Fix for defect 109 - memory leak on skin change. this.rootPane.removePropertyChangeListener(this.propertyListener); if (this.getFrame() != null) this.getFrame().removePropertyChangeListener(this.propertyListener); this.propertyListener = null; } /** * Returns the <code>JRootPane</code> this was created for. */ @Override public JRootPane getRootPane() { return this.rootPane; } /** * Returns the decoration style of the <code>JRootPane</code>. * * @return Decoration style of the <code>JRootPane</code>. */ protected int getWindowDecorationStyle() { return this.getRootPane().getWindowDecorationStyle(); } /* * (non-Javadoc) * * @see java.awt.Component#addNotify() */ @Override public void addNotify() { super.addNotify(); this.uninstallListeners(); this.window = SwingUtilities.getWindowAncestor(this); if (this.window != null) { this.setActive(this.window.isActive()); if (this.window instanceof Frame) { this.setState(((Frame) this.window).getExtendedState()); } else { this.setState(0); } if (this.getComponentCount() == 0) { // fix for issue 385 - add the sub-components uninstalled // in the removeNotify. This happens when a decorated // dialog has been disposed and then reshown. this.installSubcomponents(); } this.installListeners(); } this.setToolTipText(this.getTitle()); this.updateAppIcon(); } /* * (non-Javadoc) * * @see java.awt.Component#removeNotify() */ @Override public void removeNotify() { super.removeNotify(); this.uninstall(); this.window = null; } /** * Adds any sub-Components contained in the <code>SubstanceTitlePane</code>. */ private void installSubcomponents() { int decorationStyle = this.getWindowDecorationStyle(); if (decorationStyle == JRootPane.FRAME) { this.createActions(); this.menuBar = this.createMenuBar(); if (this.menuBar != null) { this.add(this.menuBar); } this.createButtons(); this.add(this.minimizeButton); this.add(this.toggleButton); this.add(this.closeButton); this.heapStatusPanel = new HeapStatusPanel(); this.markExtraComponent(this.heapStatusPanel, ExtraComponentKind.TRAILING); this.add(this.heapStatusPanel); boolean isHeapStatusPanelShowing = SubstanceWidgetManager.getInstance() .isAllowed(rootPane, SubstanceWidgetType.TITLE_PANE_HEAP_STATUS); this.heapStatusPanel.setVisible(isHeapStatusPanelShowing); this.heapStatusPanel .setPreferredSize(new Dimension(80, this.getPreferredSize().height)); this.heapStatusPanel.setToolTipText(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("Tooltip.heapStatusPanel")); this.heapStatusPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { System.gc(); } }); HeapStatusThread.registerPanel(this.heapStatusPanel); } else { if ((decorationStyle == JRootPane.PLAIN_DIALOG) || (decorationStyle == JRootPane.INFORMATION_DIALOG) || (decorationStyle == JRootPane.ERROR_DIALOG) || (decorationStyle == JRootPane.COLOR_CHOOSER_DIALOG) || (decorationStyle == JRootPane.FILE_CHOOSER_DIALOG) || (decorationStyle == JRootPane.QUESTION_DIALOG) || (decorationStyle == JRootPane.WARNING_DIALOG)) { this.createActions(); this.createButtons(); this.add(this.closeButton); } } } /** * Installs the fonts and necessary properties. */ private void installDefaults() { this.setFont(UIManager.getFont("InternalFrame.titleFont", this.getLocale())); } /** * Returns the <code>JMenuBar</code> displaying the appropriate system menu * items. * * @return <code>JMenuBar</code> displaying the appropriate system menu * items. */ protected JMenuBar createMenuBar() { this.menuBar = new SubstanceMenuBar(); this.menuBar.setFocusable(false); this.menuBar.setBorderPainted(true); this.menuBar.add(this.createMenu()); this.menuBar.setOpaque(false); // support for RTL this.menuBar.applyComponentOrientation(this.rootPane.getComponentOrientation()); this.markExtraComponent(this.menuBar, ExtraComponentKind.LEADING); return this.menuBar; } /** * Create the <code>Action</code>s that get associated with the buttons and * menu items. */ private void createActions() { this.closeAction = new CloseAction(); if (this.getWindowDecorationStyle() == JRootPane.FRAME) { this.iconifyAction = new IconifyAction(); this.restoreAction = new RestoreAction(); this.maximizeAction = new MaximizeAction(); } } /** * Returns the <code>JMenu</code> displaying the appropriate menu items for * manipulating the Frame. * * @return <code>JMenu</code> displaying the appropriate menu items for * manipulating the Frame. */ private JMenu createMenu() { JMenu menu = new JMenu(""); menu.setOpaque(false); menu.setBackground(null); if (this.getWindowDecorationStyle() == JRootPane.FRAME) { this.addMenuItems(menu); } return menu; } /** * Adds the necessary <code>JMenuItem</code>s to the specified menu. * * @param menu * Menu. */ private void addMenuItems(JMenu menu) { menu.add(this.restoreAction); menu.add(this.iconifyAction); if (Toolkit.getDefaultToolkit().isFrameStateSupported(Frame.MAXIMIZED_BOTH)) { menu.add(this.maximizeAction); } if (SubstanceCoreUtilities.toShowExtraWidgets(rootPane)) { menu.addSeparator(); JMenu skinMenu = new JMenu(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.skins")); Map<String, SkinInfo> allSkins = SubstanceLookAndFeel.getAllSkins(); for (Map.Entry<String, SkinInfo> skinEntry : allSkins.entrySet()) { final String skinClassName = skinEntry.getValue().getClassName(); JMenuItem jmiSkin = new JMenuItem(skinEntry.getKey()); jmiSkin.addActionListener((ActionEvent e) -> SwingUtilities .invokeLater(() -> SubstanceLookAndFeel.setSkin(skinClassName))); skinMenu.add(jmiSkin); } menu.add(skinMenu); } menu.addSeparator(); menu.add(this.closeAction); } /** * Returns a <code>JButton</code> appropriate for placement on the * TitlePane. * * @return Title button. */ private JButton createTitleButton() { JButton button = new SubstanceTitleButton(); button.setFocusPainted(false); button.setFocusable(false); button.setOpaque(true); this.markExtraComponent(button, ExtraComponentKind.TRAILING); return button; } /** * Creates the Buttons that will be placed on the TitlePane. */ private void createButtons() { this.closeButton = this.createTitleButton(); this.closeButton.setAction(this.closeAction); this.closeButton.setText(null); this.closeButton.setBorder(null); Icon closeIcon = new TransitionAwareIcon(closeButton, (SubstanceColorScheme scheme) -> SubstanceIconFactory.getTitlePaneIcon( SubstanceIconFactory.IconKind.CLOSE, scheme, SubstanceCoreUtilities.getSkin(rootPane) .getBackgroundColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE)), "substance.titlePane.closeIcon"); this.closeButton.setIcon(closeIcon); this.closeButton.setFocusable(false); this.closeButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE); this.closeButton.putClientProperty(SubstanceButtonUI.IS_TITLE_CLOSE_BUTTON, Boolean.TRUE); if (this.getWindowDecorationStyle() == JRootPane.FRAME) { this.minimizeButton = this.createTitleButton(); this.minimizeButton.setAction(this.iconifyAction); this.minimizeButton.setText(null); this.minimizeButton.setBorder(null); Icon minIcon = new TransitionAwareIcon(this.minimizeButton, (SubstanceColorScheme scheme) -> SubstanceIconFactory.getTitlePaneIcon( SubstanceIconFactory.IconKind.MINIMIZE, scheme, SubstanceCoreUtilities.getSkin(rootPane).getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE)), "substance.titlePane.minIcon"); this.minimizeButton.setIcon(minIcon); this.minimizeButton.setFocusable(false); this.minimizeButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE); this.minimizeButton.setToolTipText(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.iconify")); this.toggleButton = this.createTitleButton(); this.toggleButton.setAction(this.restoreAction); this.toggleButton.setBorder(null); this.toggleButton.setText(null); Icon maxIcon = new TransitionAwareIcon(this.toggleButton, (SubstanceColorScheme scheme) -> SubstanceIconFactory.getTitlePaneIcon( SubstanceIconFactory.IconKind.MAXIMIZE, scheme, SubstanceCoreUtilities.getSkin(rootPane).getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE)), "substance.titlePane.maxIcon"); this.toggleButton.setIcon(maxIcon); this.toggleButton.setToolTipText(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.maximize")); this.toggleButton.setFocusable(false); this.toggleButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE); } syncCloseButtonTooltip(); } /** * Returns the <code>LayoutManager</code> that should be installed on the * <code>SubstanceTitlePane</code>. * * @return Layout manager. */ protected LayoutManager createLayout() { return new TitlePaneLayout(); } /** * Updates state dependant upon the Window's active state. * * @param isActive * if <code>true</code>, the window is in active state. */ private void setActive(boolean isActive) { this.getRootPane().repaint(); } /** * Sets the state of the Window. * * @param state * Window state. */ private void setState(int state) { this.setState(state, false); } /** * Sets the state of the window. If <code>updateRegardless</code> is true * and the state has not changed, this will update anyway. * * @param state * Window state. * @param updateRegardless * if <code>true</code>, the update is done in any case. */ private void setState(int state, boolean updateRegardless) { Window w = this.getWindow(); if ((w != null) && (this.getWindowDecorationStyle() == JRootPane.FRAME)) { if ((this.state == state) && !updateRegardless) { return; } Frame frame = this.getFrame(); if (frame != null) { final JRootPane rootPane = this.getRootPane(); if (((state & Frame.MAXIMIZED_BOTH) != 0) && ((rootPane.getBorder() == null) || (rootPane.getBorder() instanceof UIResource)) && frame.isShowing()) { rootPane.setBorder(null); } else { if ((state & Frame.MAXIMIZED_BOTH) == 0) { // This is a croak, if state becomes bound, this can // be nuked. this.rootPaneUI.installBorder(rootPane); } } if (frame.isResizable()) { if ((state & Frame.MAXIMIZED_BOTH) != 0) { Icon restoreIcon = new TransitionAwareIcon(this.toggleButton, (SubstanceColorScheme scheme) -> SubstanceIconFactory .getTitlePaneIcon(SubstanceIconFactory.IconKind.RESTORE, scheme, SubstanceCoreUtilities.getSkin(rootPane) .getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE)), "substance.titlePane.restoreIcon"); this.updateToggleButton(this.restoreAction, restoreIcon); this.toggleButton.setToolTipText(SubstanceCoreUtilities .getResourceBundle(rootPane).getString("SystemMenu.restore")); this.maximizeAction.setEnabled(false); this.restoreAction.setEnabled(true); } else { Icon maxIcon = new TransitionAwareIcon(this.toggleButton, (SubstanceColorScheme scheme) -> SubstanceIconFactory .getTitlePaneIcon(SubstanceIconFactory.IconKind.MAXIMIZE, scheme, SubstanceCoreUtilities.getSkin(rootPane) .getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE)), "substance.titlePane.maxIcon"); this.updateToggleButton(this.maximizeAction, maxIcon); this.toggleButton.setToolTipText(SubstanceCoreUtilities .getResourceBundle(rootPane).getString("SystemMenu.maximize")); this.maximizeAction.setEnabled(true); this.restoreAction.setEnabled(false); } if ((this.toggleButton.getParent() == null) || (this.minimizeButton.getParent() == null)) { this.add(this.toggleButton); this.add(this.minimizeButton); this.revalidate(); this.repaint(); } this.toggleButton.setText(null); } else { this.maximizeAction.setEnabled(false); this.restoreAction.setEnabled(false); if (this.toggleButton.getParent() != null) { this.remove(this.toggleButton); this.revalidate(); this.repaint(); } } } else { // Not contained in a Frame this.maximizeAction.setEnabled(false); this.restoreAction.setEnabled(false); this.iconifyAction.setEnabled(false); this.remove(this.toggleButton); this.remove(this.minimizeButton); this.revalidate(); this.repaint(); } this.closeAction.setEnabled(true); this.state = state; } } /** * Updates the toggle button to contain the Icon <code>icon</code>, and * Action <code>action</code>. * * @param action * Action. * @param icon * Icon. */ private void updateToggleButton(Action action, Icon icon) { this.toggleButton.setAction(action); this.toggleButton.setIcon(icon); this.toggleButton.setText(null); } /** * Returns the Frame rendering in. This will return null if the * <code>JRootPane</code> is not contained in a <code>Frame</code>. * * @return Frame. */ private Frame getFrame() { Window window = this.getWindow(); if (window instanceof Frame) { return (Frame) window; } return null; } /** * Returns the <code>Window</code> the <code>JRootPane</code> is contained * in. This will return null if there is no parent ancestor of the * <code>JRootPane</code>. * * @return Window. */ private Window getWindow() { return this.window; } /** * Returns the String to display as the title. * * @return Display title. */ private String getTitle() { Window w = this.getWindow(); if (w instanceof Frame) { return ((Frame) w).getTitle(); } if (w instanceof Dialog) { return ((Dialog) w).getTitle(); } return null; } /* * (non-Javadoc) * * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) */ @Override public void paintComponent(Graphics g) { // long start = System.nanoTime(); // As state isn't bound, we need a convenience place to check // if it has changed. Changing the state typically changes the if (this.getFrame() != null) { this.setState(this.getFrame().getExtendedState()); } final JRootPane rootPane = this.getRootPane(); Window window = this.getWindow(); boolean leftToRight = (window == null) ? rootPane.getComponentOrientation().isLeftToRight() : window.getComponentOrientation().isLeftToRight(); int width = this.getWidth(); int height = this.getHeight(); SubstanceSkin skin = SubstanceCoreUtilities.getSkin(rootPane); if (skin == null) { SubstanceCoreUtilities.traceSubstanceApiUsage(this, "Substance delegate used when Substance is not the current LAF"); } SubstanceColorScheme scheme = skin .getEnabledColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE); int xOffset = 0; String theTitle = this.getTitle(); if (theTitle != null) { Rectangle titleTextRect = this.getTitleTextRectangle(); FontMetrics fm = rootPane.getFontMetrics(g.getFont()); int titleWidth = titleTextRect.width - 20; String clippedTitle = SubstanceCoreUtilities.clipString(fm, titleWidth, theTitle); // show tooltip with full title only if necessary if (theTitle.equals(clippedTitle)) { this.setToolTipText(null); } else { this.setToolTipText(theTitle); } theTitle = clippedTitle; if (leftToRight) xOffset = titleTextRect.x; else xOffset = titleTextRect.x + titleTextRect.width - fm.stringWidth(theTitle); } Graphics2D graphics = (Graphics2D) g.create(); Font font = SubstanceLookAndFeel.getFontPolicy().getFontSet("Substance", null) .getWindowTitleFont(); graphics.setFont(font); BackgroundPaintingUtils.update(graphics, SubstanceTitlePane.this, false); // DecorationPainterUtils.paintDecorationBackground(graphics, // SubstanceTitlePane.this, false); // draw the title (if needed) if (theTitle != null) { FontMetrics fm = rootPane.getFontMetrics(graphics.getFont()); int yOffset = ((height - fm.getHeight()) / 2) + fm.getAscent(); SubstanceColorScheme fillScheme = skin .getBackgroundColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE); Color echoColor = !fillScheme.isDark() ? fillScheme.getUltraDarkColor() : fillScheme.getUltraLightColor(); SubstanceTextUtilities.paintTextWithDropShadow(this, graphics, SubstanceColorUtilities.getForegroundColor(scheme), echoColor, theTitle, width, height, xOffset, yOffset); } GhostPaintingUtils.paintGhostImages(this, graphics); // long end = System.nanoTime(); // System.out.println(end - start); graphics.dispose(); } /** * Computes the rectangle of the title text. This method looks at all the * children components of the title pane, grouping them by leading and * trailing (based on {@link #EXTRA_COMPONENT_KIND} client property). The * title text rectangle is the space between the leading group and the * trailing group. * * @return Rectangle of the title text. * @throws IllegalStateException * If at least one child component of this title pane is not * marked with the {@link #EXTRA_COMPONENT_KIND} client * property. * @see #markExtraComponent(JComponent, ExtraComponentKind) * @see #EXTRA_COMPONENT_KIND */ protected Rectangle getTitleTextRectangle() { JRootPane rootPane = this.getRootPane(); Window window = this.getWindow(); boolean leftToRight = (window == null) ? rootPane.getComponentOrientation().isLeftToRight() : window.getComponentOrientation().isLeftToRight(); if (leftToRight) { int maxLeadingX = 0; int minTrailingX = this.getWidth(); for (int i = 0; i < this.getComponentCount(); i++) { Component child = this.getComponent(i); if (!child.isVisible()) continue; if (child instanceof JComponent) { ExtraComponentKind kind = (ExtraComponentKind) ((JComponent) child) .getClientProperty(EXTRA_COMPONENT_KIND); if (kind == null) { throw new IllegalStateException( "Title pane child " + child.getClass().getName() + " is not marked as leading or trailing"); } if (kind == ExtraComponentKind.LEADING) { int cx = child.getX() + child.getWidth(); if (cx > maxLeadingX) maxLeadingX = cx; } else { int cx = child.getX(); if (cx < minTrailingX) minTrailingX = cx; } } } int start = maxLeadingX + 10; int end = minTrailingX - 5; return new Rectangle(start, 0, end - start, this.getHeight()); } else { int minLeadingX = this.getWidth(); int maxTrailingX = 0; for (int i = 0; i < this.getComponentCount(); i++) { Component child = this.getComponent(i); if (!child.isVisible()) continue; if (child instanceof JComponent) { ExtraComponentKind kind = (ExtraComponentKind) ((JComponent) child) .getClientProperty(EXTRA_COMPONENT_KIND); if (kind == null) { throw new IllegalStateException( "Title pane child " + child.getClass().getName() + " is not marked as leading or trailing"); } if (kind == ExtraComponentKind.LEADING) { int cx = child.getX(); if (cx < minLeadingX) minLeadingX = cx; } else { int cx = child.getX() + child.getWidth(); if (cx > maxTrailingX) maxTrailingX = cx; } } } int start = maxTrailingX + 5; int end = minLeadingX - 10; return new Rectangle(start, 0, end - start, this.getHeight()); } } /** * Actions used to <code>close</code> the <code>Window</code>. */ private class CloseAction extends AbstractAction { /** * Creates a new close action. */ public CloseAction() { super(SubstanceCoreUtilities.getResourceBundle(rootPane).getString("SystemMenu.close"), SubstanceImageCreator.getCloseIcon( SubstanceCoreUtilities.getSkin(rootPane) .getActiveColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE), SubstanceCoreUtilities.getSkin(rootPane).getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE))); } public void actionPerformed(ActionEvent e) { Window window = SubstanceTitlePane.this.getWindow(); if (window != null) { window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING)); } } } /** * Actions used to <code>iconfiy</code> the <code>Frame</code>. */ private class IconifyAction extends AbstractAction { /** * Creates a new iconify action. */ public IconifyAction() { super(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.iconify"), SubstanceImageCreator.getMinimizeIcon( SubstanceCoreUtilities.getSkin(rootPane) .getActiveColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE), SubstanceCoreUtilities.getSkin(rootPane).getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE))); } public void actionPerformed(ActionEvent e) { Frame frame = SubstanceTitlePane.this.getFrame(); if (frame != null) { frame.setExtendedState(SubstanceTitlePane.this.state | Frame.ICONIFIED); } } } /** * Actions used to <code>restore</code> the <code>Frame</code>. */ private class RestoreAction extends AbstractAction { /** * Creates a new restore action. */ public RestoreAction() { super(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.restore"), SubstanceImageCreator.getRestoreIcon( SubstanceCoreUtilities.getSkin(rootPane) .getActiveColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE), SubstanceCoreUtilities.getSkin(rootPane).getBackgroundColorScheme( DecorationAreaType.PRIMARY_TITLE_PANE))); } public void actionPerformed(ActionEvent e) { Frame frame = SubstanceTitlePane.this.getFrame(); if (frame == null) { return; } if ((SubstanceTitlePane.this.state & Frame.ICONIFIED) != 0) { frame.setExtendedState(SubstanceTitlePane.this.state & ~Frame.ICONIFIED); } else { frame.setExtendedState(SubstanceTitlePane.this.state & ~Frame.MAXIMIZED_BOTH); } } } /** * Actions used to <code>restore</code> the <code>Frame</code>. */ private class MaximizeAction extends AbstractAction { /** * Creates a new maximize action. */ public MaximizeAction() { super(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.maximize"), SubstanceImageCreator.getMaximizeIcon( SubstanceCoreUtilities.getSkin(rootPane) .getActiveColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE), SubstanceCoreUtilities.getSkin(rootPane) .getEnabledColorScheme(DecorationAreaType.PRIMARY_TITLE_PANE))); } public void actionPerformed(ActionEvent e) { Frame frame = SubstanceTitlePane.this.getFrame(); if (frame != null) { if (frame instanceof JFrame) { SubstanceRootPaneUI rpUI = (SubstanceRootPaneUI) ((JFrame) frame).getRootPane() .getUI(); rpUI.setMaximized(); } frame.setExtendedState(SubstanceTitlePane.this.state | Frame.MAXIMIZED_BOTH); } } } /** * Class responsible for drawing the system menu. Looks up the image to draw * from the Frame associated with the <code>JRootPane</code>. */ public class SubstanceMenuBar extends JMenuBar { @Override public void paint(Graphics g) { if (appIcon != null) { int scaleFactor = SubstanceCoreUtilities.isHiDpiAwareImage(appIcon) ? 2 : 1; g.drawImage(appIcon, 0, 0, appIcon.getWidth(null) / scaleFactor, appIcon.getHeight(null) / scaleFactor, null); } else { Icon icon = UIManager.getIcon("InternalFrame.icon"); if (icon != null) { icon.paintIcon(this, g, 0, 0); } } } @Override public Dimension getMinimumSize() { return this.getPreferredSize(); } @Override public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); int iSize = SubstanceSizeUtils.getTitlePaneIconSize(); return new Dimension(Math.max(iSize, size.width), Math.max(size.height, iSize)); } } /** * Layout manager for the title pane. * * @author Kirill Graphics */ protected class TitlePaneLayout implements LayoutManager { /* * (non-Javadoc) * * @see java.awt.LayoutManager#addLayoutComponent(java.lang.String, * java.awt.Component) */ public void addLayoutComponent(String name, Component c) { } /* * (non-Javadoc) * * @see java.awt.LayoutManager#removeLayoutComponent(java.awt.Component) */ public void removeLayoutComponent(Component c) { } /* * (non-Javadoc) * * @see java.awt.LayoutManager#preferredLayoutSize(java.awt.Container) */ public Dimension preferredLayoutSize(Container c) { int height = this.computeHeight(); return new Dimension(height, height); } /** * Computes title pane height. * * @return Title pane height. */ private int computeHeight() { FontMetrics fm = SubstanceTitlePane.this.rootPane .getFontMetrics(SubstanceTitlePane.this.getFont()); int fontHeight = fm.getHeight(); fontHeight += 7; int iconHeight = 0; if (SubstanceTitlePane.this.getWindowDecorationStyle() == JRootPane.FRAME) { iconHeight = SubstanceSizeUtils.getTitlePaneIconSize(); } int finalHeight = Math.max(fontHeight, iconHeight); return finalHeight; } /* * (non-Javadoc) * * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container) */ public Dimension minimumLayoutSize(Container c) { return this.preferredLayoutSize(c); } /* * (non-Javadoc) * * @see java.awt.LayoutManager#layoutContainer(java.awt.Container) */ public void layoutContainer(Container c) { boolean leftToRight = (SubstanceTitlePane.this.window == null) ? SubstanceTitlePane.this.getRootPane().getComponentOrientation() .isLeftToRight() : SubstanceTitlePane.this.window.getComponentOrientation().isLeftToRight(); int w = SubstanceTitlePane.this.getWidth(); int x; int y = 3; int spacing; int buttonHeight; int buttonWidth; if ((SubstanceTitlePane.this.closeButton != null) && (SubstanceTitlePane.this.closeButton.getIcon() != null)) { buttonHeight = SubstanceTitlePane.this.closeButton.getIcon().getIconHeight(); buttonWidth = SubstanceTitlePane.this.closeButton.getIcon().getIconWidth(); } else { buttonHeight = SubstanceSizeUtils.getTitlePaneIconSize(); buttonWidth = SubstanceSizeUtils.getTitlePaneIconSize(); } y = (getHeight() - buttonHeight) / 2; // assumes all buttons have the same dimensions // these dimensions include the borders x = leftToRight ? w : 0; spacing = 5; x = leftToRight ? spacing : w - buttonWidth - spacing; if (SubstanceTitlePane.this.menuBar != null) { SubstanceTitlePane.this.menuBar.setBounds(x, y, buttonWidth, buttonHeight); // System.out.println(menuBar.getBounds()); } x = leftToRight ? w : 0; spacing = 3; x += leftToRight ? -spacing - buttonWidth : spacing; if (SubstanceTitlePane.this.closeButton != null) { SubstanceTitlePane.this.closeButton.setBounds(x, y, buttonWidth, buttonHeight); } if (!leftToRight) x += buttonWidth; if (SubstanceTitlePane.this.getWindowDecorationStyle() == JRootPane.FRAME) { if (Toolkit.getDefaultToolkit().isFrameStateSupported(Frame.MAXIMIZED_BOTH)) { if (SubstanceTitlePane.this.toggleButton.getParent() != null) { spacing = 10; x += leftToRight ? -spacing - buttonWidth : spacing; SubstanceTitlePane.this.toggleButton.setBounds(x, y, buttonWidth, buttonHeight); if (!leftToRight) { x += buttonWidth; } } } if ((SubstanceTitlePane.this.minimizeButton != null) && (SubstanceTitlePane.this.minimizeButton.getParent() != null)) { spacing = 2; x += leftToRight ? -spacing - buttonWidth : spacing; SubstanceTitlePane.this.minimizeButton.setBounds(x, y, buttonWidth, buttonHeight); if (!leftToRight) { x += buttonWidth; } } if ((SubstanceTitlePane.this.heapStatusPanel != null) && SubstanceTitlePane.this.heapStatusPanel.isVisible()) { spacing = 5; x += leftToRight ? (-spacing - SubstanceTitlePane.this.heapStatusPanel.getPreferredWidth()) : spacing; SubstanceTitlePane.this.heapStatusPanel.setBounds(x, 1, SubstanceTitlePane.this.heapStatusPanel.getPreferredWidth(), SubstanceTitlePane.this.getHeight() - 3); } } // buttonsWidth = leftToRight ? w - x : x; } } /** * PropertyChangeListener installed on the Window. Updates the necessary * state as the state of the Window changes. */ private class PropertyChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent pce) { String name = pce.getPropertyName(); // Frame.state isn't currently bound. if ("resizable".equals(name) || "state".equals(name)) { Frame frame = SubstanceTitlePane.this.getFrame(); if (frame != null) { SubstanceTitlePane.this.setState(frame.getExtendedState(), true); } if ("resizable".equals(name)) { SubstanceTitlePane.this.getRootPane().repaint(); } } else { if ("title".equals(name)) { SubstanceTitlePane.this.repaint(); SubstanceTitlePane.this.setToolTipText((String) pce.getNewValue()); } else if ("componentOrientation" == name) { revalidate(); repaint(); } else if ("iconImage" == name) { updateAppIcon(); revalidate(); repaint(); } } } } /** * WindowListener installed on the Window, updates the state as necessary. */ private class WindowHandler extends WindowAdapter { @Override public void windowActivated(WindowEvent ev) { SubstanceTitlePane.this.setActive(true); } @Override public void windowDeactivated(WindowEvent ev) { SubstanceTitlePane.this.setActive(false); } } /** * Sets location for heap status logfile. Relevant if * {@link #setCanHaveHeapStatusPanel(boolean)} was called with * <code>true</code>. * * @param heapStatusLogfileName * Logfile for the heap status panel. */ public static void setHeapStatusLogfileName(String heapStatusLogfileName) { SubstanceTitlePane.heapStatusLogfileName = heapStatusLogfileName; } /** * Synchronizes the tooltip of the close button. */ protected void syncCloseButtonTooltip() { if (SubstanceCoreUtilities.isRootPaneModified(this.getRootPane())) { this.closeButton.setToolTipText( SubstanceCoreUtilities.getResourceBundle(rootPane).getString("SystemMenu.close") + " [" + SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("Tooltip.contentsNotSaved") + "]"); } else { this.closeButton.setToolTipText(SubstanceCoreUtilities.getResourceBundle(rootPane) .getString("SystemMenu.close")); } this.closeButton.repaint(); } /** * Marks the specified child component with the specified extra component * kind. * * @param comp * Child component. * @param kind * Extra kind. * @see #getTitleTextRectangle() * @see #EXTRA_COMPONENT_KIND */ protected void markExtraComponent(JComponent comp, ExtraComponentKind kind) { comp.putClientProperty(EXTRA_COMPONENT_KIND, kind); } /** * Updates the application icon. */ private void updateAppIcon() { Window window = getWindow(); if (window == null) { this.appIcon = null; return; } java.util.List<Image> iconImages = window.getIconImages(); if (iconImages.size() == 0) { this.appIcon = null; } else { int prefSize = SubstanceSizeUtils.getTitlePaneIconSize(); this.appIcon = SubstanceCoreUtilities.getScaledIconImage(iconImages, prefSize, prefSize); } } public AbstractButton getCloseButton() { return this.closeButton; } }