From 051e29138b18fb4b731a72f8727475b10e4c74e4 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Fri, 15 Dec 2017 16:00:17 -0500 Subject: [PATCH] Many changes: 1. TCalendar widget. Double-click or enter for action. 2. TSpinner widget. Up/down actions. 3. TComboBox widget. Double-click on list to set value. Enter for action. 4. 24-bit color now supported, both as output (ECMA48/Swing) and in TTerminalWindow. Not thoroughly tested yet, especially around ColorTheme. 5. Many "final class" restrictions removed. --- docs/TODO.md | 14 +- docs/worklog.md | 47 +++ src/jexer/TApplication.java | 22 +- src/jexer/TButton.java | 2 +- src/jexer/TCalendar.java | 320 +++++++++++++++ src/jexer/{TCheckbox.java => TCheckBox.java} | 8 +- src/jexer/TComboBox.java | 243 +++++++++++ src/jexer/TDirectoryList.java | 2 +- src/jexer/TEditorWidget.java | 2 +- src/jexer/TField.java | 2 +- src/jexer/TFileOpenBox.java | 2 +- src/jexer/THScroller.java | 2 +- src/jexer/TInputBox.java | 2 +- src/jexer/TKeypress.java | 2 +- src/jexer/TLabel.java | 31 +- src/jexer/TPasswordField.java | 2 +- src/jexer/TProgressBar.java | 2 +- src/jexer/TRadioButton.java | 2 +- src/jexer/TRadioGroup.java | 2 +- src/jexer/TSpinner.java | 193 +++++++++ src/jexer/TStatusBar.java | 2 +- src/jexer/TTerminalWindow.java | 14 +- src/jexer/TText.java | 2 +- src/jexer/TTimer.java | 2 +- src/jexer/TVScroller.java | 2 +- src/jexer/TWidget.java | 110 ++++- src/jexer/backend/ECMA48Backend.java | 2 +- src/jexer/backend/ECMA48Terminal.java | 212 +++++++++- src/jexer/backend/SwingBackend.java | 2 +- src/jexer/backend/SwingComponent.java | 25 +- src/jexer/backend/SwingSessionInfo.java | 2 +- src/jexer/backend/SwingTerminal.java | 31 +- src/jexer/backend/TSessionInfo.java | 2 +- src/jexer/backend/TTYSessionInfo.java | 2 +- src/jexer/bits/Cell.java | 1 + src/jexer/bits/CellAttributes.java | 96 ++++- src/jexer/bits/ColorTheme.java | 86 +++- src/jexer/bits/MnemonicString.java | 2 +- src/jexer/bits/StringUtils.java | 2 +- ...boxWindow.java => DemoCheckBoxWindow.java} | 58 ++- src/jexer/demos/DemoMainWindow.java | 51 ++- src/jexer/demos/DemoTextFieldWindow.java | 27 +- src/jexer/event/TCommandEvent.java | 2 +- src/jexer/event/TKeypressEvent.java | 2 +- src/jexer/event/TMenuEvent.java | 2 +- src/jexer/event/TMouseEvent.java | 2 +- src/jexer/event/TResizeEvent.java | 2 +- src/jexer/menu/TMenu.java | 2 +- src/jexer/menu/TMenuSeparator.java | 2 +- src/jexer/menu/TSubMenu.java | 2 +- src/jexer/net/TelnetInputStream.java | 3 +- src/jexer/net/TelnetOutputStream.java | 2 +- src/jexer/net/TelnetServerSocket.java | 2 +- src/jexer/net/TelnetSocket.java | 2 +- src/jexer/tterminal/DisplayLine.java | 2 +- src/jexer/tterminal/ECMA48.java | 385 ++++++++++++++++-- 56 files changed, 1898 insertions(+), 149 deletions(-) create mode 100644 src/jexer/TCalendar.java rename src/jexer/{TCheckbox.java => TCheckBox.java} (96%) create mode 100644 src/jexer/TComboBox.java create mode 100644 src/jexer/TSpinner.java rename src/jexer/demos/{DemoCheckboxWindow.java => DemoCheckBoxWindow.java} (61%) diff --git a/docs/TODO.md b/docs/TODO.md index e0de8a2..55b6238 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -7,11 +7,6 @@ Roadmap 0.0.6 -- New widgets: - - TSpinner - - TComboBox - - TCalendar - - TEditor - Horizontal scrollbar integration - True tokenization and syntax highlighting: Java, C, Clojure, XML @@ -55,7 +50,14 @@ Roadmap - TEditor: - Undo / Redo support -0.1.0: BETA RELEASE and BUG HUNT +0.1.0: LET'S GET PRETTY + +- TChart: + - Bar chart + - XY chart + - Time series chart + +0.1.1: BETA RELEASE and BUG HUNT - Verify vttest in multiple tterminals. diff --git a/docs/worklog.md b/docs/worklog.md index 05d4f15..24aa4e7 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,53 @@ Jexer Work Log ============== +December 15, 2017 + +We now have 24-bit RGB colors working with Swing backend. +EMCA48Terminal isn't happy though, let's try to fix that... still no +dice. So RGB is there for ECMA48 backend, but it sometimes flickers +or disappears. I'm not sure yet where the fault lies. Ah, found it! +Cell.isBlank() wasn't checking RGB. + +Well, I do say it is rather pretty now. Let's get this committed and +uploaded. + +December 14, 2017 + +TComboBox is stubbed in, and it was quite simple: just a TField and +TList, and a teeny bit of glue. Along the way I renamed TCheckbox to +TCheckBox, which was almost more work than TComboBox. Heh. Things +are starting to come together indeed. + +TSpinner is in. Now working on TCalendar... ...and TCalendar is in! + +December 13, 2017 + +A user noticed that the example code given in the README.md caused the +main window to freeze when clicking close. Turns out that was due to +the addWindow(new TWindow(...)) line, which led to TWindow appearing +in TApplication's window list twice. Fixed the README, and then made +TApplication.addWindow a package private function plus a check to +ensure it isn't added twice. + +On the home front, my main box is now a Fedora 26 running Plasma +desktop. That ate a few weekends getting used to. Current-era Linux +is pretty nice, systemd so far (cross fingers) isn't creating any real +problems, audio and wifi worked out of the box (thanks to Intel +chipsets), and I can finally have all of my books and references on +the same box as dev. So woohoo! + +SwingTerminal is getting the insets wrong, which is a bit aggravating. +So let's add adjustable insets in SwingComponent with a default +2-pixel border around the whole thing, which I can tweak for my +laptop. Done! + +Alright, so where are we? Well, I will have some time in the evenings +over the next couple weeks to put into projects. This one will get a +little bit of love, probably a new widget or two; Qodem might get +libssh2 + mdebtls support in Windows if those aren't too involved; +Jermit will get a little more push towards a Kermit implementation. + October 17, 2017 I finally gave up the ghost on using gcj as the default compiler due diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index f802e2b..ee2e9de 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -1384,8 +1384,16 @@ public class TApplication implements Runnable { System.currentTimeMillis(), Thread.currentThread(), x, y); } CellAttributes attr = getScreen().getAttrXY(x, y); - attr.setForeColor(attr.getForeColor().invert()); - attr.setBackColor(attr.getBackColor().invert()); + if (attr.getForeColorRGB() < 0) { + attr.setForeColor(attr.getForeColor().invert()); + } else { + attr.setForeColorRGB(attr.getForeColorRGB() ^ 0x00ffffff); + } + if (attr.getBackColorRGB() < 0) { + attr.setBackColor(attr.getBackColor().invert()); + } else { + attr.setBackColorRGB(attr.getBackColorRGB() ^ 0x00ffffff); + } getScreen().putAttrXY(x, y, attr, false); } @@ -1902,11 +1910,12 @@ public class TApplication implements Runnable { } /** - * Add a window to my window list and make it active. + * Add a window to my window list and make it active. Note package + * private access. * * @param window new window to add */ - public final void addWindowToApplication(final TWindow window) { + final void addWindowToApplication(final TWindow window) { // Do not add menu windows to the window list. if (window instanceof TMenu) { @@ -1919,6 +1928,11 @@ public class TApplication implements Runnable { } synchronized (windows) { + if (windows.contains(window)) { + throw new IllegalArgumentException("Window " + window + + " is already in window list"); + } + // Whatever window might be moving/dragging, stop it now. for (TWindow w: windows) { if (w.inMovements()) { diff --git a/src/jexer/TButton.java b/src/jexer/TButton.java index d4e7c89..8aa6270 100644 --- a/src/jexer/TButton.java +++ b/src/jexer/TButton.java @@ -42,7 +42,7 @@ import static jexer.TKeypress.*; * * @see TAction#DO() */ -public final class TButton extends TWidget { +public class TButton extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TCalendar.java b/src/jexer/TCalendar.java new file mode 100644 index 0000000..c117fc1 --- /dev/null +++ b/src/jexer/TCalendar.java @@ -0,0 +1,320 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * TCalendar is a date picker widget. + */ +public class TCalendar extends TWidget { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The calendar being displayed. + */ + private GregorianCalendar displayCalendar = new GregorianCalendar(); + + /** + * The calendar with the selected day. + */ + private GregorianCalendar calendar = new GregorianCalendar(); + + /** + * The action to perform when the user changes the value of the calendar. + */ + private TAction updateAction = null; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param updateAction action to call when the user changes the value of + * the calendar + */ + public TCalendar(final TWidget parent, final int x, final int y, + final TAction updateAction) { + + // Set parent and window + super(parent, x, y, 28, 8); + + this.updateAction = updateAction; + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Returns true if the mouse is currently on the left arrow. + * + * @param mouse mouse event + * @return true if the mouse is currently on the left arrow + */ + private boolean mouseOnLeftArrow(final TMouseEvent mouse) { + if ((mouse.getY() == 0) + && (mouse.getX() == 1) + ) { + return true; + } + return false; + } + + /** + * Returns true if the mouse is currently on the right arrow. + * + * @param mouse mouse event + * @return true if the mouse is currently on the right arrow + */ + private boolean mouseOnRightArrow(final TMouseEvent mouse) { + if ((mouse.getY() == 0) + && (mouse.getX() == getWidth() - 2) + ) { + return true; + } + return false; + } + + /** + * Handle mouse down clicks. + * + * @param mouse mouse button down event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if ((mouseOnLeftArrow(mouse)) && (mouse.isMouse1())) { + displayCalendar.add(Calendar.MONTH, -1); + } else if ((mouseOnRightArrow(mouse)) && (mouse.isMouse1())) { + displayCalendar.add(Calendar.MONTH, 1); + } else if (mouse.isMouse1()) { + // Find the day this might correspond to, and set it. + int index = (mouse.getY() - 2) * 7 + (mouse.getX() / 4) + 1; + // System.err.println("index: " + index); + + int lastDayNumber = displayCalendar.getActualMaximum( + Calendar.DAY_OF_MONTH); + GregorianCalendar firstOfMonth = new GregorianCalendar(); + firstOfMonth.setTimeInMillis(displayCalendar.getTimeInMillis()); + firstOfMonth.set(Calendar.DAY_OF_MONTH, 1); + int dayOf1st = firstOfMonth.get(Calendar.DAY_OF_WEEK) - 1; + // System.err.println("dayOf1st: " + dayOf1st); + + int day = index - dayOf1st; + // System.err.println("day: " + day); + + if ((day < 1) || (day > lastDayNumber)) { + return; + } + calendar.setTimeInMillis(displayCalendar.getTimeInMillis()); + calendar.set(Calendar.DAY_OF_MONTH, day); + } + } + + /** + * Handle mouse double click. + * + * @param mouse mouse double click event + */ + @Override + public void onMouseDoubleClick(final TMouseEvent mouse) { + if (updateAction != null) { + updateAction.DO(); + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + int increment = 0; + + if (keypress.equals(kbUp)) { + increment = -7; + } else if (keypress.equals(kbDown)) { + increment = 7; + } else if (keypress.equals(kbLeft)) { + increment = -1; + } else if (keypress.equals(kbRight)) { + increment = 1; + } else if (keypress.equals(kbEnter)) { + if (updateAction != null) { + updateAction.DO(); + } + return; + } else { + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + return; + } + + if (increment != 0) { + calendar.add(Calendar.DAY_OF_YEAR, increment); + + if ((displayCalendar.get(Calendar.MONTH) != calendar.get( + Calendar.MONTH)) + || (displayCalendar.get(Calendar.YEAR) != calendar.get( + Calendar.YEAR)) + ) { + if (increment < 0) { + displayCalendar.add(Calendar.MONTH, -1); + } else { + displayCalendar.add(Calendar.MONTH, 1); + } + } + } + + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the combobox down arrow. + */ + @Override + public void draw() { + CellAttributes backgroundColor = getTheme().getColor( + "tcalendar.background"); + CellAttributes dayColor = getTheme().getColor( + "tcalendar.day"); + CellAttributes selectedDayColor = getTheme().getColor( + "tcalendar.day.selected"); + CellAttributes arrowColor = getTheme().getColor( + "tcalendar.arrow"); + CellAttributes titleColor = getTheme().getColor( + "tcalendar.title"); + + // Fill in the interior background + for (int i = 0; i < getHeight(); i++) { + getScreen().hLineXY(0, i, getWidth(), ' ', backgroundColor); + } + + // Draw the title + String title = String.format("%tB %tY", displayCalendar, + displayCalendar); + int titleLeft = (getWidth() - title.length() - 2) / 2; + getScreen().putCharXY(titleLeft, 0, ' ', titleColor); + getScreen().putStringXY(titleLeft + 1, 0, title, titleColor); + getScreen().putCharXY(titleLeft + title.length() + 1, 0, ' ', + titleColor); + + // Arrows + getScreen().putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor); + getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW, + arrowColor); + + /* + * Now draw out the days. + */ + getScreen().putStringXY(0, 1, " S M T W T F S ", dayColor); + int lastDayNumber = displayCalendar.getActualMaximum( + Calendar.DAY_OF_MONTH); + GregorianCalendar firstOfMonth = new GregorianCalendar(); + firstOfMonth.setTimeInMillis(displayCalendar.getTimeInMillis()); + firstOfMonth.set(Calendar.DAY_OF_MONTH, 1); + int dayOf1st = firstOfMonth.get(Calendar.DAY_OF_WEEK) - 1; + int dayColumn = dayOf1st * 4; + int row = 2; + + int dayOfMonth = 1; + while (dayOfMonth <= lastDayNumber) { + if (dayColumn == 4 * 7) { + dayColumn = 0; + row++; + } + if ((dayOfMonth == calendar.get(Calendar.DAY_OF_MONTH)) + && (displayCalendar.get(Calendar.MONTH) == calendar.get( + Calendar.MONTH)) + && (displayCalendar.get(Calendar.YEAR) == calendar.get( + Calendar.YEAR)) + ) { + getScreen().putStringXY(dayColumn, row, + String.format(" %2d ", dayOfMonth), selectedDayColor); + } else { + getScreen().putStringXY(dayColumn, row, + String.format(" %2d ", dayOfMonth), dayColor); + } + dayColumn += 4; + dayOfMonth++; + } + + } + + // ------------------------------------------------------------------------ + // TCalendar -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get calendar value. + * + * @return the current calendar value (clone instance) + */ + public Calendar getValue() { + return (Calendar) calendar.clone(); + } + + /** + * Set calendar value. + * + * @param calendar the new value to use + */ + public final void setValue(final Calendar calendar) { + this.calendar.setTimeInMillis(calendar.getTimeInMillis()); + } + + /** + * Set calendar value. + * + * @param millis the millis to set to + */ + public final void setValue(final long millis) { + this.calendar.setTimeInMillis(millis); + } + +} diff --git a/src/jexer/TCheckbox.java b/src/jexer/TCheckBox.java similarity index 96% rename from src/jexer/TCheckbox.java rename to src/jexer/TCheckBox.java index 86933a7..2b35843 100644 --- a/src/jexer/TCheckbox.java +++ b/src/jexer/TCheckBox.java @@ -35,9 +35,9 @@ import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; /** - * TCheckbox implements an on/off checkbox. + * TCheckBox implements an on/off checkbox. */ -public final class TCheckbox extends TWidget { +public class TCheckBox extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@ -66,7 +66,7 @@ public final class TCheckbox extends TWidget { * @param label label to display next to (right of) the checkbox * @param checked initial check state */ - public TCheckbox(final TWidget parent, final int x, final int y, + public TCheckBox(final TWidget parent, final int x, final int y, final String label, final boolean checked) { // Set parent and window @@ -156,7 +156,7 @@ public final class TCheckbox extends TWidget { } // ------------------------------------------------------------------------ - // TCheckbox -------------------------------------------------------------- + // TCheckBox -------------------------------------------------------------- // ------------------------------------------------------------------------ /** diff --git a/src/jexer/TComboBox.java b/src/jexer/TComboBox.java new file mode 100644 index 0000000..38224b8 --- /dev/null +++ b/src/jexer/TComboBox.java @@ -0,0 +1,243 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.util.List; + +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * TComboBox implements a combobox containing a drop-down list and edit + * field. Alt-Down can be used to show the drop-down. + */ +public class TComboBox extends TWidget { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The list of items in the drop-down. + */ + private TList list; + + /** + * The edit field containing the value to return. + */ + private TField field; + + /** + * The action to perform when the user selects an item (clicks or enter). + */ + private TAction updateAction = null; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param width visible combobox width, including the down-arrow + * @param values the possible values for the box, shown in the drop-down + * @param valuesIndex the initial index in values, or -1 for no default + * value + * @param valuesHeight the height of the values drop-down when it is + * visible + * @param updateAction action to call when a new value is selected from + * the list or enter is pressed in the edit field + */ + public TComboBox(final TWidget parent, final int x, final int y, + final int width, final List values, final int valuesIndex, + final int valuesHeight, final TAction updateAction) { + + // Set parent and window + super(parent, x, y, width, 1); + + this.updateAction = updateAction; + + field = new TField(this, 0, 0, width - 1, false, "", + updateAction, null); + if (valuesIndex >= 0) { + field.setText(values.get(valuesIndex)); + } + + list = new TList(this, values, 0, 1, width, valuesHeight, + new TAction() { + public void DO() { + field.setText(list.getSelected()); + list.setEnabled(false); + list.setVisible(false); + TComboBox.this.setHeight(1); + TComboBox.this.activate(field); + if (updateAction != null) { + updateAction.DO(); + } + } + } + ); + + list.setEnabled(false); + list.setVisible(false); + setHeight(1); + activate(field); + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Returns true if the mouse is currently on the down arrow. + * + * @param mouse mouse event + * @return true if the mouse is currently on the down arrow + */ + private boolean mouseOnArrow(final TMouseEvent mouse) { + if ((mouse.getY() == 0) + && (mouse.getX() == getWidth() - 1) + ) { + return true; + } + return false; + } + + /** + * Handle mouse down clicks. + * + * @param mouse mouse button down event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if ((mouseOnArrow(mouse)) && (mouse.isMouse1())) { + // Make the list visible or not. + if (list.isActive()) { + list.setEnabled(false); + list.setVisible(false); + setHeight(1); + activate(field); + } else { + list.setEnabled(true); + list.setVisible(true); + setHeight(list.getHeight() + 1); + activate(list); + } + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbAltDown)) { + list.setEnabled(true); + list.setVisible(true); + setHeight(list.getHeight() + 1); + activate(list); + return; + } + + if (keypress.equals(kbTab) + || (keypress.equals(kbShiftTab)) + || (keypress.equals(kbBackTab)) + ) { + if (list.isActive()) { + list.setEnabled(false); + list.setVisible(false); + setHeight(1); + activate(field); + return; + } + } + + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the combobox down arrow. + */ + @Override + public void draw() { + CellAttributes comboBoxColor; + + if (isAbsoluteActive()) { + comboBoxColor = getTheme().getColor("tcombobox.active"); + } else { + comboBoxColor = getTheme().getColor("tcombobox.inactive"); + } + + getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, + comboBoxColor); + } + + // ------------------------------------------------------------------------ + // TComboBox -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get combobox text value. + * + * @return text in the edit field + */ + public String getText() { + return field.getText(); + } + + /** + * Set combobox text value. + * + * @param text the new text in the edit field + */ + public void setText(final String text) { + field.setText(text); + for (int i = 0; i < list.getMaxSelectedIndex(); i++) { + if (list.getSelected().equals(text)) { + list.setSelectedIndex(i); + return; + } + } + list.setSelectedIndex(-1); + } + +} diff --git a/src/jexer/TDirectoryList.java b/src/jexer/TDirectoryList.java index 4b62bf3..c47b330 100644 --- a/src/jexer/TDirectoryList.java +++ b/src/jexer/TDirectoryList.java @@ -35,7 +35,7 @@ import java.util.List; /** * TDirectoryList shows the files within a directory. */ -public final class TDirectoryList extends TList { +public class TDirectoryList extends TList { /** * Files in the directory. diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 3d8d57e..5ab7a23 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -43,7 +43,7 @@ import static jexer.TKeypress.*; * TEditorWidget displays an editable text document. It is unaware of * scrolling behavior, but can respond to mouse and keyboard events. */ -public final class TEditorWidget extends TWidget { +public class TEditorWidget extends TWidget { /** * The number of lines to scroll on mouse wheel up/down. diff --git a/src/jexer/TField.java b/src/jexer/TField.java index 8f693f2..aa0d18f 100644 --- a/src/jexer/TField.java +++ b/src/jexer/TField.java @@ -384,7 +384,7 @@ public class TField extends TWidget { * * @param text the new field text */ - public final void setText(String text) { + public void setText(final String text) { this.text = text; position = 0; windowStart = 0; diff --git a/src/jexer/TFileOpenBox.java b/src/jexer/TFileOpenBox.java index 094f8fa..6f46a03 100644 --- a/src/jexer/TFileOpenBox.java +++ b/src/jexer/TFileOpenBox.java @@ -54,7 +54,7 @@ import static jexer.TKeypress.*; * * */ -public final class TFileOpenBox extends TWindow { +public class TFileOpenBox extends TWindow { /** * Translated strings. diff --git a/src/jexer/THScroller.java b/src/jexer/THScroller.java index 90133a4..eab1a15 100644 --- a/src/jexer/THScroller.java +++ b/src/jexer/THScroller.java @@ -35,7 +35,7 @@ import jexer.event.TMouseEvent; /** * THScroller implements a simple horizontal scroll bar. */ -public final class THScroller extends TWidget { +public class THScroller extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TInputBox.java b/src/jexer/TInputBox.java index 50dba36..070f277 100644 --- a/src/jexer/TInputBox.java +++ b/src/jexer/TInputBox.java @@ -42,7 +42,7 @@ package jexer; * * */ -public final class TInputBox extends TMessageBox { +public class TInputBox extends TMessageBox { /** * The input field. diff --git a/src/jexer/TKeypress.java b/src/jexer/TKeypress.java index 2470bdf..5ba3484 100644 --- a/src/jexer/TKeypress.java +++ b/src/jexer/TKeypress.java @@ -31,7 +31,7 @@ package jexer; /** * This class represents keystrokes. */ -public final class TKeypress { +public class TKeypress { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/TLabel.java b/src/jexer/TLabel.java index 2eeea91..71530f0 100644 --- a/src/jexer/TLabel.java +++ b/src/jexer/TLabel.java @@ -33,7 +33,7 @@ import jexer.bits.CellAttributes; /** * TLabel implements a simple label. */ -public final class TLabel extends TWidget { +public class TLabel extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@ -49,6 +49,11 @@ public final class TLabel extends TWidget { */ private String colorKey; + /** + * If true, use the window's background color. + */ + private boolean useWindowBackground = true; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -79,11 +84,28 @@ public final class TLabel extends TWidget { public TLabel(final TWidget parent, final String text, final int x, final int y, final String colorKey) { + this(parent, text, x, y, colorKey, true); + } + + /** + * Public constructor. + * + * @param parent parent widget + * @param text label on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param colorKey ColorTheme key color to use for foreground text + * @param useWindowBackground if true, use the window's background color + */ + public TLabel(final TWidget parent, final String text, final int x, + final int y, final String colorKey, final boolean useWindowBackground) { + // Set parent and window super(parent, false, x, y, text.length(), 1); this.label = text; this.colorKey = colorKey; + this.useWindowBackground = useWindowBackground; } // ------------------------------------------------------------------------ @@ -98,9 +120,10 @@ public final class TLabel extends TWidget { // Setup my color CellAttributes color = new CellAttributes(); color.setTo(getTheme().getColor(colorKey)); - CellAttributes background = getWindow().getBackground(); - color.setBackColor(background.getBackColor()); - + if (useWindowBackground) { + CellAttributes background = getWindow().getBackground(); + color.setBackColor(background.getBackColor()); + } getScreen().putStringXY(0, 0, label, color); } diff --git a/src/jexer/TPasswordField.java b/src/jexer/TPasswordField.java index 85f64e1..696f5e2 100644 --- a/src/jexer/TPasswordField.java +++ b/src/jexer/TPasswordField.java @@ -34,7 +34,7 @@ import jexer.bits.GraphicsChars; /** * TField implements an editable text field. */ -public final class TPasswordField extends TField { +public class TPasswordField extends TField { /** * Public constructor. diff --git a/src/jexer/TProgressBar.java b/src/jexer/TProgressBar.java index 0677199..3947a94 100644 --- a/src/jexer/TProgressBar.java +++ b/src/jexer/TProgressBar.java @@ -34,7 +34,7 @@ import jexer.bits.GraphicsChars; /** * TProgressBar implements a simple progress bar. */ -public final class TProgressBar extends TWidget { +public class TProgressBar extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TRadioButton.java b/src/jexer/TRadioButton.java index cdb56aa..da07703 100644 --- a/src/jexer/TRadioButton.java +++ b/src/jexer/TRadioButton.java @@ -37,7 +37,7 @@ import static jexer.TKeypress.*; /** * TRadioButton implements a selectable radio button. */ -public final class TRadioButton extends TWidget { +public class TRadioButton extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TRadioGroup.java b/src/jexer/TRadioGroup.java index 83bf645..d811f27 100644 --- a/src/jexer/TRadioGroup.java +++ b/src/jexer/TRadioGroup.java @@ -33,7 +33,7 @@ import jexer.bits.CellAttributes; /** * TRadioGroup is a collection of TRadioButtons with a box and label. */ -public final class TRadioGroup extends TWidget { +public class TRadioGroup extends TWidget { /** * Label for this radio button group. diff --git a/src/jexer/TSpinner.java b/src/jexer/TSpinner.java new file mode 100644 index 0000000..cdc5c0f --- /dev/null +++ b/src/jexer/TSpinner.java @@ -0,0 +1,193 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; + +/** + * TSpinner implements a simple up/down spinner. Values can be numer + */ +public class TSpinner extends TWidget { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The action to perform when the user clicks on the up arrow. + */ + private TAction upAction = null; + + /** + * The action to perform when the user clicks on the down arrow. + */ + private TAction downAction = null; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param x column relative to parent + * @param y row relative to parent + * @param upAction action to call when the up arrow is clicked or pressed + * @param downAction action to call when the down arrow is clicked or + * pressed + */ + public TSpinner(final TWidget parent, final int x, final int y, + final TAction upAction, final TAction downAction) { + + // Set parent and window + super(parent, x, y, 2, 1); + + this.upAction = upAction; + this.downAction = downAction; + } + + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Returns true if the mouse is currently on the up arrow. + * + * @param mouse mouse event + * @return true if the mouse is currently on the up arrow + */ + private boolean mouseOnUpArrow(final TMouseEvent mouse) { + if ((mouse.getY() == 0) + && (mouse.getX() == getWidth() - 1) + ) { + return true; + } + return false; + } + + /** + * Returns true if the mouse is currently on the down arrow. + * + * @param mouse mouse event + * @return true if the mouse is currently on the down arrow + */ + private boolean mouseOnDownArrow(final TMouseEvent mouse) { + if ((mouse.getY() == 0) + && (mouse.getX() == getWidth() - 2) + ) { + return true; + } + return false; + } + + /** + * Handle mouse checkbox presses. + * + * @param mouse mouse button down event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if ((mouseOnUpArrow(mouse)) && (mouse.isMouse1())) { + up(); + } else if ((mouseOnDownArrow(mouse)) && (mouse.isMouse1())) { + down(); + } + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbUp)) { + up(); + return; + } + if (keypress.equals(kbDown)) { + down(); + return; + } + + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the spinner arrows. + */ + @Override + public void draw() { + CellAttributes spinnerColor; + + if (isAbsoluteActive()) { + spinnerColor = getTheme().getColor("tspinner.active"); + } else { + spinnerColor = getTheme().getColor("tspinner.inactive"); + } + + getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW, + spinnerColor); + getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, + spinnerColor); + } + + // ------------------------------------------------------------------------ + // TSpinner --------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Perform the "up" action. + */ + private void up() { + if (upAction != null) { + upAction.DO(); + } + } + + /** + * Perform the "down" action. + */ + private void down() { + if (downAction != null) { + downAction.DO(); + } + } + +} diff --git a/src/jexer/TStatusBar.java b/src/jexer/TStatusBar.java index 975d285..f3b8038 100644 --- a/src/jexer/TStatusBar.java +++ b/src/jexer/TStatusBar.java @@ -40,7 +40,7 @@ import jexer.event.TMouseEvent; /** * TStatusBar implements a status line with clickable buttons. */ -public final class TStatusBar extends TWidget { +public class TStatusBar extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 828a353..415a1a0 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -417,8 +417,18 @@ public class TTerminalWindow extends TScrollableWindow boolean reverse = line.isReverseColor() ^ ch.isReverse(); newCell.setReverse(false); if (reverse) { - newCell.setBackColor(ch.getForeColor()); - newCell.setForeColor(ch.getBackColor()); + 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()); + } } if (line.isDoubleWidth()) { getScreen().putCharXY((i * 2) + 1, row, newCell); diff --git a/src/jexer/TText.java b/src/jexer/TText.java index d0148f2..0edb290 100644 --- a/src/jexer/TText.java +++ b/src/jexer/TText.java @@ -40,7 +40,7 @@ import static jexer.TKeypress.*; * TText implements a simple scrollable text area. It reflows automatically on * resize. */ -public final class TText extends TScrollableWidget { +public class TText extends TScrollableWidget { /** * Available text justifications. diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java index 5d3dc13..3ec9dbd 100644 --- a/src/jexer/TTimer.java +++ b/src/jexer/TTimer.java @@ -33,7 +33,7 @@ import java.util.Date; /** * TTimer implements a simple timer. */ -public final class TTimer { +public class TTimer { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TVScroller.java b/src/jexer/TVScroller.java index 7a88739..9b99cfc 100644 --- a/src/jexer/TVScroller.java +++ b/src/jexer/TVScroller.java @@ -35,7 +35,7 @@ import jexer.event.TMouseEvent; /** * TVScroller implements a simple vertical scroll bar. */ -public final class TVScroller extends TWidget { +public class TVScroller extends TWidget { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 7d2ea35..6d5d147 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -58,8 +58,8 @@ public abstract class TWidget implements Comparable { /** * Every widget has a parent widget that it may be "contained" in. For - * example, a TWindow might contain several TTextFields, or a TComboBox - * may contain a TScrollBar. + * example, a TWindow might contain several TFields, or a TComboBox may + * contain a TList that itself contains a TVScroller. */ private TWidget parent = null; @@ -113,6 +113,11 @@ public abstract class TWidget implements Comparable { */ private boolean enabled = true; + /** + * If true, this widget will be rendered. + */ + private boolean visible = true; + /** * If true, this widget has a cursor. */ @@ -710,6 +715,24 @@ public abstract class TWidget implements Comparable { } } + /** + * Set visible flag. + * + * @param visible if true, this widget will be drawn + */ + public final void setVisible(final boolean visible) { + this.visible = visible; + } + + /** + * See if this widget is visible. + * + * @return if true, this widget will be drawn + */ + public final boolean isVisible() { + return visible; + } + /** * Set visible cursor flag. * @@ -972,14 +995,16 @@ public abstract class TWidget implements Comparable { // Continue down the chain for (TWidget widget: children) { - widget.drawChildren(); + if (widget.isVisible()) { + widget.drawChildren(); + } } } /** * Repaint the screen on the next update. */ - public void doRepaint() { + public final void doRepaint() { window.getApplication().doRepaint(); } @@ -1164,13 +1189,30 @@ public abstract class TWidget implements Comparable { return new TLabel(this, text, x, y, colorKey); } + /** + * Convenience function to add a label to this container/window. + * + * @param text label + * @param x column relative to parent + * @param y row relative to parent + * @param colorKey ColorTheme key color to use for foreground text. + * Default is "tlabel" + * @param useWindowBackground if true, use the window's background color + * @return the new label + */ + public final TLabel addLabel(final String text, final int x, final int y, + final String colorKey, final boolean useWindowBackground) { + + return new TLabel(this, text, x, y, colorKey, useWindowBackground); + } + /** * Convenience function to add a button to this container/window. * * @param text label on the button * @param x column relative to parent * @param y row relative to parent - * @param action to call when button is pressed + * @param action action to call when button is pressed * @return the new button */ public final TButton addButton(final String text, final int x, final int y, @@ -1188,10 +1230,64 @@ public abstract class TWidget implements Comparable { * @param checked initial check state * @return the new checkbox */ - public final TCheckbox addCheckbox(final int x, final int y, + public final TCheckBox addCheckBox(final int x, final int y, final String label, final boolean checked) { - return new TCheckbox(this, x, y, label, checked); + return new TCheckBox(this, x, y, label, checked); + } + + /** + * Convenience function to add a combobox to this container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param width visible combobox width, including the down-arrow + * @param values the possible values for the box, shown in the drop-down + * @param valuesIndex the initial index in values, or -1 for no default + * value + * @param valuesHeight the height of the values drop-down when it is + * visible + * @param updateAction action to call when a new value is selected from + * the list or enter is pressed in the edit field + * @return the new combobox + */ + public final TComboBox addComboBox(final int x, final int y, + final int width, final List values, final int valuesIndex, + final int valuesHeight, final TAction updateAction) { + + return new TComboBox(this, x, y, width, values, valuesIndex, + valuesHeight, updateAction); + } + + /** + * Convenience function to add a spinner to this container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param upAction action to call when the up arrow is clicked or pressed + * @param downAction action to call when the down arrow is clicked or + * pressed + * @return the new spinner + */ + public final TSpinner addSpinner(final int x, final int y, + final TAction upAction, final TAction downAction) { + + return new TSpinner(this, x, y, upAction, downAction); + } + + /** + * Convenience function to add a calendar to this container/window. + * + * @param x column relative to parent + * @param y row relative to parent + * @param updateAction action to call when the user changes the value of + * the calendar + * @return the new calendar + */ + public final TCalendar addCalendar(final int x, final int y, + final TAction updateAction) { + + return new TCalendar(this, x, y, updateAction); } /** diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index b99c120..7aad3a5 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -38,7 +38,7 @@ import java.io.UnsupportedEncodingException; * This class uses an xterm/ANSI X3.64/ECMA-48 type terminal to provide a * screen, keyboard, and mouse to TApplication. */ -public final class ECMA48Backend extends GenericBackend { +public class ECMA48Backend extends GenericBackend { // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 360994a..7edb64b 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -56,8 +56,8 @@ import static jexer.TKeypress.*; * This class reads keystrokes and mouse events and emits output to ANSI * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc. */ -public final class ECMA48Terminal extends LogicalScreen - implements TerminalReader, Runnable { +public class ECMA48Terminal extends LogicalScreen + implements TerminalReader, Runnable { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- @@ -86,8 +86,9 @@ public final class ECMA48Terminal extends LogicalScreen private boolean debugToStderr = false; /** - * If true, emit T.416-style RGB colors. This is a) expensive in - * bandwidth, and b) potentially terrible looking for non-xterms. + * If true, emit T.416-style RGB colors for normal system colors. This + * is a) expensive in bandwidth, and b) potentially terrible looking for + * non-xterms. */ private static boolean doRgbColor = false; @@ -797,6 +798,7 @@ public final class ECMA48Terminal extends LogicalScreen // 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()) @@ -809,8 +811,25 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { 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) { + 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()) @@ -828,6 +847,7 @@ public final class ECMA48Terminal extends LogicalScreen } } else if ((lCell.getForeColor() != lastAttr.getForeColor()) && (lCell.getBackColor() == lastAttr.getBackColor()) + && (!lCell.isRGB()) && (lCell.isBold() == lastAttr.isBold()) && (lCell.isReverse() == lastAttr.isReverse()) && (lCell.isUnderline() == lastAttr.isUnderline()) @@ -841,8 +861,25 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { 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) { + 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()) @@ -855,8 +892,24 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { 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) { + System.err.printf("4 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()) @@ -871,16 +924,29 @@ public final class ECMA48Terminal extends LogicalScreen } } else { // Just reset everything again - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("6 Change all attributes\n"); + if (!lCell.isRGB()) { + sb.append(color(lCell.getForeColor(), + lCell.getBackColor(), + lCell.isBold(), + lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + + if (debugToStderr) { + System.err.printf("6 Change all attributes\n"); + } + } else { + sb.append(colorRGB(lCell.getForeColorRGB(), + lCell.getBackColorRGB(), + lCell.isBold(), + lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + if (debugToStderr) { + System.err.printf("6 Change all attributes (RGB)\n"); + } } + } // Emit the character sb.append(lCell.getChar()); @@ -1694,6 +1760,55 @@ public final class ECMA48Terminal extends LogicalScreen rgbColor(bold, color, foreground); } + /** + * Create a T.416 RGB parameter sequence for a single color change. + * + * @param colorRGB a 24-bit RGB value for foreground color + * @param foreground if true, this is a foreground color + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[42m" + */ + private String colorRGB(final int colorRGB, final boolean foreground) { + + int colorRed = (colorRGB >> 16) & 0xFF; + int colorGreen = (colorRGB >> 8) & 0xFF; + int colorBlue = colorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + if (foreground) { + sb.append("\033[38;2;"); + } else { + sb.append("\033[48;2;"); + } + sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue)); + return sb.toString(); + } + + /** + * Create a T.416 RGB parameter sequence for both foreground and + * background color change. + * + * @param foreColorRGB a 24-bit RGB value for foreground color + * @param backColorRGB a 24-bit RGB value for foreground color + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[42m" + */ + private String colorRGB(final int foreColorRGB, final int backColorRGB) { + int foreColorRed = (foreColorRGB >> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >> 16) & 0xFF; + int backColorGreen = (backColorRGB >> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("\033[38;2;%d;%d;%dm", + foreColorRed, foreColorGreen, foreColorBlue)); + sb.append(String.format("\033[48;2;%d;%d;%dm", + backColorRed, backColorGreen, backColorBlue)); + return sb.toString(); + } + /** * Create a T.416 RGB parameter sequence for a single color change. * @@ -1915,6 +2030,77 @@ public final class ECMA48Terminal extends LogicalScreen return sb.toString(); } + /** + * Create a SGR parameter sequence for foreground, background, and + * several attributes. This sequence first resets all attributes to + * default, then sets attributes as per the parameters. + * + * @param foreColorRGB a 24-bit RGB value for foreground color + * @param backColorRGB a 24-bit RGB value for foreground color + * @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;31;42m" + */ + private String colorRGB(final int foreColorRGB, final int backColorRGB, + final boolean bold, final boolean reverse, final boolean blink, + final boolean underline) { + + int foreColorRed = (foreColorRGB >> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >> 16) & 0xFF; + int backColorGreen = (backColorRGB >> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + if ( bold && reverse && blink && !underline ) { + sb.append("\033[0;1;7;5;"); + } else if ( bold && reverse && !blink && !underline ) { + sb.append("\033[0;1;7;"); + } else if ( !bold && reverse && blink && !underline ) { + sb.append("\033[0;7;5;"); + } else if ( bold && !reverse && blink && !underline ) { + sb.append("\033[0;1;5;"); + } else if ( bold && !reverse && !blink && !underline ) { + sb.append("\033[0;1;"); + } else if ( !bold && reverse && !blink && !underline ) { + sb.append("\033[0;7;"); + } else if ( !bold && !reverse && blink && !underline) { + sb.append("\033[0;5;"); + } else if ( bold && reverse && blink && underline ) { + sb.append("\033[0;1;7;5;4;"); + } else if ( bold && reverse && !blink && underline ) { + sb.append("\033[0;1;7;4;"); + } else if ( !bold && reverse && blink && underline ) { + sb.append("\033[0;7;5;4;"); + } else if ( bold && !reverse && blink && underline ) { + sb.append("\033[0;1;5;4;"); + } else if ( bold && !reverse && !blink && underline ) { + sb.append("\033[0;1;4;"); + } else if ( !bold && reverse && !blink && underline ) { + sb.append("\033[0;7;4;"); + } else if ( !bold && !reverse && blink && underline) { + sb.append("\033[0;5;4;"); + } else if ( !bold && !reverse && !blink && underline) { + sb.append("\033[0;4;"); + } else { + assert (!bold && !reverse && !blink && !underline); + sb.append("\033[0;"); + } + + sb.append("m\033[38;2;"); + sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen, + foreColorBlue)); + sb.append("m\033[48;2;"); + sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen, + backColorBlue)); + sb.append("m"); + return sb.toString(); + } + /** * Create a SGR parameter sequence to reset to defaults. * diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index d6e0742..bfbd07e 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -35,7 +35,7 @@ import javax.swing.JComponent; * This class uses standard Swing calls to handle screen, keyboard, and mouse * I/O. */ -public final class SwingBackend extends GenericBackend { +public class SwingBackend extends GenericBackend { // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java index 4b0b2b4..56eb8bf 100644 --- a/src/jexer/backend/SwingComponent.java +++ b/src/jexer/backend/SwingComponent.java @@ -71,6 +71,17 @@ class SwingComponent { */ private JComponent component; + /** + * An optional border in pixels to add. + */ + private static final int BORDER = 5; + + /** + * Adjustable Insets for this component. This has the effect of adding a + * black border around the drawing area. + */ + Insets adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -237,11 +248,17 @@ class SwingComponent { * @return the value of the insets property */ public Insets getInsets() { + Insets swingInsets = null; if (frame != null) { - return frame.getInsets(); + swingInsets = frame.getInsets(); } else { - return component.getInsets(); + swingInsets = component.getInsets(); } + Insets result = new Insets(swingInsets.top + adjustInsets.top, + swingInsets.left + adjustInsets.left, + swingInsets.bottom + adjustInsets.bottom, + swingInsets.right + adjustInsets.right); + return result; } /** @@ -348,12 +365,12 @@ class SwingComponent { public void setDimensions(final int width, final int height) { // Figure out the thickness of borders and use that to set the final // size. - Insets insets = getInsets(); - if (frame != null) { + Insets insets = frame.getInsets(); frame.setSize(width + insets.left + insets.right, height + insets.top + insets.bottom); } else { + Insets insets = component.getInsets(); component.setSize(width + insets.left + insets.right, height + insets.top + insets.bottom); } diff --git a/src/jexer/backend/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java index b4c0b59..b19c60f 100644 --- a/src/jexer/backend/SwingSessionInfo.java +++ b/src/jexer/backend/SwingSessionInfo.java @@ -35,7 +35,7 @@ import java.awt.Insets; * Swing to support queryWindowSize(). The username is blank, language is * "en_US", with a 80x25 text window. */ -public final class SwingSessionInfo implements SessionInfo { +public class SwingSessionInfo implements SessionInfo { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index 598f0ab..f588679 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -80,11 +80,11 @@ import static jexer.TKeypress.*; * and uses a SwingComponent wrapper class to call the JFrame or JComponent * methods. */ -public final class SwingTerminal extends LogicalScreen - implements TerminalReader, - ComponentListener, KeyListener, - MouseListener, MouseMotionListener, - MouseWheelListener, WindowListener { +public class SwingTerminal extends LogicalScreen + implements TerminalReader, + ComponentListener, KeyListener, + MouseListener, MouseMotionListener, + MouseWheelListener, WindowListener { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- @@ -742,6 +742,15 @@ public final class SwingTerminal extends LogicalScreen * @return the Swing Color */ private Color attrToForegroundColor(final CellAttributes attr) { + int rgb = attr.getForeColorRGB(); + if (rgb >= 0) { + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + + return new Color(red, green, blue); + } + if (attr.isBold()) { if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) { return MYBOLD_BLACK; @@ -790,6 +799,15 @@ public final class SwingTerminal extends LogicalScreen * @return the Swing Color */ private Color attrToBackgroundColor(final CellAttributes attr) { + int rgb = attr.getBackColorRGB(); + if (rgb >= 0) { + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + + return new Color(red, green, blue); + } + if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) { return MYBLACK; } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) { @@ -1106,7 +1124,8 @@ public final class SwingTerminal extends LogicalScreen */ } - if ((swing.getBufferStrategy() != null) + if ((swing.getFrame() != null) + && (swing.getBufferStrategy() != null) && (SwingUtilities.isEventDispatchThread()) ) { // System.err.println("paint(), skip first paint on swing thread"); diff --git a/src/jexer/backend/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java index 63c12f5..11c0faa 100644 --- a/src/jexer/backend/TSessionInfo.java +++ b/src/jexer/backend/TSessionInfo.java @@ -32,7 +32,7 @@ package jexer.backend; * TSessionInfo provides a default session implementation. The username is * blank, language is "en_US", with a 80x24 text window. */ -public final class TSessionInfo implements SessionInfo { +public class TSessionInfo implements SessionInfo { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/backend/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java index 28b4bd6..e6fa57a 100644 --- a/src/jexer/backend/TTYSessionInfo.java +++ b/src/jexer/backend/TTYSessionInfo.java @@ -38,7 +38,7 @@ import java.util.StringTokenizer; * the session information. The username is taken from user.name, language * is taken from user.language, and text window size from 'stty size'. */ -public final class TTYSessionInfo implements SessionInfo { +public class TTYSessionInfo implements SessionInfo { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/bits/Cell.java b/src/jexer/bits/Cell.java index 56633e7..f7b71a5 100644 --- a/src/jexer/bits/Cell.java +++ b/src/jexer/bits/Cell.java @@ -114,6 +114,7 @@ public final class Cell extends CellAttributes { && !isReverse() && !isUnderline() && !isProtect() + && !isRGB() && (ch == ' ') ) { return true; diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java index ec3f6dd..a037b85 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/src/jexer/bits/CellAttributes.java @@ -72,6 +72,16 @@ public class CellAttributes { */ private Color backColor; + /** + * Foreground color as 24-bit RGB value. Negative value means not set. + */ + private int foreColorRGB = -1; + + /** + * Background color as 24-bit RGB value. Negative value means not set. + */ + private int backColorRGB = -1; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -226,18 +236,65 @@ public class CellAttributes { this.backColor = backColor; } + /** + * Getter for foreColor RGB. + * + * @return foreColor value. Negative means unset. + */ + public final int getForeColorRGB() { + return foreColorRGB; + } + + /** + * Setter for foreColor RGB. + * + * @param foreColor new foreColor RGB value + */ + public final void setForeColorRGB(final int foreColorRGB) { + this.foreColorRGB = foreColorRGB; + } + + /** + * Getter for backColor RGB. + * + * @return backColor value. Negative means unset. + */ + public final int getBackColorRGB() { + return backColorRGB; + } + + /** + * Setter for backColor RGB. + * + * @param backColor new backColor RGB value + */ + public final void setBackColorRGB(final int backColorRGB) { + this.backColorRGB = backColorRGB; + } + + /** + * See if this cell uses RGB or ANSI colors. + * + * @return true if this cell has a RGB color + */ + public final boolean isRGB() { + return (foreColorRGB >= 0) || (backColorRGB >= 0); + } + /** * Set to default: white foreground on black background, no * bold/underline/blink/rever/protect. */ public void reset() { - bold = false; - blink = false; - reverse = false; - underline = false; - protect = false; - foreColor = Color.WHITE; - backColor = Color.BLACK; + bold = false; + blink = false; + reverse = false; + underline = false; + protect = false; + foreColor = Color.WHITE; + backColor = Color.BLACK; + foreColorRGB = -1; + backColorRGB = -1; } /** @@ -255,6 +312,8 @@ public class CellAttributes { CellAttributes that = (CellAttributes) rhs; return ((foreColor == that.foreColor) && (backColor == that.backColor) + && (foreColorRGB == that.foreColorRGB) + && (backColorRGB == that.backColorRGB) && (bold == that.bold) && (reverse == that.reverse) && (underline == that.underline) @@ -279,6 +338,8 @@ public class CellAttributes { hash = (B * hash) + (protect ? 1 : 0); hash = (B * hash) + foreColor.hashCode(); hash = (B * hash) + backColor.hashCode(); + hash = (B * hash) + foreColorRGB; + hash = (B * hash) + backColorRGB; return hash; } @@ -290,13 +351,15 @@ public class CellAttributes { public void setTo(final Object rhs) { CellAttributes that = (CellAttributes) rhs; - this.bold = that.bold; - this.blink = that.blink; - this.reverse = that.reverse; - this.underline = that.underline; - this.protect = that.protect; - this.foreColor = that.foreColor; - this.backColor = that.backColor; + this.bold = that.bold; + this.blink = that.blink; + this.reverse = that.reverse; + this.underline = that.underline; + this.protect = that.protect; + this.foreColor = that.foreColor; + this.backColor = that.backColor; + this.foreColorRGB = that.foreColorRGB; + this.backColorRGB = that.backColorRGB; } /** @@ -306,6 +369,11 @@ public class CellAttributes { */ @Override public String toString() { + if ((foreColorRGB >= 0) || (backColorRGB >= 0)) { + return String.format("RGB: #%06x on #%06x", + (foreColorRGB & 0xFFFFFF), + (backColorRGB & 0xFFFFFF)); + } return String.format("%s%s%s on %s", (bold == true ? "bold " : ""), (blink == true ? "blink " : ""), foreColor, backColor); } diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index 0c6f6e4..1c4670a 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -44,7 +44,7 @@ import java.util.TreeMap; * ColorTheme is a collection of colors keyed by string. A default theme is * also provided that matches the blue-and-white theme used by Turbo Vision. */ -public final class ColorTheme { +public class ColorTheme { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@ -146,6 +146,37 @@ public final class ColorTheme { StringTokenizer tokenizer = new StringTokenizer(text); token = tokenizer.nextToken(); + + if (token.toLowerCase().equals("rgb:")) { + // Foreground + int foreColorRGB = -1; + try { + foreColorRGB = Integer.parseInt(tokenizer.nextToken(), 16); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + // "on" + if (!tokenizer.nextToken().toLowerCase().equals("on")) { + // Invalid line. + return; + } + + // Background + int backColorRGB = -1; + try { + backColorRGB = Integer.parseInt(tokenizer.nextToken(), 16); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + CellAttributes color = new CellAttributes(); + color.setForeColorRGB(foreColorRGB); + color.setBackColorRGB(backColorRGB); + colors.put(key, color); + return; + } + while (token.equals("bold") || token.equals("blink")) { if (token.equals("bold")) { bold = true; @@ -350,7 +381,7 @@ public final class ColorTheme { color.setBold(true); colors.put("tfield.active", color); - // TCheckbox + // TCheckBox color = new CellAttributes(); color.setForeColor(Color.WHITE); color.setBackColor(Color.BLUE); @@ -362,6 +393,57 @@ public final class ColorTheme { color.setBold(true); colors.put("tcheckbox.active", color); + // TComboBox + color = new CellAttributes(); + color.setForeColor(Color.BLACK); + color.setBackColor(Color.WHITE); + color.setBold(false); + colors.put("tcombobox.inactive", color); + color = new CellAttributes(); + color.setForeColor(Color.BLUE); + color.setBackColor(Color.CYAN); + color.setBold(false); + colors.put("tcombobox.active", color); + + // TSpinner + color = new CellAttributes(); + color.setForeColor(Color.BLACK); + color.setBackColor(Color.WHITE); + color.setBold(false); + colors.put("tspinner.inactive", color); + color = new CellAttributes(); + color.setForeColor(Color.BLUE); + color.setBackColor(Color.CYAN); + color.setBold(false); + colors.put("tspinner.active", color); + + // TCalendar + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(false); + colors.put("tcalendar.background", color); + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(false); + colors.put("tcalendar.day", color); + color = new CellAttributes(); + color.setForeColor(Color.RED); + color.setBackColor(Color.WHITE); + color.setBold(false); + colors.put("tcalendar.day.selected", color); + color = new CellAttributes(); + color.setForeColor(Color.BLUE); + color.setBackColor(Color.CYAN); + color.setBold(false); + colors.put("tcalendar.arrow", color); + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(true); + colors.put("tcalendar.title", color); + // TRadioButton color = new CellAttributes(); color.setForeColor(Color.WHITE); diff --git a/src/jexer/bits/MnemonicString.java b/src/jexer/bits/MnemonicString.java index d0032b6..f35eb50 100644 --- a/src/jexer/bits/MnemonicString.java +++ b/src/jexer/bits/MnemonicString.java @@ -34,7 +34,7 @@ package jexer.bits; * two '&&' characters, e.g. "&File && Stuff" would be * "File & Stuff" with the first 'F' highlighted. */ -public final class MnemonicString { +public class MnemonicString { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/bits/StringUtils.java b/src/jexer/bits/StringUtils.java index 535720d..79b9110 100644 --- a/src/jexer/bits/StringUtils.java +++ b/src/jexer/bits/StringUtils.java @@ -40,7 +40,7 @@ import java.util.LinkedList; * - Unescape C0 control codes. * */ -public final class StringUtils { +public class StringUtils { /** * Left-justify a string into a list of lines. diff --git a/src/jexer/demos/DemoCheckboxWindow.java b/src/jexer/demos/DemoCheckBoxWindow.java similarity index 61% rename from src/jexer/demos/DemoCheckboxWindow.java rename to src/jexer/demos/DemoCheckBoxWindow.java index 2e45117..4668051 100644 --- a/src/jexer/demos/DemoCheckboxWindow.java +++ b/src/jexer/demos/DemoCheckBoxWindow.java @@ -28,15 +28,28 @@ */ package jexer.demos; +import java.util.ArrayList; +import java.util.List; + import jexer.*; import static jexer.TCommand.*; import static jexer.TKeypress.*; /** - * This window demonstates the TRadioGroup, TRadioButton, and TCheckbox + * This window demonstates the TRadioGroup, TRadioButton, and TCheckBox * widgets. */ -public class DemoCheckboxWindow extends TWindow { +public class DemoCheckBoxWindow extends TWindow { + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Combo box. Has to be at class scope so that it can be accessed by the + * anonymous TAction class. + */ + TComboBox comboBox = null; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- @@ -47,7 +60,7 @@ public class DemoCheckboxWindow extends TWindow { * * @param parent the main application */ - DemoCheckboxWindow(final TApplication parent) { + DemoCheckBoxWindow(final TApplication parent) { this(parent, CENTERED | RESIZABLE); } @@ -57,18 +70,19 @@ public class DemoCheckboxWindow extends TWindow { * @param parent the main application * @param flags bitmask of MODAL, CENTERED, or RESIZABLE */ - DemoCheckboxWindow(final TApplication parent, final int flags) { + DemoCheckBoxWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it will be // centered on screen. - super(parent, "Radiobuttons and Checkboxes", 0, 0, 60, 15, flags); + super(parent, "Radiobuttons, CheckBoxes, and ComboBox", + 0, 0, 60, 17, flags); int row = 1; // Add some widgets addLabel("Check box example 1", 1, row); - addCheckbox(35, row++, "Checkbox 1", false); + addCheckBox(35, row++, "CheckBox 1", false); addLabel("Check box example 2", 1, row); - addCheckbox(35, row++, "Checkbox 2", true); + addCheckBox(35, row++, "CheckBox 2", true); row += 2; TRadioGroup group = addRadioGroup(1, row, "Group 1"); @@ -76,11 +90,37 @@ public class DemoCheckboxWindow extends TWindow { group.addRadioButton("Radio option 2"); group.addRadioButton("Radio option 3"); + List comboValues = new ArrayList(); + comboValues.add("String 0"); + comboValues.add("String 1"); + comboValues.add("String 2"); + comboValues.add("String 3"); + comboValues.add("String 4"); + comboValues.add("String 5"); + comboValues.add("String 6"); + comboValues.add("String 7"); + comboValues.add("String 8"); + comboValues.add("String 9"); + comboValues.add("String 10"); + + comboBox = addComboBox(35, row, 12, comboValues, 2, 6, + new TAction() { + public void DO() { + getApplication().messageBox("ComboBox", + "You selected the following value:\n" + + "\n" + + comboBox.getText() + + "\n", + TMessageBox.Type.OK); + } + } + ); + addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4, new TAction() { public void DO() { - DemoCheckboxWindow.this.getApplication() - .closeWindow(DemoCheckboxWindow.this); + DemoCheckBoxWindow.this.getApplication() + .closeWindow(DemoCheckBoxWindow.this); } } ); diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java index eef25e2..f1cbe21 100644 --- a/src/jexer/demos/DemoMainWindow.java +++ b/src/jexer/demos/DemoMainWindow.java @@ -29,6 +29,7 @@ package jexer.demos; import java.io.*; +import java.util.*; import jexer.*; import jexer.event.*; @@ -67,6 +68,17 @@ public class DemoMainWindow extends TWindow { */ TProgressBar progressBar; + /** + * Day of week label is updated with TSpinner clicks. + */ + TLabel dayOfWeekLabel; + + /** + * Day of week to demonstrate TSpinner. Has to be at class scope so that + * it can be accessed by the anonymous TAction class. + */ + GregorianCalendar calendar = new GregorianCalendar(); + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -95,7 +107,7 @@ public class DemoMainWindow extends TWindow { // Add some widgets addLabel("Message Boxes", 1, row); - addButton("&MessageBoxes", 35, row, + TWidget first = addButton("&MessageBoxes", 35, row, new TAction() { public void DO() { new DemoMsgBoxWindow(getApplication()); @@ -114,7 +126,7 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Text fields", 1, row); + addLabel("Text fields and calendar", 1, row); addButton("Field&s", 35, row, new TAction() { public void DO() { @@ -124,11 +136,11 @@ public class DemoMainWindow extends TWindow { ); row += 2; - addLabel("Radio buttons and checkboxes", 1, row); - addButton("&Checkboxes", 35, row, + addLabel("Radio buttons, check and combobox", 1, row); + addButton("&CheckBoxes", 35, row, new TAction() { public void DO() { - new DemoCheckboxWindow(getApplication()); + new DemoCheckBoxWindow(getApplication()); } } ); @@ -214,6 +226,35 @@ public class DemoMainWindow extends TWindow { } ); + dayOfWeekLabel = addLabel("Wednesday-", 35, row - 1, "tmenu", false); + dayOfWeekLabel.setLabel(String.format("%-10s", + calendar.getDisplayName(Calendar.DAY_OF_WEEK, + Calendar.LONG, Locale.getDefault()))); + + addSpinner(35 + dayOfWeekLabel.getWidth(), row - 1, + new TAction() { + public void DO() { + calendar.add(Calendar.DAY_OF_WEEK, 1); + dayOfWeekLabel.setLabel(String.format("%-10s", + calendar.getDisplayName( + Calendar.DAY_OF_WEEK, Calendar.LONG, + Locale.getDefault()))); + } + }, + new TAction() { + public void DO() { + calendar.add(Calendar.DAY_OF_WEEK, -1); + dayOfWeekLabel.setLabel(String.format("%-10s", + calendar.getDisplayName( + Calendar.DAY_OF_WEEK, Calendar.LONG, + Locale.getDefault()))); + } + } + ); + + + activate(first); + statusBar = newStatusBar("Demo Main Window"); statusBar.addShortcutKeypress(kbF1, cmHelp, "Help"); statusBar.addShortcutKeypress(kbF2, cmShell, "Shell"); diff --git a/src/jexer/demos/DemoTextFieldWindow.java b/src/jexer/demos/DemoTextFieldWindow.java index 2d9c504..59dee82 100644 --- a/src/jexer/demos/DemoTextFieldWindow.java +++ b/src/jexer/demos/DemoTextFieldWindow.java @@ -28,6 +28,8 @@ */ package jexer.demos; +import java.util.*; + import jexer.*; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -37,6 +39,16 @@ import static jexer.TKeypress.*; */ public class DemoTextFieldWindow extends TWindow { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Calendar. Has to be at class scope so that it can be accessed by the + * anonymous TAction class. + */ + TCalendar calendar = null; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -59,7 +71,7 @@ public class DemoTextFieldWindow extends TWindow { DemoTextFieldWindow(final TApplication parent, final int flags) { // Construct a demo window. X and Y don't matter because it // will be centered on screen. - super(parent, "Text Fields", 0, 0, 60, 10, flags); + super(parent, "Text Fields", 0, 0, 60, 20, flags); int row = 1; @@ -76,6 +88,19 @@ public class DemoTextFieldWindow extends TWindow { "Very very long field text that should be outside the window"); row += 1; + calendar = addCalendar(1, row++, + new TAction() { + public void DO() { + getApplication().messageBox("Calendar", + "You selected the following date:\n" + + "\n" + + new Date(calendar.getValue().getTimeInMillis()) + + "\n", + TMessageBox.Type.OK); + } + } + ); + addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4, new TAction() { public void DO() { diff --git a/src/jexer/event/TCommandEvent.java b/src/jexer/event/TCommandEvent.java index 772f366..749e3f7 100644 --- a/src/jexer/event/TCommandEvent.java +++ b/src/jexer/event/TCommandEvent.java @@ -35,7 +35,7 @@ import jexer.TCommand; * generated by menu actions, keyboard accelerators, and other UI elements. * Commands can operate on both the application and individual widgets. */ -public final class TCommandEvent extends TInputEvent { +public class TCommandEvent extends TInputEvent { /** * Command dispatched. diff --git a/src/jexer/event/TKeypressEvent.java b/src/jexer/event/TKeypressEvent.java index 32a1615..bdf5e84 100644 --- a/src/jexer/event/TKeypressEvent.java +++ b/src/jexer/event/TKeypressEvent.java @@ -33,7 +33,7 @@ import jexer.TKeypress; /** * This class encapsulates a keyboard input event. */ -public final class TKeypressEvent extends TInputEvent { +public class TKeypressEvent extends TInputEvent { /** * Keystroke received. diff --git a/src/jexer/event/TMenuEvent.java b/src/jexer/event/TMenuEvent.java index ea75e40..491a735 100644 --- a/src/jexer/event/TMenuEvent.java +++ b/src/jexer/event/TMenuEvent.java @@ -33,7 +33,7 @@ package jexer.event; * TApplication.getMenuItem(id) can be used to obtain the TMenuItem itself, * say for setting enabled/disabled/checked/etc. */ -public final class TMenuEvent extends TInputEvent { +public class TMenuEvent extends TInputEvent { /** * MenuItem ID. diff --git a/src/jexer/event/TMouseEvent.java b/src/jexer/event/TMouseEvent.java index a6a0df2..8c967c0 100644 --- a/src/jexer/event/TMouseEvent.java +++ b/src/jexer/event/TMouseEvent.java @@ -33,7 +33,7 @@ package jexer.event; * the relative (x,y) ARE MUTABLE: TWidget's onMouse() handlers perform that * update during event dispatching. */ -public final class TMouseEvent extends TInputEvent { +public class TMouseEvent extends TInputEvent { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/event/TResizeEvent.java b/src/jexer/event/TResizeEvent.java index 4e15121..472ef3e 100644 --- a/src/jexer/event/TResizeEvent.java +++ b/src/jexer/event/TResizeEvent.java @@ -31,7 +31,7 @@ package jexer.event; /** * This class encapsulates a screen or window resize event. */ -public final class TResizeEvent extends TInputEvent { +public class TResizeEvent extends TInputEvent { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 505af3e..3120635 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -44,7 +44,7 @@ import static jexer.TKeypress.*; /** * TMenu is a top-level collection of TMenuItems. */ -public final class TMenu extends TWindow { +public class TMenu extends TWindow { /** * Translated strings. diff --git a/src/jexer/menu/TMenuSeparator.java b/src/jexer/menu/TMenuSeparator.java index 01164a5..daae13a 100644 --- a/src/jexer/menu/TMenuSeparator.java +++ b/src/jexer/menu/TMenuSeparator.java @@ -34,7 +34,7 @@ import jexer.bits.GraphicsChars; /** * TMenuSeparator is a special case menu item. */ -public final class TMenuSeparator extends TMenuItem { +public class TMenuSeparator extends TMenuItem { // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- diff --git a/src/jexer/menu/TSubMenu.java b/src/jexer/menu/TSubMenu.java index 488ccd4..b61ca83 100644 --- a/src/jexer/menu/TSubMenu.java +++ b/src/jexer/menu/TSubMenu.java @@ -38,7 +38,7 @@ import static jexer.TKeypress.*; /** * TSubMenu is a special case menu item that wraps another TMenu. */ -public final class TSubMenu extends TMenuItem { +public class TSubMenu extends TMenuItem { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/net/TelnetInputStream.java b/src/jexer/net/TelnetInputStream.java index 9ae7747..1764b88 100644 --- a/src/jexer/net/TelnetInputStream.java +++ b/src/jexer/net/TelnetInputStream.java @@ -40,8 +40,7 @@ import static jexer.net.TelnetSocket.*; /** * TelnetInputStream works with TelnetSocket to perform the telnet protocol. */ -public final class TelnetInputStream extends InputStream - implements SessionInfo { +public class TelnetInputStream extends InputStream implements SessionInfo { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/net/TelnetOutputStream.java b/src/jexer/net/TelnetOutputStream.java index 338de2c..994655c 100644 --- a/src/jexer/net/TelnetOutputStream.java +++ b/src/jexer/net/TelnetOutputStream.java @@ -36,7 +36,7 @@ import static jexer.net.TelnetSocket.*; /** * TelnetOutputStream works with TelnetSocket to perform the telnet protocol. */ -public final class TelnetOutputStream extends OutputStream { +public class TelnetOutputStream extends OutputStream { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/net/TelnetServerSocket.java b/src/jexer/net/TelnetServerSocket.java index 7523192..b368d34 100644 --- a/src/jexer/net/TelnetServerSocket.java +++ b/src/jexer/net/TelnetServerSocket.java @@ -37,7 +37,7 @@ import java.net.SocketException; /** * This class provides a ServerSocket that return TelnetSocket's in accept(). */ -public final class TelnetServerSocket extends ServerSocket { +public class TelnetServerSocket extends ServerSocket { // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- diff --git a/src/jexer/net/TelnetSocket.java b/src/jexer/net/TelnetSocket.java index 3166388..34e715f 100644 --- a/src/jexer/net/TelnetSocket.java +++ b/src/jexer/net/TelnetSocket.java @@ -38,7 +38,7 @@ import java.net.Socket; * establish an 8-bit clean no echo channel and expose window resize events * to the Jexer ECMA48 backend. */ -public final class TelnetSocket extends Socket { +public class TelnetSocket extends Socket { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/tterminal/DisplayLine.java b/src/jexer/tterminal/DisplayLine.java index 8eff2de..66036d7 100644 --- a/src/jexer/tterminal/DisplayLine.java +++ b/src/jexer/tterminal/DisplayLine.java @@ -34,7 +34,7 @@ import jexer.bits.CellAttributes; /** * This represents a single line of the display buffer. */ -public final class DisplayLine { +public class DisplayLine { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index 7a37a95..9376828 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -438,6 +438,11 @@ public class ECMA48 implements Runnable { */ private SaveableState savedState; + /** + * The 88- or 256-color support RGB colors. + */ + private List colors88; + /** * DECSC/DECRC save/restore a subset of the total state. This class * encapsulates those specific flags/modes. @@ -1077,6 +1082,99 @@ public class ECMA48 implements Runnable { } } + /** + * Reset the 88- or 256-colors. + */ + private void resetColors() { + colors88 = new ArrayList(256); + for (int i = 0; i < 256; i++) { + colors88.add(0); + } + + // Set default system colors. + colors88.set(0, 0x00000000); + colors88.set(1, 0x00a80000); + colors88.set(2, 0x0000a800); + colors88.set(3, 0x00a85400); + colors88.set(4, 0x000000a8); + colors88.set(5, 0x00a800a8); + colors88.set(6, 0x0000a8a8); + colors88.set(7, 0x00a8a8a8); + + colors88.set(8, 0x00545454); + colors88.set(9, 0x00fc5454); + colors88.set(10, 0x0054fc54); + colors88.set(11, 0x00fcfc54); + colors88.set(12, 0x005454fc); + colors88.set(13, 0x00fc54fc); + colors88.set(14, 0x0054fcfc); + colors88.set(15, 0x00fcfcfc); + } + + /** + * Get the RGB value of one of the indexed colors. + * + * @param index the color index + * @return the RGB value + */ + private int get88Color(final int index) { + // System.err.print("get88Color: " + index); + if ((index < 0) || (index > colors88.size())) { + // System.err.println(" -- UNKNOWN"); + return 0; + } + // System.err.printf(" %08x\n", colors88.get(index)); + return colors88.get(index); + } + + /** + * Set one of the indexed colors to a color specification. + * + * @param index the color index + * @param spec the specification, typically something like "rgb:aa/bb/cc" + */ + private void set88Color(final int index, final String spec) { + // System.err.println("set88Color: " + index + " '" + spec + "'"); + + if ((index < 0) || (index > colors88.size())) { + return; + } + if (spec.startsWith("rgb:")) { + String [] rgbTokens = spec.substring(4).split("/"); + if (rgbTokens.length == 3) { + try { + int rgb = (Integer.parseInt(rgbTokens[0], 16) << 16); + rgb |= Integer.parseInt(rgbTokens[1], 16) << 8; + rgb |= Integer.parseInt(rgbTokens[2], 16); + // System.err.printf(" set to %08x\n", rgb); + colors88.set(index, rgb); + } catch (NumberFormatException e) { + // SQUASH + } + } + return; + } + + if (spec.toLowerCase().equals("black")) { + colors88.set(index, 0x00000000); + } else if (spec.toLowerCase().equals("red")) { + colors88.set(index, 0x00a80000); + } else if (spec.toLowerCase().equals("green")) { + colors88.set(index, 0x0000a800); + } else if (spec.toLowerCase().equals("yellow")) { + colors88.set(index, 0x00a85400); + } else if (spec.toLowerCase().equals("blue")) { + colors88.set(index, 0x000000a8); + } else if (spec.toLowerCase().equals("magenta")) { + colors88.set(index, 0x00a800a8); + } else if (spec.toLowerCase().equals("cyan")) { + colors88.set(index, 0x0000a8a8); + } else if (spec.toLowerCase().equals("white")) { + colors88.set(index, 0x00a8a8a8); + } + + } + /** * Reset the emulation state. */ @@ -1122,6 +1220,9 @@ public class ECMA48 implements Runnable { // Tab stops resetTabStops(); + // Reset extra colors + resetColors(); + // Clear CSI stuff toGround(); } @@ -3529,8 +3630,87 @@ public class ECMA48 implements Runnable { return; } + int sgrColorMode = -1; + boolean idx88Color = false; + boolean rgbColor = false; + int rgbRed = -1; + int rgbGreen = -1; + for (Integer i: csiParams) { + if ((sgrColorMode == 38) || (sgrColorMode == 48)) { + + assert (type == DeviceType.XTERM); + + if (idx88Color) { + /* + * Indexed color mode, we now have the index number. + */ + if (sgrColorMode == 38) { + currentState.attr.setForeColorRGB(get88Color(i)); + } else { + assert (sgrColorMode == 48); + currentState.attr.setBackColorRGB(get88Color(i)); + } + sgrColorMode = -1; + idx88Color = false; + continue; + } + + if (rgbColor) { + /* + * RGB color mode, we are collecting tokens. + */ + if (rgbRed == -1) { + rgbRed = i & 0xFF; + } else if (rgbGreen == -1) { + rgbGreen = i & 0xFF; + } else { + int rgb = rgbRed << 16; + rgb |= rgbGreen << 8; + rgb |= i & 0xFF; + + // System.err.printf("RGB: %08x\n", rgb); + + if (sgrColorMode == 38) { + currentState.attr.setForeColorRGB(rgb); + } else { + assert (sgrColorMode == 48); + currentState.attr.setBackColorRGB(rgb); + } + rgbRed = -1; + rgbGreen = -1; + sgrColorMode = -1; + rgbColor = false; + } + continue; + } + + switch (i) { + + case 2: + /* + * RGB color mode. + */ + rgbColor = true; + break; + + case 5: + /* + * Indexed color mode. + */ + idx88Color = true; + break; + + default: + /* + * This is neither indexed nor RGB color. Bail out. + */ + return; + } + + } // if ((sgrColorMode == 38) || (sgrColorMode == 48)) + switch (i) { case 0: @@ -3571,6 +3751,72 @@ public class ECMA48 implements Runnable { // TODO break; + case 90: + // Set black foreground + currentState.attr.setForeColorRGB(get88Color(8)); + break; + case 91: + // Set red foreground + currentState.attr.setForeColorRGB(get88Color(9)); + break; + case 92: + // Set green foreground + currentState.attr.setForeColorRGB(get88Color(10)); + break; + case 93: + // Set yellow foreground + currentState.attr.setForeColorRGB(get88Color(11)); + break; + case 94: + // Set blue foreground + currentState.attr.setForeColorRGB(get88Color(12)); + break; + case 95: + // Set magenta foreground + currentState.attr.setForeColorRGB(get88Color(13)); + break; + case 96: + // Set cyan foreground + currentState.attr.setForeColorRGB(get88Color(14)); + break; + case 97: + // Set white foreground + currentState.attr.setForeColorRGB(get88Color(15)); + break; + + case 100: + // Set black background + currentState.attr.setBackColorRGB(get88Color(8)); + break; + case 101: + // Set red background + currentState.attr.setBackColorRGB(get88Color(9)); + break; + case 102: + // Set green background + currentState.attr.setBackColorRGB(get88Color(10)); + break; + case 103: + // Set yellow background + currentState.attr.setBackColorRGB(get88Color(11)); + break; + case 104: + // Set blue background + currentState.attr.setBackColorRGB(get88Color(12)); + break; + case 105: + // Set magenta background + currentState.attr.setBackColorRGB(get88Color(13)); + break; + case 106: + // Set cyan background + currentState.attr.setBackColorRGB(get88Color(14)); + break; + case 107: + // Set white background + currentState.attr.setBackColorRGB(get88Color(15)); + break; + default: break; } @@ -3615,34 +3861,42 @@ 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) { @@ -3652,21 +3906,28 @@ public class ECMA48 implements Runnable { * permits these ISO-8613-3 SGR sequences to be separated * by colons rather than semicolons.) * - * We will not support any of these additional color - * codes at this time: + * We will support only the following: + * + * 1. Indexed color mode (88- or 256-color modes). * - * 1. http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors - * has a detailed discussion of the current state of - * RGB in various terminals, the point of which is - * that none of them really do the same thing despite - * all appearing to be "xterm". + * 2. Direct RGB. * - * 2. As seen in - * https://bugs.kde.org/show_bug.cgi?id=107487#c3, - * even supporting just the "indexed mode" of these - * sequences (which could align easily with existing - * SGR colors) is assumed to mean full support of - * 24-bit RGB. So it is all or nothing. + * These cover most of the use cases in the real world. + * + * HOWEVER, note that this is an awful broken "standard", + * with no way to do it "right". See + * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors + * for a detailed discussion of the current state of RGB + * in various terminals, the point of which is that none + * of them really do the same thing despite all appearing + * to be "xterm". + * + * Also see + * https://bugs.kde.org/show_bug.cgi?id=107487#c3 . + * where it is assumed that supporting just the "indexed + * mode" of these sequences (which could align easily + * with existing SGR colors) is assumed to mean full + * support of 24-bit RGB. So it is all or nothing. * * Finally, these sequences break the assumptions of * standard ECMA-48 style parsers as pointed out at @@ -3674,7 +3935,8 @@ public class ECMA48 implements Runnable { * Therefore in order to keep a clean display, we cannot * parse anything else in this sequence. */ - return; + sgrColorMode = 38; + continue; } else { // Underscore on, default foreground color currentState.attr.setUnderline(true); @@ -3685,38 +3947,47 @@ 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) { @@ -3726,16 +3997,43 @@ public class ECMA48 implements Runnable { * permits these ISO-8613-3 SGR sequences to be separated * by colons rather than semicolons.) * - * We will not support this at this time. Also, in order - * to keep a clean display, we cannot parse anything else - * in this sequence. + * We will support only the following: + * + * 1. Indexed color mode (88- or 256-color modes). + * + * 2. Direct RGB. + * + * These cover most of the use cases in the real world. + * + * HOWEVER, note that this is an awful broken "standard", + * with no way to do it "right". See + * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors + * for a detailed discussion of the current state of RGB + * in various terminals, the point of which is that none + * of them really do the same thing despite all appearing + * to be "xterm". + * + * Also see + * https://bugs.kde.org/show_bug.cgi?id=107487#c3 . + * where it is assumed that supporting just the "indexed + * mode" of these sequences (which could align easily + * with existing SGR colors) is assumed to mean full + * support of 24-bit RGB. So it is all or nothing. + * + * Finally, these sequences break the assumptions of + * standard ECMA-48 style parsers as pointed out at + * https://bugs.kde.org/show_bug.cgi?id=107487#c11 . + * Therefore in order to keep a clean display, we cannot + * parse anything else in this sequence. */ - return; + sgrColorMode = 48; + continue; } break; case 49: // Default background currentState.attr.setBackColor(Color.BLACK); + currentState.attr.setBackColorRGB(-1); break; default: @@ -4186,13 +4484,22 @@ public class ECMA48 implements Runnable { * @param xtermChar the character received from the remote side */ private void oscPut(final char xtermChar) { + // System.err.println("oscPut: " + xtermChar); + // Collect first collectBuffer.append(xtermChar); // Xterm cases... - if (xtermChar == 0x07) { - String args = collectBuffer.substring(0, - collectBuffer.length() - 1); + if ((xtermChar == 0x07) + || (collectBuffer.toString().endsWith("\033\\")) + ) { + String args = null; + if (xtermChar == 0x07) { + args = collectBuffer.substring(0, collectBuffer.length() - 1); + } else { + args = collectBuffer.substring(0, collectBuffer.length() - 2); + } + String [] p = args.toString().split(";"); if (p.length > 0) { if ((p[0].equals("0")) || (p[0].equals("2"))) { @@ -4201,6 +4508,17 @@ public class ECMA48 implements Runnable { screenTitle = p[1]; } } + + if (p[0].equals("4")) { + for (int i = 1; i + 1 < p.length; i += 2) { + // Set a color index value + try { + set88Color(Integer.parseInt(p[i]), p[i + 1]); + } catch (NumberFormatException e) { + // SQUASH + } + } + } } // Go to SCAN_GROUND state @@ -4236,16 +4554,21 @@ public class ECMA48 implements Runnable { // 80-8F, 91-97, 99, 9A, 9C --> execute, then switch to SCAN_GROUND // 0x1B == ESCAPE - if ((ch == 0x1B) - && (scanState != ScanState.DCS_ENTRY) - && (scanState != ScanState.DCS_INTERMEDIATE) - && (scanState != ScanState.DCS_IGNORE) - && (scanState != ScanState.DCS_PARAM) - && (scanState != ScanState.DCS_PASSTHROUGH) - ) { + if (ch == 0x1B) { + if ((type == DeviceType.XTERM) + && (scanState == ScanState.OSC_STRING) + ) { + // Xterm can pass ESCAPE to its OSC sequence. + } else if ((scanState != ScanState.DCS_ENTRY) + && (scanState != ScanState.DCS_INTERMEDIATE) + && (scanState != ScanState.DCS_IGNORE) + && (scanState != ScanState.DCS_PARAM) + && (scanState != ScanState.DCS_PASSTHROUGH) + ) { - scanState = ScanState.ESCAPE; - return; + scanState = ScanState.ESCAPE; + return; + } } // 0x9B == CSI 8-bit sequence @@ -6100,7 +6423,7 @@ public class ECMA48 implements Runnable { case OSC_STRING: // Special case for Xterm: OSC can pass control characters - if ((ch == 0x9C) || (ch <= 0x07)) { + if ((ch == 0x9C) || (ch == 0x07) || (ch == 0x1B)) { oscPut(ch); } -- 2.27.0