/*
* 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 <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2010-2012 Martin
*/
package com.googlecode.lanterna.gui;
import com.googlecode.lanterna.gui.listener.WindowAdapter;
import com.googlecode.lanterna.input.Key;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.terminal.TerminalPosition;
import com.googlecode.lanterna.terminal.TerminalSize;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* This is the main class of the GUI system in Lanterna. To setup a GUI, you
* instantiate this class and call the showWindow(...) method on window.
* Please notice that this class doesn't have any start or stop methods, this
* must be managed by the underlying screen which is the backend for the GUI.
* @author Martin
*/
public class GUIScreen
{
private final Screen screen;
private final LinkedList<WindowPlacement> windowStack;
private final Queue<Action> actionToRunInEventThread;
private GUIScreenBackgroundRenderer backgroundRenderer;
private Theme guiTheme;
private boolean needsRefresh;
private Thread eventThread;
public GUIScreen(Screen screen)
{
this(screen, "");
}
public GUIScreen(Screen screen, String title)
{
this(screen, new DefaultBackgroundRenderer(title));
}
public GUIScreen(Screen screen, GUIScreenBackgroundRenderer backgroundRenderer)
{
if(backgroundRenderer == null)
throw new IllegalArgumentException("backgroundRenderer cannot be null");
this.backgroundRenderer = backgroundRenderer;
this.screen = screen;
this.guiTheme = Theme.getDefaultTheme();
this.windowStack = new LinkedList<WindowPlacement>();
this.actionToRunInEventThread = new LinkedList<Action>();
this.needsRefresh = false;
this.eventThread = Thread.currentThread(); //We'll be expecting the thread who created us is the same as will be the event thread later
}
/**
* @param title Title to be displayed in the top-left corner
* @deprecated Use a GUI background renderer instead
*/
@Deprecated
public void setTitle(String title)
{
if(title == null)
title = "";
if(backgroundRenderer instanceof DefaultBackgroundRenderer) {
((DefaultBackgroundRenderer)backgroundRenderer).setTitle(title);
}
}
/**
* Sets a new Theme for the entire GUI
*/
public void setTheme(Theme newTheme)
{
if(newTheme == null)
return;
this.guiTheme = newTheme;
needsRefresh = true;
}
public void setBackgroundRenderer(GUIScreenBackgroundRenderer backgroundRenderer) {
if(backgroundRenderer == null)
throw new IllegalArgumentException("backgroundRenderer cannot be null");
this.backgroundRenderer = backgroundRenderer;
needsRefresh = true;
}
public GUIScreenBackgroundRenderer getBackgroundRenderer() {
return backgroundRenderer;
}
/**
* Gets the underlying screen, which can be used for starting, stopping,
* querying for size and much more
* @return The Screen which is backing this GUI
*/
public Screen getScreen() {
return screen;
}
private synchronized void repaint()
{
if(screen.resizePending())
screen.refresh(); //Do an initial refresh if there are any resizes in the queue
final TextGraphics textGraphics = new TextGraphicsImpl(new TerminalPosition(0, 0),
new TerminalSize(screen.getTerminalSize()), screen, guiTheme);
backgroundRenderer.drawBackground(textGraphics);
int screenSizeColumns = screen.getTerminalSize().getColumns();
int screenSizeRows = screen.getTerminalSize().getRows();
//Go through the windows
for(WindowPlacement windowPlacement: windowStack) {
if(hasSoloWindowAbove(windowPlacement))
continue;
if(hasFullScreenWindowAbove(windowPlacement))
continue;
TerminalPosition topLeft = windowPlacement.getTopLeft();
TerminalSize preferredSize;
if(windowPlacement.getPositionPolicy() == Position.FULL_SCREEN)
preferredSize = new TerminalSize(screenSizeColumns, screenSizeRows);
else
preferredSize = windowPlacement.getWindow().getPreferredSize();
if(windowPlacement.positionPolicy == Position.CENTER) {
if(windowPlacement.getWindow().maximisesHorisontally())
topLeft.setColumn(2);
else
topLeft.setColumn((screenSizeColumns / 2) - (preferredSize.getColumns() / 2));
if(windowPlacement.getWindow().maximisesVertically())
topLeft.setRow(1);
else
topLeft.setRow((screenSizeRows / 2) - (preferredSize.getRows() / 2));
}
int maxSizeWidth = screenSizeColumns - windowPlacement.getTopLeft().getColumn() - 1;
int maxSizeHeight = screenSizeRows - windowPlacement.getTopLeft().getRow() - 1;
if(preferredSize.getColumns() > maxSizeWidth || windowPlacement.getWindow().maximisesHorisontally())
preferredSize.setColumns(maxSizeWidth);
if(preferredSize.getRows() > maxSizeHeight || windowPlacement.getWindow().maximisesVertically())
preferredSize.setRows(maxSizeHeight);
if(windowPlacement.getPositionPolicy() == Position.FULL_SCREEN) {
preferredSize.setColumns(preferredSize.getColumns() + 1);
preferredSize.setRows(preferredSize.getRows() + 1);
}
if(topLeft.getColumn() < 0)
topLeft.setColumn(0);
if(topLeft.getRow() < 0)
topLeft.setRow(0);
TextGraphics subGraphics = textGraphics.subAreaGraphics(topLeft,
new TerminalSize(preferredSize.getColumns(), preferredSize.getRows()));
//First draw the shadow
textGraphics.applyTheme(guiTheme.getDefinition(Theme.Category.SHADOW));
textGraphics.fillRectangle(' ', new TerminalPosition(topLeft.getColumn() + 2, topLeft.getRow() + 1),
new TerminalSize(subGraphics.getWidth(), subGraphics.getHeight()));
//Then draw the window
windowPlacement.getWindow().repaint(subGraphics);
}
if(windowStack.size() > 0 && windowStack.getLast().getWindow().getWindowHotspotPosition() != null)
screen.setCursorPosition(windowStack.getLast().getWindow().getWindowHotspotPosition());
else
screen.setCursorPosition(null);
screen.refresh();
}
private boolean update()
{
if(needsRefresh || screen.resizePending()) {
repaint();
needsRefresh = false;
return true;
}
return false;
}
/**
* Signals the the entire screen needs to be re-drawn
*/
public void invalidate()
{
needsRefresh = true;
}
private void doEventLoop()
{
int currentStackLength = windowStack.size();
if(currentStackLength == 0)
return;
while(true) {
if(currentStackLength > windowStack.size()) {
//The window was removed from the stack ( = it was closed)
break;
}
try {
synchronized(actionToRunInEventThread) {
List<Action> actions = new ArrayList<Action>(actionToRunInEventThread);
actionToRunInEventThread.clear();
for(Action nextAction: actions)
nextAction.doAction();
}
boolean repainted = update();
Key nextKey = screen.readInput();
if(nextKey != null) {
windowStack.getLast().window.onKeyPressed(nextKey);
invalidate();
}
else {
if(!repainted) {
try {
Thread.sleep(1);
}
catch(InterruptedException e) {}
}
}
}
catch(Throwable e) {
e.printStackTrace();
}
}
}
/**
* Same as calling showWindow(window, Position.OVERLAPPING)
* @param window Window to be shown
*/
public void showWindow(Window window)
{
showWindow(window, Position.OVERLAPPING);
}
/**
* This method starts the GUI system with an initial window. The method
* does not return until the window has been closed, so you need to provide
* a mechanism for closing the window using the GUI.
*
* If you call this method when already in GUI mode, it will create the new
* window stacked on top of any previous window(s) and won't return until
* this new window has been closed.
* @param window Window to display
* @param position Where to position the new window
*/
public void showWindow(Window window, Position position)
{
if(window == null)
return;
if(position == null)
position = Position.OVERLAPPING;
int newWindowX = 2;
int newWindowY = 1;
if(position == Position.OVERLAPPING &&
windowStack.size() > 0) {
WindowPlacement lastWindow = windowStack.getLast();
if(lastWindow.getPositionPolicy() != Position.CENTER) {
newWindowX = lastWindow.getTopLeft().getColumn() + 2;
newWindowY = lastWindow.getTopLeft().getRow() + 1;
}
}
window.addWindowListener(new WindowAdapter() {
@Override
public void onWindowInvalidated(Window window)
{
needsRefresh = true;
}
});
windowStack.add(new WindowPlacement(window, position, new TerminalPosition(newWindowX, newWindowY)));
window.setOwner(this);
window.onVisible();
needsRefresh = true;
doEventLoop();
}
/**
* @deprecated Call getActiveWindow().close() instead
*/
@Deprecated
public void closeWindow() {
Window activeWindow = getActiveWindow();
if(activeWindow != null) {
activeWindow.close();
}
}
/**
* Used internally to close a window; API users should call Window.close() instead
*/
void closeWindow(Window window)
{
if(windowStack.size() == 0)
return;
for(Iterator<WindowPlacement> iterator = windowStack.iterator(); iterator.hasNext(); ) {
WindowPlacement placement = iterator.next();
if(placement.window == window) {
iterator.remove();
placement.window.onClosed();
return;
}
}
}
/**
* Returns the top window in the window stack, the one which currently has user input focus.
* @return The uppermost window in the window stack, or null if there are no windows
*/
public Window getActiveWindow() {
if(windowStack.isEmpty()) {
return null;
}
else {
return windowStack.getLast().getWindow();
}
}
/**
* Since Lanterna isn't thread safe, here's a way to run code on the same
* thread as the GUI system is using. Pass an action in and it will be
* queued for execution.
* @param codeToRun Code to be executed on the same thread as the GUI
*/
public void runInEventThread(Action codeToRun)
{
synchronized(actionToRunInEventThread) {
actionToRunInEventThread.add(codeToRun);
}
}
/**
* @return True if the current thread calling this method is the same thread
* as the GUI system is using
*/
public boolean isInEventThread()
{
return eventThread == Thread.currentThread();
}
/**
* Where to position a window that is to be put on the screen
*/
public enum Position
{
/**
* Starting from the top left corner, created windows overlapping in
* a down-right direction (similar to Microsoft Windows)
*/
OVERLAPPING,
/**
* This window will be placed in the top-left corner, any windows
* created with overlapping after it will be positioned relative
*/
NEW_CORNER_WINDOW,
/**
* At the center of the screen
*/
CENTER,
/**
* The window will be maximized and take up the entire screen
*/
FULL_SCREEN,
;
}
private boolean hasSoloWindowAbove(WindowPlacement windowPlacement)
{
int index = windowStack.indexOf(windowPlacement);
for(int i = index + 1; i < windowStack.size(); i++) {
if(windowStack.get(i).window.isSoloWindow())
return true;
}
return false;
}
private boolean hasFullScreenWindowAbove(WindowPlacement windowPlacement) {
int index = windowStack.indexOf(windowPlacement);
for(int i = index + 1; i < windowStack.size(); i++) {
if(windowStack.get(i).positionPolicy == Position.FULL_SCREEN)
return true;
}
return false;
}
private class WindowPlacement
{
private Window window;
private Position positionPolicy;
private TerminalPosition topLeft;
public WindowPlacement(Window window, Position positionPolicy, TerminalPosition topLeft)
{
this.window = window;
this.positionPolicy = positionPolicy;
this.topLeft = topLeft;
}
public TerminalPosition getTopLeft()
{
if(positionPolicy == Position.FULL_SCREEN)
return new TerminalPosition(0, 0);
return topLeft;
}
public void setTopLeft(TerminalPosition topLeft)
{
this.topLeft = topLeft;
}
public Window getWindow()
{
return window;
}
public void setWindow(Window window)
{
this.window = window;
}
public Position getPositionPolicy()
{
return positionPolicy;
}
}
}