1 /*
2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.apple.laf;
27 
28 import java.awt.*;
29 import java.awt.event.*;
30 import java.awt.geom.Rectangle2D;
31 import java.beans.*;
32 
33 import javax.swing.*;
34 import javax.swing.border.Border;
35 import javax.swing.plaf.UIResource;
36 import javax.swing.text.*;
37 
38 @SuppressWarnings("serial") // Superclass is not serializable across versions
39 public class AquaCaret extends DefaultCaret
40         implements UIResource, PropertyChangeListener {
41 
42     private boolean isMultiLineEditor;
43     private boolean mFocused = false;
44     private boolean fPainting = false;
45 
46     @Override
install(final JTextComponent c)47     public void install(final JTextComponent c) {
48         super.install(c);
49         isMultiLineEditor = c instanceof JTextArea || c instanceof JEditorPane;
50         c.addPropertyChangeListener(this);
51     }
52 
53     @Override
deinstall(final JTextComponent c)54     public void deinstall(final JTextComponent c) {
55         c.removePropertyChangeListener(this);
56         super.deinstall(c);
57     }
58 
59     @Override
getSelectionPainter()60     protected Highlighter.HighlightPainter getSelectionPainter() {
61         return AquaHighlighter.getInstance();
62     }
63 
64     /**
65      * Only show the flashing caret if the selection range is zero
66      */
67     @Override
setVisible(boolean e)68     public void setVisible(boolean e) {
69         if (e) e = getDot() == getMark();
70         super.setVisible(e);
71     }
72 
73     @Override
fireStateChanged()74     protected void fireStateChanged() {
75         // If we have focus the caret should only flash if the range length is zero
76         if (mFocused) setVisible(getComponent().isEditable());
77 
78         super.fireStateChanged();
79     }
80 
81     @Override
propertyChange(final PropertyChangeEvent evt)82     public void propertyChange(final PropertyChangeEvent evt) {
83         final String propertyName = evt.getPropertyName();
84 
85         if (AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(propertyName)) {
86             final JTextComponent comp = ((JTextComponent)evt.getSource());
87 
88             if (evt.getNewValue() == Boolean.TRUE) {
89                 setVisible(comp.hasFocus());
90             } else {
91                 setVisible(false);
92             }
93 
94             if (getDot() != getMark()) comp.getUI().damageRange(comp, getDot(), getMark());
95         }
96     }
97 
98     // --- FocusListener methods --------------------------
99 
100     private boolean shouldSelectAllOnFocus = true;
101     @Override
focusGained(final FocusEvent e)102     public void focusGained(final FocusEvent e) {
103         final JTextComponent component = getComponent();
104         if (!component.isEnabled() || !component.isEditable()) {
105             super.focusGained(e);
106             return;
107         }
108 
109         mFocused = true;
110         if (!shouldSelectAllOnFocus) {
111             shouldSelectAllOnFocus = true;
112             super.focusGained(e);
113             return;
114         }
115 
116         if (isMultiLineEditor) {
117             super.focusGained(e);
118             return;
119         }
120 
121         final int end = component.getDocument().getLength();
122         final int dot = getDot();
123         final int mark = getMark();
124         if (dot == mark) {
125             if (dot == 0) {
126                 component.setCaretPosition(end);
127                 component.moveCaretPosition(0);
128             } else if (dot == end) {
129                 component.setCaretPosition(0);
130                 component.moveCaretPosition(end);
131             }
132         }
133 
134         super.focusGained(e);
135     }
136 
137     @Override
focusLost(final FocusEvent e)138     public void focusLost(final FocusEvent e) {
139         mFocused = false;
140         shouldSelectAllOnFocus = true;
141         if (isMultiLineEditor) {
142             setVisible(false);
143             getComponent().repaint();
144         } else {
145             super.focusLost(e);
146         }
147     }
148 
149     // This fixes the problem where when on the mac you have to ctrl left click to
150     // get popup triggers the caret has code that only looks at button number.
151     // see radar # 3125390
152     @Override
mousePressed(final MouseEvent e)153     public void mousePressed(final MouseEvent e) {
154         if (!e.isPopupTrigger()) {
155             super.mousePressed(e);
156             shouldSelectAllOnFocus = false;
157         }
158     }
159 
160     /**
161      * Damages the area surrounding the caret to cause
162      * it to be repainted in a new location.  If paint()
163      * is reimplemented, this method should also be
164      * reimplemented.  This method should update the
165      * caret bounds (x, y, width, and height).
166      *
167      * @param r  the current location of the caret
168      * @see #paint
169      */
170     @Override
damage(final Rectangle r)171     protected synchronized void damage(final Rectangle r) {
172         if (r == null || fPainting) return;
173 
174         x = r.x - 4;
175         y = r.y;
176         width = 10;
177         height = r.height;
178 
179         // Don't damage the border area.  We can't paint a partial border, so get the
180         // intersection of the caret rectangle and the component less the border, if any.
181         final Rectangle caretRect = new Rectangle(x, y, width, height);
182         final Border border = getComponent().getBorder();
183         if (border != null) {
184             final Rectangle alloc = getComponent().getBounds();
185             alloc.x = alloc.y = 0;
186             final Insets borderInsets = border.getBorderInsets(getComponent());
187             alloc.x += borderInsets.left;
188             alloc.y += borderInsets.top;
189             alloc.width -= borderInsets.left + borderInsets.right;
190             alloc.height -= borderInsets.top + borderInsets.bottom;
191             Rectangle2D.intersect(caretRect, alloc, caretRect);
192         }
193         x = caretRect.x;
194         y = caretRect.y;
195         width = Math.max(caretRect.width, 1);
196         height = Math.max(caretRect.height, 1);
197         repaint();
198     }
199 
200     // See <rdar://problem/3833837> 1.4.2_05-141.3: JTextField performance with
201     // Aqua L&F. We are getting into a circular condition with the BasicCaret
202     // paint code since it doesn't know about the fact that our damage routine
203     // above elminates the border. Sadly we can't easily change either one, so
204     // we will add a painting flag and not damage during a repaint.
205     @Override
paint(final Graphics g)206     public void paint(final Graphics g) {
207         if (isVisible()) {
208             fPainting = true;
209             super.paint(g);
210             fPainting = false;
211         }
212     }
213 }
214