From 5434cb2bf4d238be6fde507da5acd4860b0a0c13 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Sat, 23 Feb 2019 15:27:33 -0600 Subject: [PATCH] image thumbnail viewer example --- README.md | 4 + examples/JexerImageViewer.java | 324 +++++++++++++++++++++++++++++++++ src/jexer/TApplication.java | 1 + 3 files changed, 329 insertions(+) create mode 100644 examples/JexerImageViewer.java diff --git a/README.md b/README.md index cdfcbf38..985b9720 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,10 @@ The examples/ folder currently contains: manager](/examples/JexerTilingWindowManager.java) in less than 250 lines of code. + * A [prototype image thumbnail + viewer](/examples/JexerImageViewer.java) in less than 350 lines of + code. + jexer.demos contains official demos showing all of the existing UI controls. The demos can be run as follows: diff --git a/examples/JexerImageViewer.java b/examples/JexerImageViewer.java new file mode 100644 index 00000000..b8424a5e --- /dev/null +++ b/examples/JexerImageViewer.java @@ -0,0 +1,324 @@ +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.imageio.ImageIO; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TDesktop; +import jexer.TDirectoryList; +import jexer.TImage; +import jexer.backend.SwingTerminal; +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TKeypressEvent; +import jexer.event.TResizeEvent; +import jexer.menu.TMenu; +import jexer.ttree.TDirectoryTreeItem; +import jexer.ttree.TTreeItem; +import jexer.ttree.TTreeViewWidget; +import static jexer.TKeypress.*; + +/** + * Implements a simple image thumbnail file viewer. Much of this code was + * stripped down from TFileOpenBox. + */ +public class JexerImageViewer extends TApplication { + + /** + * Main entry point. + */ + public static void main(String [] args) throws Exception { + // For this application, we must use ptypipe so that the tile shells + // can be aware of their size. + + JexerImageViewer app = new JexerImageViewer(); + (new Thread(app)).start(); + } + + /** + * Public constructor chooses the ECMA-48 / Xterm backend. + */ + public JexerImageViewer() throws Exception { + super(BackendType.XTERM); + + // The stock tool menu has items for redrawing the screen, opening + // images, and (when using the Swing backend) setting the font. + addToolMenu(); + + // We will have one menu containing a mix of new and stock commands + TMenu fileMenu = addMenu("&File"); + + // Stock commands: a new shell, exit program. + fileMenu.addDefaultItem(TMenu.MID_SHELL); + fileMenu.addSeparator(); + fileMenu.addDefaultItem(TMenu.MID_EXIT); + + // Filter the files list to support image suffixes only. + List filters = new ArrayList(); + filters.add("^.*\\.[Jj][Pp][Gg]$"); + filters.add("^.*\\.[Jj][Pp][Ee][Gg]$"); + filters.add("^.*\\.[Pp][Nn][Gg]$"); + filters.add("^.*\\.[Gg][Ii][Ff]$"); + filters.add("^.*\\.[Bb][Mm][Pp]$"); + setDesktop(new ImageViewerDesktop(this, ".", filters)); + } + +} + +/** + * The desktop contains a tree view on the left, list of files on the top + * right, and image view on the bottom right. + */ +class ImageViewerDesktop extends TDesktop { + + /** + * The left-side tree view pane. + */ + private TTreeViewWidget treeView; + + /** + * The data behind treeView. + */ + private TDirectoryTreeItem treeViewRoot; + + /** + * The top-right-side directory list pane. + */ + private TDirectoryList directoryList; + + /** + * The bottom-right-side image pane. + */ + private TImage imageWidget; + + /** + * Public constructor. + * + * @param application the TApplication that manages this window + * @param path path of selected file + * @param filters a list of strings that files must match to be displayed + * @throws IOException of a java.io operation throws + */ + public ImageViewerDesktop(final TApplication application, final String path, + final List filters) throws IOException { + + super(application); + setActive(true); + + // Add directory treeView + treeView = addTreeViewWidget(0, 0, getWidth() / 2, getHeight() - 1, + new TAction() { + public void DO() { + TTreeItem item = treeView.getSelected(); + File selectedDir = ((TDirectoryTreeItem) item).getFile(); + try { + directoryList.setPath(selectedDir.getCanonicalPath()); + if (directoryList.getList().size() > 0) { + setThumbnail(directoryList.getPath()); + } else { + if (imageWidget != null) { + getChildren().remove(imageWidget); + } + imageWidget = null; + } + activate(treeView); + } catch (IOException e) { + // If the backend is Swing, we can emit the stack + // trace to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } + } + } + } + ); + treeViewRoot = new TDirectoryTreeItem(treeView, path, true); + + // Add directory files list + directoryList = addDirectoryList(path, getWidth() / 2 + 1, 0, + getWidth() / 2 - 1, getHeight() / 2, + + new TAction() { + public void DO() { + setThumbnail(directoryList.getPath()); + } + }, + new TAction() { + + public void DO() { + setThumbnail(directoryList.getPath()); + } + }, + filters); + + // This appears to be a bug. If I pass keystrokes to the tree view, + // it will center correctly. + treeView.onKeypress(new TKeypressEvent(kbDown)); + treeView.onKeypress(new TKeypressEvent(kbUp)); + + if (directoryList.getList().size() > 0) { + activate(directoryList); + setThumbnail(directoryList.getPath()); + } else { + activate(treeView); + } + } + + /** + * Handle window/screen resize events. + * + * @param event resize event + */ + @Override + public void onResize(final TResizeEvent event) { + + // Resize the tree and list + treeView.setY(1); + treeView.setWidth(getWidth() / 2); + treeView.setHeight(getHeight() - 1); + treeView.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + treeView.getWidth(), + treeView.getHeight())); + treeView.getTreeView().onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + treeView.getWidth() - 1, + treeView.getHeight() - 1)); + directoryList.setX(getWidth() / 2 + 1); + directoryList.setY(1); + directoryList.setWidth(getWidth() / 2 - 1); + directoryList.setHeight(getHeight() / 2 - 1); + directoryList.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, + directoryList.getWidth(), + directoryList.getHeight())); + + // Recreate the image + if (imageWidget != null) { + getChildren().remove(imageWidget); + } + imageWidget = null; + if (directoryList.getList().size() > 0) { + activate(directoryList); + setThumbnail(directoryList.getPath()); + } else { + activate(treeView); + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + + if (treeView.isActive() || directoryList.isActive()) { + if ((keypress.equals(kbEnter)) + || (keypress.equals(kbUp)) + || (keypress.equals(kbDown)) + || (keypress.equals(kbPgUp)) + || (keypress.equals(kbPgDn)) + || (keypress.equals(kbHome)) + || (keypress.equals(kbEnd)) + ) { + // Tree view will be changing, update the directory list. + super.onKeypress(keypress); + + // This is the same action as treeView's enter. + TTreeItem item = treeView.getSelected(); + File selectedDir = ((TDirectoryTreeItem) item).getFile(); + try { + if (treeView.isActive()) { + directoryList.setPath(selectedDir.getCanonicalPath()); + } + if (directoryList.getList().size() > 0) { + activate(directoryList); + setThumbnail(directoryList.getPath()); + } else { + if (imageWidget != null) { + getChildren().remove(imageWidget); + } + imageWidget = null; + activate(treeView); + } + } catch (IOException e) { + // If the backend is Swing, we can emit the stack trace + // to stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } + } + return; + } + } + + // Pass to my parent + super.onKeypress(keypress); + } + + /** + * Draw me on screen. + */ + @Override + public void draw() { + CellAttributes background = getTheme().getColor("tdesktop.background"); + putAll(' ', background); + + vLineXY(getWidth() / 2, 0, getHeight(), + GraphicsChars.WINDOW_SIDE, getBackground()); + + hLineXY(getWidth() / 2, getHeight() / 2, (getWidth() + 1) / 2, + GraphicsChars.WINDOW_TOP, getBackground()); + + putCharXY(getWidth() / 2, getHeight() / 2, + GraphicsChars.WINDOW_LEFT_TEE, getBackground()); + } + + /** + * Set the image thumbnail. + * + * @param file the image file + */ + private void setThumbnail(final File file) { + if (file == null) { + return; + } + if (!file.exists() || !file.isFile()) { + return; + } + + BufferedImage image = null; + try { + image = ImageIO.read(file); + } catch (IOException e) { + // If the backend is Swing, we can emit the stack trace to + // stderr. Otherwise, just squash it. + if (getScreen() instanceof SwingTerminal) { + e.printStackTrace(); + } + return; + } + + if (imageWidget != null) { + getChildren().remove(imageWidget); + } + int width = getWidth() / 2 - 1; + int height = getHeight() / 2 - 1; + + imageWidget = new TImage(this, getWidth() - width, + getHeight() - height, width, height, image, 0, 0, null); + + // Resize the image down until it can fit within the pane. + while ((imageWidget.getRows() > height) + || (imageWidget.getColumns() > width) + ) { + imageWidget.onKeypress(new TKeypressEvent(kbAltDown)); + } + + imageWidget.setActive(false); + activate(directoryList); + } + +} diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 4c317f30..1b396a73 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -936,6 +936,7 @@ public class TApplication implements Runnable { if (desktop != null) { desktop.setDimensions(0, 0, resize.getWidth(), resize.getHeight() - 1); + desktop.onResize(resize); } // Change menu edges if needed. -- 2.27.0