From e5145580e58696b642810f53a792e1b8e03fb82c Mon Sep 17 00:00:00 2001 From: Autumn Lamonte Date: Fri, 14 Jan 2022 11:01:47 -0600 Subject: [PATCH] Translucent windows #88 This implements: * Translucent windows. See issue text for some discussion. Also a thank you to notcurses for the inspiration. * Selection of different window border styles. * Fixes for RGB output on ECMA48. * Other performance bug fixes. --- src/jexer/TApplication.java | 192 ++++++++- src/jexer/TEditColorThemeWindow.java | 102 ++++- src/jexer/TImageWindow.java | 11 + src/jexer/TLabel.java | 9 +- src/jexer/TRadioGroup.java | 13 +- src/jexer/TScreenOptionsWindow.java | 192 ++++++++- src/jexer/TScreenOptionsWindow.properties | 3 + src/jexer/TTerminalWidget.java | 7 +- src/jexer/TTerminalWindow.java | 22 + src/jexer/TTextPicture.java | 2 - src/jexer/TWidget.java | 16 +- src/jexer/TWindow.java | 273 ++++++++++++- src/jexer/backend/ECMA48Terminal.java | 385 ++++++++++-------- src/jexer/backend/LogicalScreen.java | 471 +++++++++++++++++++--- src/jexer/backend/MultiScreen.java | 127 +++++- src/jexer/backend/Screen.java | 79 +++- src/jexer/backend/SwingTerminal.java | 4 +- src/jexer/bits/BorderStyle.java | 277 +++++++++++++ src/jexer/bits/Cell.java | 51 ++- src/jexer/bits/CellAttributes.java | 14 +- src/jexer/bits/ColorTheme.java | 62 +++ src/jexer/bits/ImageUtils.java | 22 + src/jexer/demos/DemoPixelsWindow.java | 1 + src/jexer/menu/TMenu.java | 112 ++++- src/jexer/menu/TMenuItem.java | 4 +- src/jexer/menu/TMenuSeparator.java | 20 +- src/jexer/tackboard/Bitmap.java | 13 + src/jexer/tackboard/Tackboard.java | 6 +- src/jexer/tterminal/ECMA48.java | 24 +- 29 files changed, 2180 insertions(+), 334 deletions(-) create mode 100644 src/jexer/bits/BorderStyle.java diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 2c632e1..78eb313 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -402,6 +402,11 @@ public class TApplication implements Runnable { */ protected MousePointer customWidgetMousePointer; + /** + * If true. enable translucency. + */ + protected boolean translucence = true; + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -584,6 +589,21 @@ public class TApplication implements Runnable { */ private ArrayList dirtyQueue = new ArrayList(); + /** + * The number of updates pushed out in this second. + */ + private int framesPerSecond = 0; + + /** + * The last time a frame was rendered. + */ + private long lastFlushTime = 0; + + /** + * How long it took to render the last time in millis. + */ + private long lastFrameTime = 0; + /** * Public constructor. * @@ -612,6 +632,7 @@ public class TApplication implements Runnable { * The update loop. */ private void runImpl() { + int frameCount = 0; // Loop forever while (!application.quit) { @@ -620,7 +641,11 @@ public class TApplication implements Runnable { while (!application.quit) { synchronized (dirtyQueue) { if (dirtyQueue.size() > 0) { - dirtyQueue.remove(dirtyQueue.size() - 1); + // Collapse all the dirty requests into one + // refresh. + while (dirtyQueue.size() > 0) { + dirtyQueue.remove(dirtyQueue.size() - 1); + } break; } } @@ -641,7 +666,17 @@ public class TApplication implements Runnable { System.currentTimeMillis(), Thread.currentThread()); } synchronized (getScreen()) { + long before = System.currentTimeMillis(); backend.flushScreen(); + long now = System.currentTimeMillis(); + lastFrameTime = now - before; + if ((int) (now / 1000) == (int) (lastFlushTime / 1000)) { + frameCount++; + } else { + framesPerSecond = frameCount; + frameCount = 0; + } + lastFlushTime = now; } } // while (true) (main runnable loop) @@ -841,6 +876,11 @@ public class TApplication implements Runnable { hideMenuBar = true; } + // Translucent windows (!) option + if (System.getProperty("jexer.translucence", "true").equals("false")) { + translucence = false; + } + theme = new ColorTheme(); desktopTop = (hideMenuBar ? 0 : 1); desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0); @@ -1415,6 +1455,7 @@ public class TApplication implements Runnable { * @see #secondaryHandleEvent(TInputEvent event) */ private void primaryHandleEvent(final TInputEvent event) { + assert (event != null); if (debugEvents) { System.err.printf("%s primaryHandleEvent: %s\n", @@ -1623,9 +1664,14 @@ public class TApplication implements Runnable { if (debugEvents) { System.err.printf("TApplication dispatch event: %s\n", event); + System.err.printf(" Routed to: %s\n", window); + System.err.flush(); } window.handleEvent(event); if (doubleClick != null) { + if (debugEvents) { + System.err.printf(" -- DOUBLE CLICK --\n"); + } window.handleEvent(doubleClick); } if (mouse != null) { @@ -1657,6 +1703,8 @@ public class TApplication implements Runnable { * @see #primaryHandleEvent(TInputEvent event) */ private void secondaryHandleEvent(final TInputEvent event) { + assert (event != null); + TMouseEvent doubleClick = null; if (debugEvents) { @@ -1907,7 +1955,7 @@ public class TApplication implements Runnable { } /** - * Get the color theme. + * Get the global color theme. * * @return the theme */ @@ -1915,6 +1963,60 @@ public class TApplication implements Runnable { return theme; } + /** + * Get the translucence option. + * + * @return true if translucency is enabled + */ + public boolean hasTranslucence() { + return translucence; + } + + /** + * Set the translucence option. + * + * @param enabled if true, windows will be translucent + */ + public void setTranslucence(final boolean enabled) { + translucence = enabled; + } + + /** + * Set the opacity of all windows. If opacity is 100, translucence is + * also disabled for performance. + * + * @param opacity a number between 10 (nearly transparent) and 100 (fully + * opaque) + */ + public void setWindowOpacity(final int opacity) { + if ((opacity < 10) || (opacity > 100)) { + return; + } + if (opacity == 100) { + translucence = false; + } else { + translucence = true; + } + + int alpha = opacity * 255 / 100; + for (TWindow window: windows) { + window.setAlpha(alpha); + } + } + + /** + * Get the number of frames that were emitted to output on the last + * second. + * + * @return the frames per second + */ + public int getFramesPerSecond() { + if (screenHandler != null) { + return screenHandler.framesPerSecond; + } + return 0; + } + /** * Get the clipboard. * @@ -1929,6 +2031,35 @@ public class TApplication implements Runnable { */ public void doRepaint() { repaint = true; + synchronized (drainEventQueue) { + if (fillEventQueue.size() > 0) { + // User input is waiting, that will update the screen. Wake + // the backend reader. + if (debugEvents) { + System.err.printf("Drop: input waiting in backend\n"); + } + synchronized (this) { + this.notify(); + } + return; + } + } + if (screenHandler != null) { + long now = System.currentTimeMillis(); + if (now - screenHandler.lastFlushTime < screenHandler.lastFrameTime) { + // We cannot update the screen this quickly. Drop this + // request. + if (debugEvents) { + System.err.printf("Drop: %d millis to render, %d since\n", + screenHandler.lastFrameTime, + now - screenHandler.lastFlushTime); + } + return; + } + } + + // It's been enough time since the last frame, and no user input is + // around, so allow a screen repaint. wakeEventHandler(); } @@ -2416,6 +2547,42 @@ public class TApplication implements Runnable { getScreen().invertCell(x, y); } + /** + * Draw a translucent window with a shadow on the screen. + * + * @param screen the screen + * @param window the window + */ + private void drawTranslucentWindow(final Screen screen, + final TWindow window) { + + // Alpha blending: have the window draw to a snapshot of the screen + // without alpha, and then merge it on the screen with alpha. + int windowX = window.getX(); + int windowY = window.getY(); + int windowWidth = window.getWidth(); + int windowHeight = window.getHeight(); + Screen oldSnapshot = screen.snapshot(windowX, windowY, + windowWidth + 2, windowHeight + 1); + window.drawChildren(); + Screen newSnapshot = screen.snapshot(windowX, windowY, + windowWidth, windowHeight); + screen.copyScreen(oldSnapshot, windowX, windowY, + windowWidth, windowHeight); + screen.blendScreen(newSnapshot, windowX, windowY, + windowWidth, windowHeight, window.getAlpha(), true); + screen.resetClipping(); + + // Recreate the shadow effect by blending a black rectangle over just + // the shadow region. + final int shadowOpacity = 30; + final int shadowAlpha = shadowOpacity * 255 / 100; + screen.blendRectangle(windowX + windowWidth, windowY + 1, + 2, windowHeight - 1, 0x000000, shadowAlpha); + screen.blendRectangle(windowX + 2, windowY + windowHeight, + windowWidth, 1, 0x000000, shadowAlpha); + } + /** * Draw everything. */ @@ -2528,7 +2695,11 @@ public class TApplication implements Runnable { Collections.reverse(sorted); for (TWindow window: sorted) { if (window.isShown()) { - window.drawChildren(); + if (translucence) { + drawTranslucentWindow(getScreen(), window); + } else { + window.drawChildren(); + } } } @@ -2572,7 +2743,13 @@ public class TApplication implements Runnable { } if (menu.isActive()) { - ((TWindow) menu).drawChildren(); + if (translucence) { + drawTranslucentWindow(getScreen(), menu); + } else { + ((TWindow) menu).drawChildren(); + } + + // Reset the screen clipping so we can draw the next title. getScreen().resetClipping(); } @@ -2582,7 +2759,12 @@ public class TApplication implements Runnable { for (TMenu menu: subMenus) { // Reset the screen clipping so we can draw the next sub-menu. getScreen().resetClipping(); - ((TWindow) menu).drawChildren(); + if (translucence) { + drawTranslucentWindow(getScreen(), menu); + } else { + ((TWindow) menu).drawChildren(); + } + } if (hideMenuBar == false) { diff --git a/src/jexer/TEditColorThemeWindow.java b/src/jexer/TEditColorThemeWindow.java index 1301d19..052465c 100644 --- a/src/jexer/TEditColorThemeWindow.java +++ b/src/jexer/TEditColorThemeWindow.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.List; import java.util.ResourceBundle; +import jexer.bits.BorderStyle; import jexer.bits.Color; import jexer.bits.ColorTheme; import jexer.bits.CellAttributes; @@ -230,15 +231,23 @@ public class TEditColorThemeWindow extends TWindow { CellAttributes background = getWindow().getBackground(); CellAttributes attr = new CellAttributes(); - drawBox(0, 0, getWidth(), getHeight(), border, background, 1, - false); + BorderStyle borderStyle; + borderStyle = BorderStyle.getStyle(System.getProperty( + "jexer.TEditColorTheme.options.borderStyle", "single")); + + drawBox(0, 0, getWidth(), getHeight(), border, background, + borderStyle, false); attr.setTo(getTheme().getColor("twindow.background.modal")); if (isActive()) { attr.setForeColor(getTheme().getColor("tlabel").getForeColor()); attr.setBold(getTheme().getColor("tlabel").isBold()); } - putStringXY(1, 0, i18n.getString("foregroundLabel"), attr); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(0, 0, i18n.getString("foregroundLabel"), attr); + } else { + putStringXY(1, 0, i18n.getString("foregroundLabel"), attr); + } // Have to draw the colors manually because the int value matches // SGR, not CGA. @@ -518,15 +527,23 @@ public class TEditColorThemeWindow extends TWindow { CellAttributes background = getWindow().getBackground(); CellAttributes attr = new CellAttributes(); - drawBox(0, 0, getWidth(), getHeight(), border, background, 1, - false); + BorderStyle borderStyle; + borderStyle = BorderStyle.getStyle(System.getProperty( + "jexer.TEditColorTheme.options.borderStyle", "single")); + + drawBox(0, 0, getWidth(), getHeight(), border, background, + borderStyle, false); attr.setTo(getTheme().getColor("twindow.background.modal")); if (isActive()) { attr.setForeColor(getTheme().getColor("tlabel").getForeColor()); attr.setBold(getTheme().getColor("tlabel").isBold()); } - putStringXY(1, 0, i18n.getString("backgroundLabel"), attr); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(0, 0, i18n.getString("backgroundLabel"), attr); + } else { + putStringXY(1, 0, i18n.getString("backgroundLabel"), attr); + } // Have to draw the colors manually because the int value matches // SGR, not CGA. @@ -931,4 +948,77 @@ public class TEditColorThemeWindow extends TWindow { editTheme.setColor(colorName, attr); } + /** + * Set the border style for the window when it is the foreground window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TEditColorTheme.borderStyle. + */ + @Override + public void setBorderStyleForeground(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TEditColorTheme.borderStyle", + "double"); + super.setBorderStyleForeground(style); + } else { + super.setBorderStyleForeground(borderStyle); + } + } + + /** + * Set the border style for the window when it is the modal window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TEditColorTheme.borderStyle. + */ + @Override + public void setBorderStyleModal(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TEditColorTheme.borderStyle", + "double"); + super.setBorderStyleModal(style); + } else { + super.setBorderStyleModal(borderStyle); + } + } + + /** + * Set the border style for the window when it is an inactive/background + * window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TEditColorTheme.borderStyle. + */ + @Override + public void setBorderStyleInactive(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TEditColorTheme.borderStyle", + "double"); + super.setBorderStyleInactive(style); + } else { + super.setBorderStyleInactive(borderStyle); + } + } + + /** + * Set the border style for the window when it is being dragged/resize. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TEditColorTheme.borderStyle. + */ + @Override + public void setBorderStyleMoving(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TEditColorTheme.borderStyle", + "double"); + super.setBorderStyleMoving(style); + } else { + super.setBorderStyleMoving(borderStyle); + } + } + } diff --git a/src/jexer/TImageWindow.java b/src/jexer/TImageWindow.java index 2e5b6ee..c633f81 100644 --- a/src/jexer/TImageWindow.java +++ b/src/jexer/TImageWindow.java @@ -118,6 +118,17 @@ public class TImageWindow extends TScrollableWindow { setTitle(file.getName()); + int opacity = 100; + try { + opacity = Integer.parseInt(System.getProperty( + "jexer.TImage.opacity", "100")); + opacity = Math.max(opacity, 10); + opacity = Math.min(opacity, 100); + } catch (NumberFormatException e) { + // SQUASH + } + setAlpha(opacity * 255 / 100); + setupAfterImage(); } diff --git a/src/jexer/TLabel.java b/src/jexer/TLabel.java index 64df631..5086c15 100644 --- a/src/jexer/TLabel.java +++ b/src/jexer/TLabel.java @@ -203,8 +203,13 @@ public class TLabel extends TWidget { mnemonicColor.setTo(getTheme().getColor("tlabel.mnemonic")); if (useWindowBackground) { CellAttributes background = getWindow().getBackground(); - color.setBackColor(background.getBackColor()); - mnemonicColor.setBackColor(background.getBackColor()); + if (background.getBackColorRGB() == -1) { + color.setBackColor(background.getBackColor()); + mnemonicColor.setBackColor(background.getBackColor()); + } else { + color.setBackColorRGB(background.getBackColorRGB()); + mnemonicColor.setBackColorRGB(background.getBackColorRGB()); + } } putStringXY(0, 0, mnemonic.getRawLabel(), color); if (mnemonic.getScreenShortcutIdx() >= 0) { diff --git a/src/jexer/TRadioGroup.java b/src/jexer/TRadioGroup.java index 9c07ce3..2787068 100644 --- a/src/jexer/TRadioGroup.java +++ b/src/jexer/TRadioGroup.java @@ -28,6 +28,7 @@ */ package jexer; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; import jexer.bits.StringUtils; @@ -133,11 +134,19 @@ public class TRadioGroup extends TWidget { radioGroupColor = getTheme().getColor("tradiogroup.inactive"); } + BorderStyle borderStyle; + borderStyle = BorderStyle.getStyle(System.getProperty( + "jexer.TRadioGroup.borderStyle", "singleVdoubleH")); + drawBox(0, 0, getWidth(), getHeight(), radioGroupColor, radioGroupColor, - 3, false); + borderStyle, false); hLineXY(1, 0, StringUtils.width(label) + 2, ' ', radioGroupColor); - putStringXY(2, 0, label, radioGroupColor); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(1, 0, label, radioGroupColor); + } else { + putStringXY(2, 0, label, radioGroupColor); + } } // ------------------------------------------------------------------------ diff --git a/src/jexer/TScreenOptionsWindow.java b/src/jexer/TScreenOptionsWindow.java index 60ff890..faff2be 100644 --- a/src/jexer/TScreenOptionsWindow.java +++ b/src/jexer/TScreenOptionsWindow.java @@ -37,6 +37,7 @@ import java.util.ResourceBundle; import jexer.backend.ECMA48Terminal; import jexer.backend.SwingTerminal; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.event.TKeypressEvent; @@ -137,6 +138,11 @@ public class TScreenOptionsWindow extends TWindow { */ private TCheckBox rgbColor; + /** + * The window opacity. + */ + private TField windowOpacity; + /** * The original font size. */ @@ -207,6 +213,11 @@ public class TScreenOptionsWindow extends TWindow { */ private boolean oldRgbColor = false; + /** + * The original window opacity. + */ + private int oldWindowOpacity; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -231,6 +242,15 @@ public class TScreenOptionsWindow extends TWindow { ecmaTerminal = (ECMA48Terminal) getScreen(); } + addLabel(i18n.getString("windowOpacity"), 1, 0, "ttext", false, + new TAction() { + public void DO() { + if (windowOpacity != null) { + windowOpacity.activate(); + } + } + }); + addLabel(i18n.getString("fontName"), 3, 2, "ttext", false, new TAction() { public void DO() { @@ -296,6 +316,70 @@ public class TScreenOptionsWindow extends TWindow { } }); + // Window opacity + windowOpacity = addField(31, 0, 4, true, + Integer.toString(getAlpha() * 100 / 255), + new TAction() { + public void DO() { + int currentOpacity = getAlpha() * 100 / 255; + int newOpacity = currentOpacity; + newOpacity = Math.max(newOpacity, 10); + newOpacity = Math.min(newOpacity, 100); + try { + newOpacity = Integer.parseInt(windowOpacity.getText()); + } catch (NumberFormatException e) { + fontSize.setText(Integer.toString(currentOpacity)); + } + if (newOpacity != currentOpacity) { + getApplication().setWindowOpacity(newOpacity); + System.setProperty("jexer.TWindow.opacity", + Integer.toString(newOpacity)); + } + } + }, + null); + + addSpinner(35, 0, + new TAction() { + public void DO() { + int currentOpacity = getAlpha() * 100 / 255; + int newOpacity = currentOpacity; + try { + newOpacity = Integer.parseInt(windowOpacity.getText()); + newOpacity++; + newOpacity = Math.min(newOpacity, 100); + } catch (NumberFormatException e) { + windowOpacity.setText(Integer.toString(currentOpacity)); + } + windowOpacity.setText(Integer.toString(newOpacity)); + if (newOpacity != currentOpacity) { + getApplication().setWindowOpacity(newOpacity); + System.setProperty("jexer.TWindow.opacity", + Integer.toString(newOpacity)); + } + } + }, + new TAction() { + public void DO() { + int currentOpacity = getAlpha() * 100 / 255; + int newOpacity = currentOpacity; + try { + newOpacity = Integer.parseInt(windowOpacity.getText()); + newOpacity--; + newOpacity = Math.max(newOpacity, 10); + } catch (NumberFormatException e) { + windowOpacity.setText(Integer.toString(currentOpacity)); + } + windowOpacity.setText(Integer.toString(newOpacity)); + if (newOpacity != currentOpacity) { + getApplication().setWindowOpacity(newOpacity); + System.setProperty("jexer.TWindow.opacity", + Integer.toString(newOpacity)); + } + } + } + ); + sixel = addCheckBox(3, 15, i18n.getString("sixel"), (ecmaTerminal != null ? ecmaTerminal.hasSixel() : System.getProperty("jexer.ECMA48.sixel", @@ -849,6 +933,9 @@ public class TScreenOptionsWindow extends TWindow { ecmaTerminal.setWideCharImages(oldWideCharImages); ecmaTerminal.setRgbColor(oldRgbColor); } + getApplication().setWindowOpacity(oldWindowOpacity); + System.setProperty("jexer.TWindow.opacity", + Integer.toString(oldWindowOpacity)); getApplication().closeWindow(this); return; } @@ -870,15 +957,35 @@ public class TScreenOptionsWindow extends TWindow { int left = 34; + BorderStyle borderStyle; + borderStyle = BorderStyle.getStyle(System.getProperty( + "jexer.TScreenOptions.options.borderStyle", "single")); + CellAttributes color = getTheme().getColor("ttext"); - drawBox(2, 2, left + 24, 14, color, color); - putStringXY(4, 2, i18n.getString("swingOptions"), color); + drawBox(2, 2, left + 24, 14, color, color, borderStyle, false); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(3, 2, i18n.getString("swingOptions"), color); + } else { + putStringXY(4, 2, i18n.getString("swingOptions"), color); + } - drawBox(2, 15, left + 12, 22, color, color); - putStringXY(4, 15, i18n.getString("xtermOptions"), color); - drawBox(left + 2, 5, left + 22, 10, color, color, 3, false); - putStringXY(left + 4, 5, i18n.getString("sample"), color); + drawBox(2, 15, left + 12, 22, color, color, borderStyle, false); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(3, 15, i18n.getString("xtermOptions"), color); + } else { + putStringXY(4, 15, i18n.getString("xtermOptions"), color); + } + + borderStyle = BorderStyle.getStyle(System.getProperty( + "jexer.TScreenOptions.grid.borderStyle", "singleVdoubleH")); + + drawBox(left + 2, 5, left + 22, 10, color, color, borderStyle, false); + if (borderStyle.equals(BorderStyle.NONE)) { + putStringXY(left + 2, 5, i18n.getString("sample"), color); + } else { + putStringXY(left + 4, 5, i18n.getString("sample"), color); + } for (int i = 6; i < 9; i++) { hLineXY(left + 3, i, 18, GraphicsChars.HATCH, color); } @@ -889,4 +996,77 @@ public class TScreenOptionsWindow extends TWindow { // TScreenOptionsWindow --------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Set the border style for the window when it is the foreground window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TScreenOptions.borderStyle. + */ + @Override + public void setBorderStyleForeground(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TScreenOptions.borderStyle", + "double"); + super.setBorderStyleForeground(style); + } else { + super.setBorderStyleForeground(borderStyle); + } + } + + /** + * Set the border style for the window when it is the modal window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TScreenOptions.borderStyle. + */ + @Override + public void setBorderStyleModal(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TScreenOptions.borderStyle", + "double"); + super.setBorderStyleModal(style); + } else { + super.setBorderStyleModal(borderStyle); + } + } + + /** + * Set the border style for the window when it is an inactive/background + * window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TScreenOptions.borderStyle. + */ + @Override + public void setBorderStyleInactive(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TScreenOptions.borderStyle", + "double"); + super.setBorderStyleInactive(style); + } else { + super.setBorderStyleInactive(borderStyle); + } + } + + /** + * Set the border style for the window when it is being dragged/resize. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TScreenOptions.borderStyle. + */ + @Override + public void setBorderStyleMoving(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TScreenOptions.borderStyle", + "double"); + super.setBorderStyleMoving(style); + } else { + super.setBorderStyleMoving(borderStyle); + } + } + } diff --git a/src/jexer/TScreenOptionsWindow.properties b/src/jexer/TScreenOptionsWindow.properties index 359c88d..d70d8b8 100644 --- a/src/jexer/TScreenOptionsWindow.properties +++ b/src/jexer/TScreenOptionsWindow.properties @@ -32,3 +32,6 @@ sixelPaletteSize=Sixel &palette size: sixelSharedPalette=Us&e shared sixel palette wideCharImages=D&raw fullwidth characters as images rgbColor=Use 24-bit R&GB for all colors + +xtermOptions=\ Other\ +windowOpacity=W&indow opacity (translucence): diff --git a/src/jexer/TTerminalWidget.java b/src/jexer/TTerminalWidget.java index 23603f2..8a9bc30 100644 --- a/src/jexer/TTerminalWidget.java +++ b/src/jexer/TTerminalWidget.java @@ -709,13 +709,11 @@ public class TTerminalWidget extends TScrollableWidget if (reverse) { if (ch.getForeColorRGB() < 0) { newCell.setBackColor(ch.getForeColor()); - newCell.setBackColorRGB(-1); } else { newCell.setBackColorRGB(ch.getForeColorRGB()); } if (ch.getBackColorRGB() < 0) { newCell.setForeColor(ch.getBackColor()); - newCell.setForeColorRGB(-1); } else { newCell.setForeColorRGB(ch.getBackColorRGB()); } @@ -1299,6 +1297,7 @@ public class TTerminalWidget extends TScrollableWidget } else if (getScreen() instanceof ECMA48Terminal) { ECMA48Terminal terminal = (ECMA48Terminal) getScreen(); + /* Always render double-width/height with images. if (!terminal.hasSixel() && !terminal.hasJexerImages() && !terminal.hasIterm2Images() @@ -1309,13 +1308,17 @@ public class TTerminalWidget extends TScrollableWidget putCharXY(x + 1, y, ' ', cell); return; } + */ cursorBlinkVisible = blinkState; + + /* Always render double-width/height with images. } else { // We don't know how to dray glyphs to this screen, draw them as // text and bail out. putCharXY(x, y, cell); putCharXY(x + 1, y, ' ', cell); return; + */ } if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) { diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 4098773..46989d9 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -160,6 +160,17 @@ public class TTerminalWindow extends TScrollableWindow { onShellExit(); } }); + + int opacity = 95; + try { + opacity = Integer.parseInt(System.getProperty( + "jexer.TTerminal.opacity", "95")); + opacity = Math.max(opacity, 10); + opacity = Math.min(opacity, 100); + } catch (NumberFormatException e) { + // SQUASH + } + setAlpha(opacity * 255 / 100); } /** @@ -216,6 +227,17 @@ public class TTerminalWindow extends TScrollableWindow { onShellExit(); } }); + + int opacity = 95; + try { + opacity = Integer.parseInt(System.getProperty( + "jexer.TTerminal.opacity", "95")); + opacity = Math.max(opacity, 10); + opacity = Math.min(opacity, 100); + } catch (NumberFormatException e) { + // SQUASH + } + setAlpha(opacity * 255 / 100); } // ------------------------------------------------------------------------ diff --git a/src/jexer/TTextPicture.java b/src/jexer/TTextPicture.java index 8ae4007..4ff2a6d 100644 --- a/src/jexer/TTextPicture.java +++ b/src/jexer/TTextPicture.java @@ -366,13 +366,11 @@ public class TTextPicture extends TScrollableWidget if (reverse) { if (ch.getForeColorRGB() < 0) { newCell.setBackColor(ch.getForeColor()); - newCell.setBackColorRGB(-1); } else { newCell.setBackColorRGB(ch.getForeColorRGB()); } if (ch.getBackColorRGB() < 0) { newCell.setForeColor(ch.getBackColor()); - newCell.setForeColorRGB(-1); } else { newCell.setForeColorRGB(ch.getBackColorRGB()); } diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 320bf93..13c53ad 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import jexer.backend.Screen; import jexer.bits.Animation; +import jexer.bits.BorderStyle; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Clipboard; @@ -1419,6 +1420,13 @@ public abstract class TWidget implements Comparable { * @return the ColorTheme */ public final ColorTheme getTheme() { + if (this instanceof TWindow) { + TWindow window = (TWindow) this; + if (window.theme != null) { + return window.theme; + } + } + if (window != null) { return window.getApplication().getTheme(); } @@ -2136,18 +2144,16 @@ public abstract class TWidget implements Comparable { * @param bottom bottom row of the box * @param border attributes to use for the border * @param background attributes to use for the background - * @param borderType if 1, draw a single-line border; if 2, draw a - * double-line border; if 3, draw double-line top/bottom edges and - * single-line left/right edges (like Qmodem) + * @param borderStyle style of border * @param shadow if true, draw a "shadow" on the box */ public final void drawBox(final int left, final int top, final int right, final int bottom, final CellAttributes border, final CellAttributes background, - final int borderType, final boolean shadow) { + final BorderStyle borderStyle, final boolean shadow) { getScreen().drawBox(left, top, right, bottom, border, background, - borderType, shadow); + borderStyle, shadow); } /** diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 88244b4..404e9a0 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -32,7 +32,9 @@ import java.util.HashSet; import java.util.Set; import jexer.backend.Screen; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; +import jexer.bits.ColorTheme; import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; import jexer.event.TCommandEvent; @@ -185,6 +187,13 @@ public class TWindow extends TWidget { */ boolean hidden = false; + /** + * Widgets on this window can pull colors from this ColorTheme. Note + * package private access: if null, TWidget.getTheme() will pull from + * TApplication. + */ + ColorTheme theme; + /** * A window may have a status bar associated with it. TApplication will * draw this status bar last, and will also route events to it first @@ -218,6 +227,31 @@ public class TWindow extends TWidget { */ protected Tackboard overlay = null; + /** + * The border style for an active window. + */ + protected BorderStyle borderStyleActive = BorderStyle.DOUBLE; + + /** + * The border style for an active modal window. + */ + protected BorderStyle borderStyleActiveModal = BorderStyle.DOUBLE; + + /** + * The border style for an inactive window. + */ + protected BorderStyle borderStyleInactive = BorderStyle.SINGLE; + + /** + * The border style for a window being dragged/resized. + */ + protected BorderStyle borderStyleMoving = BorderStyle.SINGLE; + + /** + * The alpha blending value. + */ + private int alpha = 255; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -307,6 +341,23 @@ public class TWindow extends TWidget { // Add me to the application application.addWindowToApplication(this); + + // Set default borders + setBorderStyleForeground(null); + setBorderStyleInactive(null); + setBorderStyleModal(null); + setBorderStyleMoving(null); + + int opacity = 95; + try { + opacity = Integer.parseInt(System.getProperty( + "jexer.TWindow.opacity", "95")); + opacity = Math.max(opacity, 10); + opacity = Math.min(opacity, 100); + } catch (NumberFormatException e) { + // SQUASH + } + setAlpha(opacity * 255 / 100); } // ------------------------------------------------------------------------ @@ -359,6 +410,7 @@ public class TWindow extends TWidget { */ protected boolean mouseOnResize() { if (((flags & RESIZABLE) != 0) + && (getBorderStyle() != BorderStyle.NONE) && !isModal() && (mouse != null) && (mouse.getAbsoluteY() == getY() + getHeight() - 1) @@ -393,6 +445,12 @@ public class TWindow extends TWidget { getChildren().remove(w); } } + if (underlay != null) { + underlay.clear(); + } + if (overlay != null) { + overlay.clear(); + } } /** @@ -662,9 +720,12 @@ public class TWindow extends TWidget { } /* - * Only permit keyboard resizing if the window was RESIZABLE. + * Only permit keyboard resizing if the window was RESIZABLE and + * there is a window border. */ - if ((flags & RESIZABLE) != 0) { + if (((flags & RESIZABLE) != 0) + && (getBorderStyle() != BorderStyle.NONE) + ) { if (keypress.equals(kbShiftLeft)) { if ((getWidth() > minimumWindowWidth) @@ -938,9 +999,9 @@ public class TWindow extends TWidget { // Draw the box and background first. CellAttributes border = getBorder(); CellAttributes background = getBackground(); - int borderType = getBorderType(); - drawBox(0, 0, getWidth(), getHeight(), border, background, borderType, - true); + BorderStyle borderStyle = getBorderStyle(); + drawBox(0, 0, getWidth(), getHeight(), border, background, borderStyle, + !getApplication().hasTranslucence()); if ((title != null) && (title.length() > 0)) { // Draw the title @@ -952,11 +1013,17 @@ public class TWindow extends TWidget { } if (isActive()) { + int lBracket = '['; + int rBracket = ']'; + if (borderStyle.equals(BorderStyle.SINGLE_ROUND)) { + lBracket = '('; + rBracket = ')'; + } // Draw the close button if ((flags & NOCLOSEBOX) == 0) { - putCharXY(2, 0, '[', border); - putCharXY(4, 0, ']', border); + putCharXY(2, 0, lBracket, border); + putCharXY(4, 0, rBracket, border); if (mouseOnClose() && mouse.isMouse1()) { putCharXY(3, 0, GraphicsChars.CP437[0x0F], getBorderControls()); @@ -969,8 +1036,8 @@ public class TWindow extends TWidget { // Draw the maximize button if (!isModal() && ((flags & NOZOOMBOX) == 0)) { - putCharXY(getWidth() - 5, 0, '[', border); - putCharXY(getWidth() - 3, 0, ']', border); + putCharXY(getWidth() - 5, 0, lBracket, border); + putCharXY(getWidth() - 3, 0, rBracket, border); if (mouseOnMaximize() && mouse.isMouse1()) { putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x0F], getBorderControls()); @@ -986,12 +1053,15 @@ public class TWindow extends TWidget { } // Draw the resize corner - if (!isModal() && ((flags & RESIZABLE) != 0)) { + if (!isModal() + && ((flags & RESIZABLE) != 0) + && (getBorderStyle() != BorderStyle.NONE) + ) { if ((flags & RESIZABLE) != 0) { putCharXY(getWidth() - 2, getHeight() - 1, - GraphicsChars.SINGLE_BAR, getBorderControls()); + getBorderStyle().getHorizontal(), getBorderControls()); putCharXY(getWidth() - 1, getHeight() - 1, - GraphicsChars.LRCORNER, getBorderControls()); + getBorderStyle().getBottomRight(), getBorderControls()); } } } @@ -1428,7 +1498,9 @@ public class TWindow extends TWidget { * @return true if this window is resizable */ public final boolean isResizable() { - if ((flags & RESIZABLE) == 0) { + if (((flags & RESIZABLE) == 0) + || (getBorderStyle() == BorderStyle.NONE) + ) { return false; } return true; @@ -1525,29 +1597,32 @@ public class TWindow extends TWidget { } /** - * Retrieve the border line type. + * Retrieve the border line style. * - * @return the border line type + * @return the border line style */ - private int getBorderType() { + public BorderStyle getBorderStyle() { if (!isModal() && (inWindowMove || inWindowResize || inKeyboardResize) ) { assert (isActive()); - return 1; + return borderStyleMoving; } else if (isModal() && inWindowMove) { assert (isActive()); - return 1; + // Modals cannot be resized, hence the separate check. + return borderStyleMoving; } else if (isModal()) { if (isActive()) { - return 2; + return borderStyleActiveModal; } else { - return 1; + // One can stack modals, so an inactive but modal window is + // possible. + return borderStyleInactive; } } else if (isActive()) { - return 2; + return borderStyleActive; } else { - return 1; + return borderStyleInactive; } } @@ -1631,4 +1706,158 @@ public class TWindow extends TWidget { } } + /** + * Set the border style for the window when it is the foreground window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TWindow.borderStyleForeground. + */ + public void setBorderStyleForeground(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TWindow.borderStyleForeground", + "double"); + borderStyleActive = BorderStyle.getStyle(style); + } else if (borderStyle.equals("default")) { + borderStyleActive = BorderStyle.DOUBLE; + } else { + borderStyleActive = BorderStyle.getStyle(borderStyle); + } + } + + /** + * Get the border style for the window when it is the foreground window. + * + * @return the border style + */ + public BorderStyle getBorderStyleForeground() { + return borderStyleActive; + } + + /** + * Set the border style for the window when it is the modal window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TWindow.borderStyleModal. + */ + public void setBorderStyleModal(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TWindow.borderStyleModal", + "double"); + borderStyleActiveModal = BorderStyle.getStyle(style); + } else if (borderStyle.equals("default")) { + borderStyleActiveModal = BorderStyle.DOUBLE; + } else { + borderStyleActiveModal = BorderStyle.getStyle(borderStyle); + } + } + + /** + * Get the border style for the window when it is the modal window. + * + * @return the border style + */ + public BorderStyle getBorderStyleModal() { + return borderStyleActiveModal; + } + + /** + * Set the border style for the window when it is an inactive/background + * window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TWindow.borderStyleInactive. + */ + public void setBorderStyleInactive(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TWindow.borderStyleInactive", + "single"); + borderStyleInactive = BorderStyle.getStyle(style); + } else if (borderStyle.equals("default")) { + borderStyleInactive = BorderStyle.SINGLE; + } else { + borderStyleInactive = BorderStyle.getStyle(borderStyle); + } + } + + /** + * Get the border style for the window when it is an inactive/background + * window. + * + * @return the border style + */ + public BorderStyle getBorderStyleInactive() { + return borderStyleInactive; + } + + /** + * Set the border style for the window when it is being dragged/resize. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TWindow.borderStyleMoving. + */ + public void setBorderStyleMoving(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TWindow.borderStyleMoving", + "single"); + borderStyleMoving = BorderStyle.getStyle(style); + } else if (borderStyle.equals("default")) { + borderStyleMoving = BorderStyle.SINGLE; + } else { + borderStyleMoving = BorderStyle.getStyle(borderStyle); + } + } + + /** + * Get the border style for the window when it is being dragged/resize. + * + * @return the border style + */ + public BorderStyle getBorderStyleMoving() { + return borderStyleMoving; + } + + /** + * Get the custom color theme for this window. + * + * @return the color theme, or null if the window does not have a custom + * color theme + */ + public final ColorTheme getWindowTheme() { + return theme; + } + + /** + * Set a custom color theme for this window. + * + * @param theme the custom theme, or null to use the application-level + * color theme + */ + public final void setWindowTheme(final ColorTheme theme) { + this.theme = theme; + } + + /** + * Set the alpha level for this window. + * + * @param alpha a value between 0 (fully transparent) and 255 (fully + * opaque) + */ + public void setAlpha(final int alpha) { + assert ((alpha >= 0) && (alpha <= 255)); + this.alpha = alpha; + } + + /** + * Get the alpha level for this window. + * + * @return a value between 0 (fully transparent) and 255 (fully opaque) + */ + public int getAlpha() { + return alpha; + } + } diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 07ab596..d100a05 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -829,19 +829,31 @@ public class ECMA48Terminal extends LogicalScreen } if (output != null) { if (hasSynchronizedOutput) { - // Begin Synchronized Update (BSU) - output.write("\033[?2026h"); - output.write(sb.toString()); - // End Synchronized Update (ESU) - output.write("\033[?2026l"); + if (sb.length() > 0) { + // Begin Synchronized Update (BSU) + output.write("\033[?2026h"); + if (debugToStderr) { + System.err.printf("Writing %d bytes to terminal (sync)\n", + sb.length()); + } + output.write(sb.toString()); + // End Synchronized Update (ESU) + output.write("\033[?2026l"); + } if (debugToStderr) { System.err.printf("flushPhysical() \033[?2026h%s\033[?2026l\n", sb.toString()); } } else { - output.write(sb.toString()); + if (sb.length() > 0) { + if (debugToStderr) { + System.err.printf("Writing %d bytes to terminal\n", + sb.length()); + } + output.write(sb.toString()); + } } - flush(); + output.flush(); } } @@ -1073,24 +1085,41 @@ public class ECMA48Terminal extends LogicalScreen char [] readBuffer = new char[128]; List events = new ArrayList(); + // boolean debugToStderr = true; + while (!done && !stopReaderThread) { try { // We assume that if inputStream has bytes available, then // input won't block on read(). + if (debugToStderr) { + System.err.printf("Looking for input..."); + } + int n = inputStream.available(); - /* - System.err.printf("inputStream.available(): %d\n", n); - System.err.flush(); - */ + if (debugToStderr) { + if (n == 0) { + System.err.println("none."); + } + if (n < 0) { + System.err.printf("WHAT?! n = %d\n", n); + } + } if (n > 0) { + if (debugToStderr) { + System.err.printf("%d bytes to read.\n", n); + } + if (readBuffer.length < n) { // The buffer wasn't big enough, make it huger readBuffer = new char[readBuffer.length * 2]; } - // System.err.printf("BEFORE read()\n"); System.err.flush(); + if (debugToStderr) { + System.err.printf("B4 read(): readBuffer.length = %d\n", + readBuffer.length); + } int rc = input.read(readBuffer, 0, readBuffer.length); @@ -1100,9 +1129,22 @@ public class ECMA48Terminal extends LogicalScreen */ if (rc == -1) { + if (debugToStderr) { + System.err.println(" ---- EOF ----"); + } + // This is EOF done = true; } else { + if (debugToStderr) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rc; i++) { + sb.append(readBuffer[i]); + } + System.err.printf("%d rc = %d INPUT: ", + System.currentTimeMillis(), rc); + System.err.println(sb.toString()); + } for (int i = 0; i < rc; i++) { int ch = readBuffer[i]; processChar(events, (char)ch); @@ -1111,27 +1153,62 @@ public class ECMA48Terminal extends LogicalScreen if (events.size() > 0) { // Add to the queue for the backend thread to // be able to obtain. + if (debugToStderr) { + System.err.printf("Checking eventQueue..."); + } + synchronized (eventQueue) { eventQueue.addAll(events); } + if (debugToStderr) { + System.err.printf("done.\n"); + } + if (listener != null) { + if (debugToStderr) { + System.err.printf("Waking up listener..."); + } + synchronized (listener) { listener.notifyAll(); } + if (debugToStderr) { + System.err.printf("done.\n"); + } + } events.clear(); } } } else { + if (debugToStderr) { + System.err.println("Looking for idle events"); + } getIdleEvents(events); if (events.size() > 0) { + if (debugToStderr) { + System.err.printf("Checking eventQueue..."); + } + synchronized (eventQueue) { eventQueue.addAll(events); } + if (debugToStderr) { + System.err.printf("done.\n"); + } + if (listener != null) { + if (debugToStderr) { + System.err.printf("Waking up listener..."); + } + synchronized (listener) { listener.notifyAll(); } + if (debugToStderr) { + System.err.printf("done.\n"); + } + } events.clear(); } @@ -1338,6 +1415,7 @@ public class ECMA48Terminal extends LogicalScreen y, x, lastX, textEnd); System.err.printf(" lCell: %s\n", lCell); System.err.printf(" pCell: %s\n", pCell); + System.err.printf(" lastAttr: %s\n", lastAttr); System.err.printf(" ==== \n"); } @@ -1420,158 +1498,84 @@ public class ECMA48Terminal extends LogicalScreen assert (!lCell.isImage()); // Now emit only the modified attributes - if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Both colors changed, attributes the same - sb.append(color(lCell.isBold(), - lCell.getForeColor(), lCell.getBackColor())); - - if (debugToStderr && reallyDebug) { - System.err.printf("1 Change only fore/back colors\n"); - } - - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Both colors changed, attributes the same - sb.append(colorRGB(lCell.getForeColorRGB(), - lCell.getBackColorRGB())); - - if (debugToStderr && reallyDebug) { - System.err.printf("1 Change only fore/back colors (RGB)\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() != lastAttr.isBold()) - && (lCell.isReverse() != lastAttr.isReverse()) - && (lCell.isUnderline() != lastAttr.isUnderline()) - && (lCell.isBlink() != lastAttr.isBlink()) - ) { - // Everything is different - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr && reallyDebug) { - System.err.printf("2 Set all attributes\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // Attributes same, foreColor different - sb.append(color(lCell.isBold(), - lCell.getForeColor(), true)); - - if (debugToStderr && reallyDebug) { - System.err.printf("3 Change foreColor\n"); - } - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) - && (lCell.getForeColorRGB() >= 0) - && (lCell.getBackColorRGB() >= 0) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, foreColor different - sb.append(colorRGB(lCell.getForeColorRGB(), true)); - - if (debugToStderr && reallyDebug) { - System.err.printf("3 Change foreColor (RGB)\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, backColor different - sb.append(color(lCell.isBold(), - lCell.getBackColor(), false)); - - if (debugToStderr && reallyDebug) { - System.err.printf("4 Change backColor\n"); - } - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, foreColor different - sb.append(colorRGB(lCell.getBackColorRGB(), false)); - - if (debugToStderr && reallyDebug) { - System.err.printf("5 Change backColor (RGB)\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // All attributes the same, just print the char - // NOP - - if (debugToStderr && reallyDebug) { - System.err.printf("6 Only emit character\n"); - } - } else { - // Just reset everything again - if (!lCell.isRGB()) { - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr && reallyDebug) { - System.err.printf("7 Change all attributes\n"); - } + StringBuilder attrSgr = new StringBuilder(8); + if (lCell.isBold() != lastAttr.isBold()) { + if (lCell.isBold()) { + attrSgr.append(";1"); } else { - sb.append(colorRGB(lCell.getForeColorRGB(), - lCell.getBackColorRGB(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - if (debugToStderr && reallyDebug) { - System.err.printf("8 Change all attributes (RGB)\n"); - } + attrSgr.append(";22"); } - } + if (lCell.isUnderline() != lastAttr.isUnderline()) { + if (lCell.isUnderline()) { + attrSgr.append(";4"); + } else { + attrSgr.append(";24"); + } + } + if (lCell.isBlink() != lastAttr.isBlink()) { + if (lCell.isBlink()) { + attrSgr.append(";5"); + } else { + attrSgr.append(";25"); + } + } + if (lCell.isReverse() != lastAttr.isReverse()) { + if (lCell.isReverse()) { + attrSgr.append(";7"); + } else { + attrSgr.append(";27"); + } + } + if (attrSgr.length() > 0) { + if (debugToStderr && reallyDebug) { + System.err.println("2 attr: " + attrSgr.substring(1)); + } + sb.append("\033["); + sb.append(attrSgr.substring(1)); + sb.append("m"); + } + + if ((lCell.getForeColorRGB() >= 0) + && ((lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) + || (lastAttr.getForeColorRGB() < 0)) + ) { + if (debugToStderr && reallyDebug) { + System.err.println("3 set foreColorRGB"); + } + sb.append(colorRGB(lCell.getForeColorRGB(), true)); + } else { + if ((lCell.getForeColorRGB() < 0) + && ((lastAttr.getForeColorRGB() >= 0) + || !lCell.getForeColor().equals(lastAttr.getForeColor())) + ) { + if (debugToStderr && reallyDebug) { + System.err.println("4 set foreColor"); + } + sb.append(color(lCell.getForeColor(), true, true)); + } + } + + if ((lCell.getBackColorRGB() >= 0) + && ((lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) + || (lastAttr.getBackColorRGB() < 0)) + ) { + if (debugToStderr && reallyDebug) { + System.err.println("5 set backColorRGB"); + } + sb.append(colorRGB(lCell.getBackColorRGB(), false)); + } else { + if ((lCell.getBackColorRGB() < 0) + && ((lastAttr.getBackColorRGB() >= 0) + || !lCell.getBackColor().equals(lastAttr.getBackColor())) + ) { + if (debugToStderr && reallyDebug) { + System.err.println("6 set backColor"); + } + sb.append(color(lCell.getBackColor(), false, true)); + } + } + // Emit the character if (wideCharImages // Don't emit the right-half of full-width chars. @@ -2457,6 +2461,10 @@ public class ECMA48Terminal extends LogicalScreen default: break; } + + // We have changed a system color. Redraw the entire screen. + clearPhysical(); + reallyCleared = true; } catch (NumberFormatException e) { return; } @@ -2489,7 +2497,7 @@ public class ECMA48Terminal extends LogicalScreen boolean alt = false; boolean shift = false; - if (debugToStderr && false) { + if (debugToStderr) { System.err.printf("state: %s ch %c\r\n", state, ch); } @@ -4341,6 +4349,59 @@ public class ECMA48Terminal extends LogicalScreen return sb.toString(); } + /** + * Create a SGR parameter sequence for several attributes. This sequence + * first resets all attributes to default, then sets attributes as per + * the parameters. + * + * @param bold if true, set bold + * @param reverse if true, set reverse + * @param blink if true, set blink + * @param underline if true, set underline + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[0;1;5m" + */ + private String attributes(final boolean bold, final boolean reverse, + final boolean blink, final boolean underline) { + + StringBuilder sb = new StringBuilder(); + if ( bold && reverse && blink && !underline ) { + sb.append("\033[0;1;7;5m"); + } else if ( bold && reverse && !blink && !underline ) { + sb.append("\033[0;1;7m"); + } else if ( !bold && reverse && blink && !underline ) { + sb.append("\033[0;7;5m"); + } else if ( bold && !reverse && blink && !underline ) { + sb.append("\033[0;1;5m"); + } else if ( bold && !reverse && !blink && !underline ) { + sb.append("\033[0;1m"); + } else if ( !bold && reverse && !blink && !underline ) { + sb.append("\033[0;7m"); + } else if ( !bold && !reverse && blink && !underline) { + sb.append("\033[0;5m"); + } else if ( bold && reverse && blink && underline ) { + sb.append("\033[0;1;7;5;4m"); + } else if ( bold && reverse && !blink && underline ) { + sb.append("\033[0;1;7;4m"); + } else if ( !bold && reverse && blink && underline ) { + sb.append("\033[0;7;5;4m"); + } else if ( bold && !reverse && blink && underline ) { + sb.append("\033[0;1;5;4m"); + } else if ( bold && !reverse && !blink && underline ) { + sb.append("\033[0;1;4m"); + } else if ( !bold && reverse && !blink && underline ) { + sb.append("\033[0;7;4m"); + } else if ( !bold && !reverse && blink && underline) { + sb.append("\033[0;5;4m"); + } else if ( !bold && !reverse && !blink && underline) { + sb.append("\033[0;4m"); + } else { + assert (!bold && !reverse && !blink && !underline); + sb.append("\033[0m"); + } + return sb.toString(); + } + /** * Create a SGR parameter sequence for foreground, background, and * several attributes. This sequence first resets all attributes to diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index d351ff9..7178edd 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -28,13 +28,16 @@ */ package jexer.backend; +import java.awt.AlphaComposite; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import jexer.backend.GlyphMaker; +import jexer.bits.BorderStyle; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Clipboard; import jexer.bits.GraphicsChars; +import jexer.bits.ImageUtils; import jexer.bits.StringUtils; /** @@ -142,12 +145,22 @@ public class LogicalScreen implements Screen { * Public constructor. Sets everything to not-bold, white-on-black. */ protected LogicalScreen() { - offsetX = 0; - offsetY = 0; - width = 80; - height = 24; - logical = null; - physical = null; + this(80, 24); + } + + /** + * Public constructor. Sets everything to not-bold, white-on-black. + * + * @param width width in cells + * @param height height in cells + */ + protected LogicalScreen(final int width, final int height) { + offsetX = 0; + offsetY = 0; + this.width = 80; + this.height = 24; + logical = null; + physical = null; reallocate(width, height); } @@ -748,7 +761,8 @@ public class LogicalScreen implements Screen { final int right, final int bottom, final CellAttributes border, final CellAttributes background) { - drawBox(left, top, right, bottom, border, background, 1, false); + drawBox(left, top, right, bottom, border, background, + BorderStyle.DEFAULT, false); } /** @@ -760,57 +774,23 @@ public class LogicalScreen implements Screen { * @param bottom bottom row of the box * @param border attributes to use for the border * @param background attributes to use for the background - * @param borderType if 1, draw a single-line border; if 2, draw a - * double-line border; if 3, draw double-line top/bottom edges and - * single-line left/right edges (like Qmodem) + * @param borderStyle style of border * @param shadow if true, draw a "shadow" on the box */ - public final void drawBox(final int left, final int top, + public void drawBox(final int left, final int top, final int right, final int bottom, final CellAttributes border, final CellAttributes background, - final int borderType, final boolean shadow) { + final BorderStyle borderStyle, final boolean shadow) { int boxWidth = right - left; int boxHeight = bottom - top; - char cTopLeft; - char cTopRight; - char cBottomLeft; - char cBottomRight; - char cHSide; - char cVSide; - - switch (borderType) { - case 1: - cTopLeft = GraphicsChars.ULCORNER; - cTopRight = GraphicsChars.URCORNER; - cBottomLeft = GraphicsChars.LLCORNER; - cBottomRight = GraphicsChars.LRCORNER; - cHSide = GraphicsChars.SINGLE_BAR; - cVSide = GraphicsChars.WINDOW_SIDE; - break; - - case 2: - cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE; - cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE; - cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE; - cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE; - cHSide = GraphicsChars.DOUBLE_BAR; - cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE; - break; - - case 3: - cTopLeft = GraphicsChars.WINDOW_LEFT_TOP; - cTopRight = GraphicsChars.WINDOW_RIGHT_TOP; - cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM; - cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM; - cHSide = GraphicsChars.WINDOW_TOP; - cVSide = GraphicsChars.WINDOW_SIDE; - break; - default: - throw new IllegalArgumentException("Invalid border type: " - + borderType); - } + int cTopLeft = borderStyle.getTopLeft(); + int cTopRight = borderStyle.getTopRight(); + int cBottomLeft = borderStyle.getBottomLeft(); + int cBottomRight = borderStyle.getBottomRight(); + int cHSide = borderStyle.getHorizontal(); + int cVSide = borderStyle.getVertical(); // Place the corner characters putCharXY(left, top, cTopLeft, border); @@ -1289,8 +1269,7 @@ public class LogicalScreen implements Screen { public Screen snapshot() { LogicalScreen other = null; synchronized (this) { - other = new LogicalScreen(); - other.setDimensions(width, height); + other = new LogicalScreen(width, height); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { other.logical[col][row] = new Cell(logical[col][row]); @@ -1300,6 +1279,83 @@ public class LogicalScreen implements Screen { return other; } + /** + * Obtain a snapshot copy of a rectangular portion of the screen. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @return a copy of the screen's data from this rectangle. Any cells + * outside the actual screen dimensions will be blank. + */ + public Screen snapshot(final int x, final int y, final int width, + final int height) { + + LogicalScreen other = null; + synchronized (this) { + other = new LogicalScreen(width, height); + for (int row = y; (row < y + height) && (row < this.height); row++) { + if (row < 0) { + continue; + } + for (int col = x; (col < x + width) && (col < this.width); col++) { + if (col < 0) { + continue; + } + other.logical[col - x][row - y] = new Cell(logical[col][row]); + } + } + } + return other; + } + + /** + * Copy all of screen's data to this screen. + * + * @param other the other screen + */ + public void copyScreen(final Screen other) { + synchronized (this) { + if ((other.getWidth() != width) || (other.getHeight() != height)) { + setDimensions(other.getWidth(), other.getHeight()); + } + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + logical[col][row] = new Cell(other.getCharXY(col, row)); + } + } + } + } + + /** + * Copy a rectangular portion of another screen to this one. Any cells + * outside this screen's dimensions will be ignored. + * + * @param other the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + */ + public void copyScreen(final Screen other, final int x, final int y, + final int width, final int height) { + + synchronized (this) { + for (int row = y; (row < y + height) && (row < this.height); row++) { + if (row < 0) { + continue; + } + for (int col = x; (col < x + width) && (col < this.width); col++) { + if (col < 0) { + continue; + } + logical[col][row] = new Cell(other.getCharXY(col - x, row - y)); + } + } + } + } + /** * Set the backend to associated with this screen. * @@ -1318,4 +1374,313 @@ public class LogicalScreen implements Screen { return backend; } + /** + * Alpha-blend a rectangle with a specified color and alpha onto this + * screen. Any cells outside this screen's dimensions will be ignored. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param color the RGB color to blend + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + */ + public void blendRectangle(final int x, final int y, + final int width, final int height, final int color, final int alpha) { + + // We just create a new blank screen and blend it. + LogicalScreen rectangle = new LogicalScreen(width, height); + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + rectangle.logical[col][row].setBackColorRGB(color); + } + } + + blendScreen(rectangle, x, y, width, height, alpha, false); + } + + /** + * Alpha-blend a rectangular portion of another screen onto this one. + * Any cells outside this screen's dimensions will be ignored. + * + * @param otherScreen the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + * @param filterHatch if true, prevent hatch-like characters from + * showing through + */ + public void blendScreen(final Screen otherScreen, final int x, final int y, + final int width, final int height, final int alpha, + final boolean filterHatch) { + + if (alpha == 255) { + // This is a raw copy. + copyScreen(otherScreen, x, y, width, height); + return; + } + + /* + * We need to blend the background colors of other's cells over the + * cells of this screen (foreground and background), honoring our + * alpha. We will create a bitmap of one pixel per cell, blend that + * via AWT, and then set the cell RGBs and char's. + */ + synchronized (this) { + + BufferedImage thisForeground = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + BufferedImage thisBackground = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + BufferedImage overBackground = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + BufferedImage thisOldBackground = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + + final int OPAQUE = 0xFF000000; + + for (int row = y; (row < y + height) && (row < this.height); row++) { + if (row < 0) { + continue; + } + for (int col = x; (col < x + width) && (col < this.width); col++) { + if (col < 0) { + continue; + } + + Cell cell = logical[col][row]; + int thisBg = cell.getBackColorRGB(); + if (thisBg < 0) { + if (backend != null) { + thisBg = backend.attrToBackgroundColor(cell).getRGB(); + } else { + thisBg = SwingTerminal.attrToBackgroundColor(cell).getRGB(); + } + } + int thisFg = cell.getForeColorRGB(); + if (thisFg < 0) { + if (backend != null) { + thisFg = backend.attrToForegroundColor(cell).getRGB(); + } else { + thisFg = SwingTerminal.attrToForegroundColor(cell).getRGB(); + } + } + + Cell over = otherScreen.getCharXY(col - x, row - y); + int overBg = over.getBackColorRGB(); + if (overBg < 0) { + if (backend != null) { + overBg = backend.attrToBackgroundColor(over).getRGB(); + } else { + overBg = SwingTerminal.attrToBackgroundColor(over).getRGB(); + } + } + thisFg |= OPAQUE; + thisBg |= OPAQUE; + overBg |= OPAQUE; + + thisForeground.setRGB(col - x, row - y, thisFg); + thisBackground.setRGB(col - x, row - y, thisBg); + thisOldBackground.setRGB(col - x, row - y, thisBg); + overBackground.setRGB(col - x, row - y, overBg); + } + } + + // The three bitmaps are ready. We have skipped over + // cells/pixels that cannot overlap. Now blit overBackground + // over both thisForeground and thisBackground, and then assign + // cell colors and cell chars/images. + float fAlpha = (float) (alpha / 255.0); + Graphics2D g2d = thisForeground.createGraphics(); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.drawImage(overBackground, 0, 0, null); + g2d.dispose(); + + g2d = thisBackground.createGraphics(); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.drawImage(overBackground, 0, 0, null); + g2d.dispose(); + + for (int row = y; (row < y + height) && (row < this.height); row++) { + if (row < 0) { + continue; + } + for (int col = x; (col < x + width) && (col < this.width); col++) { + if (col < 0) { + continue; + } + Cell thisCell = logical[col][row]; + Cell overCell = otherScreen.getCharXY(col - x, row - y); + int thisFg = thisForeground.getRGB(col - x, row - y); + int thisBg = thisBackground.getRGB(col - x, row - y); + int thisOldBg = thisOldBackground.getRGB(col - x, row - y); + int overBg = overBackground.getRGB(col - x, row - y); + + thisCell.setBackColorRGB(thisBg | OPAQUE); + thisCell.setForeColorRGB(thisFg | OPAQUE); + + if (!overCell.isImage() && (overCell.getChar() == ' ')) { + // The overlaying cell is invisible. + + if (thisCell.isImage()) { + // Our image will show through. We need to blend + // otherBg at alpha < 255 over this image. + Cell thisCopy = new Cell(thisCell); + thisCopy.flattenImage(false, backend); + BufferedImage image = thisCopy.getImage(); + BufferedImage newImage; + newImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + g2d = newImage.createGraphics(); + g2d.drawImage(image, 0, 0, null); + + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.setColor(new java.awt.Color(overBg)); + g2d.fillRect(0, 0, image.getWidth(), + image.getHeight()); + g2d.dispose(); + thisCell.setImage(newImage); + thisCell.setOpaqueImage(); + } else { + // Our character will show through. If the + // contrast between our foreground and background + // is small, then drop the character. + if (ImageUtils.rgbDistance(thisFg, thisBg) < 5) { + thisCell.setChar(' '); + } + + if (filterHatch) { + // Special case: the hatch characters are not + // allowed to show through. + int ch = thisCell.getChar(); + if ((ch == 0x2591) + || (ch == 0x2592) + || (ch == 0x2593) + ) { + thisCell.setChar(' '); + } + } + if (cursorVisible && + (col == cursorX) && + (row == cursorY) + ) { + // Don't surface the character behind the + // cursor. + thisCell.setChar(' '); + } + } + continue; + } + + // The overlaying cell has a character, use it. + thisCell.setChar(overCell.getChar()); + int fg = overCell.getForeColorRGB(); + if (fg < 0) { + thisCell.setForeColor(overCell.getForeColor()); + } else { + thisCell.setForeColorRGB(fg); + } + thisCell.setBold(overCell.isBold()); + thisCell.setBlink(overCell.isBlink()); + thisCell.setUnderline(overCell.isUnderline()); + thisCell.setProtect(overCell.isProtect()); + + if (!overCell.isImage()) { + // If we had an image, destroy it. Text ALWAYS + // overwrites images. + thisCell.setImage(null); + continue; + } + + if (overCell.isImage() && !overCell.isTransparentImage()) { + // The image from the new cell will fully cover this + // cell's image. + + // We need to blit overCell's image over thisOldBg at + // alpha < 255. + Cell overCopy = new Cell(overCell); + overCopy.flattenImage(false, backend); + BufferedImage image = overCopy.getImage(); + BufferedImage newImage; + newImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + g2d = newImage.createGraphics(); + g2d.setColor(new java.awt.Color(thisOldBg)); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + thisCell.setImage(newImage); + thisCell.setOpaqueImage(); + continue; + } + + if (thisCell.isImage() + && overCell.isImage() + && overCell.isTransparentImage() + ) { + // We need to blit overCell's image over a rectangle + // of otherBg at alpha = 255, and then blit that over + // thisCell's image at alpha < 255. + + Cell overCopy = new Cell(overCell); + overCopy.flattenImage(false, backend); + BufferedImage image = overCopy.getImage(); + BufferedImage newImage; + newImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + g2d = newImage.createGraphics(); + g2d.drawImage(thisCell.getImage(), 0, 0, null); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + thisCell.setImage(newImage); + thisCell.setOpaqueImage(); + continue; + } + + if (!thisCell.isImage() + && overCell.isImage() + && overCell.isTransparentImage() + ) { + // We need to blit overCell's image over a rectangle + // of otherBg at alpha = 255, and blit that over + // thisOldBg at alpha < 255. + + Cell overCopy = new Cell(overCell); + overCopy.flattenImage(false, backend); + BufferedImage image = overCopy.getImage(); + BufferedImage newImage; + newImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_ARGB); + g2d = newImage.createGraphics(); + g2d.setColor(new java.awt.Color(thisOldBg)); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + fAlpha)); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + thisCell.setImage(newImage); + thisCell.setOpaqueImage(); + continue; + } + + // There should be nothing to do now. We have set the + // character, or set the image, and blended backgrounds + // for each case. + } + } + } + } + + } diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java index 26183f6..d2fbca5 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -31,6 +31,7 @@ package jexer.backend; import java.util.ArrayList; import java.util.List; +import jexer.bits.BorderStyle; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Clipboard; @@ -608,7 +609,7 @@ public class MultiScreen implements Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -629,26 +630,24 @@ public class MultiScreen implements Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box * @param border attributes to use for the border * @param background attributes to use for the background - * @param borderType if 1, draw a single-line border; if 2, draw a - * double-line border; if 3, draw double-line top/bottom edges and - * single-line left/right edges (like Qmodem) + * @param borderStyle style of border * @param shadow if true, draw a "shadow" on the box */ public void drawBox(final int left, final int top, final int right, final int bottom, final CellAttributes border, final CellAttributes background, - final int borderType, final boolean shadow) { + final BorderStyle borderStyle, final boolean shadow) { synchronized (screens) { for (Screen screen: screens) { screen.drawBox(left, top, right, bottom, border, background, - borderType, shadow); + borderStyle, shadow); } } } @@ -656,7 +655,7 @@ public class MultiScreen implements Screen { /** * Draw a box shadow. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -952,6 +951,118 @@ public class MultiScreen implements Screen { } } + /** + * Obtain a snapshot copy of a rectangular portion of the screen. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @return a copy of the screen's data from this rectangle. Any cells + * outside the actual screen dimensions will be blank. + */ + public Screen snapshot(final int x, final int y, final int width, + final int height) { + + synchronized (screens) { + // Only copy from the first screen. + if (screens.size() > 0) { + return screens.get(0).snapshot(x, y, width, height); + } + + // No screens are defined, create a blank. + + LogicalScreen other = null; + + other = new LogicalScreen(); + int newWidth = x + width; + int newHeight = y + 25; + other.setDimensions(newWidth, newHeight); + return other; + } + } + + /** + * Copy all of screen's data to this screen. + * + * @param other the other screen + */ + public void copyScreen(final Screen other) { + synchronized (screens) { + for (Screen screen: screens) { + screen.copyScreen(other); + } + } + } + + /** + * Copy a rectangular portion of another screen to this one. Any cells + * outside this screen's dimensions will be ignored. + * + * @param other the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + */ + public void copyScreen(final Screen other, final int x, final int y, + final int width, final int height) { + + synchronized (screens) { + for (Screen screen: screens) { + screen.copyScreen(other, x, y, width, height); + } + } + } + + /** + * Alpha-blend a rectangular portion of another screen onto this one. + * Any cells outside this screen's dimensions will be ignored. + * + * @param otherScreen the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + * @param filterHatch if true, prevent hatch-like characters from + * showing through + */ + public void blendScreen(final Screen otherScreen, final int x, final int y, + final int width, final int height, final int alpha, + final boolean filterHatch) { + + synchronized (screens) { + for (Screen screen: screens) { + screen.blendScreen(otherScreen, x, y, width, height, alpha, + filterHatch); + } + } + } + + /** + * Alpha-blend a rectangle with a specified color and alpha onto this + * screen. Any cells outside this screen's dimensions will be ignored. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param color the RGB color to blend + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + */ + public void blendRectangle(final int x, final int y, + final int width, final int height, final int color, final int alpha) { + + synchronized (screens) { + for (Screen screen: screens) { + screen.blendRectangle(x, y, width, height, color, alpha); + } + } + } + /** * Set the backend to associated with this screen. * diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java index cbd441b..3ee732f 100644 --- a/src/jexer/backend/Screen.java +++ b/src/jexer/backend/Screen.java @@ -28,6 +28,7 @@ */ package jexer.backend; +import jexer.bits.BorderStyle; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.Clipboard; @@ -321,7 +322,7 @@ public interface Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -335,26 +336,24 @@ public interface Screen { /** * Draw a box with a border and empty background. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box * @param border attributes to use for the border * @param background attributes to use for the background - * @param borderType if 1, draw a single-line border; if 2, draw a - * double-line border; if 3, draw double-line top/bottom edges and - * single-line left/right edges (like Qmodem) + * @param borderStyle style of border * @param shadow if true, draw a "shadow" on the box */ public void drawBox(final int left, final int top, final int right, final int bottom, final CellAttributes border, final CellAttributes background, - final int borderType, final boolean shadow); + final BorderStyle borderStyle, final boolean shadow); /** * Draw a box shadow. * - * @param left left column of box. 0 is the left-most row. + * @param left left column of box. 0 is the left-most column. * @param top top row of the box. 0 is the top-most row. * @param right right column of box * @param bottom bottom row of the box @@ -490,6 +489,72 @@ public interface Screen { */ public Screen snapshot(); + /** + * Obtain a snapshot copy of a rectangular portion of the screen. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @return a copy of the screen's data from this rectangle. Any cells + * outside the actual screen dimensions will be blank. + */ + public Screen snapshot(final int x, final int y, final int width, + final int height); + + /** + * Copy all of screen's data to this screen. + * + * @param other the other screen + */ + public void copyScreen(final Screen other); + + /** + * Copy a rectangular portion of another screen to this one. Any cells + * outside this screen's dimensions will be ignored. + * + * @param other the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + */ + public void copyScreen(final Screen other, final int x, final int y, + final int width, final int height); + + /** + * Alpha-blend a rectangular portion of another screen onto this one. + * Any cells outside this screen's dimensions will be ignored. + * + * @param otherScreen the other screen + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + * @param filterHatch if true, prevent hatch-like characters from + * showing through + */ + public void blendScreen(final Screen otherScreen, final int x, final int y, + final int width, final int height, final int alpha, + final boolean filterHatch); + + /** + * Alpha-blend a rectangle with a specified color and alpha onto this + * screen. Any cells outside this screen's dimensions will be ignored. + * + * @param x left column of rectangle. 0 is the left-most column. + * @param y top row of the rectangle. 0 is the top-most row. + * @param width number of columns to copy + * @param height number of rows to copy + * @param color the RGB color to blend + * @param alpha the alpha transparency level (0 - 255) to use for cells + * from the other screen + */ + public void blendRectangle(final int x, final int y, + final int width, final int height, final int color, final int alpha); + /** * Get the backend that instantiated this screen. * diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index dd986b0..ee62011 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -119,7 +119,7 @@ public class SwingTerminal extends LogicalScreen * A value of 25 or more feels sluggish for input, but is sustainable for * the garbage collector. */ - private static final long SYNC_MIN_MILLIS_SUSTAIN = 20; + private static final long SYNC_MIN_MILLIS_SUSTAIN = 10; /** * The number of frames that can be emitted quickly (at @@ -1611,7 +1611,7 @@ public class SwingTerminal extends LogicalScreen xPixel -= textWidth; break; } - gr.setColor(attrToForegroundColor(lCell)); + gr.setColor((Color.WHITE).darker()); switch (cursorStyle) { default: // Fall through... diff --git a/src/jexer/bits/BorderStyle.java b/src/jexer/bits/BorderStyle.java new file mode 100644 index 0000000..2c896b2 --- /dev/null +++ b/src/jexer/bits/BorderStyle.java @@ -0,0 +1,277 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2022 Autumn 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 Autumn Lamonte ⚧ Trans Liberation Now + * @version 1 + */ +package jexer.bits; + +/** + * A text box border style. + */ +public class BorderStyle { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The default border style. Synonym for SINGLE. + */ + public static final BorderStyle DEFAULT; + + /** + * The "no-border" style. + */ + public static final BorderStyle NONE; + + /** + * A single-line border. + */ + public static final BorderStyle SINGLE; + + /** + * A double-line border. + */ + public static final BorderStyle DOUBLE; + + /** + * A single-line border on the vertical sections, double-line on the + * horizontal sections. + */ + public static final BorderStyle SINGLE_V_DOUBLE_H; + + /** + * A double-line border on the vertical sections, single-line on the + * horizontal sections. + */ + public static final BorderStyle SINGLE_H_DOUBLE_V; + + /** + * A single-line border with round corners. + */ + public static final BorderStyle SINGLE_ROUND; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The glyph for horizontal sections. + */ + private int horizontal; + + /** + * The glyph for vertical sections. + */ + private int vertical; + + /** + * The glyph for the top-left corner. + */ + private int topLeft; + + /** + * The glyph for the top-right corner. + */ + private int topRight; + + /** + * The glyph for the bottom-left corner. + */ + private int bottomLeft; + + /** + * The glyph for the bottom-right corner. + */ + private int bottomRight; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Static constructor. + */ + static { + + NONE = new BorderStyle(' ', ' ', ' ', ' ', ' ', ' '); + + SINGLE = new BorderStyle(GraphicsChars.SINGLE_BAR, + GraphicsChars.WINDOW_SIDE, + GraphicsChars.ULCORNER, + GraphicsChars.URCORNER, + GraphicsChars.LLCORNER, + GraphicsChars.LRCORNER); + + DOUBLE = new BorderStyle(GraphicsChars.DOUBLE_BAR, + GraphicsChars.WINDOW_SIDE_DOUBLE, + GraphicsChars.WINDOW_LEFT_TOP_DOUBLE, + GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE, + GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE, + GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE); + + SINGLE_V_DOUBLE_H = new BorderStyle(GraphicsChars.WINDOW_TOP, + GraphicsChars.WINDOW_SIDE, + GraphicsChars.WINDOW_LEFT_TOP, + GraphicsChars.WINDOW_RIGHT_TOP, + GraphicsChars.WINDOW_LEFT_BOTTOM, + GraphicsChars.WINDOW_RIGHT_BOTTOM); + + SINGLE_H_DOUBLE_V = new BorderStyle(GraphicsChars.SINGLE_BAR, + GraphicsChars.WINDOW_SIDE_DOUBLE, + 0x2553, + 0x2556, + 0x2559, + 0x255C); + + SINGLE_ROUND = new BorderStyle(GraphicsChars.SINGLE_BAR, + GraphicsChars.WINDOW_SIDE, + 0x256D, + 0x256E, + 0x2570, + 0x256F); + + DEFAULT = SINGLE; + + } + + /** + * Private constructor used to make the static BorderStyle instances. + * + * @param horizontal the horizontal section glyph + * @param vertical the vertical section glyph + * @param topLeft the top-left corner glyph + * @param topRight the top-right corner glyph + * @param bottomLeft the bottom-left corner glyph + * @param bottomRight the bottom-right corner glyph + */ + private BorderStyle(final int horizontal, final int vertical, + final int topLeft, final int topRight, + final int bottomLeft, final int bottomRight) { + + this.horizontal = horizontal; + this.vertical = vertical; + this.topLeft = topLeft; + this.topRight = topRight; + this.bottomLeft = bottomLeft; + this.bottomRight = bottomRight; + } + + // ------------------------------------------------------------------------ + // BorderStyle ------------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * Public constructor returns one of the static BorderStyle instances. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round" + * @return BorderStyle.SINGLE, BorderStyle.DOUBLE, etc. + */ + public static final BorderStyle getStyle(final String borderStyle) { + String str = borderStyle.toLowerCase(); + + if (str.equals("none")) { + return NONE; + } + if (str.equals("default")) { + return SINGLE; + } + if (str.equals("single")) { + return SINGLE; + } + if (str.equals("double")) { + return DOUBLE; + } + if (str.equals("round")) { + return SINGLE_ROUND; + } + if (str.equals("singlevdoubleh")) { + return SINGLE_V_DOUBLE_H; + } + if (str.equals("singlehdoublev")) { + return SINGLE_H_DOUBLE_V; + } + + // If they didn't get it right, return single. + return SINGLE; + } + + /** + * Get the glyph for horizontal sections. + * + * @return the glyph for horizontal sections. + */ + public final int getHorizontal() { + return horizontal; + } + + /** + * + * Get the glyph for vertical sections. + * @return the glyph for vertical sections. + */ + public final int getVertical() { + return vertical; + } + + /** + * Get the glyph for the top-left corner. + * + * @return the glyph for the top-left corner. + */ + public final int getTopLeft() { + return topLeft; + } + + /** + * Get the glyph for the top-right corner. + * + * @return the glyph for the top-right corner. + */ + public final int getTopRight() { + return topRight; + } + + /** + * Get the glyph for the bottom-left corner. + * + * @return the glyph for the bottom-left corner. + */ + public final int getBottomLeft() { + return bottomLeft; + } + + /** + * Get the glyph for the bottom-right corner. + * + * @return the glyph for the bottom-right corner. + */ + public final int getBottomRight() { + return bottomRight; + } + +} diff --git a/src/jexer/bits/Cell.java b/src/jexer/bits/Cell.java index 8863c44..ad0e20d 100644 --- a/src/jexer/bits/Cell.java +++ b/src/jexer/bits/Cell.java @@ -31,6 +31,7 @@ package jexer.bits; import java.awt.image.BufferedImage; import jexer.backend.Backend; import jexer.backend.GlyphMaker; +import jexer.backend.SwingTerminal; /** * This class represents a single text cell or bit of image on the screen. @@ -184,7 +185,11 @@ public class Cell extends CellAttributes { public void setImage(final BufferedImage image) { this.image = image; hasTransparentPixels = 0; - imageHashCode = image.hashCode(); + if (image == null) { + imageHashCode = 0; + } else { + imageHashCode = image.hashCode(); + } width = Width.SINGLE; } @@ -255,7 +260,11 @@ public class Cell extends CellAttributes { BufferedImage newImage = new BufferedImage(textWidth, textHeight, BufferedImage.TYPE_INT_ARGB); java.awt.Graphics gr = newImage.getGraphics(); - gr.setColor(backend.attrToBackgroundColor(this)); + if (backend != null) { + gr.setColor(backend.attrToBackgroundColor(this)); + } else { + gr.setColor(SwingTerminal.attrToBackgroundColor(this)); + } if (overGlyph) { // Render this cell to a flat image. The bad news is that we @@ -273,6 +282,38 @@ public class Cell extends CellAttributes { setImage(newImage); } + /** + * Flatten the image on this cell by rendering it either onto a + * background color. + * + * @param background the background color to draw on + */ + private void flattenImage(final java.awt.Color background) { + assert (isImage()); + + if (hasTransparentPixels == 2) { + // The image already covers the entire cell. + return; + } + + int textWidth = image.getWidth(); + int textHeight = image.getHeight(); + BufferedImage newImage = new BufferedImage(textWidth, + textHeight, BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics gr = newImage.getGraphics(); + gr.setColor(background); + + // Put the background color behind the pixels. + gr.fillRect(0, 0, newImage.getWidth(), newImage.getHeight()); + gr.drawImage(image, 0, 0, null, null); + gr.dispose(); + + setImage(newImage); + + // We know we are opaque now. + hasTransparentPixels = 2; + } + /** * Blit another cell's image on top of the image data for this cell. * @@ -740,9 +781,11 @@ public class Cell extends CellAttributes { */ @Override public String toString() { - return String.format("%s fore: %s back: %s bold: %s blink: %s ch %c", + return String.format("%s fore: %s RGB %06x back: %s RGB %06x bold: %s blink: %s ch %c", (isImage() ? "IMAGE" : ""), - getForeColor(), getBackColor(), isBold(), isBlink(), ch); + getForeColor(), getForeColorRGB(), + getBackColor(), getBackColorRGB(), + isBold(), isBlink(), ch); } /** diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java index 4e0d8bb..936c7dd 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/src/jexer/bits/CellAttributes.java @@ -125,7 +125,7 @@ public class CellAttributes { * @return bold value */ public final boolean isBold() { - return ((flags & BOLD) == 0 ? false : true); + return ((flags & BOLD) != 0); } /** @@ -268,7 +268,8 @@ public class CellAttributes { } /** - * Getter for foreColor RGB. + * Getter for foreColor RGB. Note that this is always a RGB value, + * i.e. alpha is 0. * * @return foreColor value. Negative means unset. */ @@ -282,11 +283,13 @@ public class CellAttributes { * @param foreColorRGB new foreColor RGB value */ public final void setForeColorRGB(final int foreColorRGB) { - this.foreColorRGB = foreColorRGB; + this.foreColorRGB = foreColorRGB & 0xFFFFFF; + this.foreColor = Color.WHITE; } /** - * Getter for backColor RGB. + * Getter for backColor RGB. Note that this is always a RGB value, + * i.e. alpha is 0. * * @return backColor value. Negative means unset. */ @@ -300,7 +303,8 @@ public class CellAttributes { * @param backColorRGB new backColor RGB value */ public final void setBackColorRGB(final int backColorRGB) { - this.backColorRGB = backColorRGB; + this.backColorRGB = backColorRGB & 0xFFFFFF; + this.backColor = Color.BLACK; } /** diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index 0304902..e7df6bb 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -738,6 +738,68 @@ public class ColorTheme { } + /** + * Set the theme to femme. I love pink. You can too! 💗 + */ + public void setFemme() { + setDefaultTheme(); + final int pink = 0xf7a8b8; + final int blue = 0x55cdfc; + + for (String key: colors.keySet()) { + CellAttributes color = colors.get(key); + + Color fg = color.getForeColor(); + Color bg = color.getBackColor(); + boolean bold = color.isBold(); + if (bg.equals(Color.WHITE) && fg.equals(Color.BLACK)) { + color.setForeColor(Color.MAGENTA); + color.setBackColorRGB(pink); + } else if (bg.equals(Color.WHITE) && fg.equals(Color.WHITE)) { + color.setForeColor(Color.MAGENTA); + color.setBackColorRGB(pink); + color.setBold(true); + } else if (bg.equals(Color.WHITE) && fg.equals(Color.WHITE)) { + color.setForeColorRGB(blue); + color.setBackColorRGB(pink); + color.setBold(true); + } else if (bg.equals(Color.WHITE) && fg.equals(Color.GREEN)) { + color.setForeColor(Color.BLUE); + color.setBackColor(Color.BLACK); + color.setBold(true); + } else if (bg.equals(Color.WHITE) && fg.equals(Color.RED)) { + color.setForeColorRGB(blue); + color.setBackColorRGB(pink); + color.setBold(true); + } else if (bg.equals(Color.BLUE) && fg.equals(Color.CYAN)) { + color.setForeColor(Color.RED); + color.setBackColor(Color.MAGENTA); + color.setBold(true); + } else if (fg.equals(Color.BLUE) && bg.equals(Color.CYAN)) { + color.setForeColor(Color.MAGENTA); + color.setBackColor(Color.RED); + color.setBold(true); + } else if (bg.equals(Color.BLUE)) { + color.setBackColor(Color.BLACK); + } else if (bg.equals(Color.GREEN)) { + color.setBackColor(Color.CYAN); + } else if (fg.equals(Color.WHITE) && bold) { + color.setForeColor(Color.RED); + } + + colors.put(key, color); + } + + /* + CellAttributes color; + color = new CellAttributes(); + color.setForeColor(Color.MAGENTA); + color.setBackColorRGB(pink); + color.setBold(false); + colors.put("twindow.background.modal", color); + */ + } + /** * Make human-readable description of this Cell. * diff --git a/src/jexer/bits/ImageUtils.java b/src/jexer/bits/ImageUtils.java index 526f490..08e7899 100644 --- a/src/jexer/bits/ImageUtils.java +++ b/src/jexer/bits/ImageUtils.java @@ -55,6 +55,8 @@ import org.w3c.dom.NodeList; * - Scale an image and preserve aspect ratio. * * - Open an animated image as an Animation. + * + * - Compute the distance between two colors in RGB space. */ public class ImageUtils { @@ -445,4 +447,24 @@ public class ImageUtils { } } + /** + * Report the absolute distance in RGB space between two RGB colors. + * + * @param first the first color + * @param second the second color + * @return the distance + */ + public static int rgbDistance(final int first, final int second) { + int red = (first >>> 16) & 0xFF; + int green = (first >>> 8) & 0xFF; + int blue = first & 0xFF; + int red2 = (second >>> 16) & 0xFF; + int green2 = (second >>> 8) & 0xFF; + int blue2 = second & 0xFF; + double diff = Math.pow(red2 - red, 2); + diff += Math.pow(green2 - green, 2); + diff += Math.pow(blue2 - blue, 2); + return (int) Math.sqrt(diff); + } + } diff --git a/src/jexer/demos/DemoPixelsWindow.java b/src/jexer/demos/DemoPixelsWindow.java index d048a73..1b98491 100644 --- a/src/jexer/demos/DemoPixelsWindow.java +++ b/src/jexer/demos/DemoPixelsWindow.java @@ -277,6 +277,7 @@ public class DemoPixelsWindow extends TWindow { */ @Override public void onClose() { + super.onClose(); getApplication().removeTimer(timer3); } diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 262b28a..efcee57 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -34,6 +34,7 @@ import jexer.TApplication; import jexer.TKeypress; import jexer.TWidget; import jexer.TWindow; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.bits.MnemonicString; @@ -200,6 +201,22 @@ public class TMenu extends TWindow { useIcons = true; } + // Set the border style from the system properties + setBorderStyleForeground(null); + setBorderStyleInactive(null); + setBorderStyleModal(null); + setBorderStyleMoving(null); + + int opacity = 95; + try { + opacity = Integer.parseInt(System.getProperty( + "jexer.TMenu.opacity", "95")); + opacity = Math.max(opacity, 10); + opacity = Math.min(opacity, 100); + } catch (NumberFormatException e) { + // SQUASH + } + setAlpha(opacity * 255 / 100); } // ------------------------------------------------------------------------ @@ -389,17 +406,13 @@ public class TMenu extends TWindow { } // Draw the box - char cTopLeft; - char cTopRight; - char cBottomLeft; - char cBottomRight; - char cHSide; - - cTopLeft = GraphicsChars.ULCORNER; - cTopRight = GraphicsChars.URCORNER; - cBottomLeft = GraphicsChars.LLCORNER; - cBottomRight = GraphicsChars.LRCORNER; - cHSide = GraphicsChars.SINGLE_BAR; + BorderStyle borderStyle = getBorderStyle(); + int cTopLeft = borderStyle.getTopLeft(); + int cTopRight = borderStyle.getTopRight(); + int cBottomLeft = borderStyle.getBottomLeft(); + int cBottomRight = borderStyle.getBottomRight(); + int cHSide = borderStyle.getHorizontal(); + int cVSide = borderStyle.getVertical(); // Place the corner characters putCharXY(1, 0, cTopLeft, background); @@ -412,7 +425,9 @@ public class TMenu extends TWindow { hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background); // Draw a shadow - drawBoxShadow(0, 0, getWidth(), getHeight()); + if (!getApplication().hasTranslucence()) { + drawBoxShadow(0, 0, getWidth(), getHeight()); + } } // ------------------------------------------------------------------------ @@ -919,4 +934,77 @@ public class TMenu extends TWindow { super.resetTabOrder(); } + /** + * Set the border style for the window when it is the foreground window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TMenu.borderStyle. + */ + @Override + public void setBorderStyleForeground(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TMenu.borderStyle", + "single"); + super.setBorderStyleForeground(style); + } else { + super.setBorderStyleForeground(borderStyle); + } + } + + /** + * Set the border style for the window when it is the modal window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TMenu.borderStyle. + */ + @Override + public void setBorderStyleModal(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TMenu.borderStyle", + "single"); + super.setBorderStyleModal(style); + } else { + super.setBorderStyleModal(borderStyle); + } + } + + /** + * Set the border style for the window when it is an inactive/background + * window. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TMenu.borderStyle. + */ + @Override + public void setBorderStyleInactive(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TMenu.borderStyle", + "single"); + super.setBorderStyleInactive(style); + } else { + super.setBorderStyleInactive(borderStyle); + } + } + + /** + * Set the border style for the window when it is being dragged/resize. + * + * @param borderStyle the border style string, one of: "default", "none", + * "single", "double", "singleVdoubleH", "singleHdoubleV", or "round"; or + * null to use the value from jexer.TMenu.borderStyle. + */ + @Override + public void setBorderStyleMoving(final String borderStyle) { + if (borderStyle == null) { + String style = System.getProperty("jexer.TMenu.borderStyle", + "single"); + super.setBorderStyleMoving(style); + } else { + super.setBorderStyleMoving(borderStyle); + } + } + } diff --git a/src/jexer/menu/TMenuItem.java b/src/jexer/menu/TMenuItem.java index 107fbc9..e9cd90d 100644 --- a/src/jexer/menu/TMenuItem.java +++ b/src/jexer/menu/TMenuItem.java @@ -31,6 +31,7 @@ package jexer.menu; import jexer.TKeypress; import jexer.TWidget; import jexer.backend.Backend; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.bits.MnemonicString; @@ -249,7 +250,8 @@ public class TMenuItem extends TWidget { boolean useIcons = ((TMenu) getParent()).useIcons; - char cVSide = GraphicsChars.WINDOW_SIDE; + BorderStyle borderStyle = ((TMenu) getParent()).getBorderStyle(); + int cVSide = borderStyle.getVertical(); vLineXY(0, 0, 1, cVSide, background); vLineXY(getWidth() - 1, 0, 1, cVSide, background); diff --git a/src/jexer/menu/TMenuSeparator.java b/src/jexer/menu/TMenuSeparator.java index 85474ca..1ece811 100644 --- a/src/jexer/menu/TMenuSeparator.java +++ b/src/jexer/menu/TMenuSeparator.java @@ -28,6 +28,7 @@ */ package jexer.menu; +import jexer.bits.BorderStyle; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; @@ -65,9 +66,22 @@ public class TMenuSeparator extends TMenuItem { public void draw() { CellAttributes background = getTheme().getColor("tmenu"); - putCharXY(0, 0, GraphicsChars.CP437[0xC3], background); - putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], background); - hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR, background); + BorderStyle borderStyle = ((TMenu) getParent()).getBorderStyle(); + int cHSide = GraphicsChars.SINGLE_BAR; + int left = GraphicsChars.CP437[0xC3]; + int right = GraphicsChars.CP437[0xB4]; + if (borderStyle.getVertical() == GraphicsChars.WINDOW_SIDE_DOUBLE) { + left = 0x255F; + right = 0x2562; + } + if (borderStyle.equals(BorderStyle.NONE)) { + left = ' '; + right = ' '; + } + + putCharXY(0, 0, left, background); + putCharXY(getWidth() - 1, 0, right, background); + hLineXY(1, 0, getWidth() - 2, cHSide, background); } } diff --git a/src/jexer/tackboard/Bitmap.java b/src/jexer/tackboard/Bitmap.java index 33e6dbe..58128fb 100644 --- a/src/jexer/tackboard/Bitmap.java +++ b/src/jexer/tackboard/Bitmap.java @@ -164,6 +164,19 @@ public class Bitmap extends TackboardItem { return renderedImage; } + /** + * Remove this item from its board. Subclasses can use this for cleanup + * also. + */ + @Override + public void remove() { + super.remove(); + + if (animation != null) { + animation.stop(); + } + } + // ------------------------------------------------------------------------ // Bitmap ----------------------------------------------------------------- // ------------------------------------------------------------------------ diff --git a/src/jexer/tackboard/Tackboard.java b/src/jexer/tackboard/Tackboard.java index 4a0d51a..07ca7a2 100644 --- a/src/jexer/tackboard/Tackboard.java +++ b/src/jexer/tackboard/Tackboard.java @@ -279,8 +279,10 @@ public class Tackboard { backImage = new BufferedImage(cellWidth, cellHeight, BufferedImage.TYPE_INT_ARGB); java.awt.Graphics gr = backImage.getGraphics(); - gr.setColor(screen.getBackend(). - attrToBackgroundColor(oldCell)); + + java.awt.Color oldColor = screen.getBackend(). + attrToBackgroundColor(oldCell); + gr.setColor(oldColor); gr.fillRect(0, 0, backImage.getWidth(), backImage.getHeight()); gr.drawImage(newImage, 0, 0, null, null); diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index c7fae5e..b3b937b 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -3735,9 +3735,7 @@ public class ECMA48 implements Runnable { // screen. We won't switch to a different buffer, // instead we will just clear the screen. currentState.attr.setForeColor(Color.WHITE); - currentState.attr.setForeColorRGB(-1); currentState.attr.setBackColor(Color.BLACK); - currentState.attr.setBackColorRGB(-1); eraseScreen(0, 0, height - 1, width - 1, false); scrollRegionTop = 0; scrollRegionBottom = height - 1; @@ -4428,9 +4426,7 @@ public class ECMA48 implements Runnable { */ private void sgr() { for (int i = 0; i < collectBuffer.length(); i++) { - if ((collectBuffer.charAt(i) == '>') - || (collectBuffer.charAt(i) == '>') - ) { + if (collectBuffer.charAt(i) == '>') { // Private-mode sequence, disregard. return; } @@ -4672,42 +4668,34 @@ public class ECMA48 implements Runnable { case 30: // Set black foreground currentState.attr.setForeColor(Color.BLACK); - currentState.attr.setForeColorRGB(-1); break; case 31: // Set red foreground currentState.attr.setForeColor(Color.RED); - currentState.attr.setForeColorRGB(-1); break; case 32: // Set green foreground currentState.attr.setForeColor(Color.GREEN); - currentState.attr.setForeColorRGB(-1); break; case 33: // Set yellow foreground currentState.attr.setForeColor(Color.YELLOW); - currentState.attr.setForeColorRGB(-1); break; case 34: // Set blue foreground currentState.attr.setForeColor(Color.BLUE); - currentState.attr.setForeColorRGB(-1); break; case 35: // Set magenta foreground currentState.attr.setForeColor(Color.MAGENTA); - currentState.attr.setForeColorRGB(-1); break; case 36: // Set cyan foreground currentState.attr.setForeColor(Color.CYAN); - currentState.attr.setForeColorRGB(-1); break; case 37: // Set white foreground currentState.attr.setForeColor(Color.WHITE); - currentState.attr.setForeColorRGB(-1); break; case 38: if (type == DeviceType.XTERM) { @@ -4758,47 +4746,38 @@ public class ECMA48 implements Runnable { // Underscore off, default foreground color currentState.attr.setUnderline(false); currentState.attr.setForeColor(Color.WHITE); - currentState.attr.setForeColorRGB(-1); break; case 40: // Set black background currentState.attr.setBackColor(Color.BLACK); - currentState.attr.setBackColorRGB(-1); break; case 41: // Set red background currentState.attr.setBackColor(Color.RED); - currentState.attr.setBackColorRGB(-1); break; case 42: // Set green background currentState.attr.setBackColor(Color.GREEN); - currentState.attr.setBackColorRGB(-1); break; case 43: // Set yellow background currentState.attr.setBackColor(Color.YELLOW); - currentState.attr.setBackColorRGB(-1); break; case 44: // Set blue background currentState.attr.setBackColor(Color.BLUE); - currentState.attr.setBackColorRGB(-1); break; case 45: // Set magenta background currentState.attr.setBackColor(Color.MAGENTA); - currentState.attr.setBackColorRGB(-1); break; case 46: // Set cyan background currentState.attr.setBackColor(Color.CYAN); - currentState.attr.setBackColorRGB(-1); break; case 47: // Set white background currentState.attr.setBackColor(Color.WHITE); - currentState.attr.setBackColorRGB(-1); break; case 48: if (type == DeviceType.XTERM) { @@ -4844,7 +4823,6 @@ public class ECMA48 implements Runnable { case 49: // Default background currentState.attr.setBackColor(Color.BLACK); - currentState.attr.setBackColorRGB(-1); break; default: