Performance tweaks
[fanfix.git] / src / jexer / io / SwingScreen.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2016 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.io;
30
31 import java.awt.Color;
32 import java.awt.Cursor;
33 import java.awt.Font;
34 import java.awt.FontMetrics;
35 import java.awt.Graphics;
36 import java.awt.Insets;
37 import java.awt.Point;
38 import java.awt.Rectangle;
39 import java.awt.Toolkit;
40 import java.awt.geom.Rectangle2D;
41 import java.awt.image.BufferedImage;
42 import java.awt.image.BufferStrategy;
43 import java.io.InputStream;
44 import java.util.Date;
45 import javax.swing.JFrame;
46 import javax.swing.SwingUtilities;
47
48 import jexer.bits.Cell;
49 import jexer.bits.CellAttributes;
50 import jexer.session.SwingSessionInfo;
51
52 /**
53 * This Screen implementation draws to a Java Swing JFrame.
54 */
55 public final class SwingScreen extends Screen {
56
57 /**
58 * If true, use triple buffering thread.
59 */
60 private static final boolean tripleBuffer = true;
61
62 /**
63 * Cursor style to draw.
64 */
65 public enum CursorStyle {
66 /**
67 * Use an underscore for the cursor.
68 */
69 UNDERLINE,
70
71 /**
72 * Use a solid block for the cursor.
73 */
74 BLOCK,
75
76 /**
77 * Use an outlined block for the cursor.
78 */
79 OUTLINE
80 }
81
82 private static Color MYBLACK;
83 private static Color MYRED;
84 private static Color MYGREEN;
85 private static Color MYYELLOW;
86 private static Color MYBLUE;
87 private static Color MYMAGENTA;
88 private static Color MYCYAN;
89 private static Color MYWHITE;
90
91 private static Color MYBOLD_BLACK;
92 private static Color MYBOLD_RED;
93 private static Color MYBOLD_GREEN;
94 private static Color MYBOLD_YELLOW;
95 private static Color MYBOLD_BLUE;
96 private static Color MYBOLD_MAGENTA;
97 private static Color MYBOLD_CYAN;
98 private static Color MYBOLD_WHITE;
99
100 private static boolean dosColors = false;
101
102 /**
103 * Setup Swing colors to match DOS color palette.
104 */
105 private static void setDOSColors() {
106 if (dosColors) {
107 return;
108 }
109 MYBLACK = new Color(0x00, 0x00, 0x00);
110 MYRED = new Color(0xa8, 0x00, 0x00);
111 MYGREEN = new Color(0x00, 0xa8, 0x00);
112 MYYELLOW = new Color(0xa8, 0x54, 0x00);
113 MYBLUE = new Color(0x00, 0x00, 0xa8);
114 MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
115 MYCYAN = new Color(0x00, 0xa8, 0xa8);
116 MYWHITE = new Color(0xa8, 0xa8, 0xa8);
117 MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
118 MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
119 MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
120 MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
121 MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
122 MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
123 MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
124 MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
125
126 dosColors = true;
127 }
128
129 /**
130 * SwingFrame is our top-level hook into the Swing system.
131 */
132 class SwingFrame extends JFrame {
133
134 /**
135 * Serializable version.
136 */
137 private static final long serialVersionUID = 1;
138
139 /**
140 * The terminus font resource filename.
141 */
142 private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
143
144 /**
145 * The BufferStrategy object needed for triple-buffering.
146 */
147 private BufferStrategy bufferStrategy;
148
149 /**
150 * The TUI Screen data.
151 */
152 SwingScreen screen;
153
154 /**
155 * Width of a character cell.
156 */
157 private int textWidth = 1;
158
159 /**
160 * Height of a character cell.
161 */
162 private int textHeight = 1;
163
164 /**
165 * Descent of a character cell.
166 */
167 private int maxDescent = 0;
168
169 /**
170 * System-dependent Y adjustment for text in the character cell.
171 */
172 private int textAdjustY = 0;
173
174 /**
175 * System-dependent X adjustment for text in the character cell.
176 */
177 private int textAdjustX = 0;
178
179 /**
180 * Top pixel absolute location.
181 */
182 private int top = 30;
183
184 /**
185 * Left pixel absolute location.
186 */
187 private int left = 30;
188
189 /**
190 * The cursor style to draw.
191 */
192 private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
193
194 /**
195 * The number of millis to wait before switching the blink from
196 * visible to invisible.
197 */
198 private long blinkMillis = 500;
199
200 /**
201 * If true, the cursor should be visible right now based on the blink
202 * time.
203 */
204 private boolean cursorBlinkVisible = true;
205
206 /**
207 * The time that the blink last flipped from visible to invisible or
208 * from invisible to visible.
209 */
210 private long lastBlinkTime = 0;
211
212 /**
213 * Convert a CellAttributes foreground color to an Swing Color.
214 *
215 * @param attr the text attributes
216 * @return the Swing Color
217 */
218 private Color attrToForegroundColor(final CellAttributes attr) {
219 if (attr.isBold()) {
220 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
221 return MYBOLD_BLACK;
222 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
223 return MYBOLD_RED;
224 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
225 return MYBOLD_BLUE;
226 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
227 return MYBOLD_GREEN;
228 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
229 return MYBOLD_YELLOW;
230 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
231 return MYBOLD_CYAN;
232 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
233 return MYBOLD_MAGENTA;
234 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
235 return MYBOLD_WHITE;
236 }
237 } else {
238 if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
239 return MYBLACK;
240 } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
241 return MYRED;
242 } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
243 return MYBLUE;
244 } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
245 return MYGREEN;
246 } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
247 return MYYELLOW;
248 } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
249 return MYCYAN;
250 } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
251 return MYMAGENTA;
252 } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
253 return MYWHITE;
254 }
255 }
256 throw new IllegalArgumentException("Invalid color: " + attr.getForeColor().getValue());
257 }
258
259 /**
260 * Convert a CellAttributes background color to an Swing Color.
261 *
262 * @param attr the text attributes
263 * @return the Swing Color
264 */
265 private Color attrToBackgroundColor(final CellAttributes attr) {
266 if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
267 return MYBLACK;
268 } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
269 return MYRED;
270 } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
271 return MYBLUE;
272 } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
273 return MYGREEN;
274 } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
275 return MYYELLOW;
276 } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
277 return MYCYAN;
278 } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
279 return MYMAGENTA;
280 } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
281 return MYWHITE;
282 }
283 throw new IllegalArgumentException("Invalid color: " + attr.getBackColor().getValue());
284 }
285
286 /**
287 * Public constructor.
288 *
289 * @param screen the Screen that Backend talks to
290 */
291 public SwingFrame(final SwingScreen screen) {
292 this.screen = screen;
293 setDOSColors();
294
295 // Figure out my cursor style
296 String cursorStyleString = System.getProperty("jexer.Swing.cursorStyle",
297 "underline").toLowerCase();
298
299 if (cursorStyleString.equals("underline")) {
300 cursorStyle = CursorStyle.UNDERLINE;
301 } else if (cursorStyleString.equals("outline")) {
302 cursorStyle = CursorStyle.OUTLINE;
303 } else if (cursorStyleString.equals("block")) {
304 cursorStyle = CursorStyle.BLOCK;
305 }
306
307 setTitle("Jexer Application");
308 setBackground(Color.black);
309
310 try {
311 // Always try to use Terminus, the one decent font.
312 ClassLoader loader = Thread.currentThread().getContextClassLoader();
313 InputStream in = loader.getResourceAsStream(FONTFILE);
314 Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
315 Font terminus = terminusRoot.deriveFont(Font.PLAIN, 22);
316 setFont(terminus);
317 } catch (Exception e) {
318 e.printStackTrace();
319 // setFont(new Font("Liberation Mono", Font.PLAIN, 24));
320 setFont(new Font(Font.MONOSPACED, Font.PLAIN, 24));
321 }
322 pack();
323
324 // Kill the X11 cursor
325 // Transparent 16 x 16 pixel cursor image.
326 BufferedImage cursorImg = new BufferedImage(16, 16,
327 BufferedImage.TYPE_INT_ARGB);
328 // Create a new blank cursor.
329 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
330 cursorImg, new Point(0, 0), "blank cursor");
331 setCursor(blankCursor);
332
333 // Be capable of seeing Tab / Shift-Tab
334 setFocusTraversalKeysEnabled(false);
335
336 // Save the text cell width/height
337 getFontDimensions();
338
339 // Setup triple-buffering
340 if (SwingScreen.tripleBuffer) {
341 setIgnoreRepaint(true);
342 createBufferStrategy(3);
343 bufferStrategy = getBufferStrategy();
344 }
345 }
346
347 /**
348 * Figure out my font dimensions.
349 */
350 private void getFontDimensions() {
351 Graphics gr = getGraphics();
352 FontMetrics fm = gr.getFontMetrics();
353 maxDescent = fm.getMaxDescent();
354 Rectangle2D bounds = fm.getMaxCharBounds(gr);
355 int leading = fm.getLeading();
356 textWidth = (int)Math.round(bounds.getWidth());
357 textHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
358 // This also produces the same number, but works better for ugly
359 // monospace.
360 textHeight = fm.getMaxAscent() + maxDescent - leading;
361
362 if (System.getProperty("os.name").startsWith("Windows")) {
363 textAdjustY = -1;
364 textAdjustX = 0;
365 }
366 }
367
368 /**
369 * Resize to font dimensions.
370 */
371 public void resizeToScreen() {
372 // Figure out the thickness of borders and use that to set the
373 // final size.
374 Insets insets = getInsets();
375 left = insets.left;
376 top = insets.top;
377
378 setSize(textWidth * screen.width + insets.left + insets.right,
379 textHeight * screen.height + insets.top + insets.bottom);
380 }
381
382 /**
383 * Update redraws the whole screen.
384 *
385 * @param gr the Swing Graphics context
386 */
387 @Override
388 public void update(final Graphics gr) {
389 // The default update clears the area. Don't do that, instead
390 // just paint it directly.
391 paint(gr);
392 }
393
394 /**
395 * Paint redraws the whole screen.
396 *
397 * @param gr the Swing Graphics context
398 */
399 @Override
400 public void paint(final Graphics gr) {
401 // Do nothing until the screen reference has been set.
402 if (screen == null) {
403 return;
404 }
405 if (screen.frame == null) {
406 return;
407 }
408
409 // See if it is time to flip the blink time.
410 long nowTime = (new Date()).getTime();
411 if (nowTime > blinkMillis + lastBlinkTime) {
412 lastBlinkTime = nowTime;
413 cursorBlinkVisible = !cursorBlinkVisible;
414 }
415
416 int xCellMin = 0;
417 int xCellMax = screen.width;
418 int yCellMin = 0;
419 int yCellMax = screen.height;
420
421 Rectangle bounds = gr.getClipBounds();
422 if (bounds != null) {
423 // Only update what is in the bounds
424 xCellMin = screen.textColumn(bounds.x);
425 xCellMax = screen.textColumn(bounds.x + bounds.width);
426 if (xCellMax > screen.width) {
427 xCellMax = screen.width;
428 }
429 if (xCellMin >= xCellMax) {
430 xCellMin = xCellMax - 2;
431 }
432 if (xCellMin < 0) {
433 xCellMin = 0;
434 }
435 yCellMin = screen.textRow(bounds.y);
436 yCellMax = screen.textRow(bounds.y + bounds.height);
437 if (yCellMax > screen.height) {
438 yCellMax = screen.height;
439 }
440 if (yCellMin >= yCellMax) {
441 yCellMin = yCellMax - 2;
442 }
443 if (yCellMin < 0) {
444 yCellMin = 0;
445 }
446 } else {
447 // We need a total repaint
448 reallyCleared = true;
449 }
450
451 // Prevent updates to the screen's data from the TApplication
452 // threads.
453 synchronized (screen) {
454
455 /*
456 System.err.printf("bounds %s X %d %d Y %d %d\n",
457 bounds, xCellMin, xCellMax, yCellMin, yCellMax);
458 */
459 Cell lCellColor = new Cell();
460
461 for (int y = yCellMin; y < yCellMax; y++) {
462 for (int x = xCellMin; x < xCellMax; x++) {
463
464 int xPixel = x * textWidth + left;
465 int yPixel = y * textHeight + top;
466
467 Cell lCell = screen.logical[x][y];
468 Cell pCell = screen.physical[x][y];
469
470 if (!lCell.equals(pCell)
471 || lCell.isBlink()
472 || reallyCleared) {
473
474 lCellColor.setTo(lCell);
475
476 // Check for reverse
477 if (lCell.isReverse()) {
478 lCellColor.setForeColor(lCell.getBackColor());
479 lCellColor.setBackColor(lCell.getForeColor());
480 }
481
482 // Draw the background rectangle, then the
483 // foreground character.
484 gr.setColor(attrToBackgroundColor(lCellColor));
485 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
486
487 // Handle blink and underline
488 if (!lCell.isBlink()
489 || (lCell.isBlink() && cursorBlinkVisible)
490 ) {
491 gr.setColor(attrToForegroundColor(lCellColor));
492 char [] chars = new char[1];
493 chars[0] = lCell.getChar();
494 gr.drawChars(chars, 0, 1, xPixel + textAdjustX,
495 yPixel + textHeight - maxDescent
496 + textAdjustY);
497
498 if (lCell.isUnderline()) {
499 gr.fillRect(xPixel, yPixel + textHeight - 2,
500 textWidth, 2);
501 }
502 }
503
504 // Physical is always updated
505 physical[x][y].setTo(lCell);
506 }
507 }
508 }
509
510 // Draw the cursor if it is visible
511 if (cursorVisible
512 && (cursorY <= screen.height - 1)
513 && (cursorX <= screen.width - 1)
514 && cursorBlinkVisible
515 ) {
516 int xPixel = cursorX * textWidth + left;
517 int yPixel = cursorY * textHeight + top;
518 Cell lCell = screen.logical[cursorX][cursorY];
519 gr.setColor(attrToForegroundColor(lCell));
520 switch (cursorStyle) {
521 default:
522 // Fall through...
523 case UNDERLINE:
524 gr.fillRect(xPixel, yPixel + textHeight - 2,
525 textWidth, 2);
526 break;
527 case BLOCK:
528 gr.fillRect(xPixel, yPixel, textWidth, textHeight);
529 break;
530 case OUTLINE:
531 gr.drawRect(xPixel, yPixel, textWidth - 1,
532 textHeight - 1);
533 break;
534 }
535 }
536
537 dirty = false;
538 reallyCleared = false;
539 } // synchronized (screen)
540 }
541
542 } // class SwingFrame
543
544 /**
545 * The raw Swing JFrame. Note package private access.
546 */
547 SwingFrame frame;
548
549 /**
550 * Restore terminal to normal state.
551 */
552 public void shutdown() {
553 frame.dispose();
554 }
555
556 /**
557 * Public constructor.
558 */
559 public SwingScreen() {
560 try {
561 SwingUtilities.invokeAndWait(new Runnable() {
562 public void run() {
563 SwingScreen.this.frame = new SwingFrame(SwingScreen.this);
564 SwingScreen.this.sessionInfo =
565 new SwingSessionInfo(SwingScreen.this.frame,
566 frame.textWidth,
567 frame.textHeight);
568
569 SwingScreen.this.setDimensions(sessionInfo.getWindowWidth(),
570 sessionInfo.getWindowHeight());
571
572 SwingScreen.this.frame.resizeToScreen();
573 SwingScreen.this.frame.setVisible(true);
574 }
575 });
576 } catch (Exception e) {
577 e.printStackTrace();
578 }
579 }
580
581 /**
582 * The sessionInfo.
583 */
584 private SwingSessionInfo sessionInfo;
585
586 /**
587 * Create the SwingSessionInfo. Note package private access.
588 *
589 * @return the sessionInfo
590 */
591 SwingSessionInfo getSessionInfo() {
592 return sessionInfo;
593 }
594
595 /**
596 * Push the logical screen to the physical device.
597 */
598 @Override
599 public void flushPhysical() {
600
601 /*
602 System.err.printf("flushPhysical(): reallyCleared %s dirty %s\n",
603 reallyCleared, dirty);
604 */
605
606 // If reallyCleared is set, we have to draw everything.
607 if ((frame.bufferStrategy != null) && (reallyCleared == true)) {
608 // Triple-buffering: we have to redraw everything on this thread.
609 Graphics gr = frame.bufferStrategy.getDrawGraphics();
610 frame.paint(gr);
611 gr.dispose();
612 frame.bufferStrategy.show();
613 // sync() doesn't seem to help the tearing for me.
614 // Toolkit.getDefaultToolkit().sync();
615 return;
616 } else if ((frame.bufferStrategy == null) && (reallyCleared == true)) {
617 // Repaint everything on the Swing thread.
618 frame.repaint();
619 return;
620 }
621
622 // Do nothing if nothing happened.
623 if (!dirty) {
624 return;
625 }
626
627 // Request a repaint, let the frame's repaint/update methods do the
628 // right thing.
629
630 // Find the minimum-size damaged region.
631 int xMin = frame.getWidth();
632 int xMax = 0;
633 int yMin = frame.getHeight();
634 int yMax = 0;
635
636 synchronized (this) {
637 for (int y = 0; y < height; y++) {
638 for (int x = 0; x < width; x++) {
639 Cell lCell = logical[x][y];
640 Cell pCell = physical[x][y];
641
642 int xPixel = x * frame.textWidth + frame.left;
643 int yPixel = y * frame.textHeight + frame.top;
644
645 if (!lCell.equals(pCell)
646 || ((x == cursorX)
647 && (y == cursorY)
648 && cursorVisible)
649 || lCell.isBlink()
650 ) {
651 if (xPixel < xMin) {
652 xMin = xPixel;
653 }
654 if (xPixel + frame.textWidth > xMax) {
655 xMax = xPixel + frame.textWidth;
656 }
657 if (yPixel < yMin) {
658 yMin = yPixel;
659 }
660 if (yPixel + frame.textHeight > yMax) {
661 yMax = yPixel + frame.textHeight;
662 }
663 }
664 }
665 }
666 }
667 if (xMin + frame.textWidth >= xMax) {
668 xMax += frame.textWidth;
669 }
670 if (yMin + frame.textHeight >= yMax) {
671 yMax += frame.textHeight;
672 }
673
674 // Repaint the desired area
675 /*
676 System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
677 yMin, yMax);
678 */
679 if (frame.bufferStrategy != null) {
680 Graphics gr = frame.bufferStrategy.getDrawGraphics();
681 Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
682 yMax - yMin);
683 gr.setClip(bounds);
684 frame.paint(gr);
685 gr.dispose();
686 frame.bufferStrategy.show();
687 // sync() doesn't seem to help the tearing for me.
688 // Toolkit.getDefaultToolkit().sync();
689 } else {
690 frame.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
691 }
692 }
693
694 /**
695 * Put the cursor at (x,y).
696 *
697 * @param visible if true, the cursor should be visible
698 * @param x column coordinate to put the cursor on
699 * @param y row coordinate to put the cursor on
700 */
701 @Override
702 public void putCursor(final boolean visible, final int x, final int y) {
703
704 if ((visible == cursorVisible) && ((x == cursorX) && (y == cursorY))) {
705 // See if it is time to flip the blink time.
706 long nowTime = (new Date()).getTime();
707 if (nowTime < frame.blinkMillis + frame.lastBlinkTime) {
708 // Nothing has changed, so don't do anything.
709 return;
710 }
711 }
712
713 if (cursorVisible
714 && (cursorY <= height - 1)
715 && (cursorX <= width - 1)
716 ) {
717 // Make the current cursor position dirty
718 if (physical[cursorX][cursorY].getChar() == 'Q') {
719 physical[cursorX][cursorY].setChar('X');
720 } else {
721 physical[cursorX][cursorY].setChar('Q');
722 }
723 }
724
725 super.putCursor(visible, x, y);
726 }
727
728 /**
729 * Convert pixel column position to text cell column position.
730 *
731 * @param x pixel column position
732 * @return text cell column position
733 */
734 public int textColumn(final int x) {
735 return ((x - frame.left) / frame.textWidth);
736 }
737
738 /**
739 * Convert pixel row position to text cell row position.
740 *
741 * @param y pixel row position
742 * @return text cell row position
743 */
744 public int textRow(final int y) {
745 return ((y - frame.top) / frame.textHeight);
746 }
747
748 }