Commit 13efaa98 authored by Robert Zenz's avatar Robert Zenz

Development of TUI2.

parent 38152ff5
......@@ -14,7 +14,7 @@
<property name="evalex.version" value="2.0" />
<property name="jline.version" value="3.8.0" />
<property name="lanterna.version" value="3.0.0" />
<property name="lanterna.version" value="3.0.1" />
<property name="picocli.version" value="3.5.2" />
<property name="swt.version" value="4.6.2" />
......@@ -81,6 +81,7 @@
<fileset dir="${build}">
<exclude name="org/bonsaimind/jmathpaper/uis/swt/**/*" />
<exclude name="org/bonsaimind/jmathpaper/uis/tui/**/*" />
<exclude name="org/bonsaimind/jmathpaper/uis/tui2/**/*" />
</fileset>
</jar>
<tar destfile="${dist}/${jar}-${version}-cli.tar.bz2" compression="bzip2">
......@@ -109,6 +110,7 @@
<jar jarfile="${dist}/${jar}-${version}-cli-tui.jar" manifest="MANIFEST.MF">
<fileset dir="${build}">
<exclude name="org/bonsaimind/jmathpaper/uis/swt/**/*" />
<exclude name="org/bonsaimind/jmathpaper/uis/tui2/**/*" />
</fileset>
</jar>
<tar destfile="${dist}/${jar}-${version}-cli-tui.tar.bz2" compression="bzip2">
......@@ -163,6 +165,7 @@
<jar jarfile="${dist}/${jar}-${version}-full-linux.jar" update="true" manifest="MANIFEST.MF">
<zipfileset src="${libs}/evalex/EvalEx-${evalex.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/jline/jline-${jline.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/lanterna/lanterna-${lanterna.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/picocli/picocli-${picocli.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/swt/linux/swt-${swt.version}.jar" excludes="META-INF/**/*" />
</jar>
......@@ -193,6 +196,7 @@
<jar jarfile="${dist}/${jar}-${version}-full-linux.jar" update="true" manifest="MANIFEST.MF">
<zipfileset src="${libs}/evalex/EvalEx-${evalex.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/jline/jline-${jline.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/lanterna/lanterna-${lanterna.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/picocli/picocli-${picocli.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/swt/macos/swt-${swt.version}.jar" excludes="META-INF/**/*" />
</jar>
......@@ -200,6 +204,7 @@
<jar jarfile="${dist}/${jar}-${version}-full-windows.jar" basedir="${build}" manifest="MANIFEST.MF">
<zipfileset src="${libs}/evalex/EvalEx-${evalex.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/jline/jline-${jline.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/lanterna/lanterna-${lanterna.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/picocli/picocli-${picocli.version}.jar" excludes="META-INF/**/*" />
<zipfileset src="${libs}/swt/windows/swt-${swt.version}.jar" excludes="META-INF/**/*" />
</jar>
......
package org.bonsaimind.jmathpaper.uis.tui2;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import org.bonsaimind.jmathpaper.core.EvaluatedExpression;
import org.bonsaimind.jmathpaper.core.InvalidExpressionException;
import org.bonsaimind.jmathpaper.core.Paper;
import org.bonsaimind.jmathpaper.core.ui.AbstractPapersUi;
import org.bonsaimind.jmathpaper.core.ui.UiParameters;
import org.bonsaimind.jmathpaper.uis.tui2.components.EventExtendedTextBox;
import org.bonsaimind.jmathpaper.uis.tui2.components.PseudoTabBar;
import com.googlecode.lanterna.TextColor.ANSI;
import com.googlecode.lanterna.graphics.SimpleTheme;
import com.googlecode.lanterna.gui2.BasicWindow;
import com.googlecode.lanterna.gui2.BorderLayout;
import com.googlecode.lanterna.gui2.BorderLayout.Location;
import com.googlecode.lanterna.gui2.Direction;
import com.googlecode.lanterna.gui2.Label;
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
import com.googlecode.lanterna.gui2.Panel;
import com.googlecode.lanterna.gui2.Separator;
import com.googlecode.lanterna.gui2.TextBox;
import com.googlecode.lanterna.gui2.TextBox.Style;
import com.googlecode.lanterna.gui2.Window;
import com.googlecode.lanterna.gui2.Window.Hint;
import com.googlecode.lanterna.gui2.table.DefaultTableRenderer;
import com.googlecode.lanterna.gui2.table.Table;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;
public class Tui2 extends AbstractPapersUi {
private String bufferedInput = null;
private Label errorLabel = null;
private Table<String> expressionsTable = null;
private EventExtendedTextBox inputTextBox = null;
private TextBox notesTextBox = null;
private int paperCounter = 0;
private Screen screen = null;
private PseudoTabBar<Paper> tabBar = null;
private Window window = null;
public Tui2() {
super();
}
@Override
public void clear() {
super.clear();
clearExpressionsTable();
updateExpressionTable();
}
@Override
public void close() {
Paper currentPaper = paper;
super.close();
if (currentPaper != null) {
tabBar.removeTab(tabBar.getSelectedTabIndex());
}
}
@Override
public void closeAll() {
super.closeAll();
clearExpressionsTable();
while (tabBar.getTabCount() > 0) {
tabBar.removeTab(0);
}
}
@Override
public void init() throws Exception {
super.init();
tabBar = new PseudoTabBar<Paper>()
.addSelectedTabChangedListener(this::onSelectedTabChanged);
Panel tabPanel = new Panel();
tabPanel.setLayoutManager(new BorderLayout());
tabPanel.addComponent(tabBar, Location.TOP);
tabPanel.addComponent(new Separator(Direction.HORIZONTAL), Location.BOTTOM);
notesTextBox = new EventExtendedTextBox(Style.MULTI_LINE)
.addTextChangedHandler(this::onNotesTextBoxChanged);
Panel notesPanel = new Panel();
notesPanel.setLayoutManager(new BorderLayout());
notesPanel.addComponent(new Separator(Direction.VERTICAL), Location.CENTER);
notesPanel.addComponent(notesTextBox, Location.RIGHT);
errorLabel = new Label("");
inputTextBox = new EventExtendedTextBox(Style.SINGLE_LINE)
.addHandler(new KeyStroke(KeyType.ArrowDown), this::onInputTextBoxDownKey)
.addHandler(new KeyStroke(KeyType.Enter), this::onInputTextBoxEnterKey)
.addHandler(new KeyStroke(KeyType.ArrowUp), this::onInputTextBoxUpKey);
inputTextBox.setHorizontalFocusSwitching(false);
inputTextBox.setVerticalFocusSwitching(false);
Panel southPanel = new Panel();
southPanel.setLayoutManager(new BorderLayout());
southPanel.addComponent(new Separator(Direction.HORIZONTAL), Location.TOP);
southPanel.addComponent(errorLabel, Location.CENTER);
southPanel.addComponent(inputTextBox, Location.BOTTOM);
expressionsTable = new Table<String>("ID", "Expression", "Result")
.setCellSelection(false)
.setEscapeByArrowKey(false);
((DefaultTableRenderer<?>)expressionsTable.getRenderer()).setExpandableColumns(Arrays.asList(Integer.valueOf(1)));
Panel paperPanel = new Panel();
paperPanel.setLayoutManager(new BorderLayout());
paperPanel.addComponent(expressionsTable, Location.CENTER);
paperPanel.addComponent(southPanel, Location.BOTTOM);
Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new BorderLayout());
mainPanel.addComponent(tabPanel, Location.TOP);
mainPanel.addComponent(paperPanel, Location.CENTER);
mainPanel.addComponent(notesPanel, Location.RIGHT);
window = new BasicWindow("Test");
window.setHints(Arrays.asList(Hint.FULL_SCREEN, Hint.NO_DECORATIONS));
window.setComponent(mainPanel);
inputTextBox.takeFocus();
}
@Override
public void new_() {
super.new_();
addTab(paper);
}
@Override
public void open(Path file) throws InvalidExpressionException, IOException {
Paper paperToBeClosed = null;
if (papers.size() == 1
&& paper != null
&& paper.getEvaluatedExpressions().isEmpty()
&& paper.getFile() == null) {
// Seems like a new and empty paper, let's close it.
paperToBeClosed = paper;
}
super.open(file);
Paper openedPaper = paper;
// If there is already a tab with the current paper, we can exit.
for (int index = 0; index < tabBar.getTabCount(); index++) {
if (tabBar.getContent(index) == paper) {
return;
}
}
setPaper(paperToBeClosed);
close();
setPaper(openedPaper);
addTab(paper);
}
@Override
public void quit() {
try {
screen.close();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run(UiParameters uiParameters) throws Exception {
super.run(uiParameters);
Terminal terminal = new DefaultTerminalFactory().createTerminal();
screen = new TerminalScreen(terminal);
screen.startScreen();
MultiWindowTextGUI gui = new MultiWindowTextGUI(screen);
gui.setTheme(SimpleTheme.makeTheme(
false,
ANSI.WHITE,
ANSI.BLACK,
ANSI.WHITE,
ANSI.BLACK,
ANSI.BLACK,
ANSI.WHITE,
ANSI.BLACK));
gui.addWindowAndWait(window);
}
@Override
protected void currentPaperHasBeenModified() {
super.currentPaperHasBeenModified();
clearExpressionsTable();
updateExpressionTable();
}
@Override
protected void currentPaperHasBeenReset() {
super.currentPaperHasBeenReset();
clearExpressionsTable();
updateExpressionTable();
}
@Override
protected void setPaper(Paper paper) throws IllegalStateException {
super.setPaper(paper);
if (paper != null) {
tabBar.setSelectedTab(papers.indexOf(paper));
}
}
private void addTab(Paper paper) {
if (paper.getFile() != null) {
tabBar.addTab(papers.indexOf(paper), paper.getFile().getFileName().toString(), paper);
} else {
paperCounter = paperCounter + 1;
tabBar.addTab(papers.indexOf(paper), "*Paper #" + Integer.toString(paperCounter), paper);
}
}
private void clearExpressionsTable() {
while (expressionsTable.getTableModel().getRowCount() > 0) {
expressionsTable.getTableModel().removeRow(0);
}
}
private void onInputTextBoxDownKey(EventExtendedTextBox textBox) {
selectNextRow(1);
}
private void onInputTextBoxEnterKey(EventExtendedTextBox textBox) {
try {
process(inputTextBox.getText());
resetInput();
} catch (Exception e) {
if (e.getMessage() != null) {
errorLabel.setText(e.getMessage());
} else {
errorLabel.setText("No details available: " + e.getClass().getSimpleName());
}
}
}
private void onInputTextBoxUpKey(EventExtendedTextBox textBox) {
selectNextRow(-1);
}
private void onNotesTextBoxChanged(EventExtendedTextBox textBox, String oldText, String newText) {
paper.setNotes(newText);
}
private void onSelectedTabChanged(PseudoTabBar<?> pseudoBar, int oldSelectedTabIndex, int newSelectedTabIndex) {
clearExpressionsTable();
if (newSelectedTabIndex >= 0) {
setPaper(papers.get(newSelectedTabIndex));
updateExpressionTable();
}
}
private void resetInput() {
inputTextBox.setText("");
errorLabel.setText("");
expressionsTable.setSelectedRow(-1);
bufferedInput = null;
}
private void selectNextRow(int direction) {
if (expressionsTable.getTableModel().getRowCount() == 0) {
return;
}
int rowCount = expressionsTable.getTableModel().getRowCount();
int selectedRow = expressionsTable.getSelectedRow();
if (selectedRow >= 0) {
selectedRow = selectedRow + direction;
if (selectedRow < 0) {
selectedRow = 0;
} else if (selectedRow >= rowCount) {
selectedRow = -1;
}
} else if (direction < 0) {
selectedRow = rowCount - 1;
}
expressionsTable.setSelectedRow(selectedRow);
expressionsTable.invalidate();
if (selectedRow < 0) {
if (bufferedInput != null) {
inputTextBox.setText(bufferedInput);
bufferedInput = null;
}
} else {
if (bufferedInput == null) {
bufferedInput = inputTextBox.getText();
}
inputTextBox.setText(expressionsTable.getTableModel().getRow(selectedRow).get(1));
}
}
private void updateExpressionTable() {
if (paper != null) {
for (int index = expressionsTable.getTableModel().getRowCount(); index < paper.getEvaluatedExpressions().size(); index++) {
EvaluatedExpression evaluatedExpression = paper.getEvaluatedExpressions().get(index);
expressionsTable.getTableModel().addRow(
evaluatedExpression.getId(),
evaluatedExpression.getExpression(),
evaluatedExpression.getFormattedResult(paper.getNumberFormat()));
expressionsTable.setSelectedRow(-1);
}
}
}
}
package org.bonsaimind.jmathpaper.uis.tui2.components;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.googlecode.lanterna.gui2.TextBox;
import com.googlecode.lanterna.input.KeyStroke;
public class EventExtendedTextBox extends TextBox {
private Map<KeyStroke, List<KeyStrokeHandler>> keyStrokeHandlers = new HashMap<>();
private List<TextChangedListener> textChangedListeners = new ArrayList<>();
public EventExtendedTextBox(Style style) {
super("", style);
}
public EventExtendedTextBox addHandler(KeyStroke keyStroke, KeyStrokeHandler handler) {
List<KeyStrokeHandler> handlers = keyStrokeHandlers.get(keyStroke);
if (handlers == null) {
handlers = new ArrayList<>();
keyStrokeHandlers.put(keyStroke, handlers);
}
handlers.add(handler);
return this;
}
public EventExtendedTextBox addTextChangedHandler(TextChangedListener handler) {
textChangedListeners.add(handler);
return this;
}
@Override
public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
List<KeyStrokeHandler> handlers = keyStrokeHandlers.get(keyStroke);
if (handlers != null && !handlers.isEmpty()) {
for (KeyStrokeHandler handler : handlers) {
handler.handleKeyStroke(this);
}
return Result.HANDLED;
} else {
String textBefore = getText();
Result result = super.handleKeyStroke(keyStroke);
if (!Objects.equals(textBefore, getText())) {
for (TextChangedListener handler : textChangedListeners) {
handler.textChanged(this, textBefore, getText());
}
}
return result;
}
}
public EventExtendedTextBox removeHandler(KeyStroke keyStroke, KeyStrokeHandler handler) {
List<KeyStrokeHandler> handlers = keyStrokeHandlers.get(keyStroke);
if (handlers != null) {
handlers.remove(handler);
}
return this;
}
public EventExtendedTextBox removeTextChangedHandler(TextChangedListener handler) {
textChangedListeners.remove(handler);
return this;
}
public interface KeyStrokeHandler {
public void handleKeyStroke(EventExtendedTextBox textBox);
}
public interface TextChangedListener {
public void textChanged(EventExtendedTextBox textBox, String oldText, String newText);
}
}
package org.bonsaimind.jmathpaper.uis.tui2.components;
import java.util.ArrayList;
import java.util.List;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.AbstractInteractableComponent;
import com.googlecode.lanterna.gui2.InteractableRenderer;
import com.googlecode.lanterna.gui2.TextGUIGraphics;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
public class PseudoTabBar<CONTENT> extends AbstractInteractableComponent<PseudoTabBar<?>> {
private List<CONTENT> contents = new ArrayList<>();
private List<SelectedTabChangedListener> selectedTabChangedListeners = new ArrayList<>();
private int selectedTabIndex = -1;
private List<String> tabs = new ArrayList<>();
public PseudoTabBar() {
super();
}
public PseudoTabBar<CONTENT> addSelectedTabChangedListener(SelectedTabChangedListener listener) {
selectedTabChangedListeners.add(listener);
return this;
}
public PseudoTabBar<CONTENT> addTab(int index, String text, CONTENT content) {
tabs.add(index, text);
contents.add(index, content);
setSelectedTab(index);
return this;
}
public PseudoTabBar<CONTENT> addTab(String text, CONTENT content) {
if (!tabs.isEmpty()) {
return addTab(tabs.size() - 1, text, content);
} else {
return addTab(0, text, content);
}
}
public CONTENT getContent(int index) {
return contents.get(index);
}
public CONTENT getSelectedTabContent() {
if (selectedTabIndex >= 0) {
return contents.get(selectedTabIndex);
} else {
return null;
}
}
public int getSelectedTabIndex() {
return selectedTabIndex;
}
public String getTab(int index) {
return tabs.get(index);
}
public int getTabCount() {
return tabs.size();
}
public PseudoTabBar<CONTENT> removeSelectedTabChangedListener(SelectedTabChangedListener listener) {
selectedTabChangedListeners.remove(listener);
return this;
}
public PseudoTabBar<CONTENT> removeTab(int index) {
tabs.remove(index);
contents.remove(index);
int oldSelectedTabIndex = selectedTabIndex;
if (tabs.isEmpty()) {
selectedTabIndex = -1;
} else if (selectedTabIndex >= tabs.size()) {
selectedTabIndex = tabs.size() - 1;
}
for (SelectedTabChangedListener listener : selectedTabChangedListeners) {
listener.selectedTabChanged(this, oldSelectedTabIndex, selectedTabIndex);
}
invalidate();
return this;
}
public PseudoTabBar<CONTENT> setSelectedTab(int index) {
int oldSelectedTabIndex = selectedTabIndex;
selectedTabIndex = index;
if (oldSelectedTabIndex != selectedTabIndex) {
for (SelectedTabChangedListener listener : selectedTabChangedListeners) {
listener.selectedTabChanged(this, oldSelectedTabIndex, selectedTabIndex);
}
}
invalidate();
return this;
}
@Override
protected InteractableRenderer<PseudoTabBar<?>> createDefaultRenderer() {
return new DefaultRenderer<PseudoTabBar<?>>();
}
@Override
protected Result handleKeyStroke(KeyStroke keyStroke) {
if (keyStroke.getKeyType() == KeyType.ArrowLeft || keyStroke.getKeyType() == KeyType.ArrowDown) {
setSelectedTab(Math.max(0, selectedTabIndex - 1));
return Result.HANDLED;
} else if (keyStroke.getKeyType() == KeyType.ArrowRight || keyStroke.getKeyType() == KeyType.ArrowUp) {
setSelectedTab(Math.min(tabs.size() - 1, selectedTabIndex + 1));
return Result.HANDLED;
}
return super.handleKeyStroke(keyStroke);
}
public interface SelectedTabChangedListener {
public void selectedTabChanged(PseudoTabBar<?> pseudoTabBar, int oldSelectedTabIndex, int newSelectedTabIndex);
}
private static final class DefaultRenderer<CONTENT> implements InteractableRenderer<PseudoTabBar<?>> {
public DefaultRenderer() {
super();
}
@Override
public void drawComponent(TextGUIGraphics graphics, PseudoTabBar<?> component) {
int column = 0;
for (int index = 0; index < component.tabs.size(); index++) {
String tab = component.tabs.get(index);
if (index == component.selectedTabIndex) {
graphics.applyThemeStyle(component.getTheme().getDefaultDefinition().getSelected());
} else {
graphics.applyThemeStyle(component.getTheme().getDefaultDefinition().getNormal());
}
graphics.putString(column, 0, tab);
column = column + tab.length();
graphics.applyThemeStyle(component.getTheme().getDefaultDefinition().getNormal());
if (index != component.tabs.size() - 1) {
graphics.putString(column, 0, " | ");
column = column + 3;
}
}
}
@Override
public TerminalPosition getCursorLocation(PseudoTabBar<?> component) {
return null;
}
@Override
public TerminalSize getPreferredSize(PseudoTabBar<?> component) {
return new TerminalSize(1, 1);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment