/*
* MainWindow.java
*
* Created on May 14, 2007, 4:25 PM
*
*/
package ika.gui;
import ika.utils.FileUtils;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.io.File;
import java.util.Vector;
import javax.swing.*;
/**
* An abstract document window that handles (1) saving, opening and closing
* documents, (2) resizing the window, (3) dirty state flag, (4) a menu listing
* all currently open windows.
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
*/
public abstract class MainWindow extends javax.swing.JFrame
implements ComponentListener {
/** static vector holding all windows currently open.
*/
protected static Vector windows = new Vector();
/** name of a new window is WINDOWNAME + windowCounter
*/
private static final String WINDOWNAME = "Untitled ";
/** counts the windows created so that each new window gets increasingly
* numbered.
*/
private static int windowCounter = 1;
/** file path to location on the hard disk where this document is stored.
* It is null if the document has never been saved.
*/
private String filePath = null;
/** dimension of the window when minimized.
*/
private java.awt.Dimension packedSize = null;
/** dirty flag: true means document must be saved before closing.
*/
private boolean dirty = false;
/** add these WindowFocusListeners to all document windows.
*/
private static Vector windowFocusListeners = new Vector();
/**
* Creates a new document window. Automatically assigns a title, and adds the
* new window to the menu listing all currently open windows. The new
* window can be removed from the menu with removeFromWindowMenu().
*/
public MainWindow() {
// assign an automatically generated name
this.setTitle(this.getWindowTitle(MainWindow.windowCounter++));
// add the window to the menu that lists all windows.
windows.add(this);
// register this as a ComponentListener to get resize events.
// the resize-event-handler makes sure the window is not gettting too small.
this.addComponentListener(this);
}
protected String getWindowTitle(int windowNumber) {
String title = WINDOWNAME + windowNumber;
if (ika.utils.Sys.isWindows()) {
title += " - " + ika.app.ApplicationInfo.getApplicationName();
}
return title;
}
/**
* This method is called after the constructor. Data models and the GUI should
* be initialized here.
* @return Returns true if the initialization is successful and the window
* should be shown, and false otherwise.
*/
protected boolean init() {
return true;
}
/**
* Return a byte array that can be stored in an external file.
* @return The document content.
*/
abstract protected byte[] getDocumentData();
/**
* Restore the document content from a passed byte array.
* @param data The document content.
* @throws An exception is thrown if the passed data cannot be used to set
* the data of the document.
*/
abstract protected void setDocumentData(byte[] data) throws Exception;
/**
* Creates a new sub-class of MainWindow and returns it. The name of the
* class that is instantiated is extracted from the MainWindow.properties
* file, where the property is called "MainWindow".
* This method only calls window.init() when told to do so.
* @param callIInit If true, window.init() is called. The window is usually
* displayed to the user if init is true.
* @return The new document window.
* @throws java.lang.ClassNotFoundException
* @throws java.lang.InstantiationException
* @throws java.lang.IllegalAccessException
*/
private static MainWindow createNewMainWindow(boolean callIInit)
throws java.lang.ClassNotFoundException,
java.lang.InstantiationException,
IllegalAccessException {
java.util.Properties props
= ika.utils.PropertiesLoader.loadProperties("ika.app.Application");
String className = props.getProperty("MainWindow");
MainWindow window = (MainWindow)Class.forName(className).newInstance();
if (window == null) {
return null;
}
if (callIInit) {
if (!window.init()) {
return null;
}
// remember the size
Dimension size = window.getSize();
// pack the window (= bring it to its smallest possible size)
// and store this minimum size
window.pack();
window.packedSize = window.getSize();
// restore the size
window.setSize(size);
}
return window;
}
/**
* Returns a clone of the menu bar used by this document window.
*/
public static JMenuBar getMenuBarClone() {
MainWindow tempWindow = null;
try {
tempWindow = MainWindow.createNewMainWindow(false);
JMenuBar menuBar = tempWindow.getJMenuBar();
// the window was added to the menu that lists all windows. Remove it.
MainWindow.windows.remove(tempWindow);
// creating the window increased the window counter. Decrease it again.
--MainWindow.windowCounter;
tempWindow.dispose();
return menuBar;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Returns the frontmost document window that currently owns the focus.
* @return The window with the focus, or null if there is no focused window.
*/
public static MainWindow getFocusedFrontWindow() {
// search the foreground window that owns the focus
Window focusedWindow = ika.utils.FocusUtils.focusedWindow();
if (focusedWindow instanceof javax.swing.JFrame == false)
return null;
javax.swing.JFrame foregroundFrame = (javax.swing.JFrame)focusedWindow;
// search the focused window in the array of document windows.
final int windowsCount = MainWindow.windows.size();
for (int i = 0; i < windowsCount; i++) {
MainWindow w = (MainWindow)MainWindow.windows.elementAt(i);
if (w == foregroundFrame)
return w;
}
return null;
}
/**
* Update the menu bar of this window. Each window has its own clone of the
* menu bar.
* @param foregroundWindow The window currently in the foreground.
* @param windowMenu The menu where each window will be appended to.
*/
public static void updateWindowMenu(JMenu windowMenu, JFrame foregroundWindow) {
final int windowsCount = MainWindow.windows.size();
// enable or disable the "Minimize" and "Zoom" menu items
boolean hasVisibleWindow = false;
for (int i = 0; i < windowsCount; i++) {
MainWindow w = (MainWindow)MainWindow.windows.elementAt(i);
if (w.isVisible() &&
(w.getState() & Frame.ICONIFIED) != Frame.ICONIFIED){
hasVisibleWindow = true;
break;
}
}
JMenuItem minimizeMenuItem = (JMenuItem)windowMenu.getMenuComponent(0);
JMenuItem zoomMenuItem = (JMenuItem)windowMenu.getMenuComponent(1);
minimizeMenuItem.setEnabled(hasVisibleWindow);
zoomMenuItem.setEnabled(hasVisibleWindow);
// remove all menu items, except the first "Minimize" item, the second
// "Zoom" item and the following separator.
final int nbrMenuItems = windowMenu.getMenuComponentCount();
for (int i = nbrMenuItems - 1; i >= 3; i--) {
windowMenu.remove(i);
}
// add each window again.
for (int i = 0; i < windowsCount; i++) {
MainWindow w = (MainWindow)MainWindow.windows.elementAt(i);
JMenuItem menuItem;
if (foregroundWindow == w && foregroundWindow != null) {
menuItem = new JCheckBoxMenuItem(foregroundWindow.getTitle(), true);
} else {
menuItem = new JMenuItem(w.getTitle());
}
windowMenu.add(menuItem);
menuItem.setName(Integer.toString(i));
menuItem.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
JMenuItem menuItem = (JMenuItem)evt.getSource();
try {
((JCheckBoxMenuItem)menuItem).setState(true);
} catch (ClassCastException ex){}
int id = Integer.parseInt(menuItem.getName());
MainWindow w = (MainWindow)MainWindow.windows.elementAt(id);
if (w.getExtendedState() == Frame.ICONIFIED) {
w.setExtendedState(Frame.NORMAL);
}
// bring the selected window to the front
w.toFront();
}
});
}
}
/**
* Creates a new empty document. The new document window is added to the
* menu listing all currently open windows.
* @return A new empty document, which is a sub-class of MainWindow.
*/
static public MainWindow newDocumentWindow() {
MainWindow w = null;
try {
w = MainWindow.createNewMainWindow(true);
if (w != null) {
w.setVisible(true);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
return w;
}
}
/**
* Asks the user to select a file and opens a new window and initializes
* its contents with the selected file.
* @return The new document window, or null if no document was opened.
*/
public MainWindow openDocumentWindow() {
try {
MainWindow w = null;
String newFilePath = FileUtils.askFile(this, "Open Document", true);
if (newFilePath == null) {
return null; // user canceled
}
// import data from the file
File file = new File(newFilePath);
byte[] data = FileUtils.getBytesFromFile(file);
// successfully imported new data. Create new window.
w = MainWindow.createNewMainWindow(true);
w.setDocumentData(data);
w.setTitle(FileUtils.getFileNameWithoutExtension(newFilePath));
w.filePath = newFilePath;
w.setVisible(true);
w.setDocumentClean();
return w;
} catch(Exception e) {
ika.utils.ErrorDialog.showErrorDialog("The file could not be opened.",
"File Error", e, this);
return null;
}
}
/**
* Ask the user whether changes to the document should be saved.
* @return True if the document window can be closed, false otherwise.
*/
protected boolean canDocumentBeClosed() {
switch (SaveFilePanel.showSaveDialogForNamedDocument(this, getTitle())) {
case DONTSAVE:
// document has possibly been edited but user does not want to save it
return true;
case CANCEL:
// document has been edited and user canceled
return false;
case SAVE:
// document has been edited and user wants to save it
if (this.filePath == null || !new File(this.filePath).exists()) {
this.filePath = this.askFileToSave("Save");
if (this.filePath == null) {
return false; // user canceled
}
}
return this.saveDocumentWindow(this.filePath);
}
return false;
}
/**
* Closes this window. Asks the user whether to save or discard changes
* if necessary, and saves the document to a file if necessary.
* @return True if the document has been closed, false otherwise.
*/
protected boolean closeDocumentWindow() {
// Ask user whether to save the document if it has been edited.
if (this.dirty && this.isVisible()) {
// first make it visible if it has been miminized
if ((this.getExtendedState() & Frame.ICONIFIED) == Frame.ICONIFIED) {
this.setExtendedState(Frame.MAXIMIZED_BOTH);
this.validate();
}
// ask the user whether to save the document
if (!canDocumentBeClosed())
return false;
}
// remove this window from the list of open windows
MainWindow.windows.remove(this);
// dispose and close it
this.setVisible(false);
this.dispose();
// close the about dialog if this was the last open document on Windows or Linux
if (MainWindow.windows.size() == 0 && !ika.utils.Sys.isMacOSX())
ika.gui.ProgramInfoPanel.hideApplicationInfo();
// if this is not running on Mac, the application should exit if
// the last window is closed.
if (MainWindow.windows.size() == 0 && !ika.utils.Sys.isMacOSX()) {
System.exit(0);
}
return true;
}
/**
* Closes all currently open document windows that are stored in
* this.windows vector.
* Asks the user for a location to store the document if it has not been
* saved yet. Stops closing windows when the user cancels.
* @return True if all windows have been closed, false otherwise.
*/
public static boolean closeAllDocumentWindows() {
final int windowsCount = MainWindow.windows.size();
boolean windowsClosed = windowsCount < 1;
for (int i = windowsCount - 1; i >= 0; i--) {
MainWindow w = (MainWindow)MainWindow.windows.get(i);
windowsClosed = w.closeDocumentWindow();
if (!windowsClosed) // stop if user cancels closing of windows
return false;
}
return true;
}
/**
* Saves this document window to a file. Asks the user for a location to
* store the document if it has not been saved yet.
* @return True if the document has been succesfully saved, false otherwise.
*/
protected boolean saveDocumentWindow() {
// ask for file path if the document has never been saved or if its
// path is invalid.
if (filePath == null || !new java.io.File(filePath).exists()) {
String newFilePath = this.askFileToSave("Save Document");
if (newFilePath == null) { // user canceled
return false;
}
filePath = newFilePath;
}
return saveDocumentWindow(filePath);
}
/**
* Saves this document to a file and displays an error message if an error
* occurs.
* @param filePath The path to the file.
* @return True if the document has been succesfully saved, false otherwise.
*/
protected boolean saveDocumentWindow(String filePath) {
if (filePath == null) {
return false;
}
java.io.OutputStream outputStream = null;
try {
byte[] data = this.getDocumentData();
outputStream = new java.io.FileOutputStream(filePath);
outputStream = new java.io.BufferedOutputStream(outputStream);
outputStream.write(data);
// ika.utils.Serializer.serialize(obj, false, filePath);
this.setDocumentClean();
this.setTitle(ika.utils.FileUtils.getFileNameWithoutExtension(filePath));
// store the path to the file
this.filePath = filePath;
return true;
} catch(Exception e) {
e.printStackTrace();
final Icon icon = ika.app.ApplicationInfo.getApplicationIcon();
String newline = System.getProperty("line.separator");
JOptionPane.showMessageDialog(this, "An error occured. " +
"The file could not be saved." + newline +
"Please try to save the file to another location.",
"File Error", JOptionPane.ERROR_MESSAGE, icon);
return false;
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (java.io.IOException e) {
}
}
}
}
/**
* Sets the dirty flag of this window. This indicates that the documents must
* be saved before it can be closed.
*/
protected void setDocumentDirty() {
if (!this.dirty) {
this.dirty = true;
this.getRootPane().putClientProperty("windowModified", Boolean.TRUE);
}
}
/**
* Clears the dirty flag of this window. This indicates that the documents
* has been saved to disk and that it is save to close it.
*/
protected void setDocumentClean() {
if (this.dirty) {
this.dirty = false;
this.getRootPane().putClientProperty("windowModified", Boolean.FALSE);
}
}
/**
* Returns whether the document must be saved before being closed.
* @return True if the document has unsaved edits, false otherwise.
*/
public boolean isDocumentDirty() {
return dirty;
}
/**
* Asks the user for a new file to save this document.
* @param message Message displayed in the file dialog (e.g. "Save As").
* @return The path to the new file, or null if the user cancels.
*/
protected String askFileToSave(String message) {
// ask for file path
String ext = ika.app.ApplicationInfo.getDocumentExtension();
String name = FileUtils.forceFileNameExtension(this.getTitle(), ext);
return FileUtils.askFile(this, message, name, false, ext);
}
/**
* Part of the ComponentListener interface.
*/
public void componentShown(ComponentEvent e) {
}
/**
* Part of the ComponentListener interface.
*/
public void componentHidden(ComponentEvent e) {
}
/**
* Part of the ComponentListener interface.
*/
public void componentMoved(ComponentEvent e) {
}
/**
* Part of the ComponentListener interface. Make sure this window is not
* getting too small.
*/
public void componentResized(ComponentEvent e) {
if (this.packedSize == null)
return;
// Check if either the width or the height are below minimum
// and reset size if necessary.
// Note: this is not elegant, but SUN recommends doing it that way.
int width = getWidth();
int height = getHeight();
boolean resize = false;
if (width < this.packedSize.width) {
resize = true;
width = this.packedSize.width;
}
if (height < this.packedSize.height) {
resize = true;
height = this.packedSize.height;
}
if (resize) {
setSize(width, height);
}
}
}