#38 fix Swing deadlock
[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 javax.swing.JComponent;
47 import javax.swing.JFrame;
48 import javax.swing.SwingUtilities;
49
50 /**
51 * Wrapper for integrating with Swing, because JFrame and JComponent have
52 * separate hierarchies.
53 */
54 class SwingComponent {
55
56 // ------------------------------------------------------------------------
57 // Variables --------------------------------------------------------------
58 // ------------------------------------------------------------------------
59
60 /**
61 * If true, use triple buffering when drawing to a JFrame.
62 */
63 public static boolean tripleBuffer = true;
64
65 /**
66 * The frame reference, if we are drawing to a JFrame.
67 */
68 private JFrame frame;
69
70 /**
71 * The component reference, if we are drawing to a JComponent.
72 */
73 private JComponent component;
74
75 /**
76 * An optional border in pixels to add.
77 */
78 private static final int BORDER = 1;
79
80 /**
81 * Adjustable Insets for this component. This has the effect of adding a
82 * black border around the drawing area.
83 */
84 Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
85
86 // ------------------------------------------------------------------------
87 // Constructors -----------------------------------------------------------
88 // ------------------------------------------------------------------------
89
90 /**
91 * Construct using a JFrame.
92 *
93 * @param frame the JFrame to draw to
94 */
95 public SwingComponent(final JFrame frame) {
96 this.frame = frame;
97 setupFrame();
98 }
99
100 /**
101 * Construct using a JComponent.
102 *
103 * @param component the JComponent to draw to
104 */
105 public SwingComponent(final JComponent component) {
106 this.component = component;
107 setupComponent();
108 }
109
110 // ------------------------------------------------------------------------
111 // SwingComponent ---------------------------------------------------------
112 // ------------------------------------------------------------------------
113
114 /**
115 * Get the BufferStrategy object needed for triple-buffering.
116 *
117 * @return the BufferStrategy
118 * @throws IllegalArgumentException if this function is called when
119 * not rendering to a JFrame
120 */
121 public BufferStrategy getBufferStrategy() {
122 if (frame != null) {
123 return frame.getBufferStrategy();
124 } else {
125 throw new IllegalArgumentException("BufferStrategy not used " +
126 "for JComponent access");
127 }
128 }
129
130 /**
131 * Get the JFrame reference.
132 *
133 * @return the frame, or null if this is drawing to a JComponent
134 */
135 public JFrame getFrame() {
136 return frame;
137 }
138
139 /**
140 * Get the JComponent reference.
141 *
142 * @return the component, or null if this is drawing to a JFrame
143 */
144 public JComponent getComponent() {
145 return component;
146 }
147
148 /**
149 * Setup to render to an existing JComponent.
150 */
151 public void setupComponent() {
152 component.setBackground(Color.black);
153
154 // Kill the X11 cursor
155 // Transparent 16 x 16 pixel cursor image.
156 BufferedImage cursorImg = new BufferedImage(16, 16,
157 BufferedImage.TYPE_INT_ARGB);
158 // Create a new blank cursor.
159 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
160 cursorImg, new Point(0, 0), "blank cursor");
161 component.setCursor(blankCursor);
162
163 // Be capable of seeing Tab / Shift-Tab
164 component.setFocusTraversalKeysEnabled(false);
165 }
166
167 /**
168 * Setup to render to an existing JFrame.
169 */
170 public void setupFrame() {
171 frame.setTitle("Jexer Application");
172 frame.setBackground(Color.black);
173 frame.pack();
174
175 // Kill the X11 cursor
176 // Transparent 16 x 16 pixel cursor image.
177 BufferedImage cursorImg = new BufferedImage(16, 16,
178 BufferedImage.TYPE_INT_ARGB);
179 // Create a new blank cursor.
180 Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
181 cursorImg, new Point(0, 0), "blank cursor");
182 frame.setCursor(blankCursor);
183
184 // Be capable of seeing Tab / Shift-Tab
185 frame.setFocusTraversalKeysEnabled(false);
186
187 // Setup triple-buffering
188 if (tripleBuffer) {
189 frame.setIgnoreRepaint(true);
190 frame.createBufferStrategy(3);
191 }
192 }
193
194 /**
195 * Set the window title.
196 *
197 * @param title the new title
198 */
199 public void setTitle(final String title) {
200 if (frame != null) {
201 frame.setTitle(title);
202 }
203 }
204
205 /**
206 * Paints this component.
207 *
208 * @param g the graphics context to use for painting
209 */
210 public void paint(Graphics g) {
211 if (frame != null) {
212 frame.paint(g);
213 } else {
214 component.paint(g);
215 }
216 }
217
218 /**
219 * Repaints this component.
220 */
221 public void repaint() {
222 if (frame != null) {
223 frame.repaint();
224 } else {
225 component.repaint();
226 }
227 }
228
229 /**
230 * Repaints the specified rectangle of this component.
231 *
232 * @param x the x coordinate
233 * @param y the y coordinate
234 * @param width the width
235 * @param height the height
236 */
237 public void repaint(int x, int y, int width, int height) {
238 if (frame != null) {
239 frame.repaint(x, y, width, height);
240 } else {
241 component.repaint(x, y, width, height);
242 }
243 }
244
245 /**
246 * If a border has been set on this component, returns the border's
247 * insets; otherwise calls super.getInsets.
248 *
249 * @return the value of the insets property
250 */
251 public Insets getInsets() {
252 Insets swingInsets = null;
253 if (frame != null) {
254 swingInsets = frame.getInsets();
255 } else {
256 swingInsets = component.getInsets();
257 }
258 Insets result = new Insets(swingInsets.top + adjustInsets.top,
259 swingInsets.left + adjustInsets.left,
260 swingInsets.bottom + adjustInsets.bottom,
261 swingInsets.right + adjustInsets.right);
262 return result;
263 }
264
265 /**
266 * Returns the current width of this component.
267 *
268 * @return the current width of this component
269 */
270 public int getWidth() {
271 if (frame != null) {
272 return frame.getWidth();
273 } else {
274 return component.getWidth();
275 }
276 }
277
278 /**
279 * Returns the current height of this component.
280 *
281 * @return the current height of this component
282 */
283 public int getHeight() {
284 if (frame != null) {
285 return frame.getHeight();
286 } else {
287 return component.getHeight();
288 }
289 }
290
291 /**
292 * Gets the font of this component.
293 *
294 * @return this component's font; if a font has not been set for this
295 * component, the font of its parent is returned
296 */
297 public Font getFont() {
298 if (frame != null) {
299 return frame.getFont();
300 } else {
301 return component.getFont();
302 }
303 }
304
305 /**
306 * Sets the font of this component.
307 *
308 * @param f the font to become this component's font; if this parameter
309 * is null then this component will inherit the font of its parent
310 */
311 public void setFont(final Font f) {
312 if (frame != null) {
313 frame.setFont(f);
314 } else {
315 component.setFont(f);
316 }
317 }
318
319 /**
320 * Shows or hides this Window depending on the value of parameter b.
321 *
322 * @param b if true, make visible, else make invisible
323 */
324 public void setVisible(final boolean b) {
325 if (frame != null) {
326 frame.setVisible(b);
327 } else {
328 component.setVisible(b);
329 }
330 }
331
332 /**
333 * Creates a graphics context for this component. This method will return
334 * null if this component is currently not displayable.
335 *
336 * @return a graphics context for this component, or null if it has none
337 */
338 public Graphics getGraphics() {
339 if (frame != null) {
340 return frame.getGraphics();
341 } else {
342 return component.getGraphics();
343 }
344 }
345
346 /**
347 * Releases all of the native screen resources used by this Window, its
348 * subcomponents, and all of its owned children. That is, the resources
349 * for these Components will be destroyed, any memory they consume will
350 * be returned to the OS, and they will be marked as undisplayable.
351 */
352 public void dispose() {
353 if (frame != null) {
354 frame.dispose();
355 } else {
356 component.getParent().remove(component);
357 }
358 }
359
360 /**
361 * Resize the component to match the font dimensions.
362 *
363 * @param width the new width in pixels
364 * @param height the new height in pixels
365 */
366 public void setDimensions(final int width, final int height) {
367 if (SwingUtilities.isEventDispatchThread()) {
368 // We are in the Swing thread and can safely set the size.
369
370 // Figure out the thickness of borders and use that to set the
371 // final size.
372 if (frame != null) {
373 Insets insets = getInsets();
374 frame.setSize(width + insets.left + insets.right,
375 height + insets.top + insets.bottom);
376 } else {
377 Insets insets = getInsets();
378 component.setSize(width + insets.left + insets.right,
379 height + insets.top + insets.bottom);
380 }
381 return;
382 }
383
384 SwingUtilities.invokeLater(new Runnable() {
385 public void run() {
386 // Figure out the thickness of borders and use that to set
387 // the final size.
388 if (frame != null) {
389 Insets insets = getInsets();
390 frame.setSize(width + insets.left + insets.right,
391 height + insets.top + insets.bottom);
392 } else {
393 Insets insets = getInsets();
394 component.setSize(width + insets.left + insets.right,
395 height + insets.top + insets.bottom);
396 }
397 }
398 });
399 }
400
401 /**
402 * Adds the specified component listener to receive component events from
403 * this component. If listener l is null, no exception is thrown and no
404 * action is performed.
405 *
406 * @param l the component listener
407 */
408 public void addComponentListener(ComponentListener l) {
409 if (frame != null) {
410 frame.addComponentListener(l);
411 } else {
412 component.addComponentListener(l);
413 }
414 }
415
416 /**
417 * Adds the specified key listener to receive key events from this
418 * component. If l is null, no exception is thrown and no action is
419 * performed.
420 *
421 * @param l the key listener.
422 */
423 public void addKeyListener(KeyListener l) {
424 if (frame != null) {
425 frame.addKeyListener(l);
426 } else {
427 component.addKeyListener(l);
428 }
429 }
430
431 /**
432 * Adds the specified mouse listener to receive mouse events from this
433 * component. If listener l is null, no exception is thrown and no action
434 * is performed.
435 *
436 * @param l the mouse listener
437 */
438 public void addMouseListener(MouseListener l) {
439 if (frame != null) {
440 frame.addMouseListener(l);
441 } else {
442 component.addMouseListener(l);
443 }
444 }
445
446 /**
447 * Adds the specified mouse motion listener to receive mouse motion
448 * events from this component. If listener l is null, no exception is
449 * thrown and no action is performed.
450 *
451 * @param l the mouse motion listener
452 */
453 public void addMouseMotionListener(MouseMotionListener l) {
454 if (frame != null) {
455 frame.addMouseMotionListener(l);
456 } else {
457 component.addMouseMotionListener(l);
458 }
459 }
460
461 /**
462 * Adds the specified mouse wheel listener to receive mouse wheel events
463 * from this component. Containers also receive mouse wheel events from
464 * sub-components.
465 *
466 * @param l the mouse wheel listener
467 */
468 public void addMouseWheelListener(MouseWheelListener l) {
469 if (frame != null) {
470 frame.addMouseWheelListener(l);
471 } else {
472 component.addMouseWheelListener(l);
473 }
474 }
475
476 /**
477 * Adds the specified window listener to receive window events from this
478 * window. If l is null, no exception is thrown and no action is
479 * performed.
480 *
481 * @param l the window listener
482 */
483 public void addWindowListener(WindowListener l) {
484 if (frame != null) {
485 frame.addWindowListener(l);
486 }
487 }
488
489 }