1/*
2 * Copyright (c) 1999, 2017, 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 */
25package javax.swing.text;
26
27import java.text.*;
28import java.awt.*;
29import java.awt.font.*;
30import java.awt.geom.Rectangle2D;
31
32/**
33 * A class to perform rendering of the glyphs.
34 * This can be implemented to be stateless, or
35 * to hold some information as a cache to
36 * facilitate faster rendering and model/view
37 * translation.  At a minimum, the GlyphPainter
38 * allows a View implementation to perform its
39 * duties independent of a particular version
40 * of JVM and selection of capabilities (i.e.
41 * shaping for i18n, etc).
42 * <p>
43 * This implementation is intended for operation
44 * under the JDK.  It uses the
45 * java.awt.font.TextLayout class to do i18n capable
46 * rendering.
47 *
48 * @author  Timothy Prinzing
49 * @see GlyphView
50 */
51class GlyphPainter2 extends GlyphView.GlyphPainter {
52
53    public GlyphPainter2(TextLayout layout) {
54        this.layout = layout;
55    }
56
57    /**
58     * Create a painter to use for the given GlyphView.
59     */
60    public GlyphView.GlyphPainter getPainter(GlyphView v, int p0, int p1) {
61        return null;
62    }
63
64    /**
65     * Determine the span the glyphs given a start location
66     * (for tab expansion).  This implementation assumes it
67     * has no tabs (i.e. TextLayout doesn't deal with tab
68     * expansion).
69     */
70    public float getSpan(GlyphView v, int p0, int p1,
71                         TabExpander e, float x) {
72
73        if ((p0 == v.getStartOffset()) && (p1 == v.getEndOffset())) {
74            return layout.getAdvance();
75        }
76        int p = v.getStartOffset();
77        int index0 = p0 - p;
78        int index1 = p1 - p;
79
80        TextHitInfo hit0 = TextHitInfo.afterOffset(index0);
81        TextHitInfo hit1 = TextHitInfo.beforeOffset(index1);
82        float[] locs = layout.getCaretInfo(hit0);
83        float x0 = locs[0];
84        locs = layout.getCaretInfo(hit1);
85        float x1 = locs[0];
86        return (x1 > x0) ? x1 - x0 : x0 - x1;
87    }
88
89    public float getHeight(GlyphView v) {
90        return layout.getAscent() + layout.getDescent() + layout.getLeading();
91    }
92
93    /**
94     * Fetch the ascent above the baseline for the glyphs
95     * corresponding to the given range in the model.
96     */
97    public float getAscent(GlyphView v) {
98        return layout.getAscent();
99    }
100
101    /**
102     * Fetch the descent below the baseline for the glyphs
103     * corresponding to the given range in the model.
104     */
105    public float getDescent(GlyphView v) {
106        return layout.getDescent();
107    }
108
109    /**
110     * Paint the glyphs for the given view.  This is implemented
111     * to only render if the Graphics is of type Graphics2D which
112     * is required by TextLayout (and this should be the case if
113     * running on the JDK).
114     */
115    public void paint(GlyphView v, Graphics g, Shape a, int p0, int p1) {
116        if (g instanceof Graphics2D) {
117            Rectangle2D alloc = a.getBounds2D();
118            Graphics2D g2d = (Graphics2D)g;
119            float y = (float) alloc.getY() + layout.getAscent() + layout.getLeading();
120            float x = (float) alloc.getX();
121            if( p0 > v.getStartOffset() || p1 < v.getEndOffset() ) {
122                try {
123                    //TextLayout can't render only part of it's range, so if a
124                    //partial range is required, add a clip region.
125                    Shape s = v.modelToView(p0, Position.Bias.Forward,
126                                            p1, Position.Bias.Backward, a);
127                    Shape savedClip = g.getClip();
128                    g2d.clip(s);
129                    layout.draw(g2d, x, y);
130                    g.setClip(savedClip);
131                } catch (BadLocationException e) {}
132            } else {
133                layout.draw(g2d, x, y);
134            }
135        }
136    }
137
138    public Shape modelToView(GlyphView v, int pos, Position.Bias bias,
139                             Shape a) throws BadLocationException {
140        int offs = pos - v.getStartOffset();
141        Rectangle2D alloc = a.getBounds2D();
142        TextHitInfo hit = (bias == Position.Bias.Forward) ?
143            TextHitInfo.afterOffset(offs) : TextHitInfo.beforeOffset(offs);
144        float[] locs = layout.getCaretInfo(hit);
145
146        // vertical at the baseline, should use slope and check if glyphs
147        // are being rendered vertically.
148        Rectangle2D rect = new Rectangle2D.Float();
149        rect.setRect(alloc.getX() + locs[0], alloc.getY(), 1, alloc.getHeight());
150        return rect;
151    }
152
153    /**
154     * Provides a mapping from the view coordinate space to the logical
155     * coordinate space of the model.
156     *
157     * @param v the view containing the view coordinates
158     * @param x the X coordinate
159     * @param y the Y coordinate
160     * @param a the allocated region to render into
161     * @param biasReturn either <code>Position.Bias.Forward</code>
162     *  or <code>Position.Bias.Backward</code> is returned as the
163     *  zero-th element of this array
164     * @return the location within the model that best represents the
165     *  given point of view
166     * @see View#viewToModel
167     */
168    public int viewToModel(GlyphView v, float x, float y, Shape a,
169                           Position.Bias[] biasReturn) {
170
171        Rectangle2D alloc = (a instanceof Rectangle2D) ? (Rectangle2D)a : a.getBounds2D();
172        //Move the y co-ord of the hit onto the baseline.  This is because TextLayout supports
173        //italic carets and we do not.
174        TextHitInfo hit = layout.hitTestChar(x - (float)alloc.getX(), 0);
175        int pos = hit.getInsertionIndex();
176
177        if (pos == v.getEndOffset()) {
178            pos--;
179        }
180
181        biasReturn[0] = hit.isLeadingEdge() ? Position.Bias.Forward : Position.Bias.Backward;
182        return pos + v.getStartOffset();
183    }
184
185    /**
186     * Determines the model location that represents the
187     * maximum advance that fits within the given span.
188     * This could be used to break the given view.  The result
189     * should be a location just shy of the given advance.  This
190     * differs from viewToModel which returns the closest
191     * position which might be proud of the maximum advance.
192     *
193     * @param v the view to find the model location to break at.
194     * @param p0 the location in the model where the
195     *  fragment should start it's representation >= 0.
196     * @param x the graphic location along the axis that the
197     *  broken view would occupy >= 0.  This may be useful for
198     *  things like tab calculations.
199     * @param len specifies the distance into the view
200     *  where a potential break is desired >= 0.
201     * @return the maximum model location possible for a break.
202     * @see View#breakView
203     */
204    public int getBoundedPosition(GlyphView v, int p0, float x, float len) {
205        if( len < 0 )
206            throw new IllegalArgumentException("Length must be >= 0.");
207        // note: this only works because swing uses TextLayouts that are
208        // only pure rtl or pure ltr
209        TextHitInfo hit;
210        if (layout.isLeftToRight()) {
211            hit = layout.hitTestChar(len, 0);
212        } else {
213            hit = layout.hitTestChar(layout.getAdvance() - len, 0);
214        }
215        return v.getStartOffset() + hit.getCharIndex();
216    }
217
218    /**
219         * Provides a way to determine the next visually represented model
220         * location that one might place a caret.  Some views may not be
221         * visible, they might not be in the same order found in the model, or
222         * they just might not allow access to some of the locations in the
223         * model.
224         *
225         * @param v the view to use
226         * @param pos the position to convert >= 0
227         * @param a the allocated region to render into
228         * @param direction the direction from the current position that can
229         *  be thought of as the arrow keys typically found on a keyboard.
230         *  This may be SwingConstants.WEST, SwingConstants.EAST,
231         *  SwingConstants.NORTH, or SwingConstants.SOUTH.
232         * @return the location within the model that best represents the next
233         *  location visual position.
234         * @exception BadLocationException
235         * @exception IllegalArgumentException for an invalid direction
236         */
237        public int getNextVisualPositionFrom(GlyphView v, int pos,
238                                             Position.Bias b, Shape a,
239                                             int direction,
240                                             Position.Bias[] biasRet)
241            throws BadLocationException {
242
243            Document doc = v.getDocument();
244            int startOffset = v.getStartOffset();
245            int endOffset = v.getEndOffset();
246            Segment text;
247            boolean viewIsLeftToRight;
248            TextHitInfo currentHit, nextHit;
249
250            switch (direction) {
251            case View.NORTH:
252                break;
253            case View.SOUTH:
254                break;
255            case View.EAST:
256                viewIsLeftToRight = AbstractDocument.isLeftToRight(doc, startOffset, endOffset);
257
258                if(startOffset == doc.getLength()) {
259                    if(pos == -1) {
260                        biasRet[0] = Position.Bias.Forward;
261                        return startOffset;
262                    }
263                    // End case for bidi text where newline is at beginning
264                    // of line.
265                    return -1;
266                }
267                if(pos == -1) {
268                    // Entering view from the left.
269                    if( viewIsLeftToRight ) {
270                        biasRet[0] = Position.Bias.Forward;
271                        return startOffset;
272                    } else {
273                        text = v.getText(endOffset - 1, endOffset);
274                        char c = text.array[text.offset];
275                        SegmentCache.releaseSharedSegment(text);
276                        if(c == '\n') {
277                            biasRet[0] = Position.Bias.Forward;
278                            return endOffset-1;
279                        }
280                        biasRet[0] = Position.Bias.Backward;
281                        return endOffset;
282                    }
283                }
284                if( b==Position.Bias.Forward )
285                    currentHit = TextHitInfo.afterOffset(pos-startOffset);
286                else
287                    currentHit = TextHitInfo.beforeOffset(pos-startOffset);
288                nextHit = layout.getNextRightHit(currentHit);
289                if( nextHit == null ) {
290                    return -1;
291                }
292                if( viewIsLeftToRight != layout.isLeftToRight() ) {
293                    // If the layout's base direction is different from
294                    // this view's run direction, we need to use the weak
295                    // carrat.
296                    nextHit = layout.getVisualOtherHit(nextHit);
297                }
298                pos = nextHit.getInsertionIndex() + startOffset;
299
300                if(pos == endOffset) {
301                    // A move to the right from an internal position will
302                    // only take us to the endOffset in a left to right run.
303                    text = v.getText(endOffset - 1, endOffset);
304                    char c = text.array[text.offset];
305                    SegmentCache.releaseSharedSegment(text);
306                    if(c == '\n') {
307                        return -1;
308                    }
309                    biasRet[0] = Position.Bias.Backward;
310                }
311                else {
312                    biasRet[0] = Position.Bias.Forward;
313                }
314                return pos;
315            case View.WEST:
316                viewIsLeftToRight = AbstractDocument.isLeftToRight(doc, startOffset, endOffset);
317
318                if(startOffset == doc.getLength()) {
319                    if(pos == -1) {
320                        biasRet[0] = Position.Bias.Forward;
321                        return startOffset;
322                    }
323                    // End case for bidi text where newline is at beginning
324                    // of line.
325                    return -1;
326                }
327                if(pos == -1) {
328                    // Entering view from the right
329                    if( viewIsLeftToRight ) {
330                        text = v.getText(endOffset - 1, endOffset);
331                        char c = text.array[text.offset];
332                        SegmentCache.releaseSharedSegment(text);
333                        if ((c == '\n') || Character.isSpaceChar(c)) {
334                            biasRet[0] = Position.Bias.Forward;
335                            return endOffset - 1;
336                        }
337                        biasRet[0] = Position.Bias.Backward;
338                        return endOffset;
339                    } else {
340                        biasRet[0] = Position.Bias.Forward;
341                        return startOffset;
342                   }
343                }
344                if( b==Position.Bias.Forward )
345                    currentHit = TextHitInfo.afterOffset(pos-startOffset);
346                else
347                    currentHit = TextHitInfo.beforeOffset(pos-startOffset);
348                nextHit = layout.getNextLeftHit(currentHit);
349                if( nextHit == null ) {
350                    return -1;
351                }
352                if( viewIsLeftToRight != layout.isLeftToRight() ) {
353                    // If the layout's base direction is different from
354                    // this view's run direction, we need to use the weak
355                    // carrat.
356                    nextHit = layout.getVisualOtherHit(nextHit);
357                }
358                pos = nextHit.getInsertionIndex() + startOffset;
359
360                if(pos == endOffset) {
361                    // A move to the left from an internal position will
362                    // only take us to the endOffset in a right to left run.
363                    text = v.getText(endOffset - 1, endOffset);
364                    char c = text.array[text.offset];
365                    SegmentCache.releaseSharedSegment(text);
366                    if(c == '\n') {
367                        return -1;
368                    }
369                    biasRet[0] = Position.Bias.Backward;
370                }
371                else {
372                    biasRet[0] = Position.Bias.Forward;
373                }
374                return pos;
375            default:
376                throw new IllegalArgumentException("Bad direction: " + direction);
377            }
378            return pos;
379
380        }
381    // --- variables ---------------------------------------------
382
383    TextLayout layout;
384
385}
386