/*
* $Id: TextViewport.java 535 2008-02-19 06:02:50Z weiju $
*
* Created on 2005/10/20
* Copyright 2005-2008 by Wei-ju Wu
* This file is part of The Z-machine Preservation Project (ZMPP).
*
* ZMPP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ZMPP 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ZMPP. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zmpp.swingui;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
import org.zmpp.io.OutputStream;
import org.zmpp.vm.Machine;
import org.zmpp.vm.ScreenModel;
import org.zmpp.vm.StoryFileHeader;
import org.zmpp.vm.TextCursor;
import org.zmpp.vm.StoryFileHeader.Attribute;
/**
* This class is a custom text component, rendering is handled by this class. As
* opposed to former versions, this inherits from JComponent, so it is even more
* lightweight.
*
* @author Wei-ju Wu
* @version 1.0
*/
public class TextViewport extends JComponent implements ScreenModel, Viewport {
private static final long serialVersionUID = 1L;
private BufferedImage imageBuffer;
private Canvas canvas;
private boolean initialized;
private ScreenOutputStream outputstream;
private static final int WINDOW_BOTTOM = 0;
private static final int WINDOW_TOP = 1;
private DisplaySettings settings;
private int defaultForeground;
private int defaultBackground;
private Font standardFont, fixedFont;
private Machine machine;
private LineEditor editor;
private SubWindow[] windows;
private int activeWindow;
private static final boolean DEBUG = false;
public TextViewport(Machine machine, LineEditor editor,
DisplaySettings settings) {
this.machine = machine;
this.editor = editor;
this.settings = settings;
standardFont = new Font("Dialog", Font.ROMAN_BASELINE,
settings.getStdFontSize());
fixedFont = new Font("Monospaced", Font.ROMAN_BASELINE,
settings.getFixedFontSize());
outputstream = new ScreenOutputStream(machine, this);
windows = new SubWindow[2];
activeWindow = WINDOW_BOTTOM;
// For efficiency, override some of this component's standard properties
setOpaque(true);
setDoubleBuffered(false);
}
public CursorWindow getCurrentWindow() {
return windows[activeWindow];
}
public LineEditor getLineEditor() {
return editor;
}
public int getDefaultBackground() {
return defaultBackground;
}
public int getDefaultForeground() {
return defaultForeground;
}
public Canvas getCanvas() {
return canvas;
}
public void reset() {
setScreenProperties();
windows[WINDOW_TOP].clear();
resizeWindows(0);
windows[WINDOW_BOTTOM].clear();
repaintInUiThread();
}
public void eraseWindow(int window) {
if (window == -1) {
resizeWindows(0);
windows[WINDOW_BOTTOM].clear();
} else if (window == -2) {
windows[WINDOW_TOP].clear();
windows[WINDOW_BOTTOM].clear();
} else {
// Note: The specification leaves unclear if the cursor position
// should be reset in this case
windows[window].clear();
}
}
public void eraseLine(int value) {
if (value == 1) {
windows[activeWindow].eraseLine();
}
}
/**
* {@inheritDoc}
*/
public TextCursor getTextCursor() {
windows[activeWindow].flushBuffer();
return windows[activeWindow].getCursor();
}
/**
* {@inheritDoc}
*/
public void setTextCursor(int line, int column, int window) {
windows[activeWindow].setCursorPosition(line, column);
}
public void splitWindow(final int linesUpperWindow) {
// The standard document suggests that a split should only take part
// if the lower window is selected (S 8.7.2.1), but Bureaucracy does
// the split with the upper window selected, so we do that resizing
// always
resizeWindows(linesUpperWindow);
// S 8.6.1.1.2: Top window is cleared in version 3
if (machine.getGameData().getStoryFileHeader().getVersion() == 3) {
windows[WINDOW_TOP].clear();
}
}
public void setWindow(final int window) {
//System.out.printf("@set_window %d\n", window);
// Flush out the current active window
getOutputStream().flush();
activeWindow = window;
// S 8.7.2: If the top window is set active, reset the cursor position
if (activeWindow == WINDOW_TOP) {
windows[activeWindow].resetCursorToHome();
}
}
/**
* This function implements text styles in our screen model.
*
* @param style the style mask as defined in the standards document
*/
public void setTextStyle(int style) {
// Flush the output before setting a new style
getOutputStream().flush();
// Reset to plain if style is roman, or get the current font style
// otherwise
int fontStyle = (style == TEXTSTYLE_ROMAN) ? Font.PLAIN
: windows[activeWindow].getFont().getStyle();
Font windowFont;
// Ensure that the top window is always set in a fixed font
if ((style & TEXTSTYLE_FIXED) > 0 || activeWindow == WINDOW_TOP) {
windowFont = fixedFont;
} else {
windowFont = standardFont;
}
windows[activeWindow].setReverseVideo(
(style & TEXTSTYLE_REVERSE_VIDEO) > 0);
fontStyle |= ((style & TEXTSTYLE_BOLD) > 0) ? Font.BOLD : 0;
fontStyle |= ((style & TEXTSTYLE_ITALIC) > 0) ? Font.ITALIC : 0;
windows[activeWindow].setFont(windowFont.deriveFont(fontStyle));
}
public void setBufferMode(boolean flag) {
// only affects bottom window
getOutputStream().flush();
windows[WINDOW_BOTTOM].setBufferMode(flag);
}
public void setPaging(boolean flag) {
windows[WINDOW_BOTTOM].setPagingEnabled(flag);
}
public synchronized boolean isInitialized() {
return initialized;
}
public synchronized void setInitialized() {
this.initialized = true;
notifyAll();
}
public synchronized void waitInitialized() {
while (!isInitialized()) {
try {
wait();
} catch (Exception ex) {
}
}
}
protected void paintComponent(Graphics g) {
if (imageBuffer == null) {
imageBuffer = new BufferedImage(getWidth(), getHeight(),
BufferedImage.TYPE_INT_RGB);
canvas = new CanvasImpl(imageBuffer, this, settings.getAntialias());
// Default colors
setDefaultColors(machine.getGameData().getStoryFileHeader(),
ColorTranslator.COLOR_WHITE, ColorTranslator.COLOR_BLACK);
// Create the two sub windows
windows[WINDOW_TOP] = new TopWindow(this);
// S. 8.7.2.4: use fixed font for upper window
windows[WINDOW_TOP].setFont(fixedFont);
windows[WINDOW_TOP].setFontNumber(ScreenModel.FONT_FIXED);
windows[WINDOW_BOTTOM] = new BottomWindow(this);
windows[WINDOW_BOTTOM].setFont(standardFont);
windows[WINDOW_BOTTOM].setFontNumber(ScreenModel.FONT_NORMAL);
activeWindow = WINDOW_BOTTOM;
Graphics g_img = imageBuffer.getGraphics();
resizeWindows(0);
windows[WINDOW_TOP].resetCursorToHome();
windows[WINDOW_BOTTOM].resetCursorToHome();
setScreenProperties();
g_img.setColor(ColorTranslator.getInstance().translate(defaultBackground));
g_img.fillRect(0, 0, getWidth(), getHeight());
windows[WINDOW_TOP].setBackground(defaultBackground);
windows[WINDOW_TOP].setForeground(defaultForeground);
windows[WINDOW_BOTTOM].setBackground(defaultBackground);
windows[WINDOW_BOTTOM].setForeground(defaultForeground);
setInitialized();
}
g.drawImage(imageBuffer, 0, 0, this);
if (DEBUG) {
// Draw separator lines
g.setColor(Color.BLACK);
g.drawLine(0, windows[WINDOW_TOP].getHeight() - 1, getWidth(),
windows[WINDOW_TOP].getHeight() - 1);
g.drawLine(0, 180 + windows[WINDOW_BOTTOM].getHeight() - 1, getWidth(),
180 + windows[WINDOW_BOTTOM].getHeight() - 1);
}
}
/**
* {@inheritDoc}
*/
public void setForegroundColor(int colornum, int window) {
if (colornum > 0) {
getOutputStream().flush();
windows[WINDOW_TOP].setForeground(colornum);
windows[WINDOW_BOTTOM].setForeground(colornum);
}
}
/**
* {@inheritDoc}
*/
public void setBackgroundColor(int colornum, int window) {
if (colornum > 0) {
getOutputStream().flush();
windows[WINDOW_TOP].setBackground(colornum);
windows[WINDOW_BOTTOM].setBackground(colornum);
}
}
/**
* {@inheritDoc}
*/
public void redraw() {
repaintInUiThread();
}
/**
* {@inheritDoc}
*/
public int setFont(int fontnum) {
getOutputStream().flush();
int previous = windows[activeWindow].getFontNumber();
switch (fontnum) {
case FONT_FIXED:
windows[activeWindow].setFont(fixedFont);
windows[activeWindow].setFontNumber(fontnum);
break;
case FONT_NORMAL:
windows[activeWindow].setFont(standardFont);
windows[activeWindow].setFontNumber(fontnum);
break;
case FONT_CHARACTER_GRAPHICS:
// CODE-DEBT:
// Note: if the @set_font command requests font 3, we will switch the
// window to fixed, so we solve the misalignment, but will return the
// 0 font anyways. This is not really correct, but will make sure
// that "Beyond Zork" does not look so weird...
windows[activeWindow].setFont(fixedFont);
windows[activeWindow].setFontNumber(fontnum);
default:
previous = 0;
break;
}
return previous;
}
/**
* {@inheritDoc}
*/
public synchronized void displayCursor(boolean showCaret) {
windows[activeWindow].drawCursor(showCaret);
}
/**
* {@inheritDoc}
*/
public OutputStream getOutputStream() {
return outputstream;
}
/**
* Reset the line counters.
*/
public void resetPagers() {
windows[WINDOW_TOP].resetPager();
windows[WINDOW_BOTTOM].resetPager();
}
// **********************************************************************
// ******** Private functions
// *************************************************
private void updateDimensionsInHeader() {
StoryFileHeader fileheader = machine.getGameData().getStoryFileHeader();
if (fileheader.getVersion() >= 4) {
FontMetrics fm = imageBuffer.getGraphics().getFontMetrics(fixedFont);
int screenWidth = imageBuffer.getWidth() / fm.charWidth('0');
int screenHeight = imageBuffer.getHeight() / fm.getHeight();
fileheader.setScreenWidth(screenWidth);
fileheader.setScreenHeight(screenHeight);
if (fileheader.getVersion() >= 5) {
fileheader.setScreenWidthUnits(screenWidth);
fileheader.setScreenHeightUnits(screenHeight);
}
}
}
private void determineStandardFont() {
// Sets the fixed font as the standard
if (machine.getGameData().getStoryFileHeader().isEnabled(
Attribute.FORCE_FIXED_FONT)) {
standardFont = fixedFont;
}
}
private void resizeWindows(int linesUpperWindow) {
windows[WINDOW_TOP].resize(linesUpperWindow);
int heightWindowTop = windows[WINDOW_TOP].getHeight();
windows[WINDOW_BOTTOM].setVerticalBounds(heightWindowTop,
getHeight() - heightWindowTop);
}
private void setScreenProperties() {
StoryFileHeader fileheader = machine.getGameData().getStoryFileHeader();
if (fileheader.getVersion() <= 3) {
fileheader.setEnabled(Attribute.DEFAULT_FONT_IS_VARIABLE, true);
fileheader.setEnabled(Attribute.SUPPORTS_STATUSLINE, true);
fileheader.setEnabled(Attribute.SUPPORTS_SCREEN_SPLITTING, true);
}
if (fileheader.getVersion() >= 4) {
fileheader.setEnabled(Attribute.SUPPORTS_BOLD, true);
fileheader.setEnabled(Attribute.SUPPORTS_FIXED_FONT, true);
fileheader.setEnabled(Attribute.SUPPORTS_ITALIC, true);
}
if (fileheader.getVersion() >= 5) {
fileheader.setEnabled(Attribute.SUPPORTS_COLOURS, true);
fileheader.setDefaultBackgroundColor(ColorTranslator.COLOR_WHITE);
fileheader.setDefaultForegroundColor(ColorTranslator.COLOR_BLACK);
fileheader.setFontWidth(1);
fileheader.setFontHeight(1);
overrideDefaults(fileheader);
}
determineStandardFont();
updateDimensionsInHeader();
}
private void repaintInUiThread() {
try {
EventQueue.invokeAndWait(new Runnable() {
public void run() {
// replace the expensive repaint() call with a fast copying of
// the double buffer
if (imageBuffer != null) {
getGraphics().drawImage(imageBuffer, 0, 0, TextViewport.this);
}
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void overrideDefaults(StoryFileHeader fileheader) {
String version = fileheader.getRelease() + "."
+ fileheader.getSerialNumber();
if (isBeyondZork(version)) {
// Some BZ-specific settings
fileheader.setInterpreterNumber(3); // set to "Macintosh"
standardFont = fixedFont;
} else if (isVaricella(version) || isOnlyAfterDark(version)) {
setDefaultColors(fileheader, ColorTranslator.COLOR_BLACK,
ColorTranslator.COLOR_WHITE);
windows[WINDOW_BOTTOM].clear();
}
}
private boolean isVaricella(String version) {
return version.equals("1.990831");
}
private boolean isOnlyAfterDark(String version) {
return version.equals("2.000913")
|| version.equals("1.990915");
}
private boolean isBeyondZork(String version) {
return (version.equals("47.870915"))
|| (version.equals("49.870917"))
|| (version.equals("51.870923"))
|| (version.equals("57.871221"));
}
/**
* Sets the default colors both in the viewport object and the file header.
* If the settings object defines colors the defined values will be taken
* instead, otherwise take the parameters.
*
* @param fileheader the file header
* @param background the background color to set
* @param foreground the foreground color to set
*/
private void setDefaultColors(StoryFileHeader fileheader,
int background, int foreground) {
defaultBackground
= (settings.getDefaultBackground() != ColorTranslator.UNDEFINED)
? settings.getDefaultBackground() : background;
defaultForeground
= (settings.getDefaultForeground() != ColorTranslator.UNDEFINED)
? settings.getDefaultForeground() : foreground;
fileheader.setDefaultBackgroundColor(defaultBackground);
fileheader.setDefaultForegroundColor(defaultForeground);
}
}