From 54eaded07d2c1c37d9e1000abdcc97be09955867 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Fri, 1 Nov 2019 10:48:55 -0500 Subject: [PATCH] Copy screen text to clipboard --- src/jexer/TApplication.java | 70 +++++++++ src/jexer/backend/LogicalScreen.java | 119 +++++++++++++++ src/jexer/backend/MultiScreen.java | 93 ++++++++++-- src/jexer/backend/Screen.java | 27 ++++ src/jexer/bits/CellAttributes.java | 1 - src/jexer/bits/Clipboard.java | 219 +++++++++++++++++++++++++++ src/jexer/bits/StringUtils.java | 5 + 7 files changed, 522 insertions(+), 12 deletions(-) create mode 100644 src/jexer/bits/Clipboard.java diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index fc833ea6..a38b2dab 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -47,6 +47,7 @@ import java.util.ResourceBundle; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; import jexer.bits.ColorTheme; import jexer.bits.StringUtils; import jexer.event.TCommandEvent; @@ -148,6 +149,11 @@ public class TApplication implements Runnable { */ private Backend backend; + /** + * The clipboard for copy and paste. + */ + private Clipboard clipboard = new Clipboard(); + /** * Actual mouse coordinate X. */ @@ -325,6 +331,36 @@ public class TApplication implements Runnable { */ private long screenResizeTime = 0; + /** + * If true, screen selection is a rectangle. + */ + private boolean screenSelectionRectangle = false; + + /** + * If true, the mouse is dragging a screen selection. + */ + private boolean inScreenSelection = false; + + /** + * Screen selection starting X. + */ + private int screenSelectionX0; + + /** + * Screen selection starting Y. + */ + private int screenSelectionY0; + + /** + * Screen selection ending X. + */ + private int screenSelectionX1; + + /** + * Screen selection ending Y. + */ + private int screenSelectionY1; + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -1157,6 +1193,28 @@ public class TApplication implements Runnable { typingHidMouse = false; TMouseEvent mouse = (TMouseEvent) event; + if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) { + // Screen selection. + if (inScreenSelection) { + screenSelectionX1 = mouse.getX(); + screenSelectionY1 = mouse.getY(); + } else { + inScreenSelection = true; + screenSelectionX0 = mouse.getX(); + screenSelectionY0 = mouse.getY(); + screenSelectionX1 = mouse.getX(); + screenSelectionY1 = mouse.getY(); + screenSelectionRectangle = mouse.isCtrl(); + } + } else { + if (inScreenSelection) { + getScreen().copySelection(clipboard, screenSelectionX0, + screenSelectionY0, screenSelectionX1, screenSelectionY1, + screenSelectionRectangle); + } + inScreenSelection = false; + } + if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) { oldMouseX = mouseX; oldMouseY = mouseY; @@ -1829,6 +1887,12 @@ public class TApplication implements Runnable { } } + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, + screenSelectionY0, screenSelectionX1, screenSelectionY1, + screenSelectionRectangle); + } + if ((textMouse == true) && (typingHidMouse == false)) { // Draw mouse at the new position. drawTextMouse(mouseX, mouseY); @@ -1966,6 +2030,12 @@ public class TApplication implements Runnable { getScreen().unsetImageRow(mouseY); } } + + if (inScreenSelection) { + getScreen().setSelection(screenSelectionX0, screenSelectionY0, + screenSelectionX1, screenSelectionY1, screenSelectionRectangle); + } + if ((textMouse == true) && (typingHidMouse == false)) { drawTextMouse(mouseX, mouseY); } diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index cc410ee9..46776fb2 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -33,6 +33,7 @@ import java.awt.image.BufferedImage; import jexer.backend.GlyphMaker; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; @@ -1104,4 +1105,122 @@ public class LogicalScreen implements Screen { } } + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle) { + + int startX = x0; + int startY = y0; + int endX = x1; + int endY = y1; + + if (((x1 < x0) && (y1 <= y0)) + || ((x1 <= x0) && (y1 < y0)) + ) { + // The user dragged from bottom-right to top-left. Reverse the + // coordinates for the inverted section. + startX = x1; + startY = y1; + endX = x0; + endY = y0; + } + if (rectangle) { + for (int y = startY; y <= endY; y++) { + for (int x = startX; x <= endX; x++) { + invertCell(x, y); + } + } + } else { + if (endY > startY) { + for (int x = startX; x < width; x++) { + invertCell(x, startY); + } + for (int y = startY + 1; y < endY; y++) { + for (int x = 0; x < width; x++) { + invertCell(x, y); + } + } + for (int x = 0; x <= endX; x++) { + invertCell(x, endY); + } + } else { + assert (startY == endY); + for (int x = startX; x <= endX; x++) { + invertCell(x, startY); + } + } + } + } + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle) { + + StringBuilder sb = new StringBuilder(); + + int startX = x0; + int startY = y0; + int endX = x1; + int endY = y1; + + if (((x1 < x0) && (y1 <= y0)) + || ((x1 <= x0) && (y1 < y0)) + ) { + // The user dragged from bottom-right to top-left. Reverse the + // coordinates for the inverted section. + startX = x1; + startY = y1; + endX = x0; + endY = y0; + } + if (rectangle) { + for (int y = startY; y <= endY; y++) { + for (int x = startX; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, y).getChar())); + } + sb.append("\n"); + } + } else { + if (endY > startY) { + for (int x = startX; x < width; x++) { + sb.append(Character.toChars(getCharXY(x, startY).getChar())); + } + sb.append("\n"); + for (int y = startY + 1; y < endY; y++) { + for (int x = 0; x < width; x++) { + sb.append(Character.toChars(getCharXY(x, y).getChar())); + } + sb.append("\n"); + } + for (int x = 0; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, endY).getChar())); + } + } else { + assert (startY == endY); + for (int x = startX; x <= endX; x++) { + sb.append(Character.toChars(getCharXY(x, startY).getChar())); + } + } + } + clipboard.copyText(sb.toString()); + } + } diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java index f6067984..45741c05 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -33,6 +33,7 @@ import java.util.List; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; /** * MultiScreen mirrors its I/O to several screens. @@ -93,7 +94,10 @@ public class MultiScreen implements Screen { * @return drawing boundary */ public int getClipRight() { - return screens.get(0).getClipRight(); + if (screens.size() > 0) { + return screens.get(0).getClipRight(); + } + return 0; } /** @@ -113,7 +117,10 @@ public class MultiScreen implements Screen { * @return drawing boundary */ public int getClipBottom() { - return screens.get(0).getClipBottom(); + if (screens.size() > 0) { + return screens.get(0).getClipBottom(); + } + return 0; } /** @@ -133,7 +140,10 @@ public class MultiScreen implements Screen { * @return drawing boundary */ public int getClipLeft() { - return screens.get(0).getClipLeft(); + if (screens.size() > 0) { + return screens.get(0).getClipLeft(); + } + return 0; } /** @@ -153,7 +163,10 @@ public class MultiScreen implements Screen { * @return drawing boundary */ public int getClipTop() { - return screens.get(0).getClipTop(); + if (screens.size() > 0) { + return screens.get(0).getClipTop(); + } + return 0; } /** @@ -190,7 +203,10 @@ public class MultiScreen implements Screen { * @return attributes at (x, y) */ public CellAttributes getAttrXY(final int x, final int y) { - return screens.get(0).getAttrXY(x, y); + if (screens.size() > 0) { + return screens.get(0).getAttrXY(x, y); + } + return new CellAttributes(); } /** @@ -201,7 +217,10 @@ public class MultiScreen implements Screen { * @return the character + attributes */ public Cell getCharXY(final int x, final int y) { - return screens.get(0).getCharXY(x, y); + if (screens.size() > 0) { + return screens.get(0).getCharXY(x, y); + } + return new Cell(); } /** @@ -410,7 +429,10 @@ public class MultiScreen implements Screen { */ public int getHeight() { // Return the smallest height of the screens. - int height = screens.get(0).getHeight(); + int height = 25; + if (screens.size() > 0) { + height = screens.get(0).getHeight(); + } for (Screen screen: screens) { if (screen.getHeight() < height) { height = screen.getHeight(); @@ -426,7 +448,10 @@ public class MultiScreen implements Screen { */ public int getWidth() { // Return the smallest width of the screens. - int width = screens.get(0).getWidth(); + int width = 80; + if (screens.size() > 0) { + width = screens.get(0).getWidth(); + } for (Screen screen: screens) { if (screen.getWidth() < width) { width = screen.getWidth(); @@ -582,7 +607,10 @@ public class MultiScreen implements Screen { * @return true if the cursor is visible */ public boolean isCursorVisible() { - return screens.get(0).isCursorVisible(); + if (screens.size() > 0) { + return screens.get(0).isCursorVisible(); + } + return true; } /** @@ -591,7 +619,10 @@ public class MultiScreen implements Screen { * @return the cursor x column position */ public int getCursorX() { - return screens.get(0).getCursorX(); + if (screens.size() > 0) { + return screens.get(0).getCursorX(); + } + return 0; } /** @@ -600,7 +631,10 @@ public class MultiScreen implements Screen { * @return the cursor y row position */ public int getCursorY() { - return screens.get(0).getCursorY(); + if (screens.size() > 0) { + return screens.get(0).getCursorY(); + } + return 0; } /** @@ -699,4 +733,41 @@ public class MultiScreen implements Screen { } } + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle) { + + for (Screen screen: screens) { + screen.setSelection(x0, y0, x1, y1, rectangle); + } + } + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle) { + + // Only copy from the first screen. + if (screens.size() > 0) { + screens.get(0).copySelection(clipboard, x0, y0, x1, y1, rectangle); + } + } + } diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java index f1f42db3..a9a20535 100644 --- a/src/jexer/backend/Screen.java +++ b/src/jexer/backend/Screen.java @@ -30,6 +30,7 @@ package jexer.backend; import jexer.bits.Cell; import jexer.bits.CellAttributes; +import jexer.bits.Clipboard; /** * Drawing operations API. @@ -429,4 +430,30 @@ public interface Screen { public void invertCell(final int x, final int y, final boolean onlyThisCell); + /** + * Set a selection area on the screen. + * + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void setSelection(final int x0, final int y0, + final int x1, final int y1, final boolean rectangle); + + /** + * Copy the screen selection area to the clipboard. + * + * @param clipboard the clipboard to use + * @param x0 the starting X position of the selection + * @param y0 the starting Y position of the selection + * @param x1 the ending X position of the selection + * @param y1 the ending Y position of the selection + * @param rectangle if true, this is a rectangle select + */ + public void copySelection(final Clipboard clipboard, + final int x0, final int y0, final int x1, final int y1, + final boolean rectangle); + } diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java index 99366fda..ad861989 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/src/jexer/bits/CellAttributes.java @@ -62,7 +62,6 @@ public class CellAttributes { */ private static final int PROTECT = 0x10; - // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ diff --git a/src/jexer/bits/Clipboard.java b/src/jexer/bits/Clipboard.java new file mode 100644 index 00000000..114a732b --- /dev/null +++ b/src/jexer/bits/Clipboard.java @@ -0,0 +1,219 @@ +/* + * 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.bits; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * Clipboard provides convenience methods to copy text and images to and from + * a shared clipboard. When the system clipboard is available it is used. + */ +public class Clipboard { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The image last copied to the clipboard. + */ + private BufferedImage image = null; + + /** + * The text string last copied to the clipboard. + */ + private String text = null; + + /** + * The system clipboard, or null if it is not available. + */ + private java.awt.datatransfer.Clipboard systemClipboard = null; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + */ + public Clipboard() { + try { + systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (java.awt.HeadlessException e) { + // SQUASH + } + } + + // ------------------------------------------------------------------------ + // Clipboard -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Copy an image to the clipboard. + * + * @param image image to copy + */ + public void copyImage(final BufferedImage image) { + this.image = image; + if (systemClipboard != null) { + // TODO + } + } + + /** + * Copy a text string to the clipboard. + * + * @param text string to copy + */ + public void copyText(final String text) { + this.text = text; + if (systemClipboard != null) { + StringSelection stringSelection = new StringSelection(text); + systemClipboard.setContents(stringSelection, null); + } + } + + /** + * Obtain an image from the clipboard. + * + * @return image from the clipboard, or null if no image is available + */ + public BufferedImage pasteImage() { + if (systemClipboard != null) { + getClipboardImage(); + } + return image; + } + + /** + * Obtain a text string from the clipboard. + * + * @return text string from the clipboard, or null if no text is + * available + */ + public String pasteText() { + if (systemClipboard != null) { + getClipboardText(); + } + return text; + } + + /** + * Returns true if the clipboard has an image. + * + * @return true if an image is available from the clipboard + */ + public boolean isImage() { + if (image == null) { + getClipboardImage(); + } + return (image != null); + } + + /** + * Returns true if the clipboard has a text string. + * + * @return true if a text string is available from the clipboard + */ + public boolean isText() { + if (text == null) { + getClipboardText(); + } + return (text != null); + } + + /** + * Returns true if the clipboard is empty. + * + * @return true if the clipboard is empty + */ + public boolean isEmpty() { + return ((isText() == false) && (isImage() == false)); + } + + /** + * Copy image from the clipboard to text. + */ + private void getClipboardImage() { + if (systemClipboard != null) { + Transferable contents = systemClipboard.getContents(null); + if (contents != null) { + if (contents.isDataFlavorSupported(DataFlavor.imageFlavor)) { + try { + Image img = (Image) contents.getTransferData(DataFlavor.imageFlavor); + image = new BufferedImage(img.getWidth(null), + img.getHeight(null), BufferedImage.TYPE_INT_ARGB); + image.getGraphics().drawImage(img, 0, 0, null); + } catch (IOException e) { + // SQUASH + } catch (UnsupportedFlavorException e) { + // SQUASH + } + } + } + } + } + + /** + * Copy text string from the clipboard to text. + */ + private void getClipboardText() { + if (systemClipboard != null) { + Transferable contents = systemClipboard.getContents(null); + if (contents != null) { + if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + try { + text = (String) contents.getTransferData(DataFlavor.stringFlavor); + } catch (IOException e) { + // SQUASH + } catch (UnsupportedFlavorException e) { + // SQUASH + } + } + } + } + } + + /** + * Clear whatever is on the local clipboard. Note that this will not + * clear the system clipboard. + */ + public void clear() { + image = null; + text = null; + } + +} diff --git a/src/jexer/bits/StringUtils.java b/src/jexer/bits/StringUtils.java index 2a4fc1dc..36f7b4e9 100644 --- a/src/jexer/bits/StringUtils.java +++ b/src/jexer/bits/StringUtils.java @@ -42,6 +42,11 @@ import java.util.Arrays; * * - Read/write a line of RFC4180 comma-separated values strings to/from a * list of strings. + * + * - Compute number of visible text cells for a given Unicode codepoint or + * string. + * + * - Convert bytes to and from base-64 encoding. */ public class StringUtils { -- 2.27.0