view FrmMainWindow.cs @ 159:89e2442bbb60

Re #417: Improve install/first use experience * Handle errors in GTK# interface
author IBBoard <dev@ibboard.co.uk>
date Sat, 19 May 2012 20:26:44 +0100
parents 6b4cc1fc3f42
children 354c1d2ad086
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.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using GLib;
using Gtk;
using IBBoard;
using IBBoard.Commands;
using IBBoard.CustomMath;
using IBBoard.GtkSharp;
using IBBoard.GtkSharp.Translatable;
using IBBoard.IO;
using IBBoard.Lang;
using IBBoard.Logging;
using IBBoard.WarFoundry.API;
using IBBoard.WarFoundry.API.Commands;
using IBBoard.WarFoundry.API.Exporters;
using IBBoard.WarFoundry.API.Factories;
using IBBoard.WarFoundry.API.Factories.Xml;
using IBBoard.WarFoundry.API.Objects;
using IBBoard.WarFoundry.API.Savers;
using IBBoard.WarFoundry.GUI.GTK.Widgets;
using IBBoard.Xml;
using log4net;
using WFObjects = IBBoard.WarFoundry.API.Objects;
using IBBoard.WarFoundry.API.Savers.Xml;
using IBBoard.WarFoundry.API.Objects.Requirement;
using System.Text;

namespace IBBoard.WarFoundry.GUI.GTK
{
	public partial class FrmMainWindow: TranslatableWindowWithActions
	{
		private static readonly string VALIDATION_RESULTS_KEY = "WarFoundryValidationFailureMessages";
		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<WFObjects.Unit, UnitDisplayWidget> unitToWidgetMap = new Dictionary<WFObjects.Unit,UnitDisplayWidget>();
		private ObjectAddDelegate UnitAddedMethod;
		private ObjectRemoveDelegate UnitRemovedMethod;
		private DoubleValChangedDelegate PointsValueChangedMethod;
		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();
			dialog.Hide();
			dialog.Dispose();
		}

		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.Name = "bttnRedo";
			redoMenuButton.Clicked += redoTBButtonActivated;
			redoMenuButton.Sensitive = false;
			toolbar.Insert(redoMenuButton, CATEGORY_BUTTON_SEPARATOR_INDEX);
			undoMenuButton = new MenuToolButton("gtk-undo");
			undoMenuButton.Name = "bttnUndo";
			undoMenuButton.Clicked += undoTBButtonActivated;
			undoMenuButton.Sensitive = false;
			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();

			TreeViewColumn mainColumn = new TreeViewColumn();
			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("WarFoundry-GTK");
			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);
			Translation.TranslationChanged += Retranslate;
			Translate();
			UnitAddedMethod = new ObjectAddDelegate(OnUnitAdded);
			UnitRemovedMethod = new ObjectRemoveDelegate(OnUnitRemoved);
			PointsValueChangedMethod = new DoubleValChangedDelegate(OnPointsValueChanged);
			UnitNameChangedMethod = new StringValChangedDelegate(OnUnitNameChanged);
			WarFoundryLoader.GetDefault().FileLoadingFinished += FileLoadingFinished;
			SetStatusBarText();

			LoadFilesFromArgs(args);
		}

		private void LoadFilesFromArgs(string[] args)
		{
			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);
					}
				}
			}
		}

		protected override void Translate()
		{
			base.Translate();
			SetAppTitle();
			treeUnits.GetColumn(0).Title = Translation.GetTranslation("armyCategoryColumnTitle", "categories");
			RebuildUndoRedoMenus();
		}

		private void Retranslate()
		{
			Translate();
		}

		public override void Dispose()
		{
			Translation.TranslationChanged -= Retranslate;
			base.Dispose();
		}

		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}{2}", c.Name, c.Points, WarFoundryCore.CurrentGameSystem.GetPointsAbbrev(c.Points));
				}
				else
				{
					name = Translation.GetTranslation("categoryTreeCatNamePercentage", "{0} - {1}{3} ({2}%)", c.Name, c.Points, (c.ParentArmy.Points > 0 ? Math.Round((c.Points / c.ParentArmy.Points) * 100) : 0), WarFoundryCore.CurrentGameSystem.GetPointsAbbrev(c.Points));
				}

				(cell as CellRendererText).Text = name;
			}
			else
			{
				if (o is WFObjects.Unit)
				{
					WFObjects.Unit u = (WFObjects.Unit)o;
					string name = Translation.GetTranslation("unitTreeCatName", "{0} - {1}{2}", u.Name, u.Points, WarFoundryCore.CurrentGameSystem.GetPointsAbbrev(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;
			logger.DebugFormat("Unit name changed for {0} - now called {1}", unit.ID, unit.Name);
			UnitDisplayWidget widget;
			unitToWidgetMap.TryGetValue(unit, out widget);
			
			if (widget != null)
			{
				unitsNotebook.SetTabLabel(widget, NotebookUtil.CreateNotebookTabLabelWithClose(unitsNotebook, widget, unit.Name));
			}
			
			treeUnits.QueueDraw();
		}

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

		private void HandleUnitPointsValueChanged(WarFoundryObject obj, double oldValue, double newValue)
		{			
			treeUnits.QueueDraw();
		}

		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)
		{
			SetStatusBarText();
		}

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

		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)
			{
				ToolButton toolButton = (ToolButton)sender;
				Category cat = (Category)toolButton.Data["Category"];

				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);
				logger.DebugFormat("Game system set to {0} with ID {1}", system.Name, system.ID);
			}
			else
			{
				logger.Debug("Game system set to null");
			}
		}

		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.NameChanged -= OnArmyNameChanged;
				oldArmy.MaxPointsValueChanged -= OnMaxPointsValueChanged;
				oldArmy.ArmyCompositionChanged -= HandleArmyCompositionChanged;
			}

			unitToWidgetMap.Clear();

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

			if (newArmy == null)
			{
				DisableCategoryButtons();
			}
			else
			{
				newArmy.UnitAdded += UnitAddedMethod;
				newArmy.UnitRemoved += UnitRemovedMethod;
				newArmy.PointsValueChanged += PointsValueChangedMethod;
				newArmy.NameChanged += OnArmyNameChanged;
				newArmy.MaxPointsValueChanged += OnMaxPointsValueChanged;
				newArmy.ArmyCompositionChanged += HandleArmyCompositionChanged;
				//TODO: Clear all buttons
				EnableCategoryButtons();

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

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

			CommandStack.Reset();
			SetStatusBarText();
		}

		private void OnArmyNameChanged(WarFoundryObject obj, string oldValue, string newValue)
		{
			SetAppTitle();
		}

		private void OnMaxPointsValueChanged(WarFoundryObject obj, int oldValue, int newValue)
		{
			SetStatusBarText();
		}

		private void HandleArmyCompositionChanged()
		{
			SetStatusBarText();
		}

		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]);
			}
		}

		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 = Translation.GetTranslation("bttnCreateFromCat", "{0}", cat.Name);
					button.Data["Category"] = 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 SetStatusBarText()
		{
			if (WarFoundryCore.CurrentArmy != null)
			{
				statusbar.Push(1, GetPointsText());
				SetValidationText();
			}
			else
			{
				statusbar.Push(1, "");
				SetBlankValidationText();
			}
		}

		private string GetPointsText()
		{
			int maxPts = WarFoundryCore.CurrentArmy.MaxPoints;
			string maxPtsAbbrev = WarFoundryCore.CurrentGameSystem.GetPointsAbbrev(WarFoundryCore.CurrentArmy.MaxPoints);
			double points = WarFoundryCore.CurrentArmy.Points;
			string ptsAbbrev = WarFoundryCore.CurrentGameSystem.GetPointsAbbrev(WarFoundryCore.CurrentArmy.Points);
			return Translation.GetTranslation("statusPanelPoints", "{0}{2} of {1}{3}", points, maxPts, ptsAbbrev, maxPtsAbbrev);
		}

		private void SetValidationText()
		{
			if (WarFoundryCore.CurrentGameSystem != null && WarFoundryCore.CurrentGameSystem.WarnOnError)
			{
				SetValidationTextAndColour();
			}
			else
			{
				SetBlankValidationText();
			}
		}

		private void SetValidationTextAndColour()
		{
			ICollection<string> failureMessages;
			Validation result = RequirementHandler.ValidateArmy(WarFoundryCore.CurrentArmy, out failureMessages);
			string pluralHack = (failureMessages.Count == 1 ? "" : "s");
			lblValidationWarning.Text = String.Format("{0} validation warning{1}", failureMessages.Count, pluralHack);
			lblValidationWarning.Data[VALIDATION_RESULTS_KEY] = failureMessages;
			
			if (Validates.AsOkay(result))
			{
				lblValidationWarning.ModifyFg(StateType.Normal, Gdk.Color.Zero);
			}
			else
			{
				Gdk.Color red = Gdk.Color.Zero;
				Gdk.Color.Parse("#cc0000", ref red);
				lblValidationWarning.ModifyFg(StateType.Normal, red);
			}
		}

		private void SetBlankValidationText()
		{
			lblValidationWarning.Text = "";
			lblValidationWarning.Data[VALIDATION_RESULTS_KEY] = new string[0];
			lblValidationWarning.ModifyFg(StateType.Normal, Gdk.Color.Zero);
		}

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

			RebuildUndoRedoMenus();

			bttnSaveArmy.Sensitive = commandStack.IsDirty() && WarFoundryCore.CurrentArmy != null;
			miSaveArmy.Sensitive = commandStack.IsDirty() && WarFoundryCore.CurrentArmy != null;
		}

		private void RebuildUndoRedoMenus()
		{
			int redoLength = commandStack.RedoLength;
			int maxRedo = Math.Min(10, redoLength);
			
			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;
			}
		}

		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()
		{
			string title = Translation.GetTranslation("openArmyDialog", "open army");
			string cancelText = Translation.GetTranslation("bttnCancel", "cancel");
			string openText = Translation.GetTranslation("bttnOpen", "open");
			FileChooserDialog fileDialog = new FileChooserDialog(title, this, FileChooserAction.Open, cancelText, ResponseType.Cancel, openText, ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.army");
			filter.Name = Translation.GetTranslation("armyFileFilter", "WarFoundry .army files");
			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 = null;

				try
				{
					army = WarFoundryLoader.GetDefault().LoadArmy(file);
				}
				catch (Exception ex)
				{
					logger.Error("Error while loading army file " + filePath, ex);
				}

				if (army != null)
				{
					logger.Debug("Loaded army " + army.ID);
					success = true;
					WarFoundryCore.CurrentArmy = army;
					loadedArmyPath = filePath;
					logger.Debug("Army loading complete");
				}
				else
				{
					MessageDialog dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Translation.GetTranslation("OpenFailed", "File open failed. Please check log file"));
					dialog.Title = Translation.GetTranslation("OpenFailedTitle", "File open failed");
					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()
		{
			string title = Translation.GetTranslation("saveArmyDialog", "save army");
			string cancelText = Translation.GetTranslation("bttnCancel", "cancel");
			string saveText = Translation.GetTranslation("bttnSave", "save");
			FileChooserDialog fileDialog = new FileChooserDialog(title, this, FileChooserAction.Save, cancelText, ResponseType.Cancel, saveText, ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.army");
			filter.Name = Translation.GetTranslation("armyFileFilter", "WarFoundry .army files");
			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)
			{
				bool saveSuccess = false;

				try
				{
					saveSuccess = WarFoundrySaver.GetSaver().Save(filePath, WarFoundryCore.CurrentArmy);
				}
				catch (Exception ex)
				{
					logger.Error("Error while saving army file to " + filePath, ex);
				}

				if (saveSuccess)
				{
					miSaveArmy.Sensitive = false;
					bttnSaveArmy.Sensitive = false;
					CommandStack.setCleanMark();
					loadedArmyPath = filePath;
					success = true;
				}
				else
				{
					MessageDialog dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, Translation.GetTranslation("SaveFailed", "File save failed. Please check log file"));
					dialog.Title = Translation.GetTranslation("SaveFailedTitle", "File 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())
				{
					string message = Translation.GetTranslation("SaveChangesQuestion", "Save changes to army before closing?", WarFoundryCore.CurrentArmy.Name);
					MessageDialog dia = new MessageDialog(this, DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, message);
					dia.AddButton(Translation.GetTranslation("bttnDiscard", "lose changes"), ResponseType.No);
					Button button = (Button)dia.AddButton(Translation.GetTranslation("bttnCancel", "cancel"), ResponseType.Cancel);
					button.Image = new Image("gtk-cancel", IconSize.Button);
					button = (Button)dia.AddButton(Translation.GetTranslation("bttnSave", "save"), ResponseType.Yes);
					button.Image = new Image("gtk-save", IconSize.Button);
					dia.Title = Translation.GetTranslation("SaveChangesTitle", "Save changes?");
					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)
		{
			string exportArmyTitle = Translation.GetTranslation("exportBasicHtmlDialogTitle", "export army");
			string cancelText = Translation.GetTranslation("exportBasicHtmlCancel", "cancel");
			string exportText = Translation.GetTranslation("exportBasicHtmlExport", "export");
			FileChooserDialog fileDialog = new FileChooserDialog(exportArmyTitle, this, FileChooserAction.Save, cancelText, ResponseType.Cancel, exportText, ResponseType.Accept);
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.html");
			filter.Name = Translation.GetTranslation("exportBasicHtmlHtmlFilter", "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(Translation.GetTranslation("menuRemoveUnit", "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);
			}
		}

		protected virtual void HelpAboutActivated(object sender, System.EventArgs e)
		{
			FrmAbout form = FrmAbout.GetForm();
			form.Run();
			form.Hide();
		}

		protected virtual void miPreferencesClicked(object sender, System.EventArgs e)
		{
			FrmPreferences form = new FrmPreferences(Preferences);
			form.Run();
			form.Hide();
		}

		public override ICollection<Gtk.Action> Actions
		{
			get
			{
				List<Gtk.Action> actions = new List<Gtk.Action>();

				foreach (ActionGroup actionGroup in this.UIManager.ActionGroups)
				{
					actions.AddRange(actionGroup.ListActions());
				}

				return actions;
			}
		}

		protected void OnTransformedXmlActionActivated(object sender, System.EventArgs e)
		{
			FrmExportXml form = new FrmExportXml(WarFoundryCore.CurrentArmy);
			form.ParentWindow = this.GdkWindow;
			form.Run();
			form.Hide();
			form.Dispose();
		}

		protected void OnMiEditArmyActivated(object sender, System.EventArgs e)
		{
			FrmEditArmy form = new FrmEditArmy(commandStack, WarFoundryCore.CurrentArmy);
			form.TransientFor = this;
			form.WindowPosition = WindowPosition.CenterOnParent;
			form.Run();
			form.Hide();
			form.Dispose();
		}

		protected void OnLblValidationWarningButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args)
		{
			if (args.Event.Button != (uint)MouseButton.Left)
			{
				return;
			}

			ICollection<string> failureMessages = lblValidationWarning.Data[VALIDATION_RESULTS_KEY] as ICollection<string>;

			if (failureMessages != null && failureMessages.Count > 0)
			{
				ValidationFailureWidget failureWidget = new ValidationFailureWidget(failureMessages);
				Dialog dialog = new SimpleDialog("Validation errors", this, DialogFlags.Modal, failureWidget);
				dialog.Run();
				dialog.Hide();
				dialog.Dispose();
			}
		}

		protected void OnAddNewFileActionActivated(object sender, EventArgs e)
		{
			string cancelText = Translation.GetTranslation("bttnCancel", "cancel");
			string openText = Translation.GetTranslation("bttnOpen", "open");
			FileChooserDialog fileDialog = new FileChooserDialog("Add data file", this, FileChooserAction.Open, cancelText, ResponseType.Cancel, openText, ResponseType.Accept);
			fileDialog.SelectMultiple = true;
			FileFilter filter = new FileFilter();
			filter.AddPattern("*.race");
			filter.AddPattern("*.system");
			filter.Name = "WarFoundry data files";
			fileDialog.AddFilter(filter);
			int response = fileDialog.Run();
			string[] filePaths = fileDialog.Filenames;
			fileDialog.Hide();
			fileDialog.Dispose();

			if (response == (int)ResponseType.Accept)
			{
				List<Exception> exceptions = new List<Exception>();

				foreach (string filePath in filePaths)
				{
					try
					{
						WarFoundryLoader.AddNewDataFile(filePath);
					}
					catch (Exception ex)
					{
						exceptions.Add(ex);
					}
				}

				WarFoundryLoader.GetDefault().LoadFiles();

				if (exceptions.Count > 0)
				{
					string errorMsg = MakeAddDataFileFailedMessage(exceptions);
					MessageDialog dialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, false, errorMsg);
					dialog.Run();
					dialog.Hide();
					dialog.Dispose();
				}
			}
		}

		private string MakeAddDataFileFailedMessage(List<Exception> exceptions)
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("errors occurred while adding the new data files: ");

			foreach (Exception ex in exceptions)
			{
				sb.Append("\n\t• ");
				sb.Append(ex.Message);
			}

			return sb.ToString();
		}
	}
}