view FrmMainWindow.cs @ 57:293d204e40db

Re #60: Add UI to add/remove/edit weapons in GTK * Make enabling/disabling fit the values passed (copied from existing WinForms checks) * Add extra interface methods and properties to get necessary values from UI to use in command * Implement new interface methods * Implement command execution * Make use of IDisposable interface instead of defining Dispose ourselves Also: * Use existing method of printing exception stack traces in main window
author IBBoard <dev@ibboard.co.uk>
date Thu, 26 Aug 2010 19:30:22 +0000
parents 4bad8cb3f889
children e3fe48c4d794
line wrap: on
line source

// This file (FrmMainWindow.cs) is a part of the IBBoard.WarFoundry.GTK project and is copyright 2008, 2009 IBBoard.
//
// The file and the library/program it is in are licensed and distributed, without warranty, under the GNU Affero GPL license, either version 3 of the License or (at your option) any later version. Please see COPYING for more information and the full license.

using System;
using System.IO;
using System.Collections.Generic;
using System.Configuration;
using Gtk;
using IBBoard;
using IBBoard.Commands;
using IBBoard.GtkSharp;
using IBBoard.IO;
using IBBoard.Lang;
using IBBoard.Logging;
using IBBoard.CustomMath;
using IBBoard.WarFoundry.API;
using IBBoard.WarFoundry.API.Exporters;
using IBBoard.WarFoundry.API.Factories;
using IBBoard.WarFoundry.API.Factories.Xml;
using IBBoard.WarFoundry.API.Objects;
using WFObjects = IBBoard.WarFoundry.API.Objects;
using IBBoard.WarFoundry.API.Commands;
using IBBoard.WarFoundry.API.Savers;
using IBBoard.WarFoundry.API.Requirements;
using IBBoard.WarFoundry.GTK.Widgets;
using IBBoard.WarFoundry.Plugin.Rollcall;
using IBBoard.Xml;
using log4net;
using GLib;

namespace IBBoard.WarFoundry.GTK
{
	public partial class FrmMainWindow: Gtk.Window
	{
		private static readonly string AppTitle = "WarFoundry";
		private const int CATEGORY_BUTTON_SEPARATOR_INDEX = 6;

		private Preferences preferences;
		private ILog logger = LogManager.GetLogger(typeof(FrmMainWindow));

		private CommandStack commandStack;
		private Dictionary<ToolButton, Category> categoryMap = new Dictionary<ToolButton, Category>();
		private Dictionary<WFObjects.Unit, UnitDisplayWidget> unitToWidgetMap = new Dictionary<WFObjects.Unit,UnitDisplayWidget>();

		private ObjectAddDelegate UnitAddedMethod;
		private ObjectRemoveDelegate UnitRemovedMethod;
		private DoubleValChangedDelegate PointsValueChangedMethod;
		private FailedUnitRequirementDelegate FailedUnitRequirementMethod;
		private StringValChangedDelegate UnitNameChangedMethod;

		private GameSystem system;
		private string loadedArmyPath;

		private MenuToolButton undoMenuButton, redoMenuButton;

		public static void Main(string[] args)
		{
			try
			{
				ExceptionManager.UnhandledException += HandleUnhandledException;
				Application.Init();
				FrmMainWindow win = new FrmMainWindow(args);
				win.Show();
				Application.Run();
				LogManager.GetLogger(typeof(FrmMainWindow)).Debug("Application ended");
			}
			catch(Exception ex)
			{
				HandleUnhandledException(ex);
			}
		}

		private static void HandleUnhandledException(UnhandledExceptionArgs args)
		{
			object obj = args.ExceptionObject;
			Exception ex = null;
			
			if (obj is Exception)
			{
				ex = (Exception)obj;
			}
			else
			{
				ex = new Exception("GLib returned unexpected exception object type "+obj.GetType());
			}
			
			HandleUnhandledException(ex);
		}

		private static void HandleUnhandledException(Exception ex)
		{
			string msg = String.Format("({0}) {1}", ex.GetType().FullName, ex.Message);
			LogManager.GetLogger(typeof(FrmMainWindow)).Fatal(msg, ex);
			MessageDialog dialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, false, "An unhandled exception occurred. Please check the log for more details.");
			dialog.Run();
		}
		
		private static string GetStackTrace(Exception ex)
		{
			string message = "";
			
			if (ex != null)
			{
				message  = "Caused by: " + ex.GetType().FullName + Environment.NewLine + ex.StackTrace + Environment.NewLine ;
			}
			
			return message;
		}

		public FrmMainWindow() : this(new string[0])
		{
			//Do nothing extra
		}

		public FrmMainWindow(string[] args) : base(Gtk.WindowType.Toplevel)
		{
			logger.Info("Opening FrmMainWindow");
			Build();
			//Replace the undo/redo buttons with menu versions, which Monodevelop's GUI editor doesn't currently support
			redoMenuButton = new MenuToolButton("gtk-redo");
			redoMenuButton.Label = "Redo";
			redoMenuButton.TooltipText = "Redo";
			redoMenuButton.Clicked += redoTBButtonActivated;
			toolbar.Insert(redoMenuButton, CATEGORY_BUTTON_SEPARATOR_INDEX);
			undoMenuButton = new MenuToolButton("gtk-undo");
			undoMenuButton.Label = "Undo";
			undoMenuButton.TooltipText = "Undo";
			undoMenuButton.Clicked += undoTBButtonActivated;
			toolbar.Insert(undoMenuButton, CATEGORY_BUTTON_SEPARATOR_INDEX);
			toolbar.Remove(toolbar.Children[CATEGORY_BUTTON_SEPARATOR_INDEX-1]);
			toolbar.Remove(toolbar.Children[CATEGORY_BUTTON_SEPARATOR_INDEX-2]);
			toolbar.ShowAll();

			Title = AppTitle;
			TreeViewColumn mainColumn = new TreeViewColumn ();
			mainColumn.Title = "Army Categories";
			CellRendererText mainCell = new CellRendererText ();
			mainColumn.PackStart (mainCell, true);
			treeUnits.AppendColumn(mainColumn);
			mainColumn.SetCellDataFunc(mainCell, new TreeCellDataFunc(RenderCategoryTreeObjectName));
			treeUnits.Model = new TreeStore(typeof(WarFoundryObject));
			logger.Debug("Loading preferences");
			Preferences = new Preferences("WarFoundryGTK");
			logger.Debug("Loading translations");

			try
			{
				Translation.InitialiseTranslations(Constants.ExecutablePath, Preferences["language"].ToString());
			}
			catch (TranslationLoadException ex)
			{
				logger.Error(ex);
				MessageDialog dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, ex.Message);
				dialog.Title = "Translation loading failed";
				dialog.Run();
				dialog.Destroy();
			}

			logger.Debug("Initialising");
			commandStack = new CommandStack();
			commandStack.CommandStackUpdated+=new MethodInvoker(commandStack_CommandStackUpdated);
			WarFoundryCore.GameSystemChanged+= new GameSystemChangedDelegate(OnGameSystemChanged);
			WarFoundryCore.ArmyChanged+= new ArmyChangedDelegate(OnArmyChanged);
			Destroyed+= new EventHandler(OnWindowDestroyed);
			//TODO: Translate and subscribe to other events
			UnitAddedMethod = new ObjectAddDelegate(OnUnitAdded);
			UnitRemovedMethod = new ObjectRemoveDelegate(OnUnitRemoved);
			PointsValueChangedMethod = new DoubleValChangedDelegate(OnPointsValueChanged);
			FailedUnitRequirementMethod = new FailedUnitRequirementDelegate(OnFailedUnitRequirement);
			UnitNameChangedMethod = new StringValChangedDelegate(OnUnitNameChanged);

			//FIXME: Temporary hack to add paths and factories before we get preferences and plugins
			WarFoundryLoader.GetDefault().AddLoadDirectory(new DirectoryInfo(Constants.ExecutablePath + Constants.DirectoryString + "data"));
			WarFoundryLoader.GetDefault().RegisterFactory(WarFoundryXmlFactory.GetFactory());
			WarFoundryLoader.GetDefault().RegisterNonNativeFactory(RollcallFactory.GetFactory());
			WarFoundryLoader.GetDefault().FileLoadingFinished += FileLoadingFinished;
			WarFoundrySaver.SetFileSaver(new WarFoundryXmlSaver());

			logger.Debug("Initialising complete - seeing if we can load default army or system");

			if (args.Length == 1)
			{
				logger.Debug("Attempting to load from file");
				FileInfo file = new FileInfo(args[0]);

				try
				{
					ICollection<IWarFoundryObject> objects = WarFoundryLoader.GetDefault().LoadFile(file);

					if (objects.Count == 1)
					{
						List<IWarFoundryObject> objectList = new List<IWarFoundryObject>();
						objectList.AddRange(objects);
						IWarFoundryObject loadedObject = objectList[0];

						if (loadedObject is Army)
						{
							WarFoundryCore.CurrentArmy = (Army)loadedObject;
							logger.InfoFormat("Loaded army from {0}", file.FullName);
						}
						else if (loadedObject is GameSystem)
						{
							WarFoundryCore.CurrentGameSystem = (GameSystem)loadedObject;
							logger.InfoFormat("Loaded game system from {0}", file.FullName);
						}
					}
				}
				catch (InvalidFileException ex)
				{
					//TODO: show error dialog
					logger.Error(ex);
				}
			}
			else
			{
				string gameSystemID = Preferences.GetStringProperty("currSystem");

				if (gameSystemID!=null && !"".Equals(gameSystemID))
				{
					logger.Debug("Attempting to load current game system from properties");
					GameSystem sys = WarFoundryLoader.GetDefault().GetGameSystem(gameSystemID);

					if (sys!=null)
					{
						WarFoundryCore.CurrentGameSystem = sys;
						logger.InfoFormat("Loaded game system {0} from properties", gameSystemID);
					}
				}
			}
		}

		private void FileLoadingFinished (List<FileLoadFailure> failures)
		{
			foreach(FileLoadFailure failure in failures)
			{
				logger.Warn("Failed to load " + failure.FailedFile.FullName + ": " + failure.Message);
			}
		}

		private void RenderCategoryTreeObjectName(TreeViewColumn column, CellRenderer cell, TreeModel model, TreeIter iter)
		{
			object o = model.GetValue(iter, 0);

			if (o is ArmyCategory)
			{
				ArmyCategory c = (ArmyCategory)o;
				string name = "";

				if (!Preferences.GetBooleanProperty("ShowCatPercentage"))
				{
					name = Translation.GetTranslation("categoryTreeCatName", "{0} - {1}pts", c.Name, c.Points);
				}
				else
				{
					name = Translation.GetTranslation("categoryTreeCatNamePercentage", "{0} - {1}pts ({2}%)", c.Name, c.Points, (c.ParentArmy.Points > 0 ? Math.Round((c.Points / c.ParentArmy.Points) * 100) : 0));
				}

				(cell as CellRendererText).Text = name;
			}
			else if (o is WFObjects.Unit)
			{
				WFObjects.Unit u = (WFObjects.Unit)o;
				string name = Translation.GetTranslation("unitTreeCatName", "{0} - {1}pts", u.Name, u.Points);
				(cell as CellRendererText).Text = name;
			}
		}

		private void OnWindowDestroyed(object source, EventArgs args)
		{
			logger.Info("Exiting");
			Application.Quit();
		}

		private void OnUnitNameChanged(WarFoundryObject val, string oldValue, string newValue)
		{
			WFObjects.Unit unit = (WFObjects.Unit)val;
			UnitDisplayWidget widget;
			unitToWidgetMap.TryGetValue(unit, out widget);
			logger.DebugFormat("Unit name changed for {0} - now called {1}", unit.ID, unit.Name);
			treeUnits.QueueDraw();

			if (widget!=null)
			{
				unitsNotebook.SetTabLabel(widget, NotebookUtil.CreateNotebookTabLabelWithClose(unitsNotebook, widget, newValue));
			}
		}

		private void OnUnitAdded(WarFoundryObject val)
		{
			WFObjects.Unit unit = (WFObjects.Unit)val;
			unit.NameChanged+= UnitNameChangedMethod;
			AddUnitToTree(unit);
		}

		private void AddUnitToTree(WFObjects.Unit unit)
		{
			TreeStore model = (TreeStore)treeUnits.Model;
			TreeIter iter;
			model.GetIterFirst(out iter);

			do
			{
				object obj = model.GetValue(iter, 0);

				if (obj is ArmyCategory)
				{
					ArmyCategory cat = (ArmyCategory)obj;

					if (cat.Equals(unit.Category))
					{
						model.AppendValues(iter, unit);
						TreePath path = model.GetPath(iter);
						treeUnits.ExpandToPath(path);
					}
				}
			}
			while (model.IterNext(ref iter));
		}

		private void OnUnitRemoved(WarFoundryObject obj)
		{
			WFObjects.Unit unit = (WFObjects.Unit)obj;
			unit.NameChanged-= UnitNameChangedMethod;
			RemoveUnitFromTree(unit);
			RemoveUnitTab(unit);
		}

		private void RemoveUnitFromTree(WFObjects.Unit unit)
		{
			TreeStore model = (TreeStore)treeUnits.Model;
			TreeIter iter;
			model.GetIterFirst(out iter);
			bool removed = false;

			do
			{
				object obj = model.GetValue(iter, 0);

				if (obj is ArmyCategory)
				{
					ArmyCategory cat = (ArmyCategory)obj;

					if (unit.Category == null || cat.Equals(unit.Category))
					{
						TreeIter innerIter;
						model.IterChildren(out innerIter, iter);

						do
						{
							object innerObj = model.GetValue(innerIter, 0);

							if (unit.Equals(innerObj))
							{
								model.Remove(ref innerIter);
								removed = true;
								break;
							}
						}
						while (model.IterNext(ref innerIter));

						if (removed)
						{
							break;
						}
					}
				}
			}
			while (model.IterNext(ref iter));
		}
		
		private void RemoveUnitTab(WFObjects.Unit unit)
		{
			UnitDisplayWidget widget = DictionaryUtils.GetValue(unitToWidgetMap, unit);
			
			if (widget != null)
			{
				unitsNotebook.Remove(widget);
			}
		}

		private void OnPointsValueChanged(WarFoundryObject obj, double before, double after)
		{
			//Set points in panel
		}

		private void OnFailedUnitRequirement(List<FailedUnitRequirement> failedRequirement)
		{
			//Show error message in panel
		}

		public Preferences Preferences
		{
			get { return preferences; }
			set { preferences = value; }
		}

		/*public AbstractNativeWarFoundryFactory Factory
		{
			get { return WarFoundryFactoryFactory.GetFactoryFactory().GetFactory(Constants.ExecutablePath, factoryType); }
		}*/

		protected void OnDeleteEvent (object sender, DeleteEventArgs a)
		{
			Application.Quit ();
			a.RetVal = true;
		}

		protected virtual void OnExitActivated(object sender, System.EventArgs e)
		{
			Application.Quit();
		}

		protected virtual void OnCreateArmyActivated(object sender, System.EventArgs e)
		{
			CreateNewArmy();
		}

		protected virtual void OnReloadFilesActivated(object sender, System.EventArgs e)
		{
			WarFoundryLoader.GetDefault().LoadFiles();
		}

		protected virtual void OnSaveArmyAsActivated(object sender, System.EventArgs e)
		{
			SaveCurrentArmyAs();
		}

		protected virtual void OnCloseArmyActivated(object sender, System.EventArgs e)
		{
			CloseCurrentArmy();
		}

		protected virtual void OnOpenArmyActivated(object sender, System.EventArgs e)
		{
			OpenArmy();
		}

		protected virtual void OnSaveArmyActivated(object sender, System.EventArgs e)
		{
			SaveCurrentArmyOrSaveAs();
		}

		protected virtual void OnAddUnitActivated(object sender, System.EventArgs e)
		{
			if (sender is ToolButton)
			{
				Category cat = null;
				categoryMap.TryGetValue((ToolButton)sender, out cat);

				if (cat != null)
				{
					logger.DebugFormat("Show FrmNewUnit for {0}", cat.Name);
					FrmNewUnit newUnit = new FrmNewUnit(WarFoundryCore.CurrentArmy.Race, cat, WarFoundryCore.CurrentArmy);
					ResponseType response = (ResponseType)newUnit.Run();
					newUnit.Hide();

					if (response == ResponseType.Ok)
					{
						CreateAndAddUnitCommand cmd = new CreateAndAddUnitCommand(newUnit.SelectedUnit, WarFoundryCore.CurrentArmy.GetCategory(cat));
						commandStack.Execute(cmd);
						ShowUnitWidget(cmd.Unit);
					}

					newUnit.Dispose();
				}
			}
		}

		public CommandStack CommandStack
		{
			get { return commandStack; }
		}

		private void SetAppTitle()
		{
			if (WarFoundryCore.CurrentArmy!=null)
			{
				Title = AppTitle + " - " + WarFoundryCore.CurrentGameSystem.Name + " - " + WarFoundryCore.CurrentArmy.Name;
			}
			else if (WarFoundryCore.CurrentGameSystem!=null)
			{
				Title = AppTitle + " - " + WarFoundryCore.CurrentGameSystem.Name;
			}
			else
			{
				Title = AppTitle;
			}
		}

		private void OnGameSystemChanged(GameSystem oldSys, GameSystem newSys)
		{
			system = newSys;
			SetAppTitle();
			RemoveCategoryButtons();

			if (system!=null)
			{
				AddCategoryButtons(system.Categories);
			}
		}

		private void OnArmyChanged(Army oldArmy, Army newArmy)
		{
			loadedArmyPath = null;
			SetAppTitle();
			SetArmyTree(newArmy);

			if (oldArmy != null)
			{
				oldArmy.UnitAdded -= UnitAddedMethod;
				oldArmy.UnitRemoved -= UnitRemovedMethod;
				oldArmy.PointsValueChanged -= PointsValueChangedMethod;
				oldArmy.FailedRequirement -= FailedUnitRequirementMethod;
			}

			unitToWidgetMap.Clear();

			while (unitsNotebook.NPages > 0)
			{
				unitsNotebook.RemovePage(0);
			}

			if (newArmy == null)
			{
				DisableCategoryButtons();
			}
			else
			{
				newArmy.UnitAdded += UnitAddedMethod;
				newArmy.UnitRemoved += UnitRemovedMethod;
				newArmy.PointsValueChanged += PointsValueChangedMethod;
				newArmy.FailedRequirement += FailedUnitRequirementMethod;
				//TODO: Clear all buttons
				EnableCategoryButtons();

				if (newArmy.Race.HasCategoryOverrides())
				{
					RemoveCategoryButtons();
					AddCategoryButtons(newArmy.Race.Categories);
				}
			}

			bool nonNullNewArmy = (newArmy != null);
			miCloseArmy.Sensitive = nonNullNewArmy;
			miSaveArmyAs.Sensitive = nonNullNewArmy;
			miExportArmy.Sensitive = nonNullNewArmy;
			hpaned2.Visible = nonNullNewArmy;
			loadedArmyPath = null;
			//New army has no changes, so we can't save it
			miSaveArmy.Sensitive = false;
			saveArmyButton.Sensitive = false;

			CommandStack.Reset();
			SetPointsPanelText();
		}

		private void SetArmyTree(Army army)
		{
			logger.Debug("Resetting tree");
			TreeStore store = (TreeStore)treeUnits.Model;
			store.Clear();
			TreeIter iter;

			if (army!=null)
			{
				logger.Debug("Loading in categories to tree");

				foreach (ArmyCategory cat in army.Categories)
				{
					logger.DebugFormat("Append category {0}", cat.Name);
					iter = store.AppendValues(cat);

					foreach (WFObjects.Unit unit in cat.GetUnits())
					{
						store.AppendValues(iter, unit);
					}
				}

				logger.Debug("Finished loading tree categories");
			}
		}

		private void DisableCategoryButtons()
		{
			SetCategoryButtonsSensitive(false);
		}

		private void EnableCategoryButtons()
		{
			SetCategoryButtonsSensitive(true);
		}

		private void SetCategoryButtonsSensitive(bool state)
		{
			int toolbarButtonCount = toolbar.Children.Length - 1;
			logger.Debug("Last button index: "+toolbarButtonCount);

			for (int i = toolbarButtonCount; i > CATEGORY_BUTTON_SEPARATOR_INDEX; i--)
			{
				logger.DebugFormat("Setting button {0} state to {1}", i, state);
				toolbar.Children[i].Sensitive = state;
			}
		}

		private void RemoveCategoryButtons()
		{
			int toolbarButtonCount = toolbar.Children.Length - 1;

			for (int i = toolbarButtonCount; i > CATEGORY_BUTTON_SEPARATOR_INDEX; i--)
			{
				toolbar.Remove(toolbar.Children[i]);
			}

			categoryMap.Clear();
		}

		private void AddCategoryButtons(Category[] cats)
		{
			if (cats!=null && cats.Length > 0)
			{
				logger.DebugFormat("Toolbar button count: {0}. Adding {1} categories.", toolbar.Children.Length, cats.Length);

				foreach (Category cat in cats)
				{
					ToolButton button = new ToolButton("gtk-add");
					button.Label = cat.Name;
					button.TooltipText = "Add unit from "+cat.Name;
					//TODO: See if we can associate data in some way, the same as we can with SWF. For now we just use the map.
					categoryMap.Add(button, cat);
					button.Clicked+= new System.EventHandler(OnAddUnitActivated);
					toolbar.Insert(button, -1);
				}
			}

			toolbar.Children[CATEGORY_BUTTON_SEPARATOR_INDEX].Visible = cats!=null && cats.Length>0;

			toolbar.ShowAll();
		}

		private void SetPointsPanelText()
		{
			//TODO: Set the points value in the status bar
		}

		private void commandStack_CommandStackUpdated()
		{
			undoMenuButton.Sensitive = commandStack.CanUndo();
			miUndo.Sensitive = undoMenuButton.Sensitive;
			redoMenuButton.Sensitive = commandStack.CanRedo();
			miRedo.Sensitive = redoMenuButton.Sensitive;
			int redoLength = commandStack.RedoLength;

			int maxRedo = Math.Min(10, redoLength);
			
			//TODO: Add tooltips
			if (redoLength > 0)
			{
				Menu menu = new Menu();
				Command com;
				MenuItem mi;

				for (int i = 0; i < maxRedo; i++)
				{
					com = commandStack.PeekRedoCommand(i + 1);

					if (com == null)
					{
						break;
					}

					mi = new MenuItem(com.Description);
					mi.Activated += new EventHandler(RedoMenuActivated);
					menu.Append(mi);
				}
				
				menu.ShowAll();
				redoMenuButton.Menu = menu;
			}
			else
			{
				redoMenuButton.Menu = null;
			}
			
			int undoLength = commandStack.UndoLength;
			int maxUndo = Math.Min(10, undoLength);

			if (undoLength > 0)
			{
				Menu menu = new Menu();
				Command com;
				MenuItem mi;

				for (int i = 0; i < maxUndo; i++)
				{
					com = commandStack.PeekUndoCommand(i + 1);

					if (com == null)
					{
						break;
					}

					mi = new MenuItem(com.UndoDescription);
					mi.Activated += new EventHandler(UndoMenuActivated);
					menu.Add(mi);
				}
				
				menu.ShowAll();
				undoMenuButton.Menu = menu;
			}
			else
			{
				undoMenuButton.Menu = null;
			}

			saveArmyButton.Sensitive = commandStack.IsDirty() && WarFoundryCore.CurrentArmy != null;
			miSaveArmy.Sensitive = commandStack.IsDirty() && WarFoundryCore.CurrentArmy != null;
		}
		
		private void RedoMenuActivated(object sender, EventArgs e)
		{
			if (sender is MenuItem)
			{
				MenuItem item = (MenuItem)sender;
				//we know it's an redo menu item so find it's index and redo everything
				
				int max = Arrays.IndexOf(((Menu)redoMenuButton.Menu).Children, item);
				
				if (max >= 0)
				{
					for (int i = 0; i <= max; i++)
					{
						commandStack.Redo();
					}
				}
			}
		}

		private void UndoMenuActivated(object sender, EventArgs e)
		{
			if (sender is MenuItem)
			{
				
				MenuItem item = (MenuItem)sender;
				//we know it's an undo menu item so find it's index and undo everything
				
				int max = Arrays.IndexOf(((Menu)undoMenuButton.Menu).Children, item);
				
				if (max >= 0)
				{
					for (int i = 0; i <= max; i++)
					{
						commandStack.Undo();
					}
				}
			}
		}

		private bool SaveCurrentArmyOrSaveAs()
		{
			if (loadedArmyPath != null)
			{
				return SaveCurrentArmy();
			}
			else
			{
				return SaveCurrentArmyAs();
			}
		}

		private bool OpenArmy()
		{
			FileChooserDialog fileDialog = new FileChooserDialog("Open army", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.army");
			filter.Name = "WarFoundry Army files (*.army)";
			fileDialog.AddFilter(filter);
			int response = fileDialog.Run();
			string filePath = null;
			
			if (response == (int)ResponseType.Accept)
			{
				filePath = fileDialog.Filename;
			}
			
			fileDialog.Hide();			
			fileDialog.Dispose();
			
			bool success = false;

			if (filePath != null)
			{
				FileInfo file = new FileInfo(filePath);
				Army army = WarFoundryLoader.GetDefault().LoadArmy(file);

				if (army != null)
				{
					logger.Debug("Loaded army " + army.ID);
					success = true;
					WarFoundryCore.CurrentArmy = army;
					loadedArmyPath = filePath;
					logger.Debug("Army loading complete");
				}
				else
				{
					logger.ErrorFormat("Failed to load {0} as an army file", filePath);
					MessageDialog dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, file.Name + " could not be loaded.\n\nIf the file is an army file then please check your file loaders.");
					dialog.Title = "Failed to open army";
					dialog.Run();
					dialog.Hide();
					dialog.Dispose();
				}
			}
			else
			{
				logger.Debug("Army open requested but cancelled");
			}	
			
			return success;
		}

		private bool SaveCurrentArmy()
		{
			bool success = false;

			if (loadedArmyPath!=null)
			{
				success = SaveArmyToPath(WarFoundryCore.CurrentArmy, loadedArmyPath);
			}

			return success;
		}

		private bool SaveCurrentArmyAs()
		{
			FileChooserDialog fileDialog = new FileChooserDialog("Save file as", this, FileChooserAction.Save, "Cancel", ResponseType.Cancel, "Save", ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.army");
			filter.Name = "WarFoundry Army files (*.army)";
			fileDialog.AddFilter(filter);
			int response = fileDialog.Run();
			string filePath = null;
			
			if (response == (int)ResponseType.Accept)
			{
				filePath = fileDialog.Filename;
			}
			
			fileDialog.Hide();			
			fileDialog.Dispose();
			
			return SaveArmyToPath(WarFoundryCore.CurrentArmy, filePath);
		}
		
		private bool SaveArmyToPath(Army army, string filePath)
		{
			bool success = false;
			
			if (filePath!=null)
			{
				if (WarFoundrySaver.GetSaver().Save(WarFoundryCore.CurrentArmy, filePath))
				{
					miSaveArmy.Sensitive = false;
					saveArmyButton.Sensitive = false;
					CommandStack.setCleanMark();
					loadedArmyPath = filePath;
					success = true;
				}
				else
				{
					MessageDialog dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "Failed to save file to "+filePath);
					dialog.Title = "Army save failed";
					dialog.Run();
					dialog.Hide();
					dialog.Dispose();
				}	
			}
			//else user cancelled
			
			return success;
		}

		private bool CloseCurrentArmy()
		{
			if (WarFoundryCore.CurrentArmy != null)
			{
				bool canClose = false;

				if (CommandStack.IsDirty())
				{
					MessageDialog dia = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.YesNo, "The army \"" + WarFoundryCore.CurrentArmy.Name + "\" has been modified.\r\nSave changes before closing army?");
					dia.AddButton("Cancel", ResponseType.Cancel);
					ResponseType dr = (ResponseType)dia.Run();
					dia.Hide();
					dia.Dispose();

					if (dr == ResponseType.Yes)
					{
						//They want to save so try to save it or prompt for save as
						//If they cancel the save as then assume they don't want to close
						canClose = SaveCurrentArmyOrSaveAs();
					}
					else if (dr == ResponseType.No)
					{
						//They don't care about their changes
						canClose = true;
					}
					else
					{
						//Assume cancel or close with the X button
						canClose = false;
					}
				}
				else
				{
					//Nothing has changed so we can safely close
					canClose = true;
				}

				if (canClose)
				{
					//do close
					WarFoundryCore.CurrentArmy = null;
					return true;
				}
				else
				{
					return false;
				}
			}
			else
			{
				//pretend we succeeded
				return true;
			}
		}

		private void CreateNewArmy()
		{
			logger.Debug("Create new army");
			FrmNewArmy newArmy = new FrmNewArmy(WarFoundryCore.CurrentGameSystem);
			ResponseType type = (ResponseType)newArmy.Run();
			newArmy.Hide();

			if (type == ResponseType.Ok)
			{
				if (CloseCurrentArmy())
				{
					WarFoundryCore.CurrentArmy = new Army(newArmy.SelectedRace, newArmy.ArmyName, newArmy.ArmySize);
				}
			}
			else
			{
				logger.Debug("Create new army cancelled");
			}

			newArmy.Destroy();
		}

		protected virtual void undoTBButtonActivated (object sender, System.EventArgs e)
		{
			CommandStack.Undo();
		}

		protected virtual void redoTBButtonActivated (object sender, System.EventArgs e)
		{
			CommandStack.Redo();
		}

		protected virtual void saveTBButtonActivated (object sender, System.EventArgs e)
		{
			SaveCurrentArmyOrSaveAs();
		}

		protected virtual void openTBButtonActivated (object sender, System.EventArgs e)
		{
			OpenArmy();
		}

		protected virtual void newTBButtonActivated (object sender, System.EventArgs e)
		{
			CreateNewArmy();
		}

		protected virtual void ArmyRowActivated(object o, Gtk.RowActivatedArgs args)
		{
			object obj = TreeUtils.GetItemAtPath(treeUnits, args.Path);

			if (obj is WFObjects.Unit)
			{
				WFObjects.Unit unit = (WFObjects.Unit)obj;
				ShowUnitWidget(unit);
			}
		}
		
		private void ShowUnitWidget(WFObjects.Unit unit)
		{
			UnitDisplayWidget widget;
			unitToWidgetMap.TryGetValue(unit, out widget);

			if (widget != null)
			{
				logger.DebugFormat("Selecting existing page for " + unit.Name);
				unitsNotebook.Page = unitsNotebook.PageNum(widget);
			}
			else
			{
				widget = new UnitDisplayWidget(unit, CommandStack);
				logger.Debug("Adding page for " + unit.Name);
				unitToWidgetMap[unit] = widget;
				int pageNum = NotebookUtil.AddPageToNotebookWithCloseButton(unitsNotebook, widget, unit.Name);
				logger.Debug("Page added at index " + pageNum);
				unitsNotebook.ShowAll();
				unitsNotebook.CurrentPage = pageNum;
				unitsNotebook.SetTabReorderable(widget, true);
			}
		}

		protected virtual void OnMiExportAsBasicHtmlActivated (object sender, System.EventArgs e)
		{
			FileChooserDialog fileDialog = new FileChooserDialog("Export army", this, FileChooserAction.Save, "Cancel", ResponseType.Cancel, "Export", ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.html");
			filter.Name = "HTML pages (*.html)";
			fileDialog.AddFilter(filter);
			int response = fileDialog.Run();
			string filePath = null;
			
			if (response == (int)ResponseType.Accept)
			{
				filePath = fileDialog.Filename;
			}
			
			fileDialog.Hide();			
			fileDialog.Dispose();

			if (filePath != null)
			{
				Army army = WarFoundryCore.CurrentArmy;
				logger.DebugFormat("Exporting {0} to {1} as basic HTML", army.Name, filePath);
				WarFoundryHtmlExporter.GetDefault().ExportArmy(army, filePath);
			}
		}

		protected virtual void OnTreeUnitsPopupMenu (object o, Gtk.PopupMenuArgs args)
		{
			object selectedItem = TreeUtils.GetSelectedItem(treeUnits);

			if (selectedItem is WFObjects.Unit)
			{
				Menu menu = new Menu();
				ImageMenuItem delete = new ImageMenuItem("Remove unit");
				delete.Image = new Gtk.Image(Stock.Delete, IconSize.Menu);
				delete.Activated+= new EventHandler(OnUnitDelete);
				delete.Data["unit"] = selectedItem;
				menu.Append(delete);
				menu.ShowAll();
				menu.Popup();
			}
		}

		private void OnUnitDelete(object o, EventArgs args)
		{
			RemoveUnitCommand command = new RemoveUnitCommand((WFObjects.Unit)((ImageMenuItem)o).Data["unit"]);
			commandStack.Execute(command);
		}

		[GLib.ConnectBefore]
		protected virtual void UnitTreeButtonPressed(object o, Gtk.ButtonPressEventArgs args)
		{
			TreePath path;
			treeUnits.GetPathAtPos((int)args.Event.X, (int)args.Event.Y, out path);
			
			if (!treeUnits.Selection.PathIsSelected(path))
			{
				treeUnits.Selection.SelectPath(path);
			}
			
			if (args.Event.Type == Gdk.EventType.ButtonPress && args.Event.Button == 3)
			{
				OnTreeUnitsPopupMenu(o, null);
			}
		}
		
		protected virtual void NotebookPageRemoved(object o, Gtk.RemovedArgs args)
		{
			Widget widget = args.Widget;
			
			if (widget is UnitDisplayWidget)
			{
				unitToWidgetMap.Remove(((UnitDisplayWidget)widget).Unit);
			}
		}
		
		
	}
}