/* * Copyright (c) 2009 Kathryn Huxtable and Kenneth Orr. * * This file is part of the SeaGlass Pluggable Look and Feel. * * 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. * * $Id$ */ package com.seaglasslookandfeel.ui; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTextFieldUI; import javax.swing.plaf.synth.ColorType; import javax.swing.plaf.synth.Region; import javax.swing.plaf.synth.SynthContext; import javax.swing.plaf.synth.SynthStyle; import javax.swing.text.AbstractDocument; import javax.swing.text.Caret; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.View; import sun.swing.SwingUtilities2; import com.seaglasslookandfeel.SeaGlassContext; import com.seaglasslookandfeel.SeaGlassLookAndFeel; import com.seaglasslookandfeel.SeaGlassRegion; import com.seaglasslookandfeel.SeaGlassStyle; import com.seaglasslookandfeel.SeaGlassSynthPainterImpl; import com.seaglasslookandfeel.component.SeaGlassBorder; import com.seaglasslookandfeel.state.SearchFieldHasPopupState; import com.seaglasslookandfeel.state.State; import com.seaglasslookandfeel.state.TextFieldIsSearchState; /** * Sea Glass TextField UI delegate. * * <p>Based on SynthTextFieldUI, but we need to set preferred sizes and handle * search fields.</p> */ public class SeaGlassTextFieldUI extends BasicTextFieldUI implements SeaglassUI, FocusListener { private static final State isSearchField = new TextFieldIsSearchState(); private static final State hasPopupMenu = new SearchFieldHasPopupState(); private TextFieldBorder textFieldBorder; private MouseAdapter mouseListener; private boolean isCancelArmed; private SynthStyle style; private SynthStyle findStyle; private SynthStyle cancelStyle; private String placeholderText; private Color placeholderColor; private ActionListener findAction; private JPopupMenu findPopup; private ActionListener cancelAction; private int searchIconWidth; private int popupIconWidth; private int cancelIconWidth; private int searchLeftInnerMargin; private int searchRightInnerMargin; /** * Creates a new SeaGlassTextFieldUI object. */ public SeaGlassTextFieldUI() { super(); } /** * Creates a UI for a JTextField. * * @param c the text field * * @return the UI */ public static ComponentUI createUI(JComponent c) { return new SeaGlassTextFieldUI(); } /** * DOCUMENT ME! * * @param c DOCUMENT ME! */ private void updateStyle(JTextComponent c) { SeaGlassContext context = getContext(c, ENABLED); SynthStyle oldStyle = style; SynthStyle s = SeaGlassLookAndFeel.updateStyle(context, this); if (s instanceof SeaGlassStyle) { style = (SeaGlassStyle) s; updateSearchStyle(c, context, getPropertyPrefix()); if (style != oldStyle) { updateStyle(c, context, getPropertyPrefix()); if (oldStyle != null) { uninstallKeyboardActions(); installKeyboardActions(); } } } context.dispose(); context = getContext(c, SeaGlassRegion.SEARCH_FIELD_FIND_BUTTON, ENABLED); findStyle = SeaGlassLookAndFeel.updateStyle(context, this); context.dispose(); context = getContext(c, SeaGlassRegion.SEARCH_FIELD_CANCEL_BUTTON, ENABLED); cancelStyle = SeaGlassLookAndFeel.updateStyle(context, this); context.dispose(); } /** * Sea Glass code to support the search JTextField.variant. * * @param c the JTextField component. * @param context the SeaGlassContext. * @param prefix the control prefix, e.g. "TextField", * "FormattedTextField", or "PasswordField". */ private void updateSearchStyle(JTextComponent c, SeaGlassContext context, String prefix) { searchIconWidth = 0; Object o = style.get(context, prefix + ".searchIconWidth"); if (o != null && o instanceof Integer) { searchIconWidth = (Integer) o; } popupIconWidth = 0; o = style.get(context, prefix + ".popupIconWidth"); if (o != null && o instanceof Integer) { popupIconWidth = (Integer) o; } cancelIconWidth = 0; o = style.get(context, prefix + ".cancelIconWidth"); if (o != null && o instanceof Integer) { cancelIconWidth = (Integer) o; } searchLeftInnerMargin = 0; o = style.get(context, prefix + ".searchLeftInnerMargin"); if (o != null && o instanceof Integer) { searchLeftInnerMargin = (Integer) o; } searchRightInnerMargin = 0; o = style.get(context, prefix + ".searchRightInnerMargin"); if (o != null && o instanceof Integer) { searchRightInnerMargin = (Integer) o; } placeholderColor = Color.GRAY; o = style.get(context, "placeholderTextColor"); if (o != null && o instanceof Color) { placeholderColor = (Color) o; } Border border = c.getBorder(); if (border == null || border instanceof UIResource && !(border instanceof TextFieldBorder)) { c.setBorder(createTextFieldBorder(context)); } if (isSearchField.isInState(c)) { o = c.getClientProperty("JTextField.Search.PlaceholderText"); if (o != null && o instanceof String) { placeholderText = (String) o; } else if (placeholderText != null) { placeholderText = null; } o = c.getClientProperty("JTextField.Search.FindAction"); if (o != null && o instanceof ActionListener) { if (findAction == null) { findAction = (ActionListener) o; } } o = c.getClientProperty("JTextField.Search.FindPopup"); if (o != null && o instanceof JPopupMenu) { if (findPopup == null) { findPopup = (JPopupMenu) o; } } o = c.getClientProperty("JTextField.Search.CancelAction"); if (o != null && o instanceof ActionListener) { if (cancelAction == null) { cancelAction = (ActionListener) o; } } installMouseListeners(); } else { placeholderText = null; if (findAction != null) { findAction = null; } if (findPopup != null) { findPopup = null; } if (cancelAction != null) { cancelAction = null; } uninstallMouseListeners(); } } /** * Private method to update styles. * * @param c the JTextField component. * @param context the SeaGlassContext. * @param prefix the control prefix, e.g. "TextField", * "FormattedTextField", or "PasswordField". */ static void updateStyle(JTextComponent c, SeaGlassContext context, String prefix) { SeaGlassStyle style = (SeaGlassStyle) context.getStyle(); Color color = c.getCaretColor(); if (color == null || color instanceof UIResource) { c.setCaretColor((Color) style.get(context, prefix + ".caretForeground")); } Color fg = c.getForeground(); if (fg == null || fg instanceof UIResource) { fg = style.getColorForState(context, ColorType.TEXT_FOREGROUND); if (fg != null) { c.setForeground(fg); } } Object ar = style.get(context, prefix + ".caretAspectRatio"); if (ar instanceof Number) { c.putClientProperty("caretAspectRatio", ar); } context.setComponentState(SELECTED | FOCUSED); Color s = c.getSelectionColor(); if (s == null || s instanceof UIResource) { c.setSelectionColor(style.getColor(context, ColorType.TEXT_BACKGROUND)); } Color sfg = c.getSelectedTextColor(); if (sfg == null || sfg instanceof UIResource) { c.setSelectedTextColor(style.getColor(context, ColorType.TEXT_FOREGROUND)); } context.setComponentState(DISABLED); Color dfg = c.getDisabledTextColor(); if (dfg == null || dfg instanceof UIResource) { c.setDisabledTextColor(style.getColor(context, ColorType.TEXT_FOREGROUND)); } Insets margin = c.getMargin(); if (margin == null || margin instanceof UIResource) { margin = (Insets) style.get(context, prefix + ".margin"); if (margin == null) { // Some places assume margins are non-null. margin = SeaGlassLookAndFeel.EMPTY_UIRESOURCE_INSETS; } c.setMargin(margin); } Caret caret = c.getCaret(); if (caret instanceof UIResource) { Object o = style.get(context, prefix + ".caretBlinkRate"); if (o != null && o instanceof Integer) { Integer rate = (Integer) o; caret.setBlinkRate(rate.intValue()); } } } /** * @see SeaglassUI#getContext(javax.swing.JComponent) */ public SeaGlassContext getContext(JComponent c) { return getContext(c, getComponentState(c)); } /** * DOCUMENT ME! * * @param c DOCUMENT ME! * @param state DOCUMENT ME! * * @return DOCUMENT ME! */ private SeaGlassContext getContext(JComponent c, int state) { return SeaGlassContext.getContext(SeaGlassContext.class, c, SeaGlassLookAndFeel.getRegion(c), style, state); } /** * DOCUMENT ME! * * @param c DOCUMENT ME! * @param region DOCUMENT ME! * * @return DOCUMENT ME! */ private SeaGlassContext getContext(JComponent c, Region region) { return getContext(c, region, getComponentState(c, region)); } /** * DOCUMENT ME! * * @param c DOCUMENT ME! * @param region DOCUMENT ME! * @param state DOCUMENT ME! * * @return DOCUMENT ME! */ private SeaGlassContext getContext(JComponent c, Region region, int state) { SynthStyle style = findStyle; if (region == SeaGlassRegion.SEARCH_FIELD_CANCEL_BUTTON) { style = cancelStyle; } return SeaGlassContext.getContext(SeaGlassContext.class, c, region, style, state); } /** * DOCUMENT ME! * * @param c DOCUMENT ME! * * @return DOCUMENT ME! */ private int getComponentState(JComponent c) { int state = SeaGlassLookAndFeel.getComponentState(c); return state; } /** * DOCUMENT ME! * * @param c DOCUMENT ME! * @param region DOCUMENT ME! * * @return DOCUMENT ME! */ private int getComponentState(JComponent c, Region region) { if (region == SeaGlassRegion.SEARCH_FIELD_CANCEL_BUTTON && c.isEnabled()) { if (((JTextComponent) c).getText().length() == 0) { return DISABLED; } else if (isCancelArmed) { return PRESSED; } return ENABLED; } return SeaGlassLookAndFeel.getComponentState(c); } /** * @see javax.swing.plaf.basic.BasicTextUI#update(java.awt.Graphics, javax.swing.JComponent) */ public void update(Graphics g, JComponent c) { SeaGlassContext context = getContext(c); SeaGlassLookAndFeel.update(context, g); paintBackground(context, g, c); paint(context, g); context.dispose(); } /** * Paints the interface. This is routed to the paintSafely method under the * guarantee that the model won't change from the view of this thread while * it's rendering (if the associated model is derived from * AbstractDocument). This enables the model to potentially be updated * asynchronously. * * @param context DOCUMENT ME! * @param g DOCUMENT ME! */ protected void paint(SeaGlassContext context, Graphics g) { JTextComponent c = getComponent(); super.paint(g, c); } /** * DOCUMENT ME! * * @param context DOCUMENT ME! * @param g DOCUMENT ME! * @param c DOCUMENT ME! */ void paintBackground(SeaGlassContext context, Graphics g, JComponent c) { context.getPainter().paintTextFieldBackground(context, g, 0, 0, c.getWidth(), c.getHeight()); // If necessary, paint the placeholder text. if (placeholderText != null && ((JTextComponent) c).getText().length() == 0 && !c.hasFocus()) { paintPlaceholderText(context, g, c); } } /** * @see SeaglassUI#paintBorder(javax.swing.plaf.synth.SynthContext, * java.awt.Graphics, int, int, int, int) */ public void paintBorder(SynthContext context, Graphics g, int x, int y, int w, int h) { ((SeaGlassContext) context).getPainter().paintTextFieldBorder(context, g, x, y, w, h); JTextComponent c = getComponent(); if (isSearchField.isInState(c)) { paintSearchButton(g, c, SeaGlassRegion.SEARCH_FIELD_FIND_BUTTON); paintSearchButton(g, c, SeaGlassRegion.SEARCH_FIELD_CANCEL_BUTTON); } } /** * @see javax.swing.plaf.basic.BasicTextUI#paintBackground(java.awt.Graphics) */ protected void paintBackground(Graphics g) { // Overridden to do nothing, all our painting is done from update/paint. } /** * DOCUMENT ME! * * @param context DOCUMENT ME! * @param g DOCUMENT ME! * @param c DOCUMENT ME! */ protected void paintPlaceholderText(SeaGlassContext context, Graphics g, JComponent c) { g.setColor(placeholderColor); g.setFont(c.getFont()); Rectangle innerArea = SwingUtilities.calculateInnerArea(c, null); Rectangle cancelBounds = getCancelButtonBounds(); context.getStyle().getGraphicsUtils(context).paintText(context, g, getPlaceholderText(g, innerArea.width + cancelBounds.width), innerArea.x, innerArea.y, -1); } /** * Get the placeholder text, clipped if necessary. * * @param g fm the font metrics to compute size with. * @param availTextWidth the available space to display the title in. * * @return the text, clipped to fit the available space. */ private String getPlaceholderText(Graphics g, int availTextWidth) { JTextComponent c = getComponent(); FontMetrics fm = SwingUtilities2.getFontMetrics(c, g); return SwingUtilities2.clipStringIfNecessary(c, fm, placeholderText, availTextWidth); } /** * DOCUMENT ME! * * @param g DOCUMENT ME! * @param c DOCUMENT ME! * @param region DOCUMENT ME! */ protected void paintSearchButton(Graphics g, JTextComponent c, Region region) { Rectangle bounds; if (region == SeaGlassRegion.SEARCH_FIELD_FIND_BUTTON) { bounds = getFindButtonBounds(); } else { bounds = getCancelButtonBounds(); } SeaGlassContext subcontext = getContext(c, region); SeaGlassLookAndFeel.updateSubregion(subcontext, g, bounds); SeaGlassSynthPainterImpl painter = (SeaGlassSynthPainterImpl) subcontext.getPainter(); painter.paintSearchButtonForeground(subcontext, g, bounds.x, bounds.y, bounds.width, bounds.height); subcontext.dispose(); } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ protected Rectangle getFindButtonBounds() { JTextComponent c = getComponent(); final int x = c.getHeight() / 2 - 6; final int y = c.getHeight() / 2 - 6; return new Rectangle(x, y, 22, 17); } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ protected Rectangle getCancelButtonBounds() { JTextComponent c = getComponent(); final int x = c.getWidth() - c.getHeight() / 2 - 9; final int y = c.getHeight() / 2 - 8; return new Rectangle(x, y, 17, 17); } /** * This method gets called when a bound property is changed on the * associated JTextComponent. This is a hook which UI implementations may * change to reflect how the UI displays bound properties of JTextComponent * subclasses. This is implemented to do nothing (i.e. the response to * properties in JTextComponent itself are handled prior to calling this * method). * * @param evt the property change event */ protected void propertyChange(PropertyChangeEvent evt) { if (SeaGlassLookAndFeel.shouldUpdateStyle(evt)) { updateStyle((JTextComponent) evt.getSource()); } super.propertyChange(evt); } /** * @see java.awt.event.FocusListener#focusGained(java.awt.event.FocusEvent) */ public void focusGained(FocusEvent e) { getComponent().repaint(); } /** * @see java.awt.event.FocusListener#focusLost(java.awt.event.FocusEvent) */ public void focusLost(FocusEvent e) { getComponent().repaint(); } /** * @see javax.swing.plaf.basic.BasicTextUI#installDefaults() */ protected void installDefaults() { // Installs the text cursor on the component super.installDefaults(); JTextComponent c = getComponent(); updateStyle(c); c.addFocusListener(this); } /** * @see javax.swing.plaf.basic.BasicTextUI#uninstallDefaults() */ protected void uninstallDefaults() { JTextComponent c = getComponent(); SeaGlassContext context = getContext(c, ENABLED); // Remove the search border, if present. Border border = c.getBorder(); if (border instanceof TextFieldBorder) { c.setBorder(null); } c.putClientProperty("caretAspectRatio", null); c.removeFocusListener(this); style.uninstallDefaults(context); context.dispose(); style = null; super.uninstallDefaults(); } /** * @see javax.swing.plaf.basic.BasicTextUI#uninstallListeners() */ @Override protected void uninstallListeners() { super.uninstallListeners(); uninstallMouseListeners(); } /** * DOCUMENT ME! */ private void installMouseListeners() { if (mouseListener == null) { mouseListener = createMouseListener(); getComponent().addMouseListener(mouseListener); getComponent().addMouseMotionListener(mouseListener); } } /** * DOCUMENT ME! */ private void uninstallMouseListeners() { if (mouseListener != null) { getComponent().removeMouseListener(mouseListener); getComponent().removeMouseMotionListener(mouseListener); mouseListener = null; } } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ protected MouseAdapter createMouseListener() { if (mouseListener == null) { mouseListener = new MouseButtonListener(); } return mouseListener; } /** * @see javax.swing.plaf.basic.BasicTextUI#installUI(javax.swing.JComponent) */ public void installUI(JComponent c) { super.installUI(c); updateStyle((JTextComponent) c); } /** * DOCUMENT ME! * * @param context DOCUMENT ME! * * @return DOCUMENT ME! */ protected TextFieldBorder createTextFieldBorder(SeaGlassContext context) { if (textFieldBorder == null) { textFieldBorder = new TextFieldBorder(this, context.getStyle().getInsets(context, null)); } return textFieldBorder; } /** * @see javax.swing.plaf.basic.BasicTextUI#getPreferredSize(javax.swing.JComponent) */ public Dimension getPreferredSize(JComponent c) { // The following code has been derived from BasicTextUI. Document doc = ((JTextComponent) c).getDocument(); Insets i = c.getInsets(); Dimension d = c.getSize(); View rootView = getRootView((JTextComponent) c); if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readLock(); } try { if ((d.width > (i.left + i.right)) && (d.height > (i.top + i.bottom))) { rootView.setSize(d.width - i.left - i.right, d.height - i.top - i.bottom); } else if (d.width == 0 && d.height == 0) { // Probably haven't been layed out yet, force some sort of // initial sizing. rootView.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE); } d.width = (int) Math.min((long) rootView.getPreferredSpan(View.X_AXIS) + (long) i.left + (long) i.right, Integer.MAX_VALUE); d.height = (int) Math.min((long) rootView.getPreferredSpan(View.Y_AXIS) + (long) i.top + (long) i.bottom, Integer.MAX_VALUE); } finally { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readUnlock(); } } // Fix: The preferred width is always two pixels too small on a Mac. d.width += 2; // We'd like our heights to be odd by default. if ((d.height & 1) == 0) { d.height--; } return d; } /** * DOCUMENT ME! */ private void doFind() { if (findAction != null) { fireAction(findAction); } doPopup(); } /** * DOCUMENT ME! */ private void doCancel() { // Erase the text in the search field. getComponent().setText(""); if (cancelAction != null) { fireAction(cancelAction); } } /** * DOCUMENT ME! */ private void doPopup() { if (findPopup != null) { JTextComponent c = getComponent(); findPopup.pack(); // The "-1" just snugs us up a bit under the text field. findPopup.show(c, 0, c.getHeight() - 1); // Set focus back to the text field. // TODO Fix caret positioning, selection, etc. c.requestFocusInWindow(); } } /** * DOCUMENT ME! * * @param action DOCUMENT ME! */ protected void fireAction(ActionListener action) { int modifiers = 0; AWTEvent currentEvent = EventQueue.getCurrentEvent(); if (currentEvent instanceof InputEvent) { modifiers = ((InputEvent) currentEvent).getModifiers(); } else if (currentEvent instanceof ActionEvent) { modifiers = ((ActionEvent) currentEvent).getModifiers(); } ActionEvent e = new ActionEvent(getComponent(), ActionEvent.ACTION_PERFORMED, getComponent().getText(), EventQueue.getMostRecentEventTime(), modifiers); action.actionPerformed(e); } /** * DOCUMENT ME! * * @author $author$ * @version $Revision$, $Date$ */ protected class TextFieldBorder extends SeaGlassBorder { private static final long serialVersionUID = -3475926670707905862L; /** * Creates a new TextFieldBorder object. * * @param ui DOCUMENT ME! * @param insets DOCUMENT ME! */ public TextFieldBorder(SeaglassUI ui, Insets insets) { super(ui, insets); } /** * Reinitialize the insets parameter with this Border's current Insets. * * @param c the component for which this border insets value * applies * @param insets the object to be reinitialized * * @return DOCUMENT ME! */ public Insets getBorderInsets(Component c, Insets insets) { if (insets == null) { insets = new Insets(0, 0, 0, 0); } super.getBorderInsets(c, insets); if (c instanceof JComponent && isSearchField.isInState((JComponent) c)) { insets.left += searchIconWidth + searchLeftInnerMargin; if (hasPopupMenu.isInState((JComponent) c)) { insets.left += popupIconWidth; } insets.right += cancelIconWidth + searchRightInnerMargin; } return insets; } } /** * Track mouse clicks and moves. */ protected class MouseButtonListener extends MouseAdapter { /** DOCUMENT ME! */ protected transient int currentMouseX; /** DOCUMENT ME! */ protected transient int currentMouseY; /** * Creates a new MouseButtonListener object. */ public MouseButtonListener() { isCancelArmed = false; } /** * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(MouseEvent e) { if (isCancelArmed) { isCancelArmed = false; if (isOverCancelButton()) { doCancel(); } getComponent().repaint(); } } /** * @see java.awt.event.MouseAdapter#mouseDragged(java.awt.event.MouseEvent) */ public void mouseDragged(MouseEvent e) { currentMouseX = e.getX(); currentMouseY = e.getY(); if (isCancelArmed && !isOverCancelButton()) { isCancelArmed = false; getComponent().repaint(); } } /** * @see java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || !getComponent().isEnabled()) { return; } currentMouseX = e.getX(); currentMouseY = e.getY(); if (isOverFindButton()) { doFind(); } if (isOverCancelButton()) { isCancelArmed = true; getComponent().repaint(); } else if (isCancelArmed) { isCancelArmed = false; getComponent().repaint(); } } /** * {@inheritDoc} */ @Override public void mouseMoved(MouseEvent e) { currentMouseX = e.getX(); currentMouseY = e.getY(); Cursor cursorToUse = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR); if (isOverCancelButton() || isOverFindButton()) { cursorToUse = Cursor.getDefaultCursor(); } JComponent c = (JComponent) e.getSource(); if (!cursorToUse.equals(c.getCursor())) { c.setCursor(cursorToUse); } super.mouseMoved(e); } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ private boolean isOverFindButton() { return getFindButtonBounds().contains(currentMouseX, currentMouseY); } /** * DOCUMENT ME! * * @return DOCUMENT ME! */ public boolean isOverCancelButton() { return getCancelButtonBounds().contains(currentMouseX, currentMouseY); } } }