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
26package com.apple.laf;
27
28import java.awt.*;
29import java.awt.event.*;
30import java.awt.geom.Rectangle2D;
31import java.beans.*;
32
33import javax.swing.*;
34import javax.swing.border.Border;
35import javax.swing.plaf.UIResource;
36import javax.swing.text.*;
37
38@SuppressWarnings("serial") // Superclass is not serializable across versions
39public 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
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
54    public void deinstall(final JTextComponent c) {
55        c.removePropertyChangeListener(this);
56        super.deinstall(c);
57    }
58
59    @Override
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
68    public void setVisible(boolean e) {
69        if (e) e = getDot() == getMark();
70        super.setVisible(e);
71    }
72
73    @Override
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
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
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
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
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
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
206    public void paint(final Graphics g) {
207        if (isVisible()) {
208            fPainting = true;
209            super.paint(g);
210            fPainting = false;
211        }
212    }
213}
214