blinking cursor and SGR 1006 mode
[nikiroo-utils.git] / src / jexer / io / SwingScreen.java
CommitLineData
1ac2ccb1
KL
1/**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31package jexer.io;
32
1ac2ccb1
KL
33import java.awt.Color;
34import java.awt.Cursor;
35import java.awt.Font;
36import java.awt.FontMetrics;
1ac2ccb1 37import java.awt.Graphics;
84614868 38import java.awt.Insets;
30bd4abd
KL
39import java.awt.Point;
40import java.awt.Rectangle;
41import java.awt.Toolkit;
1ac2ccb1 42import java.awt.geom.Rectangle2D;
30bd4abd 43import java.awt.image.BufferedImage;
84614868 44import java.io.InputStream;
b5f2a6db 45import java.util.Date;
aed33687
KL
46import javax.swing.JFrame;
47import javax.swing.SwingUtilities;
1ac2ccb1 48
b5f2a6db
KL
49import jexer.bits.Cell;
50import jexer.bits.CellAttributes;
51import jexer.session.SwingSessionInfo;
52
1ac2ccb1 53/**
a4406f4e 54 * This Screen implementation draws to a Java Swing JFrame.
1ac2ccb1 55 */
a4406f4e 56public final class SwingScreen extends Screen {
1ac2ccb1 57
b5f2a6db
KL
58 /**
59 * Cursor style to draw.
60 */
61 public enum CursorStyle {
62 /**
63 * Use an underscore for the cursor.
64 */
65 UNDERLINE,
66
67 /**
68 * Use a solid block for the cursor.
69 */
70 BLOCK,
71
72 /**
73 * Use an outlined block for the cursor.
74 */
75 OUTLINE
76 }
77
84614868
KL
78 private static Color MYBLACK;
79 private static Color MYRED;
80 private static Color MYGREEN;
81 private static Color MYYELLOW;
82 private static Color MYBLUE;
83 private static Color MYMAGENTA;
84 private static Color MYCYAN;
85 private static Color MYWHITE;
86
87 private static Color MYBOLD_BLACK;
88 private static Color MYBOLD_RED;
89 private static Color MYBOLD_GREEN;
90 private static Color MYBOLD_YELLOW;
91 private static Color MYBOLD_BLUE;
92 private static Color MYBOLD_MAGENTA;
93 private static Color MYBOLD_CYAN;
94 private static Color MYBOLD_WHITE;
95
96 private static boolean dosColors = false;
97
98 /**
a4406f4e 99 * Setup Swing colors to match DOS color palette.
84614868
KL
100 */
101 private static void setDOSColors() {
102 if (dosColors) {
103 return;
104 }
105 MYBLACK = new Color(0x00, 0x00, 0x00);
106 MYRED = new Color(0xa8, 0x00, 0x00);
107 MYGREEN = new Color(0x00, 0xa8, 0x00);
108 MYYELLOW = new Color(0xa8, 0x54, 0x00);
109 MYBLUE = new Color(0x00, 0x00, 0xa8);
110 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
111 MYCYAN = new Color(0x00, 0xa8, 0xa8);
112 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
113 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
114 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
115 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
116 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
117 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
118 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
119 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
120 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
121
122 dosColors = true;
123 }
124
1ac2ccb1 125 /**
a4406f4e 126 * SwingFrame is our top-level hook into the Swing system.
1ac2ccb1 127 */
a4406f4e 128 class SwingFrame extends JFrame {
1ac2ccb1 129
cf9af8df
KL
130 /**
131 * Serializable version.
132 */
133 private static final long serialVersionUID = 1;
134
84614868
KL
135 /**
136 * The terminus font resource filename.
137 */
138 private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
139
1ac2ccb1
KL
140 /**
141 * The TUI Screen data.
142 */
a4406f4e 143 SwingScreen screen;
1ac2ccb1
KL
144
145 /**
146 * Width of a character cell.
147 */
148 private int textWidth = 1;
149
150 /**
151 * Height of a character cell.
152 */
153 private int textHeight = 1;
154
84614868
KL
155 /**
156 * Descent of a character cell.
157 */
158 private int maxDescent = 0;
159
1ac2ccb1 160 /**
b5f2a6db 161 * Top pixel absolute location.
1ac2ccb1
KL
162 */
163 private int top = 30;
84614868 164
1ac2ccb1 165 /**
b5f2a6db 166 * Left pixel absolute location.
1ac2ccb1
KL
167 */
168 private int left = 30;
84614868 169
b5f2a6db
KL
170 /**
171 * The cursor style to draw.
172 */
173 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
174
175 /**
176 * The number of millis to wait before switching the blink from
177 * visible to invisible.
178 */
179 private long blinkMillis = 500;
180
181 /**
182 * If true, the cursor should be visible right now based on the blink
183 * time.
184 */
185 private boolean cursorBlinkVisible = true;
186
187 /**
188 * The time that the blink last flipped from visible to invisible or
189 * from invisible to visible.
190 */
191 private long lastBlinkTime = 0;
192
84614868 193 /**
a4406f4e 194 * Convert a CellAttributes foreground color to an Swing Color.
84614868
KL
195 *
196 * @param attr the text attributes
a4406f4e 197 * @return the Swing Color
84614868
KL
198 */
199 private Color attrToForegroundColor(final CellAttributes attr) {
200 /*
201 * TODO:
202 * reverse
203 * blink
204 * underline
205 */
7c870d89 206 if (attr.isBold()) {
84614868
KL
207 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
208 return MYBOLD_BLACK;
209 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
210 return MYBOLD_RED;
211 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
212 return MYBOLD_BLUE;
213 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
214 return MYBOLD_GREEN;
215 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
216 return MYBOLD_YELLOW;
217 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
218 return MYBOLD_CYAN;
219 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
220 return MYBOLD_MAGENTA;
221 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
222 return MYBOLD_WHITE;
223 }
224 } else {
225 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
226 return MYBLACK;
227 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
228 return MYRED;
229 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
230 return MYBLUE;
231 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
232 return MYGREEN;
233 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
234 return MYYELLOW;
235 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
236 return MYCYAN;
237 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
238 return MYMAGENTA;
239 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
240 return MYWHITE;
241 }
242 }
243 throw new IllegalArgumentException("Invalid color: " + attr.getForeColor().getValue());
244 }
245
246 /**
a4406f4e 247 * Convert a CellAttributes background color to an Swing Color.
84614868
KL
248 *
249 * @param attr the text attributes
a4406f4e 250 * @return the Swing Color
84614868
KL
251 */
252 private Color attrToBackgroundColor(final CellAttributes attr) {
253 /*
254 * TODO:
255 * reverse
256 * blink
257 * underline
258 */
259 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
260 return MYBLACK;
261 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
262 return MYRED;
263 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
264 return MYBLUE;
265 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
266 return MYGREEN;
267 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
268 return MYYELLOW;
269 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
270 return MYCYAN;
271 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
272 return MYMAGENTA;
273 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
274 return MYWHITE;
275 }
276 throw new IllegalArgumentException("Invalid color: " + attr.getBackColor().getValue());
277 }
278
1ac2ccb1
KL
279 /**
280 * Public constructor.
84614868
KL
281 *
282 * @param screen the Screen that Backend talks to
1ac2ccb1 283 */
a4406f4e 284 public SwingFrame(final SwingScreen screen) {
84614868
KL
285 this.screen = screen;
286 setDOSColors();
287
b5f2a6db
KL
288 // Figure out my cursor style
289 String cursorStyleString = System.getProperty("jexer.Swing.cursorStyle",
290 "underline").toLowerCase();
291
292 if (cursorStyleString.equals("underline")) {
293 cursorStyle = CursorStyle.UNDERLINE;
294 } else if (cursorStyleString.equals("outline")) {
295 cursorStyle = CursorStyle.OUTLINE;
296 } else if (cursorStyleString.equals("block")) {
297 cursorStyle = CursorStyle.BLOCK;
298 }
299
1ac2ccb1 300 setTitle("Jexer Application");
aed33687 301 setBackground(Color.black);
84614868
KL
302
303 try {
30bd4abd 304 // Always try to use Terminus, the one decent font.
84614868
KL
305 ClassLoader loader = Thread.currentThread().getContextClassLoader();
306 InputStream in = loader.getResourceAsStream(FONTFILE);
307 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
308 Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
309 setFont(terminus);
310 } catch (Exception e) {
311 e.printStackTrace();
312 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
313 setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
314 }
aed33687 315 pack();
30bd4abd
KL
316
317 // Kill the X11 cursor
318 // Transparent 16 x 16 pixel cursor image.
319 BufferedImage cursorImg = new BufferedImage(16, 16,
320 BufferedImage.TYPE_INT_ARGB);
30bd4abd
KL
321 // Create a new blank cursor.
322 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
323 cursorImg, new Point(0, 0), "blank cursor");
324 setCursor(blankCursor);
091d8a06
KL
325
326 // Be capable of seeing Tab / Shift-Tab
327 setFocusTraversalKeysEnabled(false);
328
329 // Save the text cell width/height
bd8d51fa 330 getFontDimensions();
1ac2ccb1
KL
331 }
332
333 /**
bd8d51fa 334 * Figure out my font dimensions.
1ac2ccb1 335 */
bd8d51fa 336 private void getFontDimensions() {
84614868
KL
337 Graphics gr = getGraphics();
338 FontMetrics fm = gr.getFontMetrics();
339 maxDescent = fm.getMaxDescent();
340 Rectangle2D bounds = fm.getMaxCharBounds(gr);
341 int leading = fm.getLeading();
342 textWidth = (int)Math.round(bounds.getWidth());
343 textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
344 // This also produces the same number, but works better for ugly
345 // monospace.
346 textHeight = fm.getMaxAscent() + maxDescent - leading;
bd8d51fa 347 }
84614868 348
bd8d51fa
KL
349 /**
350 * Resize to font dimensions.
351 */
352 public void resizeToScreen() {
84614868
KL
353 // Figure out the thickness of borders and use that to set the
354 // final size.
355 Insets insets = getInsets();
356 left = insets.left;
357 top = insets.top;
358
359 setSize(textWidth * screen.width + insets.left + insets.right,
360 textHeight * screen.height + insets.top + insets.bottom);
1ac2ccb1
KL
361 }
362
30bd4abd
KL
363 /**
364 * Update redraws the whole screen.
365 *
a4406f4e 366 * @param gr the Swing Graphics context
30bd4abd
KL
367 */
368 @Override
369 public void update(final Graphics gr) {
370 // The default update clears the area. Don't do that, instead
371 // just paint it directly.
372 paint(gr);
373 }
374
1ac2ccb1
KL
375 /**
376 * Paint redraws the whole screen.
377 *
a4406f4e 378 * @param gr the Swing Graphics context
1ac2ccb1
KL
379 */
380 @Override
84614868 381 public void paint(final Graphics gr) {
87a17f3c
KL
382 // Do nothing until the screen reference has been set.
383 if (screen == null) {
384 return;
385 }
386 if (screen.frame == null) {
387 return;
388 }
389
b5f2a6db
KL
390 // See if it is time to flip the blink time.
391 long nowTime = (new Date()).getTime();
392 if (nowTime > blinkMillis + lastBlinkTime) {
393 lastBlinkTime = nowTime;
394 cursorBlinkVisible = !cursorBlinkVisible;
395 }
396
87a17f3c
KL
397 int xCellMin = 0;
398 int xCellMax = screen.width;
399 int yCellMin = 0;
400 int yCellMax = screen.height;
401
30bd4abd 402 Rectangle bounds = gr.getClipBounds();
87a17f3c
KL
403 if (bounds != null) {
404 // Only update what is in the bounds
405 xCellMin = screen.textColumn(bounds.x);
bd8d51fa 406 xCellMax = screen.textColumn(bounds.x + bounds.width);
87a17f3c
KL
407 if (xCellMax > screen.width) {
408 xCellMax = screen.width;
409 }
410 if (xCellMin >= xCellMax) {
411 xCellMin = xCellMax - 2;
412 }
413 if (xCellMin < 0) {
414 xCellMin = 0;
415 }
416 yCellMin = screen.textRow(bounds.y);
bd8d51fa 417 yCellMax = screen.textRow(bounds.y + bounds.height);
87a17f3c
KL
418 if (yCellMax > screen.height) {
419 yCellMax = screen.height;
420 }
421 if (yCellMin >= yCellMax) {
422 yCellMin = yCellMax - 2;
423 }
424 if (yCellMin < 0) {
425 yCellMin = 0;
426 }
bd8d51fa
KL
427 } else {
428 // We need a total repaint
429 reallyCleared = true;
87a17f3c 430 }
1ac2ccb1 431
87a17f3c
KL
432 // Prevent updates to the screen's data from the TApplication
433 // threads.
434 synchronized (screen) {
435 /*
436 System.err.printf("bounds %s X %d %d Y %d %d\n",
437 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
438 */
439
440 for (int y = yCellMin; y < yCellMax; y++) {
441 for (int x = xCellMin; x < xCellMax; x++) {
442
443 int xPixel = x * textWidth + left;
444 int yPixel = y * textHeight + top;
445
446 Cell lCell = screen.logical[x][y];
447 Cell pCell = screen.physical[x][y];
448
449 if (!lCell.equals(pCell) || reallyCleared) {
450 // Draw the background rectangle, then the
451 // foreground character.
452 gr.setColor(attrToBackgroundColor(lCell));
453 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
454 gr.setColor(attrToForegroundColor(lCell));
455 char [] chars = new char[1];
456 chars[0] = lCell.getChar();
457 gr.drawChars(chars, 0, 1, xPixel,
458 yPixel + textHeight - maxDescent);
459
460 // Physical is always updated
461 physical[x][y].setTo(lCell);
30bd4abd
KL
462 }
463 }
87a17f3c 464 }
84614868 465
87a17f3c 466 // Draw the cursor if it is visible
b5f2a6db 467 if (cursorVisible
87a17f3c
KL
468 && (cursorY <= screen.height - 1)
469 && (cursorX <= screen.width - 1)
b5f2a6db 470 && cursorBlinkVisible
87a17f3c
KL
471 ) {
472 int xPixel = cursorX * textWidth + left;
473 int yPixel = cursorY * textHeight + top;
474 Cell lCell = screen.logical[cursorX][cursorY];
475 gr.setColor(attrToForegroundColor(lCell));
b5f2a6db
KL
476 switch (cursorStyle) {
477 case UNDERLINE:
478 gr.fillRect(xPixel, yPixel + textHeight - 2,
479 textWidth, 2);
480 break;
481 case BLOCK:
482 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
483 break;
484 case OUTLINE:
485 gr.drawRect(xPixel, yPixel, textWidth - 1,
486 textHeight - 1);
487 break;
488 }
1ac2ccb1 489 }
30bd4abd 490
87a17f3c
KL
491 dirty = false;
492 reallyCleared = false;
493 } // synchronized (screen)
1ac2ccb1 494 }
bd8d51fa 495
a4406f4e 496 } // class SwingFrame
1ac2ccb1
KL
497
498 /**
a4406f4e 499 * The raw Swing JFrame. Note package private access.
1ac2ccb1 500 */
a4406f4e 501 SwingFrame frame;
1ac2ccb1
KL
502
503 /**
504 * Public constructor.
505 */
a4406f4e 506 public SwingScreen() {
aed33687
KL
507 try {
508 SwingUtilities.invokeAndWait(new Runnable() {
509 public void run() {
a4406f4e
KL
510 SwingScreen.this.frame = new SwingFrame(SwingScreen.this);
511 SwingScreen.this.sessionInfo =
512 new SwingSessionInfo(SwingScreen.this.frame,
bd8d51fa
KL
513 frame.textWidth,
514 frame.textHeight);
515
a4406f4e 516 SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
bd8d51fa
KL
517 sessionInfo.getWindowHeight());
518
a4406f4e
KL
519 SwingScreen.this.frame.resizeToScreen();
520 SwingScreen.this.frame.setVisible(true);
aed33687
KL
521 }
522 } );
523 } catch (Exception e) {
524 e.printStackTrace();
525 }
1ac2ccb1
KL
526 }
527
bd8d51fa
KL
528 /**
529 * The sessionInfo.
530 */
a4406f4e 531 private SwingSessionInfo sessionInfo;
bd8d51fa 532
30bd4abd 533 /**
a4406f4e 534 * Create the SwingSessionInfo. Note package private access.
30bd4abd
KL
535 *
536 * @return the sessionInfo
537 */
a4406f4e 538 SwingSessionInfo getSessionInfo() {
30bd4abd
KL
539 return sessionInfo;
540 }
541
1ac2ccb1
KL
542 /**
543 * Push the logical screen to the physical device.
544 */
545 @Override
546 public void flushPhysical() {
87a17f3c
KL
547
548 if (reallyCleared) {
549 // Really refreshed, do it all
550 frame.repaint();
551 return;
552 }
553
554 // Do nothing if nothing happened.
555 if (!dirty) {
556 return;
557 }
558
30bd4abd
KL
559 // Request a repaint, let the frame's repaint/update methods do the
560 // right thing.
87a17f3c 561
30bd4abd
KL
562 // Find the minimum-size damaged region.
563 int xMin = frame.getWidth();
564 int xMax = 0;
565 int yMin = frame.getHeight();
566 int yMax = 0;
30bd4abd 567
87a17f3c
KL
568 synchronized (this) {
569 for (int y = 0; y < height; y++) {
570 for (int x = 0; x < width; x++) {
571 Cell lCell = logical[x][y];
572 Cell pCell = physical[x][y];
573
574 int xPixel = x * frame.textWidth + frame.left;
575 int yPixel = y * frame.textHeight + frame.top;
576
577 if (!lCell.equals(pCell)
578 || ((x == cursorX)
579 && (y == cursorY)
580 && cursorVisible)
581 ) {
582 if (xPixel < xMin) {
583 xMin = xPixel;
584 }
585 if (xPixel + frame.textWidth > xMax) {
586 xMax = xPixel + frame.textWidth;
587 }
588 if (yPixel < yMin) {
589 yMin = yPixel;
590 }
591 if (yPixel + frame.textHeight > yMax) {
592 yMax = yPixel + frame.textHeight;
593 }
30bd4abd
KL
594 }
595 }
596 }
597 }
87a17f3c
KL
598 if (xMin + frame.textWidth >= xMax) {
599 xMax += frame.textWidth;
600 }
601 if (yMin + frame.textHeight >= yMax) {
602 yMax += frame.textHeight;
603 }
30bd4abd 604
87a17f3c
KL
605 // Repaint the desired area
606 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
607 // System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, yMin, yMax);
1ac2ccb1 608 }
30bd4abd
KL
609
610 /**
611 * Put the cursor at (x,y).
612 *
613 * @param visible if true, the cursor should be visible
614 * @param x column coordinate to put the cursor on
615 * @param y row coordinate to put the cursor on
616 */
617 @Override
618 public void putCursor(final boolean visible, final int x, final int y) {
b5f2a6db
KL
619
620 if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) {
621 // See if it is time to flip the blink time.
622 long nowTime = (new Date()).getTime();
623 if (nowTime < frame.blinkMillis + frame.lastBlinkTime) {
624 // Nothing has changed, so don't do anything.
625 return;
626 }
627 }
628
629 if (cursorVisible
30bd4abd
KL
630 && (cursorY <= height - 1)
631 && (cursorX <= width - 1)
632 ) {
633 // Make the current cursor position dirty
87a17f3c 634 if (physical[cursorX][cursorY].getChar() == 'Q') {
30bd4abd
KL
635 physical[cursorX][cursorY].setChar('X');
636 } else {
87a17f3c 637 physical[cursorX][cursorY].setChar('Q');
30bd4abd
KL
638 }
639 }
640
641 super.putCursor(visible, x, y);
642 }
643
87a17f3c
KL
644 /**
645 * Convert pixel column position to text cell column position.
646 *
647 * @param x pixel column position
648 * @return text cell column position
649 */
650 public int textColumn(final int x) {
651 return ((x - frame.left) / frame.textWidth);
652 }
653
654 /**
655 * Convert pixel row position to text cell row position.
656 *
657 * @param y pixel row position
658 * @return text cell row position
659 */
660 public int textRow(final int y) {
661 return ((y - frame.top) / frame.textHeight);
662 }
663
1ac2ccb1 664}