/*
* This file is part of lanterna (http://code.google.com/p/lanterna/).
*
* lanterna is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
* Copyright (C) 2010-2015 Martin
*/
package com.googlecode.lanterna.gui2;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.graphics.PropertiesTheme;
import com.googlecode.lanterna.graphics.Theme;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.screen.Screen;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* This abstract implementation of TextGUI contains some basic management of the underlying Screen and other common code
* that can be shared between different implementations.
* @author Martin
*/
public abstract class AbstractTextGUI implements TextGUI {
private final Screen screen;
private final List listeners;
private boolean blockingIO;
private boolean dirty;
private TextGUIThread textGUIThread;
private Theme guiTheme;
/**
* Constructor for {@code AbstractTextGUI} that requires a {@code Screen} and a factory for creating the GUI thread
* @param textGUIThreadFactory Factory class to use for creating the {@code TextGUIThread} class
* @param screen What underlying {@code Screen} to use for this text GUI
*/
protected AbstractTextGUI(TextGUIThreadFactory textGUIThreadFactory, Screen screen) {
if(screen == null) {
throw new IllegalArgumentException("Creating a TextGUI requires an underlying Screen");
}
this.screen = screen;
this.listeners = new CopyOnWriteArrayList();
this.blockingIO = false;
this.dirty = false;
this.guiTheme = new PropertiesTheme(loadDefaultThemeProperties());
this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this);
}
private static Properties loadDefaultThemeProperties() {
Properties properties = new Properties();
try {
ClassLoader classLoader = AbstractTextGUI.class.getClassLoader();
InputStream resourceAsStream = classLoader.getResourceAsStream("default-theme.properties");
if(resourceAsStream == null) {
resourceAsStream = new FileInputStream("src/main/resources/default-theme.properties");
}
properties.load(resourceAsStream);
resourceAsStream.close();
return properties;
}
catch(IOException e) {
return properties;
}
}
/**
* Reads one key from the input queue, blocking or non-blocking depending on if blocking I/O has been enabled. To
* enable blocking I/O (disabled by default), use {@code setBlockingIO(true)}.
* @return One piece of user input as a {@code KeyStroke} or {@code null} if blocking I/O is disabled and there was
* no input waiting
* @throws IOException In case of an I/O error while reading input
*/
protected KeyStroke readKeyStroke() throws IOException {
return blockingIO ? screen.readInput() : pollInput();
}
/**
* Polls the underlying input queue for user input, returning either a {@code KeyStroke} or {@code null}
* @return {@code KeyStroke} representing the user input or {@code null} if there was none
* @throws IOException In case of an I/O error while reading input
*/
protected KeyStroke pollInput() throws IOException {
return screen.pollInput();
}
@Override
public synchronized boolean processInput() throws IOException {
boolean gotInput = false;
KeyStroke keyStroke = readKeyStroke();
if(keyStroke != null) {
gotInput = true;
do {
if (keyStroke.getKeyType() == KeyType.EOF) {
throw new EOFException();
}
boolean handled = handleInput(keyStroke);
if(!handled) {
handled = fireUnhandledKeyStroke(keyStroke);
}
dirty = handled || dirty;
keyStroke = pollInput();
} while(keyStroke != null);
}
return gotInput;
}
@Override
public void setTheme(Theme theme) {
this.guiTheme = theme;
}
@Override
public synchronized void updateScreen() throws IOException {
screen.doResizeIfNecessary();
drawGUI(new TextGUIGraphics(this, screen.newTextGraphics(), guiTheme));
screen.setCursorPosition(getCursorPosition());
screen.refresh();
dirty = false;
}
@Override
public boolean isPendingUpdate() {
return screen.doResizeIfNecessary() != null || dirty;
}
@Override
public TextGUIThread getGUIThread() {
return textGUIThread;
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
/**
* Enables blocking I/O, causing calls to {@code readKeyStroke()} to block until there is input available. Notice
* that you can still poll for input using {@code pollInput()}.
* @param blockingIO Set this to {@code true} if blocking I/O should be enabled, otherwise {@code false}
*/
public void setBlockingIO(boolean blockingIO) {
this.blockingIO = blockingIO;
}
/**
* Checks if blocking I/O is enabled or not
* @return {@code true} if blocking I/O is enabled, otherwise {@code false}
*/
public boolean isBlockingIO() {
return blockingIO;
}
/**
* This method should be called when there was user input that wasn't handled by the GUI. It will fire the
* {@code onUnhandledKeyStroke(..)} method on any registered listener.
* @param keyStroke The {@code KeyStroke} that wasn't handled by the GUI
* @return {@code true} if at least one of the listeners handled the key stroke, this will signal to the GUI that it
* needs to be redrawn again.
*/
protected final boolean fireUnhandledKeyStroke(KeyStroke keyStroke) {
boolean handled = false;
for(Listener listener: listeners) {
handled = listener.onUnhandledKeyStroke(this, keyStroke) || handled;
}
return handled;
}
/**
* Marks the whole text GUI as invalid and that it needs to be redrawn at next opportunity
*/
protected void invalidate() {
dirty = true;
}
/**
* Draws the entire GUI using a {@code TextGUIGraphics} object
* @param graphics Graphics object to draw using
*/
protected abstract void drawGUI(TextGUIGraphics graphics);
/**
* Top-level method for drilling in to the GUI and figuring out, in global coordinates, where to place the text
* cursor on the screen at this time.
* @return Where to place the text cursor, or {@code null} if the cursor should be hidden
*/
protected abstract TerminalPosition getCursorPosition();
/**
* This method should take the user input and feed it to the focused component for handling.
* @param key {@code KeyStroke} representing the user input
* @return {@code true} if the input was recognized and handled by the GUI, indicating that the GUI should be redrawn
*/
protected abstract boolean handleInput(KeyStroke key);
}