/*
* Copyright (C) 2006-2011 IsmAvatar <IsmAvatar@gmail.com>
* Copyright (C) 2006, 2007 TGMG <thegamemakerguru@gmail.com>
* Copyright (C) 2007, 2008 Quadduc <quadduc@gmail.com>
* Copyright (C) 2006, 2007, 2008 Clam <clamisgood@gmail.com>
*
* This file is part of LateralGM.
*
* LateralGM 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.
*
* LateralGM 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 (COPYING) for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.lateralgm.main;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.SplashScreen;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Scanner;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.imageio.ImageIO;
import javax.swing.AbstractButton;
import javax.swing.DropMode;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.lateralgm.components.GmMenuBar;
import org.lateralgm.components.GmTreeGraphics;
import org.lateralgm.components.impl.CustomFileFilter;
import org.lateralgm.components.impl.FramePrefsHandler;
import org.lateralgm.components.impl.GmTreeEditor;
import org.lateralgm.components.impl.ResNode;
import org.lateralgm.components.mdi.MDIPane;
import org.lateralgm.file.GmFile;
import org.lateralgm.messages.Messages;
import org.lateralgm.resources.Resource;
import org.lateralgm.resources.library.LibManager;
import org.lateralgm.subframes.EventFrame;
import org.lateralgm.subframes.GameInformationFrame;
import org.lateralgm.subframes.GameSettingFrame;
public final class LGM
{
private static final long serialVersionUID = 1L;
public static int javaVersion;
public static File tempDir, workDir;
static
{
//java6u10 regression causes graphical xor to be very slow
System.setProperty("sun.java2d.d3d","false"); //$NON-NLS-1$ //$NON-NLS-2$
//Put the Mac menu bar where it belongs (ignored by other systems)
System.setProperty("apple.laf.useScreenMenuBar","true"); //$NON-NLS-1$ //$NON-NLS-2$
//Set the Mac menu bar title to the correct name (also adds a useless About entry, so disabled)
//System.setProperty("com.apple.mrj.application.apple.menu.about.name",Messages.getString("LGM.NAME")); //$NON-NLS-1$ //$NON-NLS-2$
//annoyingly, Metal bolds almost all components by default. This unbolds them.
UIManager.put("swing.boldMetal",Boolean.FALSE); //$NON-NLS-1$
//Get Java Version
String jv = System.getProperty("java.version"); //$NON-NLS-1$
Scanner s = new Scanner(jv).useDelimiter("[\\._-]"); //$NON-NLS-1$
javaVersion = s.nextInt() * 10000 + s.nextInt() * 100 + s.nextInt();
System.out.format("Java Version: %d (%s)\n",javaVersion,jv); //$NON-NLS-1$
if (javaVersion < 10600)
System.out.println("Some program functionality will be limited due to your outdated version"); //$NON-NLS-1$
SplashProgress.start();
//Set up temp dir and work dir
Util.tweakIIORegistry();
tempDir = new File(System.getProperty("java.io.tmpdir"),"lgm"); //$NON-NLS-1$ //$NON-NLS-2$
if (!tempDir.exists())
{
tempDir.mkdir();
if (javaVersion >= 10600)
{
tempDir.setReadable(true,false);
tempDir.setWritable(true,false);
}
}
try
{
workDir = new File(LGM.class.getProtectionDomain().getCodeSource().getLocation().toURI());
}
catch (URISyntaxException e1)
{
e1.printStackTrace();
}
}
public static JFrame frame = new JFrame(Messages.format("LGM.TITLE", //$NON-NLS-1$
Messages.getString("LGM.NEWGAME"))); //$NON-NLS-1$
public static Listener listener = new Listener();
public static JToolBar tool;
public static JTree tree;
public static ResNode root;
public static GmFile currentFile = new GmFile();
public static MDIPane mdi;
public static Thread gameInformationFrameBuilder;
private static GameInformationFrame gameInfo;
public static Thread gameSettingFrameBuilder;
private static GameSettingFrame gameSet;
public static EventFrame eventSelect;
public static GameInformationFrame getGameInfo()
{
try
{
gameInformationFrameBuilder.join();
}
catch (InterruptedException e)
{
}
return gameInfo;
}
public static GameSettingFrame getGameSettings()
{
try
{
gameSettingFrameBuilder.join();
}
catch (InterruptedException e)
{
}
return gameSet;
}
private LGM()
{
}
public static ImageIcon findIcon(String filename)
{
String location = "org/lateralgm/icons/" + filename; //$NON-NLS-1$
ImageIcon ico = new ImageIcon(location);
if (ico.getIconWidth() == -1)
{
URL url = LGM.class.getClassLoader().getResource(location);
if (url != null)
{
ico = new ImageIcon(url);
}
}
return ico;
}
public static ImageIcon getIconForKey(String key)
{
Properties iconProps = new Properties();
InputStream is = LGM.class.getClassLoader().getResourceAsStream(
"org/lateralgm/main/icons.properties"); //$NON-NLS-1$
try
{
iconProps.load(is);
}
catch (IOException e)
{
System.err.println("Unable to read icons.properties"); //$NON-NLS-1$
}
String filename = iconProps.getProperty(key,""); //$NON-NLS-1$
if (!filename.isEmpty()) return findIcon(filename);
return null;
}
public static JButton makeButton(String key)
{
JButton but = new JButton();
makeButton(but,key);
return but;
}
public static AbstractButton makeButton(AbstractButton but, String key)
{
Icon ico = LGM.getIconForKey(key);
if (ico != null)
but.setIcon(ico);
else
but.setIcon(GmTreeGraphics.getBlankIcon());
but.setActionCommand(key);
but.setToolTipText(Messages.getString(key));
but.addActionListener(listener);
return but;
}
private static JToolBar createToolBar()
{
tool = new JToolBar();
tool.setFloatable(false);
tool.add(makeButton("LGM.NEW")); //$NON-NLS-1$
tool.add(makeButton("LGM.OPEN")); //$NON-NLS-1$
tool.add(makeButton("LGM.SAVE")); //$NON-NLS-1$
tool.addSeparator();
tool.add(makeButton("LGM.SAVEAS")); //$NON-NLS-1$
tool.addSeparator();
tool.add(makeButton("Toolbar.ADD_SPRITE")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_SOUND")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_BACKGROUND")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_PATH")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_SCRIPT")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_FONT")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_TIMELINE")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_OBJECT")); //$NON-NLS-1$
tool.add(makeButton("Toolbar.ADD_ROOM")); //$NON-NLS-1$
tool.addSeparator();
tool.add(makeButton("LGM.EVENT_BUTTON")); //$NON-NLS-1$
return tool;
}
private static JComponent createTree()
{
return createTree(newRoot());
}
private static JComponent createTree(ResNode newroot)
{
tree = new JTree(new DefaultTreeModel(root));
GmTreeGraphics renderer = new GmTreeGraphics();
GmTreeEditor editor = new GmTreeEditor(tree,renderer);
editor.addCellEditorListener(listener);
tree.setEditable(true);
tree.addMouseListener(listener.mListener);
if (javaVersion >= 10600)
{
tree.setTransferHandler(listener);
tree.setDragEnabled(true);
tree.setDropMode(DropMode.ON_OR_INSERT);
}
tree.setCellRenderer(renderer);
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
tree.setCellEditor(editor);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
//remove the cut, copy, and paste bindings
InputMap im = tree.getInputMap();
for (KeyStroke s : im.allKeys())
{
Object o = im.get(s);
if (o.equals("cut") || o.equals("copy") || o.equals("paste")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
im.put(s,"none"); //null doesn't remove them //$NON-NLS-1$
}
// Setup the rest of the main window
JScrollPane scroll = new JScrollPane(tree);
scroll.setPreferredSize(new Dimension(200,100));
return scroll;
}
public static ResNode newRoot()
{
return root = new ResNode("Root",(byte) 0,null,null); //$NON-NLS-1$
}
private static JComponent createMDI()
{
mdi = new MDIPane();
JScrollPane scroll = new JScrollPane(mdi);
mdi.setScrollPane(scroll);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
mdi.setBackground(Color.BLACK);
eventSelect = new EventFrame();
mdi.add(eventSelect);
return scroll;
}
public static void loadPlugins()
{
File dir = new File(workDir.getParent(),"plugins"); //$NON-NLS-1$
if (!dir.exists()) dir = new File(workDir.getParent(),"Plugins"); //$NON-NLS-1$
File[] ps = dir.listFiles(new CustomFileFilter(null,".jar")); //$NON-NLS-1$s
if (ps == null) return;
for (File f : ps)
{
if (!f.exists()) continue;
try
{
String pluginEntry = "LGM-Plugin"; //$NON-NLS-1$
Manifest mf = new JarFile(f).getManifest();
String clastr = mf.getMainAttributes().getValue(pluginEntry);
if (clastr == null)
throw new Exception(Messages.format("LGM.PLUGIN_MISSING_ENTRY",pluginEntry));
URLClassLoader ucl = new URLClassLoader(new URL[] { f.toURI().toURL() });
ucl.loadClass(clastr).newInstance();
}
catch (Exception e)
{
String msgInd = "LGM.PLUGIN_LOAD_ERROR"; //$NON-NLS-1$
System.out.println(Messages.format(msgInd,f.getName(),e.getClass().getName(),e.getMessage()));
continue;
}
}
}
public static void populateTree()
{
root.addChild(Messages.getString("LGM.SPRITES"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.SPRITE);
root.addChild(Messages.getString("LGM.SOUNDS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.SOUND);
root.addChild(Messages.getString("LGM.BACKGROUNDS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.BACKGROUND);
root.addChild(Messages.getString("LGM.PATHS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.PATH);
root.addChild(Messages.getString("LGM.SCRIPTS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.SCRIPT);
root.addChild(Messages.getString("LGM.FONTS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.FONT);
root.addChild(Messages.getString("LGM.TIMELINES"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.TIMELINE);
root.addChild(Messages.getString("LGM.OBJECTS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.OBJECT);
root.addChild(Messages.getString("LGM.ROOMS"), //$NON-NLS-1$
ResNode.STATUS_PRIMARY,Resource.Kind.ROOM);
root.addChild(Messages.getString("LGM.GAMEINFO"), //$NON-NLS-1$
ResNode.STATUS_SECONDARY,Resource.Kind.GAMEINFO);
root.addChild(Messages.getString("LGM.GAMESETTINGS"), //$NON-NLS-1$
ResNode.STATUS_SECONDARY,Resource.Kind.GAMESETTINGS);
root.addChild(Messages.getString("LGM.EXTENSIONS"), //$NON-NLS-1$
ResNode.STATUS_SECONDARY,Resource.Kind.EXTENSIONS);
tree.setSelectionPath(new TreePath(root).pathByAddingChild(root.getChildAt(0)));
}
/**
* Commits all front-end changes to the back-end.
* Usually do this in preparation for writing the back-end to some output stream,
* such as a file (e.g. saving) or a plugin (e.g. compiling).
* Notice that LGM actually traverses the tree committing *all* ResNodes with frames,
* rather than just the open MDI frames. Since GameSettings and GameInfo do not have
* ResourceFrames associated with them, we commit them separately.
* @see LGM#commitAll()
*/
public static void commitAll()
{
Enumeration<?> nodes = LGM.root.preorderEnumeration();
while (nodes.hasMoreElements())
{
ResNode node = (ResNode) nodes.nextElement();
if (node.frame != null) node.frame.updateResource(); // update open frames
}
LGM.getGameSettings().commitChanges();
LGM.getGameInfo().updateResource();
}
public static void reload(boolean newRoot)
{
LGM.mdi.closeAll();
LGM.tree.setModel(new DefaultTreeModel(LGM.root));
LGM.tree.setSelectionRow(0);
//This hack ensures EventSelector.linkSelect knows of the new root
LGM.mdi.remove(LGM.eventSelect);
LGM.eventSelect = new EventFrame();
LGM.mdi.add(LGM.eventSelect);
LGM.getGameSettings().setComponents(LGM.currentFile.gameSettings);
LGM.getGameSettings().setVisible(false);
LGM.getGameInfo().setComponents(LGM.currentFile.gameInfo);
LGM.getGameInfo().setVisible(false);
LGM.fireReloadPerformed(newRoot);
}
public static interface ReloadListener
{
/**
* Called after LGM performs a reload, e.g. when a new file is created or loaded.
* A reload causes the MDI to be flushed, the tree to refresh
* (especially if a new root is provided), the EventFrame is recreated
* (a hack to ensure that the events' link selectors know of the new root),
* and Game Settings and Game Info are re-calibrated with their new settings
* (but not recreated). Note that the Menu bar is left untouched and remains in tact.
* @param newRoot indicates if a new root was provided to the tree
* (e.g. the tree had to be re-populated)
* @see LGM#reload(boolean newRoot)
*/
void reloadPerformed(boolean newRoot);
}
protected static ArrayList<ReloadListener> reloadListeners = new ArrayList<ReloadListener>();
public static void addReloadListener(ReloadListener l)
{
reloadListeners.add(l);
}
public static void removeReloadListener(ReloadListener l)
{
reloadListeners.remove(l);
}
protected static void fireReloadPerformed(boolean newRoot)
{
for (ReloadListener rl : reloadListeners)
rl.reloadPerformed(newRoot);
}
public static void main(String[] args)
{
SplashProgress.progress(20,Messages.getString("LGM.SPLASH_LIBS")); //$NON-NLS-1$
LibManager.autoLoad();
SplashProgress.progress(30,Messages.getString("LGM.SPLASH_TOOLS")); //$NON-NLS-1$
JComponent toolbar = createToolBar();
JComponent left = createTree();
JComponent right = createMDI();
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true,left,right);
split.setDividerLocation(170);
split.setOneTouchExpandable(true);
SplashProgress.progress(40,Messages.getString("LGM.SPLASH_THREAD")); //$NON-NLS-1$
gameInformationFrameBuilder = new Thread()
{
public void run()
{
gameInfo = new GameInformationFrame(currentFile.gameInfo);
mdi.add(gameInfo);
}
};
gameSettingFrameBuilder = new Thread()
{
public void run()
{
gameSet = new GameSettingFrame(currentFile.gameSettings,currentFile.constants,
currentFile.includes);
mdi.add(gameSet);
}
};
gameInformationFrameBuilder.start(); //must occur after createMDI
gameSettingFrameBuilder.start(); //must occur after createMDI
SplashProgress.progress(50,Messages.getString("LGM.SPLASH_MENU")); //$NON-NLS-1$
frame.setJMenuBar(new GmMenuBar());
SplashProgress.progress(60,Messages.getString("LGM.SPLASH_UI")); //$NON-NLS-1$
JPanel f = new JPanel(new BorderLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(f);
f.add(BorderLayout.NORTH,toolbar);
f.add(BorderLayout.CENTER,split);
f.setOpaque(true);
new FramePrefsHandler(frame);
SplashProgress.progress(70,Messages.getString("LGM.SPLASH_LOGO")); //$NON-NLS-1$
try
{
frame.setIconImage(ImageIO.read(LGM.class.getClassLoader().getResource(
"org/lateralgm/main/lgm-logo.png"))); //$NON-NLS-1$
}
catch (Exception e)
{
e.printStackTrace();
}
SplashProgress.progress(80,Messages.getString("LGM.SPLASH_TREE")); //$NON-NLS-1$
populateTree();
SplashProgress.progress(90,Messages.getString("LGM.SPLASH_PLUGINS")); //$NON-NLS-1$
loadPlugins();
if (args.length != 0)
{
SplashProgress.progress(95,Messages.getString("LGM.SPLASH_PRELOAD")); //$NON-NLS-1$
listener.fc.openFile(new File(args[0]));
}
SplashProgress.complete();
frame.setVisible(true);
}
static final class SplashProgress
{
static final SplashScreen SPLASH;
static final Graphics2D SPLASH_GRAPHICS;
static final JProgressBar BAR;
static final Graphics BAR_GRAPHICS;
private static String text = null;
static final boolean TIMER = System.getProperty("lgm.progresstimer") != null; //$NON-NLS-1$
private static long startTime, completeTime;
private static ArrayList<Integer> progressValues;
private static ArrayList<Long> progressTimes;
static
{
SplashScreen ss = null;
Graphics2D sg = null;
JProgressBar b = null;
Graphics bg = null;
try
{
ss = SplashScreen.getSplashScreen();
sg = ss.createGraphics();
Dimension sss = ss.getSize();
Rectangle bb = new Rectangle(0,sss.height - 24,sss.width,24);
b = new JProgressBar();
b.setBounds(bb);
bg = sg.create(bb.x,bb.y,bb.width,bb.height);
}
catch (Throwable t)
{
ss = null;
sg = null;
b = null;
bg = null;
}
finally
{
SPLASH = ss;
SPLASH_GRAPHICS = sg;
BAR = b;
BAR_GRAPHICS = bg;
}
if (TIMER)
{
progressValues = new ArrayList<Integer>();
progressTimes = new ArrayList<Long>();
}
}
private SplashProgress()
{
}
static void start()
{
if (TIMER) startTime = System.currentTimeMillis();
progress(0,Messages.getString("LGM.SPLASH_START")); //$NON-NLS-1$
}
static void complete()
{
if (TIMER)
{
completeTime = System.currentTimeMillis();
long tt = completeTime - startTime;
System.out.print("Progress/% "); //$NON-NLS-1$
for (Integer v : progressValues)
{
System.out.print("\t" + v); //$NON-NLS-1$
}
System.out.println();
System.out.print("Time/ms "); //$NON-NLS-1$
for (Long t : progressTimes)
{
System.out.print("\t" + t); //$NON-NLS-1$
}
System.out.println();
System.out.print("Actual progress/%"); //$NON-NLS-1$
for (Long t : progressTimes)
{
System.out.print("\t" + Math.round(100.0 * t / tt)); //$NON-NLS-1$
}
System.out.println();
}
}
static void progress(int p)
{
progress(p,text);
}
static void progress(int p, String t)
{
if (TIMER)
{
progressValues.add(p);
progressTimes.add(System.currentTimeMillis() - startTime);
}
text = t;
if (SPLASH != null)
{
BAR.setValue(p);
BAR.setStringPainted(t != null);
BAR.setString(t);
update();
}
}
private static void update()
{
BAR.paint(BAR_GRAPHICS);
SPLASH.update();
}
}
}