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 */ 25 package javax.swing.text; 26 27 import java.text.*; 28 import java.awt.*; 29 import java.awt.font.*; 30 import 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 */ 51 class 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 }