/* * $Id$ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx.plaf.basic; import static org.jdesktop.swingx.SwingXUtilities.isUIInstallable; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.LookAndFeel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import javax.swing.plaf.ActionMapUIResource; import javax.swing.plaf.ColorUIResource; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.FontUIResource; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicGraphicsUtils; import org.jdesktop.swingx.JXCollapsiblePane; import org.jdesktop.swingx.JXHyperlink; import org.jdesktop.swingx.JXTaskPane; import org.jdesktop.swingx.SwingXUtilities; import org.jdesktop.swingx.icon.EmptyIcon; import org.jdesktop.swingx.plaf.TaskPaneUI; /** * Base implementation of the <code>JXTaskPane</code> UI. * * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a> */ public class BasicTaskPaneUI extends TaskPaneUI { private static FocusListener focusListener = new RepaintOnFocus(); public static ComponentUI createUI(JComponent c) { return new BasicTaskPaneUI(); } protected int titleHeight = 25; protected int roundHeight = 5; protected JXTaskPane group; protected boolean mouseOver; protected MouseInputListener mouseListener; protected PropertyChangeListener propertyListener; /** * {@inheritDoc} */ @Override public void installUI(JComponent c) { super.installUI(c); group = (JXTaskPane) c; installDefaults(); installListeners(); installKeyboardActions(); } /** * Installs default properties. Following properties are installed: * <ul> * <li>TaskPane.background</li> * <li>TaskPane.foreground</li> * <li>TaskPane.font</li> * <li>TaskPane.borderColor</li> * <li>TaskPane.titleForeground</li> * <li>TaskPane.titleBackgroundGradientStart</li> * <li>TaskPane.titleBackgroundGradientEnd</li> * <li>TaskPane.titleOver</li> * <li>TaskPane.specialTitleOver</li> * <li>TaskPane.specialTitleForeground</li> * <li>TaskPane.specialTitleBackground</li> * </ul> */ protected void installDefaults() { LookAndFeel.installColorsAndFont(group, "TaskPane.background", "TaskPane.foreground", "TaskPane.font"); LookAndFeel.installProperty(group, "opaque", false); if (isUIInstallable(group.getBorder())) { group.setBorder(createPaneBorder()); } if (group.getContentPane() instanceof JComponent) { JComponent content = (JComponent) group.getContentPane(); LookAndFeel.installColorsAndFont(content, "TaskPane.background", "TaskPane.foreground", "TaskPane.font"); if (isUIInstallable(content.getBorder())) { content.setBorder(createContentPaneBorder()); } } } /** * Installs listeners for UI delegate. */ protected void installListeners() { mouseListener = createMouseInputListener(); group.addMouseMotionListener(mouseListener); group.addMouseListener(mouseListener); group.addFocusListener(focusListener); propertyListener = createPropertyListener(); group.addPropertyChangeListener(propertyListener); } /** * Installs keyboard actions to allow task pane to react on hot keys. */ protected void installKeyboardActions() { InputMap inputMap = (InputMap) UIManager.get("TaskPane.focusInputMap"); if (inputMap != null) { SwingUtilities.replaceUIInputMap(group, JComponent.WHEN_FOCUSED, inputMap); } ActionMap map = getActionMap(); if (map != null) { SwingUtilities.replaceUIActionMap(group, map); } } ActionMap getActionMap() { ActionMap map = new ActionMapUIResource(); map.put("toggleCollapsed", new ToggleCollapsedAction()); return map; } @Override public void uninstallUI(JComponent c) { uninstallListeners(); super.uninstallUI(c); } /** * Uninstalls previously installed listeners to free component for garbage collection. */ protected void uninstallListeners() { group.removeMouseListener(mouseListener); group.removeMouseMotionListener(mouseListener); group.removeFocusListener(focusListener); group.removePropertyChangeListener(propertyListener); } /** * Creates new toggle listener. * @return MouseInputListener reacting on toggle events of task pane. */ protected MouseInputListener createMouseInputListener() { return new ToggleListener(); } /** * Creates property change listener for task pane. * @return Property change listener reacting on changes to the task pane. */ protected PropertyChangeListener createPropertyListener() { return new ChangeListener(); } /** * Evaluates whenever given mouse even have occurred within borders of task pane. * @param event Evaluated event. * @return True if event occurred within task pane area, false otherwise. */ protected boolean isInBorder(MouseEvent event) { return event.getY() < getTitleHeight(event.getComponent()); } /** * Gets current title height. Default value is 25 if not specified otherwise. Method checks * provided component for user set font (!instanceof FontUIResource), if font is set, height * will be calculated from font metrics instead of using internal preset height. * @return Current title height. */ protected int getTitleHeight(Component c) { if (c instanceof JXTaskPane) { JXTaskPane taskPane = (JXTaskPane) c; Font font = taskPane.getFont(); int height = titleHeight; if (font != null && !(font instanceof FontUIResource)) { height = Math.max(height, taskPane.getFontMetrics(font).getHeight()); } Icon icon = taskPane.getIcon(); if (icon != null) { height = Math.max(height, icon.getIconHeight() + 4); } return height; } return titleHeight; } /** * Creates new border for task pane. * @return Fresh border on every call. */ protected Border createPaneBorder() { return new PaneBorder(); } @Override public Dimension getPreferredSize(JComponent c) { Component component = group.getComponent(0); if (!(component instanceof JXCollapsiblePane)) { // something wrong in this JXTaskPane return super.getPreferredSize(c); } JXCollapsiblePane collapsible = (JXCollapsiblePane) component; Dimension dim = collapsible.getPreferredSize(); Border groupBorder = group.getBorder(); if (groupBorder instanceof PaneBorder) { ((PaneBorder) groupBorder).label.setDisplayedMnemonic(group .getMnemonic()); Dimension border = ((PaneBorder) groupBorder) .getPreferredSize(group); dim.width = Math.max(dim.width, border.width); dim.height += border.height; } else { dim.height += getTitleHeight(c); } return dim; } /** * Creates content pane border. * @return Fresh content pane border initialized with current value of TaskPane.borderColor * on every call. */ protected Border createContentPaneBorder() { Color borderColor = UIManager.getColor("TaskPane.borderColor"); return new CompoundBorder(new ContentPaneBorder(borderColor), BorderFactory.createEmptyBorder(10, 10, 10, 10)); } @Override public Component createAction(Action action) { JXHyperlink link = new JXHyperlink(action) { @Override public void updateUI() { super.updateUI(); // ensure the ui of this link is correctly update on l&f changes configure(this); } }; configure(link); return link; } /** * Configures internally used hyperlink on new action creation and on every call to * <code>updateUI()</code>. * @param link Configured hyperlink. */ protected void configure(JXHyperlink link) { link.setOpaque(false); link.setBorderPainted(false); link.setFocusPainted(true); link.setForeground(UIManager.getColor("TaskPane.titleForeground")); } /** * Ensures expanded group is visible. Issues delayed request for scrolling to visible. */ protected void ensureVisible() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { group.scrollRectToVisible(new Rectangle(group.getWidth(), group .getHeight())); } }); } /** * Focus listener responsible for repainting of the taskpane on focus change. */ static class RepaintOnFocus implements FocusListener { @Override public void focusGained(FocusEvent e) { e.getComponent().repaint(); } @Override public void focusLost(FocusEvent e) { e.getComponent().repaint(); } } /** * Change listener responsible for change handling. */ class ChangeListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { // if group is expanded but not animated // or if animated has reached expanded state // scroll to visible if scrollOnExpand is enabled if (("collapsed".equals(evt.getPropertyName()) // JW: fix for 1572-swingx - scrollOnExpand broken in 1.6.5-1 && Boolean.FALSE.equals(evt.getNewValue()))) { // && Boolean.TRUE.equals(evt.getNewValue()) && !group // .isAnimated())) { if (group.isScrollOnExpand()) { ensureVisible(); } } else if (JXTaskPane.ICON_CHANGED_KEY .equals(evt.getPropertyName()) || JXTaskPane.TITLE_CHANGED_KEY.equals(evt .getPropertyName()) || JXTaskPane.SPECIAL_CHANGED_KEY.equals(evt .getPropertyName())) { // icon, title, special must lead to a repaint() group.repaint(); } else if ("mnemonic".equals(evt.getPropertyName())) { SwingXUtilities.updateMnemonicBinding(group, "toggleCollapsed"); Border b = group.getBorder(); if (b instanceof PaneBorder) { int key = (Integer) evt.getNewValue(); ((PaneBorder) b).label.setDisplayedMnemonic(key); } } } } /** * Mouse listener responsible for handling of toggle events. */ class ToggleListener extends MouseInputAdapter { @Override public void mouseEntered(MouseEvent e) { if (isInBorder(e)) { e.getComponent().setCursor( Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { mouseOver = false; group.repaint(0, 0, group.getWidth(), getTitleHeight(group)); } } @Override public void mouseExited(MouseEvent e) { e.getComponent().setCursor(null); mouseOver = false; group.repaint(0, 0, group.getWidth(), getTitleHeight(group)); } @Override public void mouseMoved(MouseEvent e) { if (isInBorder(e)) { e.getComponent().setCursor( Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); mouseOver = true; } else { e.getComponent().setCursor(null); mouseOver = false; } group.repaint(0, 0, group.getWidth(), getTitleHeight(group)); } @Override public void mouseReleased(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e) && isInBorder(e)) { group.setCollapsed(!group.isCollapsed()); } } } /** * Toggle expanded action. */ class ToggleCollapsedAction extends AbstractAction { /** * Serial version UID. */ private static final long serialVersionUID = 5676859881615358815L; public ToggleCollapsedAction() { super("toggleCollapsed"); } @Override public void actionPerformed(ActionEvent e) { group.setCollapsed(!group.isCollapsed()); } @Override public boolean isEnabled() { return group.isVisible(); } } /** * Toggle icon. */ protected static class ChevronIcon implements Icon { boolean up = true; public ChevronIcon(boolean up) { this.up = up; } @Override public int getIconHeight() { return 3; } @Override public int getIconWidth() { return 6; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { if (up) { g.drawLine(x + 3, y, x, y + 3); g.drawLine(x + 3, y, x + 6, y + 3); } else { g.drawLine(x, y, x + 3, y + 3); g.drawLine(x + 3, y + 3, x + 6, y); } } } /** * The border around the content pane */ protected static class ContentPaneBorder implements Border, UIResource { Color color; public ContentPaneBorder(Color color) { this.color = color; } @Override public Insets getBorderInsets(Component c) { return new Insets(0, 1, 1, 1); } @Override public boolean isBorderOpaque() { return true; } @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { g.setColor(color); g.drawLine(x, y, x, y + height - 1); g.drawLine(x, y + height - 1, x + width - 1, y + height - 1); g.drawLine(x + width - 1, y, x + width - 1, y + height - 1); } } /** * The border of the taskpane group paints the "text", the "icon", the * "expanded" status and the "special" type. * */ protected class PaneBorder implements Border, UIResource { protected Color borderColor; protected Color titleForeground; protected Color specialTitleBackground; protected Color specialTitleForeground; protected Color titleBackgroundGradientStart; protected Color titleBackgroundGradientEnd; protected Color titleOver; protected Color specialTitleOver; protected JLabel label; /** * Creates new instance of individual pane border. */ public PaneBorder() { borderColor = UIManager.getColor("TaskPane.borderColor"); titleForeground = UIManager.getColor("TaskPane.titleForeground"); specialTitleBackground = UIManager .getColor("TaskPane.specialTitleBackground"); specialTitleForeground = UIManager .getColor("TaskPane.specialTitleForeground"); titleBackgroundGradientStart = UIManager .getColor("TaskPane.titleBackgroundGradientStart"); titleBackgroundGradientEnd = UIManager .getColor("TaskPane.titleBackgroundGradientEnd"); titleOver = UIManager.getColor("TaskPane.titleOver"); if (titleOver == null) { titleOver = specialTitleBackground.brighter(); } specialTitleOver = UIManager.getColor("TaskPane.specialTitleOver"); if (specialTitleOver == null) { specialTitleOver = specialTitleBackground.brighter(); } label = new JLabel(); label.setOpaque(false); label.setIconTextGap(8); } @Override public Insets getBorderInsets(Component c) { return new Insets(getTitleHeight(c), 0, 0, 0); } /** * Overwritten to always return <code>true</code> to speed up * painting. Don't use transparent borders unless providing UI delegate * that provides proper return value when calling this method. * * @see javax.swing.border.Border#isBorderOpaque() */ @Override public boolean isBorderOpaque() { return true; } /** * Calculates the preferred border size, its size so all its content * fits. * * @param group * Selected group. */ public Dimension getPreferredSize(JXTaskPane group) { // calculate the title width so it is fully visible // it starts with the title width configureLabel(group); Dimension dim = label.getPreferredSize(); // add the title left offset dim.width += 3; // add the controls width dim.width += getTitleHeight(group); // and some space between label and controls dim.width += 3; dim.height = getTitleHeight(group); return dim; } /** * Paints background of the title. This may differ based on properties * of the group. * * @param group * Selected group. * @param g * Target graphics. */ protected void paintTitleBackground(JXTaskPane group, Graphics g) { if (group.isSpecial()) { g.setColor(specialTitleBackground); } else { g.setColor(titleBackgroundGradientStart); } g.fillRect(0, 0, group.getWidth(), getTitleHeight(group) - 1); } /** * Paints current group title. * * @param group * Selected group. * @param g * Target graphics. * @param textColor * Title color. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintTitle(JXTaskPane group, Graphics g, Color textColor, int x, int y, int width, int height) { configureLabel(group); label.setForeground(textColor); if (group.getFont() != null && ! (group.getFont() instanceof FontUIResource)) { label.setFont(group.getFont()); } g.translate(x, y); label.setBounds(0, 0, width, height); label.paint(g); g.translate(-x, -y); } /** * Configures label for the group using its title, font, icon and * orientation. * * @param group * Selected group. */ protected void configureLabel(JXTaskPane group) { label.applyComponentOrientation(group.getComponentOrientation()); label.setFont(group.getFont()); label.setText(group.getTitle()); label.setIcon(group.getIcon() == null ? new EmptyIcon() : group .getIcon()); } /** * Paints expanded controls. Default implementation does nothing. * * @param group * Expanded group. * @param g * Target graphics. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintExpandedControls(JXTaskPane group, Graphics g, int x, int y, int width, int height) { } /** * Gets current paint color. * * @param group * Selected group. * @return Color to be used for painting provided group. */ protected Color getPaintColor(JXTaskPane group) { Color paintColor; if (isMouseOverBorder()) { if (mouseOver) { if (group.isSpecial()) { paintColor = specialTitleOver; } else { paintColor = titleOver; } } else { if (group.isSpecial()) { paintColor = specialTitleForeground; } else { paintColor = group.getForeground() == null || group.getForeground() instanceof ColorUIResource ? titleForeground : group.getForeground(); } } } else { if (group.isSpecial()) { paintColor = specialTitleForeground; } else { paintColor = group.getForeground() == null || group.getForeground() instanceof ColorUIResource ? titleForeground : group.getForeground(); } } return paintColor; } /* * @see javax.swing.border.Border#paintBorder(java.awt.Component, * java.awt.Graphics, int, int, int, int) */ @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { JXTaskPane group = (JXTaskPane) c; // calculate position of title and toggle controls int controlWidth = getTitleHeight(group) - 2 * getRoundHeight(); int controlX = group.getWidth() - getTitleHeight(group); int controlY = getRoundHeight() - 1; int titleX = 3; int titleY = 0; int titleWidth = group.getWidth() - getTitleHeight(group) - 3; int titleHeight = getTitleHeight(group); if (!group.getComponentOrientation().isLeftToRight()) { controlX = group.getWidth() - controlX - controlWidth; titleX = group.getWidth() - titleX - titleWidth; } // paint the title background paintTitleBackground(group, g); // paint the the toggles paintExpandedControls(group, g, controlX, controlY, controlWidth, controlWidth); // paint the title text and icon Color paintColor = getPaintColor(group); // focus painted same color as text if (group.hasFocus()) { paintFocus(g, paintColor, 3, 3, width - 6, getTitleHeight(group) - 6); } paintTitle(group, g, paintColor, titleX, titleY, titleWidth, titleHeight); } /** * Paints oval 'border' area around the control itself. * * @param group * Expanded group. * @param g * Target graphics. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintRectAroundControls(JXTaskPane group, Graphics g, int x, int y, int width, int height, Color highColor, Color lowColor) { if (mouseOver) { int x2 = x + width; int y2 = y + height; g.setColor(highColor); g.drawLine(x, y, x2, y); g.drawLine(x, y, x, y2); g.setColor(lowColor); g.drawLine(x2, y, x2, y2); g.drawLine(x, y2, x2, y2); } } /** * Paints oval 'border' area around the control itself. * * @param group * Expanded group. * @param g * Target graphics. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintOvalAroundControls(JXTaskPane group, Graphics g, int x, int y, int width, int height) { if (group.isSpecial()) { g.setColor(specialTitleBackground.brighter()); g.drawOval(x, y, width, height); } else { g.setColor(titleBackgroundGradientStart); g.fillOval(x, y, width, height); g.setColor(titleBackgroundGradientEnd.darker()); g.drawOval(x, y, width, width); } } /** * Paints controls for the group. * * @param group * Expanded group. * @param g * Target graphics. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintChevronControls(JXTaskPane group, Graphics g, int x, int y, int width, int height) { ChevronIcon chevron; if (group.isCollapsed()) { chevron = new ChevronIcon(false); } else { chevron = new ChevronIcon(true); } int chevronX = x + width / 2 - chevron.getIconWidth() / 2; int chevronY = y + (height / 2 - chevron.getIconHeight()); chevron.paintIcon(group, g, chevronX, chevronY); chevron.paintIcon(group, g, chevronX, chevronY + chevron.getIconHeight() + 1); } /** * Paints focused group. * * @param g * Target graphics. * @param paintColor * Focused group color. * @param x * X coordinate of the top left corner. * @param y * Y coordinate of the top left corner. * @param width * Width of the box. * @param height * Height of the box. */ protected void paintFocus(Graphics g, Color paintColor, int x, int y, int width, int height) { g.setColor(paintColor); BasicGraphicsUtils.drawDashedRect(g, x, y, width, height); } /** * Default implementation returns false. * * @return true if this border wants to display things differently when * the mouse is over it */ protected boolean isMouseOverBorder() { return false; } } /** * Gets size of arc used to round corners. * * @return size of arc used to round corners of the panel. */ protected int getRoundHeight() { return roundHeight; } }