--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.backend;
+
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.ComponentListener;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelListener;
+import java.awt.event.WindowListener;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferStrategy;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+/**
+ * Wrapper for integrating with Swing, because JFrame and JComponent have
+ * separate hierarchies.
+ */
+class SwingComponent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, use triple buffering when drawing to a JFrame.
+ */
+ public static boolean tripleBuffer = true;
+
+ /**
+ * The frame reference, if we are drawing to a JFrame.
+ */
+ private JFrame frame;
+
+ /**
+ * The component reference, if we are drawing to a JComponent.
+ */
+ private JComponent component;
+
+ /**
+ * An optional border in pixels to add.
+ */
+ private static final int BORDER = 1;
+
+ /**
+ * Adjustable Insets for this component. This has the effect of adding a
+ * black border around the drawing area.
+ */
+ Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct using a JFrame.
+ *
+ * @param frame the JFrame to draw to
+ */
+ public SwingComponent(final JFrame frame) {
+ this.frame = frame;
+ setupFrame();
+ }
+
+ /**
+ * Construct using a JComponent.
+ *
+ * @param component the JComponent to draw to
+ */
+ public SwingComponent(final JComponent component) {
+ this.component = component;
+ setupComponent();
+ }
+
+ // ------------------------------------------------------------------------
+ // SwingComponent ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the BufferStrategy object needed for triple-buffering.
+ *
+ * @return the BufferStrategy
+ * @throws IllegalArgumentException if this function is called when
+ * not rendering to a JFrame
+ */
+ public BufferStrategy getBufferStrategy() {
+ if (frame != null) {
+ return frame.getBufferStrategy();
+ } else {
+ throw new IllegalArgumentException("BufferStrategy not used " +
+ "for JComponent access");
+ }
+ }
+
+ /**
+ * Get the JFrame reference.
+ *
+ * @return the frame, or null if this is drawing to a JComponent
+ */
+ public JFrame getFrame() {
+ return frame;
+ }
+
+ /**
+ * Get the JComponent reference.
+ *
+ * @return the component, or null if this is drawing to a JFrame
+ */
+ public JComponent getComponent() {
+ return component;
+ }
+
+ /**
+ * Setup to render to an existing JComponent.
+ */
+ public void setupComponent() {
+ component.setBackground(Color.black);
+
+ if (System.getProperty("jexer.Swing.mouseImage") != null) {
+ component.setCursor(getMouseImage());
+ } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
+ component.setCursor(getMouseCursor());
+ } else if (System.getProperty("jexer.textMouse",
+ "true").equals("false")
+ ) {
+ // If the user has suppressed the text mouse, don't kill the X11
+ // mouse.
+ component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ } else {
+ // Kill the X11 cursor
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ component.setCursor(blankCursor);
+ }
+
+ // Be capable of seeing Tab / Shift-Tab
+ component.setFocusTraversalKeysEnabled(false);
+ }
+
+ /**
+ * Setup to render to an existing JFrame.
+ */
+ public void setupFrame() {
+ frame.setTitle("Jexer Application");
+ frame.setBackground(Color.black);
+ frame.pack();
+
+ if (System.getProperty("jexer.Swing.mouseImage") != null) {
+ frame.setCursor(getMouseImage());
+ } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
+ frame.setCursor(getMouseCursor());
+ } else if (System.getProperty("jexer.textMouse",
+ "true").equals("false")
+ ) {
+ // If the user has suppressed the text mouse, don't kill the X11
+ // mouse.
+ frame.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ } else {
+ // Kill the X11 cursor
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ frame.setCursor(blankCursor);
+ }
+
+ // Be capable of seeing Tab / Shift-Tab
+ frame.setFocusTraversalKeysEnabled(false);
+
+ // Setup triple-buffering
+ if (tripleBuffer) {
+ frame.setIgnoreRepaint(true);
+ frame.createBufferStrategy(3);
+ }
+ }
+
+ /**
+ * Load an image named in jexer.Swing.mouseImage as the mouse cursor.
+ * The image must be on the classpath.
+ *
+ * @return the cursor
+ */
+ private Cursor getMouseImage() {
+ Cursor cursor = Cursor.getDefaultCursor();
+ String filename = System.getProperty("jexer.Swing.mouseImage");
+ assert (filename != null);
+
+ try {
+ ClassLoader loader = Thread.currentThread().
+ getContextClassLoader();
+
+ java.net.URL url = loader.getResource(filename);
+ if (url == null) {
+ // User named a file, but it's not on the classpath. Bail
+ // out.
+ return cursor;
+ }
+
+ BufferedImage cursorImage = ImageIO.read(url);
+ java.awt.Dimension cursorSize = Toolkit.getDefaultToolkit().
+ getBestCursorSize(
+ cursorImage.getWidth(), cursorImage.getHeight());
+
+ cursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage,
+ new Point((int) Math.min(cursorImage.getWidth() / 2,
+ cursorSize.getWidth() - 1),
+ (int) Math.min(cursorImage.getHeight() / 2,
+ cursorSize.getHeight() - 1)),
+ "custom cursor");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Get the appropriate mouse cursor based on jexer.Swing.mouseStyle.
+ *
+ * @return the cursor
+ */
+ private Cursor getMouseCursor() {
+ Cursor cursor = Cursor.getDefaultCursor();
+ String style = System.getProperty("jexer.Swing.mouseStyle");
+ assert (style != null);
+
+ style = style.toLowerCase();
+
+ if (style.equals("none")) {
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ cursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ } else if (style.equals("default")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
+ } else if (style.equals("hand")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
+ } else if (style.equals("text")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
+ } else if (style.equals("move")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
+ } else if (style.equals("crosshair")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ if (frame != null) {
+ frame.setTitle(title);
+ }
+ }
+
+ /**
+ * Paints this component.
+ *
+ * @param g the graphics context to use for painting
+ */
+ public void paint(Graphics g) {
+ if (frame != null) {
+ frame.paint(g);
+ } else {
+ component.paint(g);
+ }
+ }
+
+ /**
+ * Repaints this component.
+ */
+ public void repaint() {
+ if (frame != null) {
+ frame.repaint();
+ } else {
+ component.repaint();
+ }
+ }
+
+ /**
+ * Repaints the specified rectangle of this component.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param width the width
+ * @param height the height
+ */
+ public void repaint(int x, int y, int width, int height) {
+ if (frame != null) {
+ frame.repaint(x, y, width, height);
+ } else {
+ component.repaint(x, y, width, height);
+ }
+ }
+
+ /**
+ * If a border has been set on this component, returns the border's
+ * insets; otherwise calls super.getInsets.
+ *
+ * @return the value of the insets property
+ */
+ public Insets getInsets() {
+ Insets swingInsets = null;
+ if (frame != null) {
+ swingInsets = frame.getInsets();
+ } else {
+ swingInsets = component.getInsets();
+ }
+ Insets result = new Insets(swingInsets.top + adjustInsets.top,
+ swingInsets.left + adjustInsets.left,
+ swingInsets.bottom + adjustInsets.bottom,
+ swingInsets.right + adjustInsets.right);
+ return result;
+ }
+
+ /**
+ * Returns the current width of this component.
+ *
+ * @return the current width of this component
+ */
+ public int getWidth() {
+ if (frame != null) {
+ return frame.getWidth();
+ } else {
+ return component.getWidth();
+ }
+ }
+
+ /**
+ * Returns the current height of this component.
+ *
+ * @return the current height of this component
+ */
+ public int getHeight() {
+ if (frame != null) {
+ return frame.getHeight();
+ } else {
+ return component.getHeight();
+ }
+ }
+
+ /**
+ * Gets the font of this component.
+ *
+ * @return this component's font; if a font has not been set for this
+ * component, the font of its parent is returned
+ */
+ public Font getFont() {
+ if (frame != null) {
+ return frame.getFont();
+ } else {
+ return component.getFont();
+ }
+ }
+
+ /**
+ * Sets the font of this component.
+ *
+ * @param f the font to become this component's font; if this parameter
+ * is null then this component will inherit the font of its parent
+ */
+ public void setFont(final Font f) {
+ if (frame != null) {
+ frame.setFont(f);
+ } else {
+ component.setFont(f);
+ }
+ }
+
+ /**
+ * Shows or hides this Window depending on the value of parameter b.
+ *
+ * @param b if true, make visible, else make invisible
+ */
+ public void setVisible(final boolean b) {
+ if (frame != null) {
+ frame.setVisible(b);
+ } else {
+ component.setVisible(b);
+ }
+ }
+
+ /**
+ * Creates a graphics context for this component. This method will return
+ * null if this component is currently not displayable.
+ *
+ * @return a graphics context for this component, or null if it has none
+ */
+ public Graphics getGraphics() {
+ if (frame != null) {
+ return frame.getGraphics();
+ } else {
+ return component.getGraphics();
+ }
+ }
+
+ /**
+ * Releases all of the native screen resources used by this Window, its
+ * subcomponents, and all of its owned children. That is, the resources
+ * for these Components will be destroyed, any memory they consume will
+ * be returned to the OS, and they will be marked as undisplayable.
+ */
+ public void dispose() {
+ if (frame != null) {
+ frame.dispose();
+ } else {
+ component.getParent().remove(component);
+ }
+ }
+
+ /**
+ * Resize the component to match the font dimensions.
+ *
+ * @param width the new width in pixels
+ * @param height the new height in pixels
+ */
+ public void setDimensions(final int width, final int height) {
+ if (SwingUtilities.isEventDispatchThread()) {
+ // We are in the Swing thread and can safely set the size.
+
+ // Figure out the thickness of borders and use that to set the
+ // final size.
+ if (frame != null) {
+ Insets insets = getInsets();
+ frame.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ } else {
+ Insets insets = getInsets();
+ component.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ }
+ return;
+ }
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ // Figure out the thickness of borders and use that to set
+ // the final size.
+ if (frame != null) {
+ Insets insets = getInsets();
+ frame.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ } else {
+ Insets insets = getInsets();
+ component.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ }
+ }
+ });
+ }
+
+ /**
+ * Adds the specified component listener to receive component events from
+ * this component. If listener l is null, no exception is thrown and no
+ * action is performed.
+ *
+ * @param l the component listener
+ */
+ public void addComponentListener(ComponentListener l) {
+ if (frame != null) {
+ frame.addComponentListener(l);
+ } else {
+ component.addComponentListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified key listener to receive key events from this
+ * component. If l is null, no exception is thrown and no action is
+ * performed.
+ *
+ * @param l the key listener.
+ */
+ public void addKeyListener(KeyListener l) {
+ if (frame != null) {
+ frame.addKeyListener(l);
+ } else {
+ component.addKeyListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse listener to receive mouse events from this
+ * component. If listener l is null, no exception is thrown and no action
+ * is performed.
+ *
+ * @param l the mouse listener
+ */
+ public void addMouseListener(MouseListener l) {
+ if (frame != null) {
+ frame.addMouseListener(l);
+ } else {
+ component.addMouseListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse motion listener to receive mouse motion
+ * events from this component. If listener l is null, no exception is
+ * thrown and no action is performed.
+ *
+ * @param l the mouse motion listener
+ */
+ public void addMouseMotionListener(MouseMotionListener l) {
+ if (frame != null) {
+ frame.addMouseMotionListener(l);
+ } else {
+ component.addMouseMotionListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse wheel listener to receive mouse wheel events
+ * from this component. Containers also receive mouse wheel events from
+ * sub-components.
+ *
+ * @param l the mouse wheel listener
+ */
+ public void addMouseWheelListener(MouseWheelListener l) {
+ if (frame != null) {
+ frame.addMouseWheelListener(l);
+ } else {
+ component.addMouseWheelListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified window listener to receive window events from this
+ * window. If l is null, no exception is thrown and no action is
+ * performed.
+ *
+ * @param l the window listener
+ */
+ public void addWindowListener(WindowListener l) {
+ if (frame != null) {
+ frame.addWindowListener(l);
+ }
+ }
+
+ /**
+ * Requests that this Component get the input focus, if this Component's
+ * top-level ancestor is already the focused Window.
+ */
+ public void requestFocusInWindow() {
+ if (frame != null) {
+ frame.requestFocusInWindow();
+ } else {
+ component.requestFocusInWindow();
+ }
+ }
+
+}