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