// // Thud.java // // Created by asp on Wed Nov 28 2001. // Copyright (c) 2001-2006 Anthony Parker & the THUD team. // All rights reserved. See LICENSE.TXT for more information. // package net.sourceforge.btthud.ui; import net.sourceforge.btthud.data.*; import net.sourceforge.btthud.engine.*; import net.sourceforge.btthud.util.*; import net.sourceforge.btthud.engine.commands.HUDSession; import net.sourceforge.btthud.engine.commands.UserCommand; import net.sourceforge.btthud.script.ScriptRunner; import net.sourceforge.btthud.script.Interactor; import java.io.*; import java.net.URL; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.DocumentListener; import javax.swing.event.DocumentEvent; import javax.swing.text.DefaultEditorKit; import javax.swing.text.Keymap; import javax.swing.plaf.basic.BasicTextUI.BasicHighlighter; import java.util.*; import java.lang.ref.WeakReference; public class Thud extends JFrame implements Runnable { // TODO: Make the escape character configurable? static private final char COMMAND_CHAR = '/'; Font mFont = new Font("Monospaced", Font.PLAIN, 10); // default main font private JTextField textField; private DocumentWatcher textFieldWatcher; private JTextPane textPane; private BulkStyledDocument bsd; private JTextPaneWriter textPaneWriter; boolean connected = false; MUConnection conn = null; MUParse parse = null; MUData data = null; MUContactList conList = null; MUStatus status = null; MUTacticalMap tacMap = null; MUPrefs prefs = null; MUCommands commands = null; private LinkedList<String> commandHistory = new LinkedList<String> (); private ListIterator<String> historyCursor = null; private boolean historyForward = true; static final int DEBUG = 1; private String[] args; boolean firstLaunch = false; private DataStore dataStore = null; private ScriptRunner scriptRunner = null; private Interactor interactor = null; // ------------------ private PrefsDialog prefsDialog; private ThudAction taFocusInputField; // Private InputMap for numeric keypad. private final InputMap numpadInputMap = new InputMap (); // Declarations for menus private JMenuBar mainMenuBar = new JMenuBar(); private JMenu fileMenu; private ThudAction taLoadMap; private ThudAction taSaveMapAs; private ThudAction taViewReleaseNotes; private ThudAction taQuit; private JMenu editMenu; private ThudAction taUndo; private ThudAction taCut; private ThudAction taCopy; private ThudAction taPaste; private ThudAction taClear; private ThudAction taSelectAll; private ThudAction taRepeatPreviousCommand; private ThudAction taRepeatNextCommand; private ThudAction taEraseCurrentCommand; private ThudAction taMuteMainWindowText; private JMenu hudMenu; private JMenuItem[] miConnections; private ThudAction taPreferences; private ThudAction taStartStop; private ThudAction taUpdateTacticalMapNow; private ThudAction taConnect; private ThudAction taAddNewHost; private ThudAction taRemoveHost; private ThudAction taDisconnect; private JMenu mapMenu; private ThudAction taZoomIn; private ThudAction taZoomOut; private ThudAction taShowWeaponsArcs; private ThudAction taMakeArcsWeaponRanges; private ThudAction taRetractArcRange; private ThudAction taExtendArcRange; private ThudAction taShowHexNumbers; private ThudAction taShowUnitNames; private ThudAction taDarkenElevations; private ThudAction taShowArmorDiagram; private ThudAction taShowLOSInfo; private ThudAction taMoveMapLeft; private ThudAction taMoveMapRight; private ThudAction taMoveMapUp; private ThudAction taMoveMapDown; private ThudAction taCenterMapOnUnit; private ThudAction taShowCliffs; private ThudAction taShowHeatArmoronTactical; private JMenu debugMenu; private ThudAction taDumpDocumentStructure; private JMenu windowMenu; private ThudAction taShowContactsWindow; private ThudAction taShowStatusWindow; private ThudAction taShowTacticalWindow; // Entry point. public static void main (String args[]) { try { //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { // It was only a suggestion. } EventQueue.invokeLater(new Thud (args)); } // Initialize main Thud window. private Thud (String[] arguments) { super("Thud"); this.args = arguments; final ClassLoader loader = getClass().getClassLoader(); System.out.println ("Loading icon"); // Set frame icon. try { System.out.println ("Loading Icon 001"); //final URL appIconURL = loader.getResource("media/icon/icon.gif"); URL appIconURL = getClass().getResource("/media/icon/icon.gif"); System.out.println ("Loading Icon 002"); if (appIconURL == null) System.out.println ("appIconURL is null"); final ImageIcon appIcon = new ImageIcon (appIconURL, "application icon"); System.out.println ("Loading Icon 003"); setIconImage(appIcon.getImage()); System.out.println ("Loading Icon 004"); } catch (Exception e) { System.err.println("Couldn't load Thud icon"); } System.out.println ("Loaded icon"); // Read preferences. readPrefs(); // TODO: Shouldn't we call this /after/ we set up the text // areas? Or maybe we need a better way of updating fonts. mainFontChanged(); // setup a new font prefsDialog = new PrefsDialog (this); // Initialize data store. dataStore = new DataStore (); // Initialize scripting support system. scriptRunner = new ScriptRunner (); interactor = new Interactor (scriptRunner); // TODO: If we ever want to run an rc script sometime. //dataStore.getReader("scripts/test.js"); //scriptRunner.execute(scriptReader, "test.js"); // Setup the main text areas setupNewTextFields(); // Register all our actions. registerActions(); // Add all of our menus addMenus(); setJMenuBar(mainMenuBar); setupListeners(); // Initialization strings String buildNumber = getClass().getPackage().getImplementationVersion(); if (buildNumber == null) buildNumber = ""; else buildNumber = "(r" + buildNumber + ")"; textPaneWriter.println(" *** Thud, (c) 2001-2007 Anthony Parker & the THUD team ***"); textPaneWriter.println(" *** bt-thud.sourceforge.net ***"); textPaneWriter.format(" *** Version: 1.5B %-41s ***", buildNumber); textPaneWriter.println(); textPaneWriter.println(" *** To get started, connect to a MUX via the HUD menu, ***"); textPaneWriter.println(" *** then hit Ctrl-G when in a combat unit to activate Thud! ***"); // Attempt auto-connect, if given parameters if (args.length == 2) { try { textPaneWriter.println(); textPaneWriter.println(" *** Auto-connecting to " + args[0] + " port " + args[1] + "..."); startConnection(new MUHost (args[0], Integer.parseInt(args[1]))); } catch (Exception e) { System.out.println("Error auto-connecting to " + args[0] + " port " + args[1]); } } } // Finish setting up GUI from event dispatch thread. public void run () { // Locate the window properly pack(); textField.requestFocusInWindow(); setSize(prefs.mainSizeX, prefs.mainSizeY); setLocation(prefs.mainLoc); setAlwaysOnTop(prefs.mainAlwaysOnTop); setVisible(true); // Show version notes // TODO: Fix PreferenceStore to detect version upgrades. if (firstLaunch) { doReleaseNotes(); firstLaunch = false; } } // // Menus. // // TODO: Convert all these menus to constructors or something. // Main menu bar. void addMenus() { // FIXME: All of the menus get re-created every time we // add/remove hosts, which is kinda silly. At most, we only // need to update the "HUD" menu. addFileMenu(); addEditMenu(); addHUDMenu(); addMapMenu(); addDebugMenu(); addWindowMenu(); } // "File" menu. private void addFileMenu () { if (fileMenu != null) return; fileMenu = new JMenu ("File"); fileMenu.add(taLoadMap); fileMenu.add(taSaveMapAs); fileMenu.addSeparator(); fileMenu.add(taViewReleaseNotes); fileMenu.addSeparator(); fileMenu.add(taQuit); mainMenuBar.add(fileMenu); } // "Edit" menu. private void addEditMenu () { if (editMenu != null) return; editMenu = new JMenu ("Edit"); editMenu.add(taUndo); editMenu.addSeparator(); editMenu.add(taCut); editMenu.add(taCopy); editMenu.add(taPaste); editMenu.add(taClear); editMenu.addSeparator(); editMenu.add(taSelectAll); editMenu.addSeparator(); editMenu.add(taRepeatPreviousCommand); editMenu.add(taRepeatNextCommand); editMenu.add(taEraseCurrentCommand); editMenu.addSeparator(); addCheckBoxItem(editMenu, taMuteMainWindowText); mainMenuBar.add(editMenu); } // "HUD" menu. private void addHUDMenu () { boolean firstTime; if (hudMenu != null) { firstTime = false; hudMenu.removeAll(); } else { firstTime = true; hudMenu = new JMenu ("HUD"); } // Setup the connection menu items initConnectionMenus(); hudMenu.add(taPreferences); hudMenu.addSeparator(); hudMenu.add(taStartStop); hudMenu.add(taUpdateTacticalMapNow); hudMenu.addSeparator(); for (int ii = 0; ii < prefs.hosts.size(); ii++) { final MUHost host = prefs.hosts.get(ii); miConnections[ii] = new JMenuItem (taConnect); miConnections[ii].setText(host.toString()); miConnections[ii].setActionCommand(Integer.toString(ii)); acceleratorForConnectionItem(miConnections[ii], ii); hudMenu.add(miConnections[ii]); } hudMenu.addSeparator(); hudMenu.add(taAddNewHost); hudMenu.add(taRemoveHost); hudMenu.addSeparator(); hudMenu.add(taDisconnect); if (firstTime) { mainMenuBar.add(hudMenu); } } // "Map" menu. private void addMapMenu () { if (mapMenu != null) return; mapMenu = new JMenu ("Map"); mapMenu.add(taZoomIn); mapMenu.add(taZoomOut); mapMenu.addSeparator(); addCheckBoxItem(mapMenu, taShowWeaponsArcs); addCheckBoxItem(mapMenu, taMakeArcsWeaponRanges); mapMenu.add(taRetractArcRange); mapMenu.add(taExtendArcRange); mapMenu.addSeparator(); addCheckBoxItem(mapMenu, taShowHexNumbers); addCheckBoxItem(mapMenu, taShowUnitNames); addCheckBoxItem(mapMenu, taDarkenElevations); addCheckBoxItem(mapMenu, taShowArmorDiagram); addCheckBoxItem(mapMenu, taShowLOSInfo); mapMenu.addSeparator(); mapMenu.add(taMoveMapLeft); mapMenu.add(taMoveMapRight); mapMenu.add(taMoveMapUp); mapMenu.add(taMoveMapDown); mapMenu.add(taCenterMapOnUnit); mapMenu.addSeparator(); addCheckBoxItem(mapMenu, taShowCliffs); addCheckBoxItem(mapMenu, taShowHeatArmoronTactical); // Disable the map menu until we're actually connected mapMenu.setEnabled(false); mainMenuBar.add(mapMenu); } // "Debug" menu. private void addDebugMenu () { if (DEBUG == 0 || debugMenu != null) return; debugMenu = new JMenu("Debug"); debugMenu.add(taDumpDocumentStructure); mainMenuBar.add(debugMenu); } // "Window" menu. private void addWindowMenu () { if (windowMenu != null) return; windowMenu = new JMenu ("Window"); windowMenu.add(taShowContactsWindow); windowMenu.add(taShowStatusWindow); windowMenu.add(taShowTacticalWindow); // Disable the window menu until we're actually connected windowMenu.setEnabled(false); mainMenuBar.add(windowMenu); } // // Main Thud window actions. // private void registerActions () { // // Generic actions. // // Switch focus to this window's input text field whenever // someone tries to type without input text field focus. Also, // relay the typed character to the text field, so we don't // lose the input. // // This action will usually be paired with a // WHEN_ANCESTOR_OF_FOCUSED_COMPONENT input mapping, so there's // no problem with specific components capturing input. Only // unhandled input will be passed up to trigger this action. taFocusInputField = new ThudSimpleAction ("Focus Input Field") { protected void doAction () { //doLoadMap(); } }; // // Key bindings. // // FIXME: This is a bit of a hack. We should do this in a more // organized way, and maybe not on the root pane. // // We'll want some sort of KeyBindingManager, that will provide // a GUI for the user to set/save/restore custom bindings, and // also supply input/action maps to inherit from. // // Probably just input maps, as action maps will be fixed for // any particular component, although having an Action manager // might be useful for enabling/disabling all related actions // simultaneously (for connections, for example). bindCommand(KeyEvent.VK_NUMPAD1, "heading 240"); bindCommand(KeyEvent.VK_NUMPAD1, Event.SHIFT_MASK, "heading 210"); bindCommand(KeyEvent.VK_NUMPAD2, "heading 180"); bindCommand(KeyEvent.VK_NUMPAD3, Event.SHIFT_MASK, "heading 150"); bindCommand(KeyEvent.VK_NUMPAD3, "heading 120"); bindCommand(KeyEvent.VK_NUMPAD4, "heading 270"); bindAction(KeyEvent.VK_NUMPAD5, new StayHeadingAction (this)); bindAction(KeyEvent.VK_NUMPAD5, Event.SHIFT_MASK, new ReverseHeadingAction (this)); bindCommand(KeyEvent.VK_NUMPAD6, "heading 90"); bindCommand(KeyEvent.VK_NUMPAD7, "heading 300"); bindCommand(KeyEvent.VK_NUMPAD7, Event.SHIFT_MASK, "heading 330"); bindCommand(KeyEvent.VK_NUMPAD8, "heading 0"); bindCommand(KeyEvent.VK_NUMPAD9, Event.SHIFT_MASK, "heading 30"); bindCommand(KeyEvent.VK_NUMPAD9, "heading 60"); // TIC bindings. bindCommand(KeyEvent.VK_F1, "firetic 0"); bindCommand(KeyEvent.VK_F2, "firetic 1"); bindCommand(KeyEvent.VK_F3, "firetic 2"); bindCommand(KeyEvent.VK_F4, "firetic 3"); bindCommand(KeyEvent.VK_F1, Event.SHIFT_MASK, "listtic 0"); bindCommand(KeyEvent.VK_F2, Event.SHIFT_MASK, "listtic 1"); bindCommand(KeyEvent.VK_F3, Event.SHIFT_MASK, "listtic 2"); bindCommand(KeyEvent.VK_F4, Event.SHIFT_MASK, "listtic 3"); // Weapon bindings. // FIXME: We were already using CTRL + # (or rather, menu // shortcut + #) for host accelerators. Changed the host // accelerators to menu shortcut + SHIFT + # for now. bindCommand(KeyEvent.VK_1, Event.CTRL_MASK, "sight 1"); bindCommand(KeyEvent.VK_2, Event.CTRL_MASK, "sight 2"); bindCommand(KeyEvent.VK_3, Event.CTRL_MASK, "sight 3"); bindCommand(KeyEvent.VK_4, Event.CTRL_MASK, "sight 4"); bindCommand(KeyEvent.VK_5, Event.CTRL_MASK, "sight 5"); bindCommand(KeyEvent.VK_6, Event.CTRL_MASK, "sight 6"); bindCommand(KeyEvent.VK_7, Event.CTRL_MASK, "sight 7"); bindCommand(KeyEvent.VK_8, Event.CTRL_MASK, "sight 8"); bindCommand(KeyEvent.VK_9, Event.CTRL_MASK, "sight 9"); bindCommand(KeyEvent.VK_0, Event.CTRL_MASK, "sight 0"); bindCommand(KeyEvent.VK_1, Event.ALT_MASK, "fire 1"); bindCommand(KeyEvent.VK_2, Event.ALT_MASK, "fire 2"); bindCommand(KeyEvent.VK_3, Event.ALT_MASK, "fire 3"); bindCommand(KeyEvent.VK_4, Event.ALT_MASK, "fire 4"); bindCommand(KeyEvent.VK_5, Event.ALT_MASK, "fire 5"); bindCommand(KeyEvent.VK_6, Event.ALT_MASK, "fire 6"); bindCommand(KeyEvent.VK_7, Event.ALT_MASK, "fire 7"); bindCommand(KeyEvent.VK_8, Event.ALT_MASK, "fire 8"); bindCommand(KeyEvent.VK_9, Event.ALT_MASK, "fire 9"); bindCommand(KeyEvent.VK_0, Event.ALT_MASK, "fire 0"); // Targeting bindings // TODO: If we're locked on a tank/VTOL/whatever, adapt. bindCommand(KeyEvent.VK_NUMPAD1, Event.CTRL_MASK, "target ll"); bindCommand(KeyEvent.VK_NUMPAD2, Event.CTRL_MASK, "target -"); bindCommand(KeyEvent.VK_NUMPAD3, Event.CTRL_MASK, "target rl"); bindCommand(KeyEvent.VK_NUMPAD4, Event.CTRL_MASK, "target la"); bindCommand(KeyEvent.VK_NUMPAD5, Event.CTRL_MASK, "target ct"); bindCommand(KeyEvent.VK_NUMPAD6, Event.CTRL_MASK, "target ra"); bindCommand(KeyEvent.VK_NUMPAD7, Event.CTRL_MASK, "target lt"); bindCommand(KeyEvent.VK_NUMPAD8, Event.CTRL_MASK, "target h"); bindCommand(KeyEvent.VK_NUMPAD9, Event.CTRL_MASK, "target rt"); // Misc bindings // TODO: If we're in a tank, rotate turret instead. bindCommand(KeyEvent.VK_NUMPAD7, Event.ALT_MASK, "rottorso l"); bindCommand(KeyEvent.VK_NUMPAD8, Event.ALT_MASK, "rottorso c"); bindCommand(KeyEvent.VK_NUMPAD9, Event.ALT_MASK, "rottorso r"); // // Menu-related actions. // // Register file menu actions. taLoadMap = new ThudSimpleAction ("Load Map...") { protected void doAction () { doLoadMap(); } }; taSaveMapAs = new ThudSimpleAction ("Save Map As...") { protected void doAction () { doSaveMapAs(); } }; taViewReleaseNotes = new ThudSimpleAction ("View Release Notes...") { protected void doAction () { doReleaseNotes(); } }; taQuit = new ThudSimpleAction ("Quit", KeyEvent.VK_Q) { protected void doAction () { doQuit(); } }; // Register edit menu actions. // TODO: Use DefaultEditorKit bindings. taUndo = getEmptyAction("Undo", KeyEvent.VK_Z); taUndo.setEnabled(false); taCut = getEmptyAction("Cut", KeyEvent.VK_X); taCopy = getEmptyAction("Copy", KeyEvent.VK_C); taPaste = getEmptyAction("Paste", KeyEvent.VK_V); taClear = getEmptyAction("Clear"); taClear.setEnabled(false); taSelectAll = getEmptyAction("Select All", KeyEvent.VK_A); taRepeatPreviousCommand = new ThudSimpleAction ("Repeat Previous Command", KeyEvent.VK_P) { protected void doAction () { doPreviousCommand(); } }; taRepeatNextCommand = new ThudSimpleAction ("Repeat Next Command", KeyEvent.VK_N) { protected void doAction () { doNextCommand(); } }; taEraseCurrentCommand = new ThudSimpleAction ("Erase Current Command", KeyEvent.VK_U) { protected void doAction () { doEraseCommand(); } }; taMuteMainWindowText = new ThudSimpleAction ("Mute Main Window Text", KeyEvent.VK_SEMICOLON, Event.SHIFT_MASK) { protected void doAction () { doMuteMainWindow(); } }; // Register HUD menu actions. taPreferences = new ThudSimpleAction ("Preferences...") { protected void doAction () { doPreferences(); } }; taStartStop = new ThudSimpleAction ("Start/Stop", KeyEvent.VK_G) { protected void doAction () { doStartStop(); } }; taStartStop.setEnabled(false); taUpdateTacticalMapNow = new ThudSimpleAction ("Update Tactical Map Now", KeyEvent.VK_N, Event.SHIFT_MASK) { protected void doAction () { commands.forceTactical(); } }; taUpdateTacticalMapNow.setEnabled(false); taConnect = new ThudAction ("Connect") { public void actionPerformed (final ActionEvent ae) { doNewConnection(Integer.valueOf(ae.getActionCommand())); } }; taAddNewHost = new ThudSimpleAction ("Add New Host...") { protected void doAction () { doAddNewHost(); } }; taRemoveHost = new ThudSimpleAction ("Remove Host...") { protected void doAction () { doRemoveHost(); } }; taDisconnect = new ThudSimpleAction ("Disconnect", KeyEvent.VK_Q, Event.SHIFT_MASK) { protected void doAction () { stopConnection(); } }; taDisconnect.setEnabled(false); // Register map menu actions. // TODO: Maybe we should move these actions to MUTacticalMap. taZoomIn = new ThudSimpleAction ("Zoom In", KeyEvent.VK_CLOSE_BRACKET) { protected void doAction () { doZoom(5); } }; taZoomOut = new ThudSimpleAction ("Zoom Out", KeyEvent.VK_OPEN_BRACKET) { protected void doAction () { doZoom(-5); } }; taShowWeaponsArcs = new ThudSimpleAction ("Show Weapons Arcs", KeyEvent.VK_R) { protected void doAction () { doShowArcs(); } }; taShowWeaponsArcs.setSelected(prefs.tacShowArcs); taMakeArcsWeaponRanges = new ThudSimpleAction ("Make Arcs Weapon Ranges", KeyEvent.VK_M) { protected void doAction () { doMakeArcsWeaponRange(); } }; taMakeArcsWeaponRanges.setSelected(prefs.makeArcsWeaponRange); taMakeArcsWeaponRanges.setEnabled(prefs.tacShowArcs); taRetractArcRange = new ThudSimpleAction ("Retract Arc Range", KeyEvent.VK_SEMICOLON) { protected void doAction () { doChangeArc(-1); } }; taRetractArcRange.setEnabled(prefs.tacShowArcs && !prefs.makeArcsWeaponRange); taExtendArcRange = new ThudSimpleAction ("Extend Arc Range", KeyEvent.VK_QUOTE) { protected void doAction () { doChangeArc(1); } }; taExtendArcRange.setEnabled(prefs.tacShowArcs && !prefs.makeArcsWeaponRange); taShowHexNumbers = new ThudSimpleAction ("Show Hex Numbers", KeyEvent.VK_B) { protected void doAction () { doShowHexNumbers(); } }; taShowHexNumbers.setSelected(prefs.tacShowHexNumbers); taShowUnitNames = new ThudSimpleAction ("Show Unit Names", KeyEvent.VK_U) { protected void doAction () { doShowUnitNames(); } }; taShowUnitNames.setSelected(prefs.tacShowUnitNames); taDarkenElevations = new ThudSimpleAction ("Darken Elevations", KeyEvent.VK_D) { protected void doAction () { doDarkenElevations(); } }; taDarkenElevations.setSelected(prefs.tacDarkenElev); taShowArmorDiagram = new ThudSimpleAction ("Show Armor Diagram") { protected void doAction () { doShowArmorDiagrams(); } }; taShowArmorDiagram.setSelected(prefs.tacShowArmorDiagram); taShowLOSInfo = new ThudSimpleAction ("Show LOS Info", KeyEvent.VK_L) { protected void doAction () { doShowLOSInfo(); } }; taShowLOSInfo.setSelected(prefs.tacShowLOSInfo); taMoveMapLeft = new ThudSimpleAction ("Move Map Left", KeyEvent.VK_A, Event.SHIFT_MASK) { protected void doAction () { doChangeXOffset(-1); } }; taMoveMapRight = new ThudSimpleAction ("Move Map Right", KeyEvent.VK_D, Event.SHIFT_MASK) { protected void doAction () { doChangeXOffset(1); } }; taMoveMapUp = new ThudSimpleAction ("Move Map Up", KeyEvent.VK_W, Event.SHIFT_MASK) { protected void doAction () { doChangeYOffset(-1); } }; taMoveMapDown = new ThudSimpleAction ("Move Map Down", KeyEvent.VK_S, Event.SHIFT_MASK) { protected void doAction () { doChangeYOffset(1); } }; taCenterMapOnUnit = new ThudSimpleAction ("Center Map On Unit", KeyEvent.VK_R, Event.SHIFT_MASK) { protected void doAction () { doCenterMap(); } }; taShowCliffs = new ThudSimpleAction ("Show Cliffs", KeyEvent.VK_F) { protected void doAction () { doShowCliffs(); } }; taShowCliffs.setSelected(prefs.tacShowCliffs); taShowHeatArmoronTactical = new ThudSimpleAction ("Show Heat/Armor on Tactical", KeyEvent.VK_I) { protected void doAction () { doShowIndicators(); } }; taShowHeatArmoronTactical.setSelected(prefs.tacShowIndicators); // Register debug menu actions. taDumpDocumentStructure = new ThudSimpleAction ("Dump Document Structure") { protected void doAction () { bsd.dump(System.out); } }; // Register window menu actions. taShowContactsWindow = new ThudSimpleAction ("Show Contacts Window") { protected void doAction () { conList.setVisible(true); } }; taShowStatusWindow = new ThudSimpleAction ("Show Status Window") { protected void doAction () { status.setVisible(true); } }; taShowTacticalWindow = new ThudSimpleAction ("Show Tactical Window") { protected void doAction () { tacMap.setVisible(true); } }; } // TODO: Could add this to a ThudMenu class. private void addCheckBoxItem (final JMenu menu, final ThudAction act) { final JMenuItem item = new JCheckBoxMenuItem (act); act.addButton(item); menu.add(item); } // Helpers to map a KeyStroke to a specific command string. private void bindCommand (final KeyStroke key, final String command) { bindAction(key, new SendCommandAction (this, command)); } private void bindCommand (final int keycode, final int modmask, final String command) { // Convenience. bindCommand(KeyStroke.getKeyStroke(keycode, modmask), command); } private void bindCommand (final int keycode, final String command) { // Convenience. bindCommand(keycode, 0, command); } // Helpers to map a KeyStroke to an arbitrary Action. private void bindAction (final KeyStroke key, final Action action) { // TODO: WHEN_ANCESTOR_OF_FOCUSED_COMPONENT may not always be // the best choice, but more options might be confusing. InputMap inputMap; switch (key.getKeyCode()) { case KeyEvent.VK_NUMPAD0: case KeyEvent.VK_NUMPAD1: case KeyEvent.VK_NUMPAD2: case KeyEvent.VK_NUMPAD3: case KeyEvent.VK_NUMPAD4: case KeyEvent.VK_NUMPAD5: case KeyEvent.VK_NUMPAD6: case KeyEvent.VK_NUMPAD7: case KeyEvent.VK_NUMPAD8: case KeyEvent.VK_NUMPAD9: // Numeric keypad input map. // TODO: Add support for non-digit keypad keys. inputMap = numpadInputMap; break; default: // Regular input map. inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); break; } final ActionMap actionMap = getRootPane().getActionMap(); final Object nameActionKey = action.getValue(Action.NAME); inputMap.put(key, nameActionKey); actionMap.put(nameActionKey, action); } private void bindAction (final int keycode, final int modmask, final Action action) { // Convenience. bindAction(KeyStroke.getKeyStroke(keycode, modmask), action); } private void bindAction (final int keycode, final Action action) { // Convenience. bindAction(keycode, 0, action); } // XXX: Debugging code that adds empty actions. private ThudAction getEmptyAction (final String name) { return new ThudAction (name) { public void actionPerformed (final ActionEvent e) { System.err.println("No action: " + e); } }; } private ThudAction getEmptyAction (final String name, final int accel) { final ThudAction action = getEmptyAction(name); action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(accel, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); return action; } // Base class for various main window actions. private static final int menuShortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); private abstract class ThudAction extends AbstractAction { private ThudAction (final String name) { super(name); } private ThudAction (final String name, final int accel) { this(name, accel, 0); } private ThudAction (final String name, final int accel, final int modmask) { this(name); putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(accel, menuShortcutMask | modmask)); } // Java 6 supports setting the selected state on Actions. // Unfortunately, we have to be compatible with more than just // Java 6, so we sorta implement the same thing here. private Set<WeakReference<AbstractButton>> listeningButtons = new HashSet<WeakReference<AbstractButton>> (); public void addButton (final AbstractButton button) { button.setSelected(selected); listeningButtons.add(new WeakReference<AbstractButton> (button)); } private boolean selected = false; public boolean isSelected () { return selected; } public void setSelected (final boolean selected) { this.selected = selected; final Iterator<WeakReference<AbstractButton>> iter = listeningButtons.iterator(); while (iter.hasNext()) { final AbstractButton button = iter.next().get(); if (button == null) { iter.remove(); continue; } button.setSelected(selected); } } } private abstract class ThudSimpleAction extends ThudAction { private ThudSimpleAction (final String name) { super(name); } private ThudSimpleAction (final String name, final int accel) { super(name, accel, 0); } private ThudSimpleAction (final String name, final int accel, final int modmask) { super(name, accel, modmask); } public void actionPerformed (final ActionEvent ae) { doAction(); } protected abstract void doAction (); } /** Repaint ourselves */ public void paint (Graphics g) { // TODO: Is there a cleaner way to do this? final Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, prefs.antiAliasText ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); super.paint(g2); } // -------------------------------------------------------------------- // ACTION IMPLEMENTATION // -------------------------------------------------------------------- /** Load map from file. */ private void doLoadMap () { final JFileChooser fc = new JFileChooser (); final int returnVal = fc.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { data.mapFileName = fc.getSelectedFile().getAbsolutePath(); if (data.loadMapFromDisk()) { parse.messageLine("*** Map " + data.mapFileName + " loaded successfully ***"); } else { parse.messageLine("*** Error loading map " + data.mapFileName + " ***"); } } } /** Save map to file. */ private void doSaveMapAs () { final JFileChooser fc = new JFileChooser (); final int returnVal = fc.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { data.mapFileName = fc.getSelectedFile().getAbsolutePath(); if (data.saveMapToDisk()) { parse.messageLine("*** Map " + data.mapFileName + " saved successfully ***"); } else { parse.messageLine("*** Error saving map " + data.mapFileName + " ***"); } } } /** Show the release notes */ private void doReleaseNotes () { new ReleaseNotesDialog(this, true).setVisible(true); } /** Quit cleanly */ private void doQuit () { try { // Close our connection if (connected) stopConnection(); // Write out our preferences file writePrefs(); // Write out map //if (data != null) // data.saveMapToDisk(); } catch (final Exception e) { System.out.println("Error: doQuit: " + e); } // We're done System.exit(0); } private void setupListeners () { addWindowListener(new WindowAdapter () { public void windowClosing (final WindowEvent we) { doQuit(); } }); } // ------------------------------------------------------------------------ // MAIN SETUP // ------------------------------------------------------------------------ /** * Utility function to get the proper accelerator for connection items * in the HUD menu */ private void acceleratorForConnectionItem (JMenuItem mi, int i) { int keycode = KeyEvent.VK_UNDEFINED; switch (i) { case 0: keycode = KeyEvent.VK_1; break; case 1: keycode = KeyEvent.VK_2; break; case 2: keycode = KeyEvent.VK_3; break; case 3: keycode = KeyEvent.VK_4; break; case 4: keycode = KeyEvent.VK_5; break; case 5: keycode = KeyEvent.VK_6; break; case 6: keycode = KeyEvent.VK_7; break; case 7: keycode = KeyEvent.VK_8; break; case 8: keycode = KeyEvent.VK_9; break; case 9: keycode = KeyEvent.VK_0; break; } // TODO: We could have just computed this and used the String // (or even char) version of getKeyStroke(). if (keycode != KeyEvent.VK_UNDEFINED) { mi.setAccelerator(KeyStroke.getKeyStroke(keycode, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | Event.SHIFT_MASK)); } } /** Initialize the connection items for the HUD menu */ public void initConnectionMenus() { miConnections = new JMenuItem[prefs.hosts.size()]; } protected void setupNewTextFields() { // Setup the text pane bsd = new BulkStyledDocument (prefs.maxScrollbackSize, mFont); textPane = new JTextPane (bsd); textPane.setStyledDocument(bsd); textPane.setForeground(Color.white); textPane.setBackground(Color.black); textPane.setEditable(false); textPane.setFont(mFont); textPane.setRequestFocusEnabled(false); textPane.setRequestFocusEnabled(true); textPane.setFocusable(true); textPaneWriter = new JTextPaneWriter (textPane); JScrollPane scrollPane = new JScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); add(scrollPane, BorderLayout.CENTER); // Setup the text field textField = new JTextField (80); textField.setFont(mFont); textField.setEnabled(true); textFieldWatcher = new DocumentWatcher (); textField.getDocument().addDocumentListener(textFieldWatcher); add(textField, BorderLayout.SOUTH); // Link a few key bindings. final InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); // we can also use WHEN_IN_FOCUSED_WINDOW in any subcomponent, but it's messier final ActionMap actionMap = getRootPane().getActionMap(); // TODO: Relate these to DefaultEditorKit? // so they compare equal as Objects. inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "PAGE UP"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "PAGE DOWN"); // XXX: We're making use of the fact that string literals are interned. actionMap.put("PAGE UP", new ActionRedirector (textPane, DefaultEditorKit.pageUpAction)); actionMap.put("PAGE DOWN", new ActionRedirector (textPane, DefaultEditorKit.pageDownAction)); // Install our custom numeric keypad handling on focusable components. // TODO: We probably want to make this configurable, for people who // like to use their numeric pads to, you know, type in numbers. final NumpadKeyListener numpadListener = new NumpadKeyListener (getRootPane(), numpadInputMap); textField.addKeyListener(numpadListener); textPane.addKeyListener(numpadListener); final InputMap tfInputMap = textField.getInputMap(); textField.setInputMap(JComponent.WHEN_FOCUSED, new NumpadInputMap (tfInputMap)); // FIXME: This is an even bigger hack. Mostly because we're meddling // directly with the textField; the ginormous anonymous class we can // obviously always refactor later. textField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "btthud.ENTER"); textField.getActionMap().put("btthud.ENTER", new AbstractAction () { public void actionPerformed (final ActionEvent ae) { final String typedText = textField.getText(); String text = typedText; boolean isThudCommand = false; if (text.length() > 0 && text.charAt(0) == COMMAND_CHAR) { // Thud command. text = text.substring(1); if (text.length() > 0 && text.charAt(0) == COMMAND_CHAR) { // Escaped COMMAND_CHAR. isThudCommand = false; } else { // COMMAND_CHAR-prefixed command. isThudCommand = true; } } if (!isThudCommand && (conn == null || !conn.connected)) { // Not connected. textPaneWriter.println(); textPaneWriter.println("*** Can't Send Text: Not Connected ***", BulkStyledDocument.STYLE_HUD_MESSAGE); return; } // Execute command. if (isThudCommand) { if (prefs.echoCommands) { textPaneWriter.println(); textPaneWriter.println(COMMAND_CHAR + text, BulkStyledDocument.STYLE_HUD_MESSAGE); } interactor.doCommand(text); } else { if (prefs.echoCommands) { textPaneWriter.println(); textPaneWriter.println("> " + text, BulkStyledDocument.STYLE_COMMAND); } try { conn.sendCommand(new UserCommand (text)); } catch (IOException e) { textPaneWriter.println(); textPaneWriter.println("> Couldn't send: " + e, BulkStyledDocument.STYLE_COMMAND); // TODO: Break connection? } } // Clear the text field textField.setText(null); // Add this command to our history addCommand(typedText); } }); } /** Start the connection, including creating new objects */ public void startConnection (final MUHost host) { if (connected) // We must already have a connection. Let's clean up that one, then go to this new one stopConnection(); try { // Setup some of the helper classes data = new MUData(); parse = new MUParse(textPaneWriter, data, prefs); parse.messageLine("*** Connecting... ***"); // Setup the connection conn = new MUConnection(host, this, parse); this.setTitle("Thud - " + host.getHost() + " " + host.getPort()); // Setup the rest of the helper classes. tacMap = new MUTacticalMap (this); status = new MUStatus (this); conList = new MUContactList (this); data.addActionListener(tacMap); data.addActionListener(status); data.addActionListener(conList); commands = new MUCommands(conn, data, prefs); // Let our parsing class know where to send commands parse.setCommands(commands); // Let the text field get the keyboard focus setVisible(true); textField.grabFocus(); // Okay we're connected connected = true; // Enable some menu stuff taStartStop.setEnabled(true); taUpdateTacticalMapNow.setEnabled(true); taDisconnect.setEnabled(true); mapMenu.setEnabled(true); windowMenu.setEnabled(true); } catch (Exception e) { System.out.println("Error: " + e); } } // --------------------- public void stopConnection() { if (connected) { connected = false; if (commands != null) commands.endTimers(); // TODO: Are all these null checks really necessary? if (data != null) data.pleaseStop(); if (conList != null) conList.dispose(); if (status != null) status.dispose(); if (tacMap != null) tacMap.dispose(); if (conn != null) conn.pleaseStop(); if (parse != null) parse.messageLine("*** Disconnected ***"); this.setTitle("Thud"); // Disable some menu stuff taStartStop.setEnabled(false); taUpdateTacticalMapNow.setEnabled(false); taDisconnect.setEnabled(false); mapMenu.setEnabled(false); windowMenu.setEnabled(false); } } /** Display the preferences dialog */ public void doPreferences() { prefsDialog.setVisible(true); // Send messages around in case something changed if (tacMap != null) tacMap.newPreferences(prefs); if (conList != null) conList.newPreferences(prefs); if (status != null) status.newPreferences(prefs); mainFontChanged(); bsd.setMaxLines(prefs.maxScrollbackSize); this.setAlwaysOnTop(prefs.mainAlwaysOnTop); // Why not write the prefs to disk right now? Save ourselves some grief writePrefs(); } /** Display the "Add New Host" dialog */ public void doAddNewHost() { AddHostDialog addDialog = new AddHostDialog(this, true); addDialog.setVisible(true); } /** Display the "Remove Host" dialog */ public void doRemoveHost() { RemoveHostDialog removeDialog = new RemoveHostDialog(this, true); removeDialog.setVisible(true); } // ----------------------- // A thread-safe DocumentListener, to watch for changes to the input // text field. It doesn't really need to be thread-safe, but we might // need it later. For example, the scripting engine, which runs in a // separate thread, might modify the user's input field, to implement a // /grab-like feature. static private class DocumentWatcher implements DocumentListener { private boolean modified; private boolean wasModified () { boolean oldModified; // Only clearing needs to be synchronized, as Java // guarantees atomicity for boolean reads/writes. Thus // we only need to ensure that multiple threads don't // clear the flag simultaneously. // // Of course, the only time we're really going to call // this is from the Swing thread, so even this level of // synchronization is unnecessary. synchronized (this) { oldModified = modified; modified = false; } return oldModified; } public void changedUpdate (final DocumentEvent e) { modified = true; } public void insertUpdate (final DocumentEvent e) { modified = true; } public void removeUpdate (final DocumentEvent e) { modified = true; } } /** Add command to history. */ private void addCommand (final String command) { // Check history cursor. if (historyCursor != null) { // Stop traversing history. historyCursor = null; commandHistory.removeFirst(); // forget temp } // Check if we need to add anything to the history. if (command.length() == 0) { // Don't add empty commands to the history. return; } if (!commandHistory.isEmpty() && commandHistory.getFirst().equals(command)) { // Don't add redundant commands to the history. return; } // Add command as most recent. commandHistory.addFirst(command); if (commandHistory.size() > prefs.commandHistory) { // Exceeded history size, remove least recent. commandHistory.removeLast(); } } /** Step to the previous command in the history. */ private void doPreviousCommand () { // Check history cursor. if (historyCursor != null && textFieldWatcher.wasModified()) { // Stop traversing history. historyCursor = null; commandHistory.removeFirst(); // forget temp } if (historyCursor == null) { // Start traversing history. commandHistory.addFirst(textField.getText()); // temp historyCursor = commandHistory.listIterator(); historyForward = false; // skip temp } // Recall command from history. if (!historyForward) { // Reversed direction. Java iterators return the same // item twice if you go forward, then backwards. This // is obviously not what a human would expect, so we // remember by setting a flag. historyCursor.next(); // skip past last previous() historyForward = true; } if (!historyCursor.hasNext()) { // Already at least recent. return; } textField.setText(historyCursor.next()); textField.setCaretPosition(textField.getDocument().getLength()); textFieldWatcher.wasModified(); // clear modification state } /** Step to the next command in the history. */ private void doNextCommand () { // Check history cursor. if (historyCursor != null && textFieldWatcher.wasModified()) { // Stop traversing history. historyCursor = null; commandHistory.removeFirst(); // forget temp } if (historyCursor == null) { // Not traversing history. return; } // Recall command from history. if (historyForward) { // Reversed direction. Java iterators return the same // item twice if you go forward, then backwards. This // is obviously not what a human would expect, so we // remember by setting a flag. historyCursor.previous(); // skip past last next() historyForward = false; } if (!historyCursor.hasPrevious()) { // Already at most recent. return; } textField.setText(historyCursor.previous()); textField.setCaretPosition(textField.getDocument().getLength()); textFieldWatcher.wasModified(); // clear modification state } /** Erase the current command from the text box. */ private void doEraseCommand () { textField.setText(null); } /** Mute the text in the main window */ public void doMuteMainWindow() { data.mainWindowMuted = !data.mainWindowMuted; taMuteMainWindowText.setSelected(data.mainWindowMuted); if (parse != null) { if (data.mainWindowMuted) parse.messageLine("*** Main Window Text Output Muted ***"); else parse.messageLine("*** Main Window Text Output Unmuted ***"); } } /** Toggle HUD status */ public void doStartStop() { if (data.hudRunning) { doStop(); } else { doStart(); } } /** Starts the HUD. */ public void doStart() { if(connected && !data.hudStarted) { // only start if we're connected and not already started data.hudStarted = true; data.lastDataTime = System.currentTimeMillis(); parse.messageLine("*** Display Started ***"); // Set our session key to something not too easily duplicated String sessionKey = String.valueOf(Calendar.getInstance().get(Calendar.MILLISECOND)); parse.setSessionKey(sessionKey); try { // Set the HUDINFO key conn.sendCommand(new HUDSession (sessionKey)); } catch (Exception e) { System.out.println("Error: hudinfo key set: " + e); } data.clearData(); // Start sending commands commands.startTimers(); taUpdateTacticalMapNow.setEnabled(true); } } /** Stops the HUD. */ public void doStop() { if(data.hudStarted) {//only stop if we're started data.hudStarted = false; parse.messageLine("*** Display Stopped ***"); data.hudRunning = false; commands.endTimers(); taUpdateTacticalMapNow.setEnabled(false); } } /** Resume the HUD */ public void doResume() { if(conn.connected && !data.hudRunning) { // only resume if connected and not already running data.hudRunning = true; commands.forceTactical(); } } /** Resume the HUD, with an optional 'Resumed' message. * * @param display If true, display '*** Display Resumed ***' in console */ public void doResume(boolean display) { doResume(); if(display) parse.messageLine("*** Display Resumed ***"); } /** Suspend the HUD */ public void doSuspend() { if(data.hudRunning) data.hudRunning = false; } /** Suspend the HUD * * @param display If true, display '*** Display Suspended***' in console */ public void doSuspend(boolean display) { doSuspend(); if(display) parse.messageLine("*** Display Suspended ***"); } /** Set the zoom level on the map */ public void doZoom(int z) { // Let's try to keep the hex height even, since there are a lot of places that divide it by 2 - and it's an int prefs.hexHeight += z; if (prefs.hexHeight < 5) prefs.hexHeight = 5; if (prefs.hexHeight > 300) prefs.hexHeight = 300; tacMap.newPreferences(prefs); } /** Change the offset of the map in x */ public void doChangeXOffset(int mod) { prefs.xOffset += mod; tacMap.newPreferences(prefs); } /** Change the offset of the map in y */ public void doChangeYOffset(float mod) { prefs.yOffset += mod; tacMap.newPreferences(prefs); } /** Recenter the map */ public void doCenterMap() { prefs.xOffset = 0; prefs.yOffset = 0; tacMap.newPreferences(prefs); } /** Show the weapons arcs */ public void doShowArcs() { prefs.tacShowArcs = !prefs.tacShowArcs; taShowWeaponsArcs.setSelected(prefs.tacShowArcs); taMakeArcsWeaponRanges.setEnabled(prefs.tacShowArcs); taRetractArcRange.setEnabled(!prefs.makeArcsWeaponRange && prefs.tacShowArcs); taExtendArcRange.setEnabled(!prefs.makeArcsWeaponRange && prefs.tacShowArcs); tacMap.newPreferences(prefs); } /** Make the weapons arcs reflect actual weapon range */ public void doMakeArcsWeaponRange() { prefs.makeArcsWeaponRange = !prefs.makeArcsWeaponRange; taMakeArcsWeaponRanges.setSelected(prefs.makeArcsWeaponRange); taRetractArcRange.setEnabled(!prefs.makeArcsWeaponRange); taExtendArcRange.setEnabled(!prefs.makeArcsWeaponRange); } /** Handle changing of arc length */ public void doChangeArc(int d) { prefs.arcIndicatorRange += d; if (prefs.arcIndicatorRange < 1) prefs.arcIndicatorRange = 1; if (prefs.arcIndicatorRange > 200) prefs.arcIndicatorRange = 200; tacMap.newPreferences(prefs); } /** Show the hex numbers? */ public void doShowHexNumbers() { prefs.tacShowHexNumbers = !prefs.tacShowHexNumbers; taShowHexNumbers.setSelected(prefs.tacShowHexNumbers); tacMap.newPreferences(prefs); } /** Show the unit names on the tactical map? */ public void doShowUnitNames() { prefs.tacShowUnitNames = !prefs.tacShowUnitNames; taShowUnitNames.setSelected(prefs.tacShowUnitNames); tacMap.newPreferences(prefs); } /** Darken elevations on the map? */ public void doDarkenElevations() { prefs.tacDarkenElev = !prefs.tacDarkenElev; taDarkenElevations.setSelected(prefs.tacDarkenElev); tacMap.newPreferences(prefs); } /** Show armor diagrams? */ public void doShowArmorDiagrams() { prefs.tacShowArmorDiagram = !prefs.tacShowArmorDiagram; taShowArmorDiagram.setSelected(prefs.tacShowArmorDiagram); tacMap.newPreferences(prefs); } /** Show LOS info? */ public void doShowLOSInfo() { prefs.tacShowLOSInfo = !prefs.tacShowLOSInfo; taShowLOSInfo.setSelected(prefs.tacShowLOSInfo); tacMap.newPreferences(prefs); } /** Show cliffs on the map? */ public void doShowCliffs() { prefs.tacShowCliffs = !prefs.tacShowCliffs; taShowCliffs.setSelected(prefs.tacShowCliffs); tacMap.newPreferences(prefs); } /** Show indicators on the map? */ public void doShowIndicators() { prefs.tacShowIndicators = !prefs.tacShowIndicators; taShowHeatArmoronTactical.setSelected(prefs.tacShowIndicators); tacMap.newPreferences(prefs); } // These two are for future expansion of setting the colors in the main window public void doGetBackgroundColor() { prefs.backgroundColor = JColorChooser.showDialog(this, "Choose a background color", prefs.backgroundColor); } public void doGetForegroundColor() { prefs.foregroundColor = JColorChooser.showDialog(this, "Choose a foreground color", prefs.foregroundColor); } /** Start a new connection */ public void doNewConnection (final int ii) { if (connected) // Clear our current connection first stopConnection(); startConnection(prefs.hosts.get(ii)); } /** Called when main font size changes */ public void mainFontChanged () { mFont = new Font(prefs.mainFont, Font.PLAIN, prefs.mainFontSize); if (bsd != null) bsd.setFont(mFont); if (textPane != null) textPane.setFont(mFont); if (textField != null) textField.setFont(mFont); } /** Read our prefs from disk */ public void readPrefs() { prefs = new MUPrefs(); prefs.defaultPrefs(); PreferenceStore.load(prefs); // FIXME: New code doesn't know if this is the first launch or not, so // I'm going to disable showing release notes on first run for now. // // This really should be more like 'newVersion' to show release notes. // Also, we need a way to upgrade preferences when new versions suggest // better defaults. firstLaunch = false; // FIXME: Only change font if we load something from preferences. mainFontChanged(); } /** Write our prefs to disk */ public void writePrefs() { // We should really be listening for events to determine when the size/location of each // window has changed, and then they can set these values themselves // For now though, I'm putting the code here that just gets the info and puts it into the // prefs object prefs.mainLoc = getLocation(); prefs.mainSizeX = getSize().width; prefs.mainSizeY = getSize().height; if (tacMap != null) { prefs.tacLoc = tacMap.getLocation(); prefs.tacSizeX = tacMap.getSize().width; prefs.tacSizeY = tacMap.getSize().height; } if (conList != null) { prefs.contactsLoc = conList.getLocation(); prefs.contactsSizeX = conList.getSize().width; prefs.contactsSizeY = conList.getSize().height; } if(status != null) { prefs.statusLoc = status.getLocation(); prefs.statusSizeX = status.getSize().width; prefs.statusSizeY = status.getSize().height; } PreferenceStore.save(prefs); } public MUConnection getConn () { return conn; } }