Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / backend / SwingComponent.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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.backend;
30
31 import java.awt.Color;
32 import java.awt.Cursor;
33 import java.awt.Font;
34 import java.awt.Graphics;
35 import java.awt.Insets;
36 import java.awt.Point;
37 import java.awt.Toolkit;
38 import java.awt.event.ComponentListener;
39 import java.awt.event.KeyListener;
40 import java.awt.event.MouseListener;
41 import java.awt.event.MouseMotionListener;
42 import java.awt.event.MouseWheelListener;
43 import java.awt.event.WindowListener;
44 import java.awt.image.BufferedImage;
45 import java.awt.image.BufferStrategy;
46 import java.io.IOException;
47 import javax.imageio.ImageIO;
48 import javax.swing.JComponent;
49 import javax.swing.JFrame;
50 import javax.swing.SwingUtilities;
51
52 /**
53 * Wrapper for integrating with Swing, because JFrame and JComponent have
54 * separate hierarchies.
55 */
56 class SwingComponent {
57
58 // ------------------------------------------------------------------------
59 // Variables --------------------------------------------------------------
60 // ------------------------------------------------------------------------
61
62 /**
63 * If true, use triple buffering when drawing to a JFrame.
64 */
65 public static boolean tripleBuffer = true;
66
67 /**
68 * The frame reference, if we are drawing to a JFrame.
69 */
70 private JFrame frame;
71
72 /**
73 * The component reference, if we are drawing to a JComponent.
74 */
75 private JComponent component;
76
77 /**
78 * An optional border in pixels to add.
79 */
80 private static final int BORDER = 1;
81
82 /**
83 * Adjustable Insets for this component. This has the effect of adding a
84 * black border around the drawing area.
85 */
86 Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
87
88 // ------------------------------------------------------------------------
89 // Constructors -----------------------------------------------------------
90 // ------------------------------------------------------------------------
91
92 /**
93 * Construct using a JFrame.
94 *
95 * @param frame the JFrame to draw to
96 */
97 public SwingComponent(final JFrame frame) {
98 this.frame = frame;
99 setupFrame();
100 }
101
102 /**
103 * Construct using a JComponent.
104 *
105 * @param component the JComponent to draw to
106 */
107 public SwingComponent(final JComponent component) {
108 this.component = component;
109 setupComponent();
110 }
111
112 // ------------------------------------------------------------------------
113 // SwingComponent ---------------------------------------------------------
114 // ------------------------------------------------------------------------
115
116 /**
117 * Get the BufferStrategy object needed for triple-buffering.
118 *
119 * @return the BufferStrategy
120 * @throws IllegalArgumentException if this function is called when
121 * not rendering to a JFrame
122 */
123 public BufferStrategy getBufferStrategy() {
124 if (frame != null) {
125 return frame.getBufferStrategy();
126 } else {
127 throw new IllegalArgumentException("BufferStrategy not used " +
128 "for JComponent access");
129 }
130 }
131
132 /**
133 * Get the JFrame reference.
134 *
135 * @return the frame, or null if this is drawing to a JComponent
136 */
137 public JFrame getFrame() {
138 return frame;
139 }
140
141 /**
142 * Get the JComponent reference.
143 *
144 * @return the component, or null if this is drawing to a JFrame
145 */
146 public JComponent getComponent() {
147 return component;
148 }
149
150 /**
151 * Setup to render to an existing JComponent.
152 */
153 public void setupComponent() {
154 component.setBackground(Color.black);
155
156 if (System.getProperty("jexer.Swing.mouseImage") != null) {
157 component.setCursor(getMouseImage());
158 } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
159 component.setCursor(getMouseCursor());
160 } else if (System.getProperty("jexer.textMouse",
161 "true").equals("false")
162 ) {
163 // If the user has suppressed the text mouse, don't kill the X11
164 // mouse.
165 component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
166 } else {
167 // Kill the X11 cursor
168 // Transparent 16 x 16 pixel cursor image.
169 BufferedImage cursorImg = new BufferedImage(16, 16,
170 BufferedImage.TYPE_INT_ARGB);
171 // Create a new blank cursor.
172 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
173 cursorImg, new Point(0, 0), "blank cursor");
174 component.setCursor(blankCursor);
175 }
176
177 // Be capable of seeing Tab / Shift-Tab
178 component.setFocusTraversalKeysEnabled(false);
179 }
180
181 /**
182 * Setup to render to an existing JFrame.
183 */
184 public void setupFrame() {
185 frame.setTitle("Jexer Application");
186 frame.setBackground(Color.black);
187 frame.pack();
188
189 if (System.getProperty("jexer.Swing.mouseImage") != null) {
190 frame.setCursor(getMouseImage());
191 } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
192 frame.setCursor(getMouseCursor());
193 } else if (System.getProperty("jexer.textMouse",
194 "true").equals("false")
195 ) {
196 // If the user has suppressed the text mouse, don't kill the X11
197 // mouse.
198 frame.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
199 } else {
200 // Kill the X11 cursor
201 // Transparent 16 x 16 pixel cursor image.
202 BufferedImage cursorImg = new BufferedImage(16, 16,
203 BufferedImage.TYPE_INT_ARGB);
204 // Create a new blank cursor.
205 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
206 cursorImg, new Point(0, 0), "blank cursor");
207 frame.setCursor(blankCursor);
208 }
209
210 // Be capable of seeing Tab / Shift-Tab
211 frame.setFocusTraversalKeysEnabled(false);
212
213 // Setup triple-buffering
214 if (tripleBuffer) {
215 frame.setIgnoreRepaint(true);
216 frame.createBufferStrategy(3);
217 }
218 }
219
220 /**
221 * Load an image named in jexer.Swing.mouseImage as the mouse cursor.
222 * The image must be on the classpath.
223 *
224 * @return the cursor
225 */
226 private Cursor getMouseImage() {
227 Cursor cursor = Cursor.getDefaultCursor();
228 String filename = System.getProperty("jexer.Swing.mouseImage");
229 assert (filename != null);
230
231 try {
232 ClassLoader loader = Thread.currentThread().
233 getContextClassLoader();
234
235 java.net.URL url = loader.getResource(filename);
236 if (url == null) {
237 // User named a file, but it's not on the classpath. Bail
238 // out.
239 return cursor;
240 }
241
242 BufferedImage cursorImage = ImageIO.read(url);
243 java.awt.Dimension cursorSize = Toolkit.getDefaultToolkit().
244 getBestCursorSize(
245 cursorImage.getWidth(), cursorImage.getHeight());
246
247 cursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage,
248 new Point((int) Math.min(cursorImage.getWidth() / 2,
249 cursorSize.getWidth() - 1),
250 (int) Math.min(cursorImage.getHeight() / 2,
251 cursorSize.getHeight() - 1)),
252 "custom cursor");
253 } catch (IOException e) {
254 e.printStackTrace();
255 }
256
257 return cursor;
258 }
259
260 /**
261 * Get the appropriate mouse cursor based on jexer.Swing.mouseStyle.
262 *
263 * @return the cursor
264 */
265 private Cursor getMouseCursor() {
266 Cursor cursor = Cursor.getDefaultCursor();
267 String style = System.getProperty("jexer.Swing.mouseStyle");
268 assert (style != null);
269
270 style = style.toLowerCase();
271
272 if (style.equals("none")) {
273 // Transparent 16 x 16 pixel cursor image.
274 BufferedImage cursorImg = new BufferedImage(16, 16,
275 BufferedImage.TYPE_INT_ARGB);
276 // Create a new blank cursor.
277 cursor = Toolkit.getDefaultToolkit().createCustomCursor(
278 cursorImg, new Point(0, 0), "blank cursor");
279 } else if (style.equals("default")) {
280 cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
281 } else if (style.equals("hand")) {
282 cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
283 } else if (style.equals("text")) {
284 cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
285 } else if (style.equals("move")) {
286 cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
287 } else if (style.equals("crosshair")) {
288 cursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
289 }
290
291 return cursor;
292 }
293
294 /**
295 * Set the window title.
296 *
297 * @param title the new title
298 */
299 public void setTitle(final String title) {
300 if (frame != null) {
301 frame.setTitle(title);
302 }
303 }
304
305 /**
306 * Paints this component.
307 *
308 * @param g the graphics context to use for painting
309 */
310 public void paint(Graphics g) {
311 if (frame != null) {
312 frame.paint(g);
313 } else {
314 component.paint(g);
315 }
316 }
317
318 /**
319 * Repaints this component.
320 */
321 public void repaint() {
322 if (frame != null) {
323 frame.repaint();
324 } else {
325 component.repaint();
326 }
327 }
328
329 /**
330 * Repaints the specified rectangle of this component.
331 *
332 * @param x the x coordinate
333 * @param y the y coordinate
334 * @param width the width
335 * @param height the height
336 */
337 public void repaint(int x, int y, int width, int height) {
338 if (frame != null) {
339 frame.repaint(x, y, width, height);
340 } else {
341 component.repaint(x, y, width, height);
342 }
343 }
344
345 /**
346 * If a border has been set on this component, returns the border's
347 * insets; otherwise calls super.getInsets.
348 *
349 * @return the value of the insets property
350 */
351 public Insets getInsets() {
352 Insets swingInsets = null;
353 if (frame != null) {
354 swingInsets = frame.getInsets();
355 } else {
356 swingInsets = component.getInsets();
357 }
358 Insets result = new Insets(swingInsets.top + adjustInsets.top,
359 swingInsets.left + adjustInsets.left,
360 swingInsets.bottom + adjustInsets.bottom,
361 swingInsets.right + adjustInsets.right);
362 return result;
363 }
364
365 /**
366 * Returns the current width of this component.
367 *
368 * @return the current width of this component
369 */
370 public int getWidth() {
371 if (frame != null) {
372 return frame.getWidth();
373 } else {
374 return component.getWidth();
375 }
376 }
377
378 /**
379 * Returns the current height of this component.
380 *
381 * @return the current height of this component
382 */
383 public int getHeight() {
384 if (frame != null) {
385 return frame.getHeight();
386 } else {
387 return component.getHeight();
388 }
389 }
390
391 /**
392 * Gets the font of this component.
393 *
394 * @return this component's font; if a font has not been set for this
395 * component, the font of its parent is returned
396 */
397 public Font getFont() {
398 if (frame != null) {
399 return frame.getFont();
400 } else {
401 return component.getFont();
402 }
403 }
404
405 /**
406 * Sets the font of this component.
407 *
408 * @param f the font to become this component's font; if this parameter
409 * is null then this component will inherit the font of its parent
410 */
411 public void setFont(final Font f) {
412 if (frame != null) {
413 frame.setFont(f);
414 } else {
415 component.setFont(f);
416 }
417 }
418
419 /**
420 * Shows or hides this Window depending on the value of parameter b.
421 *
422 * @param b if true, make visible, else make invisible
423 */
424 public void setVisible(final boolean b) {
425 if (frame != null) {
426 frame.setVisible(b);
427 } else {
428 component.setVisible(b);
429 }
430 }
431
432 /**
433 * Creates a graphics context for this component. This method will return
434 * null if this component is currently not displayable.
435 *
436 * @return a graphics context for this component, or null if it has none
437 */
438 public Graphics getGraphics() {
439 if (frame != null) {
440 return frame.getGraphics();
441 } else {
442 return component.getGraphics();
443 }
444 }
445
446 /**
447 * Releases all of the native screen resources used by this Window, its
448 * subcomponents, and all of its owned children. That is, the resources
449 * for these Components will be destroyed, any memory they consume will
450 * be returned to the OS, and they will be marked as undisplayable.
451 */
452 public void dispose() {
453 if (frame != null) {
454 frame.dispose();
455 } else {
456 component.getParent().remove(component);
457 }
458 }
459
460 /**
461 * Resize the component to match the font dimensions.
462 *
463 * @param width the new width in pixels
464 * @param height the new height in pixels
465 */
466 public void setDimensions(final int width, final int height) {
467 if (SwingUtilities.isEventDispatchThread()) {
468 // We are in the Swing thread and can safely set the size.
469
470 // Figure out the thickness of borders and use that to set the
471 // final size.
472 if (frame != null) {
473 Insets insets = getInsets();
474 frame.setSize(width + insets.left + insets.right,
475 height + insets.top + insets.bottom);
476 } else {
477 Insets insets = getInsets();
478 component.setSize(width + insets.left + insets.right,
479 height + insets.top + insets.bottom);
480 }
481 return;
482 }
483
484 SwingUtilities.invokeLater(new Runnable() {
485 public void run() {
486 // Figure out the thickness of borders and use that to set
487 // the final size.
488 if (frame != null) {
489 Insets insets = getInsets();
490 frame.setSize(width + insets.left + insets.right,
491 height + insets.top + insets.bottom);
492 } else {
493 Insets insets = getInsets();
494 component.setSize(width + insets.left + insets.right,
495 height + insets.top + insets.bottom);
496 }
497 }
498 });
499 }
500
501 /**
502 * Adds the specified component listener to receive component events from
503 * this component. If listener l is null, no exception is thrown and no
504 * action is performed.
505 *
506 * @param l the component listener
507 */
508 public void addComponentListener(ComponentListener l) {
509 if (frame != null) {
510 frame.addComponentListener(l);
511 } else {
512 component.addComponentListener(l);
513 }
514 }
515
516 /**
517 * Adds the specified key listener to receive key events from this
518 * component. If l is null, no exception is thrown and no action is
519 * performed.
520 *
521 * @param l the key listener.
522 */
523 public void addKeyListener(KeyListener l) {
524 if (frame != null) {
525 frame.addKeyListener(l);
526 } else {
527 component.addKeyListener(l);
528 }
529 }
530
531 /**
532 * Adds the specified mouse listener to receive mouse events from this
533 * component. If listener l is null, no exception is thrown and no action
534 * is performed.
535 *
536 * @param l the mouse listener
537 */
538 public void addMouseListener(MouseListener l) {
539 if (frame != null) {
540 frame.addMouseListener(l);
541 } else {
542 component.addMouseListener(l);
543 }
544 }
545
546 /**
547 * Adds the specified mouse motion listener to receive mouse motion
548 * events from this component. If listener l is null, no exception is
549 * thrown and no action is performed.
550 *
551 * @param l the mouse motion listener
552 */
553 public void addMouseMotionListener(MouseMotionListener l) {
554 if (frame != null) {
555 frame.addMouseMotionListener(l);
556 } else {
557 component.addMouseMotionListener(l);
558 }
559 }
560
561 /**
562 * Adds the specified mouse wheel listener to receive mouse wheel events
563 * from this component. Containers also receive mouse wheel events from
564 * sub-components.
565 *
566 * @param l the mouse wheel listener
567 */
568 public void addMouseWheelListener(MouseWheelListener l) {
569 if (frame != null) {
570 frame.addMouseWheelListener(l);
571 } else {
572 component.addMouseWheelListener(l);
573 }
574 }
575
576 /**
577 * Adds the specified window listener to receive window events from this
578 * window. If l is null, no exception is thrown and no action is
579 * performed.
580 *
581 * @param l the window listener
582 */
583 public void addWindowListener(WindowListener l) {
584 if (frame != null) {
585 frame.addWindowListener(l);
586 }
587 }
588
589 /**
590 * Requests that this Component get the input focus, if this Component's
591 * top-level ancestor is already the focused Window.
592 */
593 public void requestFocusInWindow() {
594 if (frame != null) {
595 frame.requestFocusInWindow();
596 } else {
597 component.requestFocusInWindow();
598 }
599 }
600
601 }