Mercurial > repos > IBDev-IBBoard.WarFoundry.GUI.GTK
changeset 61:e7ad676a7344
Re #60: Add UI to add/remove/edit weapons in GTK
* Add events to track equipment amount and amount changing
* Select most appropriate equipment type by default
* Add methods an implementation for enabling/disabling Okay button
author | IBBoard <dev@ibboard.co.uk> |
---|---|
date | Sun, 29 Aug 2010 15:09:34 +0000 |
parents | 04c0f6a7625c |
children | f733073967a2 |
files | FrmAddEquipment.cs UIControl/AddEquipmentUIControl.cs UIControl/Interfaces/IAddEquipmentUI.cs gtk-gui/IBBoard.WarFoundry.GTK.Widgets.UnitDisplayWidget.cs gtk-gui/IBBoard.WarFoundry.GUI.GTK.FrmAddEquipment.cs gtk-gui/gui.stetic gtk-gui/objects.xml |
diffstat | 7 files changed, 198 insertions(+), 87 deletions(-) [+] |
line wrap: on
line diff
--- a/FrmAddEquipment.cs Fri Aug 27 14:44:48 2010 +0000 +++ b/FrmAddEquipment.cs Sun Aug 29 15:09:34 2010 +0000 @@ -16,6 +16,8 @@ private static ILog log = LogManager.GetLogger(typeof(FrmAddEquipment)); public event SingleArgMethodInvoker<UnitEquipmentItem> UnitEquipmentItemChoiceChanged; + public event MethodInvoker UnitEquipmentAmountTypeChanged; + public event MethodInvoker UnitEquipmentAmountChanged; private bool limitsEnabled = false; private bool ratioLimited = false; @@ -29,10 +31,26 @@ CellRendererText equipCell = new CellRendererText(); equipColumn.PackStart(equipCell, true); equipColumn.SetCellDataFunc(equipCell, GtkWarFoundryUtil.RenderWarFoundryObjectName); - lstEquipment.AppendColumn(equipColumn); + lstEquipment.AppendColumn(equipColumn); } - protected virtual void OnSelectionChanged(object o, EventArgs e) + private void OnUnitEquipmentAmountChanged() + { + if (UnitEquipmentAmountChanged != null) + { + UnitEquipmentAmountChanged(); + } + } + + private void OnUnitEquipmentAmountTypeChanged() + { + if (UnitEquipmentAmountChanged != null) + { + UnitEquipmentAmountTypeChanged(); + } + } + + protected void OnSelectionChanged(object o, EventArgs e) { if (UnitEquipmentItemChoiceChanged!=null) { @@ -56,9 +74,26 @@ { log.DebugFormat("IsRatio? {0}. Limits: {1}->{2}, {3}%->{4}%", isRatioLimit, minNumber, maxNumber, minPercent, maxPercent); ratioLimited = isRatioLimit; - SetEnabledState(); numericAmount.SetRange(minNumber, maxNumber); percentageAmount.SetRange(minPercent, maxPercent); + + if (isRatioLimit) + { + if (minPercent == 100) + { + rbEquipAll.Active = true; + } + else + { + rbEquipPercent.Active = true; + } + } + else + { + rbEquipNumeric.Active = true; + } + + SetEnabledState(); } public void SetUnitEquipmentLimitsEnabled(bool isEnabled) @@ -75,8 +110,8 @@ rbEquipPercent.Sensitive = limitsEnabled && minPercentage != 100; percentageAmount.Sensitive = limitsEnabled && minPercentage != 100; double maxPercentage = GetMaxPercentage(); - rbEquipAll.Sensitive = limitsEnabled && maxPercentage == 100; - lblEquipAll.Sensitive = limitsEnabled && maxPercentage == 100; + rbEquipAll.Sensitive = limitsEnabled && ratioLimited && maxPercentage == 100; + lblEquipAll.Sensitive = limitsEnabled && ratioLimited && maxPercentage == 100; } private double GetMaxPercentage() @@ -113,6 +148,22 @@ Respond(ResponseType.Ok); } + public void SetOkayEnabledState (bool enabled) + { + buttonOk.Sensitive = enabled; + } + + protected virtual void SpinButtonValueChanged (object sender, System.EventArgs e) + { + OnUnitEquipmentAmountChanged(); + } + + protected virtual void RadioButtonClicked(object sender, System.EventArgs e) + { + OnUnitEquipmentAmountTypeChanged(); + } + + public UnitEquipmentItem SelectedUnitEquipmentItem { get
--- a/UIControl/AddEquipmentUIControl.cs Fri Aug 27 14:44:48 2010 +0000 +++ b/UIControl/AddEquipmentUIControl.cs Sun Aug 29 15:09:34 2010 +0000 @@ -28,7 +28,15 @@ ui.SetUnitEquipmentLimitsEnabled(false); UnitEquipmentItem[] items = Arrays.Subtract(UnitEquipmentUtil.GetAllowedEquipmentItems(unit), unit.GetEquipment()); ui.SetUnitEquipmentItems(items); + ui.SetOkayEnabledState(false); ui.UnitEquipmentItemChoiceChanged += HandleUiUnitEquipmentItemChoiceChanged; + ui.UnitEquipmentAmountChanged += HandleUnitEquipmentAmountChanged; + ui.UnitEquipmentAmountTypeChanged += HandleUnitEquipmentAmountChanged; + } + + private void HandleUnitEquipmentAmountChanged () + { + ui.SetOkayEnabledState(ui.SelectedUnitEquipmentItem != null && HasNonZeroEquipmentAmount()); } //TODO Make abstract @@ -53,14 +61,33 @@ ui.SetUnitEquipmentLimits(equipIsRatioLimit, minPercent, maxPercent, minNumber, maxNumber); ui.SetUnitEquipmentLimitsEnabled(true); + ui.SetOkayEnabledState(HasNonZeroEquipmentAmount()); } else { ui.SetUnitEquipmentLimits(true, 0, 0, 0, 0); ui.SetUnitEquipmentLimitsEnabled(false); + ui.SetOkayEnabledState(false); } } + private bool HasNonZeroEquipmentAmount() + { + bool nonZero; + + if (ui.IsRatioEquipmentAmount) + { + nonZero = (ui.EquipmentPercentageAmount > 0); + } + + else + { + nonZero = (ui.EquipmentNumericAmount > 0); + } + + return nonZero; + } + private double RoundPercentage(double percent) { return Math.Round(percent, 1);
--- a/UIControl/Interfaces/IAddEquipmentUI.cs Fri Aug 27 14:44:48 2010 +0000 +++ b/UIControl/Interfaces/IAddEquipmentUI.cs Sun Aug 29 15:09:34 2010 +0000 @@ -16,6 +16,16 @@ event SingleArgMethodInvoker<UnitEquipmentItem> UnitEquipmentItemChoiceChanged; /// <summary> + /// Occurs when the unit equipment amount type changes (e.g. percentage to numeric) + /// </summary> + event MethodInvoker UnitEquipmentAmountTypeChanged; + + /// <summary> + /// Occurs when the unit equipment amount changes + /// </summary> + event MethodInvoker UnitEquipmentAmountChanged; + + /// <summary> /// Sets the equipment items that should be displayed on the form /// </summary> /// <param name='items'> @@ -90,6 +100,14 @@ /// The number of items taken as a percentage of the unit size. /// </value> double EquipmentPercentageAmount { get; } + + /// <summary> + /// Sets the state of the Okay button. + /// </summary> + /// <param name='enabled'> + /// <code>true</code> to enable the button, else <code>false</code> + /// </param> + void SetOkayEnabledState(bool enabled); } }
--- a/gtk-gui/IBBoard.WarFoundry.GTK.Widgets.UnitDisplayWidget.cs Fri Aug 27 14:44:48 2010 +0000 +++ b/gtk-gui/IBBoard.WarFoundry.GTK.Widgets.UnitDisplayWidget.cs Sun Aug 29 15:09:34 2010 +0000 @@ -18,6 +18,8 @@ private global::Gtk.HSeparator hseparator1; + private global::Gtk.HBox hbox2; + private global::Gtk.Table table1; private global::Gtk.Label equipmentLabel; @@ -104,7 +106,11 @@ w6.Expand = false; w6.Fill = false; // Container child vbox1.Gtk.Box+BoxChild - this.table1 = new global::Gtk.Table(((uint)(2)), ((uint)(3)), false); + this.hbox2 = new global::Gtk.HBox(); + this.hbox2.Name = "hbox2"; + this.hbox2.Spacing = 6; + // Container child hbox2.Gtk.Box+BoxChild + this.table1 = new global::Gtk.Table(((uint)(2)), ((uint)(2)), false); this.table1.Name = "table1"; this.table1.RowSpacing = ((uint)(6)); this.table1.ColumnSpacing = ((uint)(6)); @@ -158,7 +164,10 @@ w12.BottomAttach = ((uint)(2)); w12.XOptions = ((global::Gtk.AttachOptions)(4)); w12.YOptions = ((global::Gtk.AttachOptions)(4)); - // Container child table1.Gtk.Table+TableChild + this.hbox2.Add(this.table1); + global::Gtk.Box.BoxChild w13 = ((global::Gtk.Box.BoxChild)(this.hbox2[this.table1])); + w13.Position = 0; + // Container child hbox2.Gtk.Box+BoxChild this.vbox3 = new global::Gtk.VBox(); this.vbox3.Name = "vbox3"; this.vbox3.Spacing = 6; @@ -169,54 +178,56 @@ this.bttnAddEquipment.UseUnderline = true; this.bttnAddEquipment.Label = global::Mono.Unix.Catalog.GetString("Add"); this.vbox3.Add(this.bttnAddEquipment); - global::Gtk.Box.BoxChild w13 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnAddEquipment])); - w13.Position = 0; - w13.Expand = false; - w13.Fill = false; + global::Gtk.Box.BoxChild w14 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnAddEquipment])); + w14.Position = 0; + w14.Expand = false; + w14.Fill = false; // Container child vbox3.Gtk.Box+BoxChild this.bttnEditEquipment = new global::Gtk.Button(); + this.bttnEditEquipment.Sensitive = false; this.bttnEditEquipment.CanFocus = true; this.bttnEditEquipment.Name = "bttnEditEquipment"; this.bttnEditEquipment.UseUnderline = true; this.bttnEditEquipment.Label = global::Mono.Unix.Catalog.GetString("Edit"); this.vbox3.Add(this.bttnEditEquipment); - global::Gtk.Box.BoxChild w14 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnEditEquipment])); - w14.Position = 1; - w14.Expand = false; - w14.Fill = false; + global::Gtk.Box.BoxChild w15 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnEditEquipment])); + w15.Position = 1; + w15.Expand = false; + w15.Fill = false; // Container child vbox3.Gtk.Box+BoxChild this.bttnReplaceEquipment = new global::Gtk.Button(); + this.bttnReplaceEquipment.Sensitive = false; this.bttnReplaceEquipment.CanFocus = true; this.bttnReplaceEquipment.Name = "bttnReplaceEquipment"; this.bttnReplaceEquipment.UseUnderline = true; this.bttnReplaceEquipment.Label = global::Mono.Unix.Catalog.GetString("Replace"); this.vbox3.Add(this.bttnReplaceEquipment); - global::Gtk.Box.BoxChild w15 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnReplaceEquipment])); - w15.Position = 2; - w15.Expand = false; - w15.Fill = false; + global::Gtk.Box.BoxChild w16 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnReplaceEquipment])); + w16.Position = 2; + w16.Expand = false; + w16.Fill = false; // Container child vbox3.Gtk.Box+BoxChild this.bttnRemoveEquipment = new global::Gtk.Button(); + this.bttnRemoveEquipment.Sensitive = false; this.bttnRemoveEquipment.CanFocus = true; this.bttnRemoveEquipment.Name = "bttnRemoveEquipment"; this.bttnRemoveEquipment.UseUnderline = true; this.bttnRemoveEquipment.Label = global::Mono.Unix.Catalog.GetString("Remove"); this.vbox3.Add(this.bttnRemoveEquipment); - global::Gtk.Box.BoxChild w16 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnRemoveEquipment])); - w16.Position = 3; - w16.Expand = false; - w16.Fill = false; - this.table1.Add(this.vbox3); - global::Gtk.Table.TableChild w17 = ((global::Gtk.Table.TableChild)(this.table1[this.vbox3])); - w17.LeftAttach = ((uint)(2)); - w17.RightAttach = ((uint)(3)); - w17.XOptions = ((global::Gtk.AttachOptions)(4)); - w17.YOptions = ((global::Gtk.AttachOptions)(4)); - this.vbox1.Add(this.table1); - global::Gtk.Box.BoxChild w18 = ((global::Gtk.Box.BoxChild)(this.vbox1[this.table1])); - w18.Position = 3; + global::Gtk.Box.BoxChild w17 = ((global::Gtk.Box.BoxChild)(this.vbox3[this.bttnRemoveEquipment])); + w17.Position = 3; + w17.Expand = false; + w17.Fill = false; + this.hbox2.Add(this.vbox3); + global::Gtk.Box.BoxChild w18 = ((global::Gtk.Box.BoxChild)(this.hbox2[this.vbox3])); + w18.Position = 1; w18.Expand = false; w18.Fill = false; + this.vbox1.Add(this.hbox2); + global::Gtk.Box.BoxChild w19 = ((global::Gtk.Box.BoxChild)(this.vbox1[this.hbox2])); + w19.Position = 3; + w19.Expand = false; + w19.Fill = false; this.Add(this.vbox1); if ((this.Child != null)) {
--- a/gtk-gui/IBBoard.WarFoundry.GUI.GTK.FrmAddEquipment.cs Fri Aug 27 14:44:48 2010 +0000 +++ b/gtk-gui/IBBoard.WarFoundry.GUI.GTK.FrmAddEquipment.cs Sun Aug 29 15:09:34 2010 +0000 @@ -18,16 +18,16 @@ private global::Gtk.Label lblPercent; + private global::Gtk.SpinButton numericAmount; + + private global::Gtk.SpinButton percentageAmount; + private global::Gtk.RadioButton rbEquipAll; private global::Gtk.RadioButton rbEquipNumeric; private global::Gtk.RadioButton rbEquipPercent; - private global::Gtk.SpinButton numericAmount; - - private global::Gtk.SpinButton percentageAmount; - private global::Gtk.Label lblEquipAmount; private global::Gtk.Label lblEquipment; @@ -99,43 +99,6 @@ w5.XOptions = ((global::Gtk.AttachOptions)(4)); w5.YOptions = ((global::Gtk.AttachOptions)(4)); // Container child table2.Gtk.Table+TableChild - this.rbEquipAll = new global::Gtk.RadioButton(""); - this.rbEquipAll.CanFocus = true; - this.rbEquipAll.Name = "rbEquipAll"; - this.rbEquipAll.DrawIndicator = true; - this.rbEquipAll.UseUnderline = true; - this.rbEquipAll.Group = new global::GLib.SList(global::System.IntPtr.Zero); - this.table2.Add(this.rbEquipAll); - global::Gtk.Table.TableChild w6 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipAll])); - w6.TopAttach = ((uint)(2)); - w6.BottomAttach = ((uint)(3)); - w6.XOptions = ((global::Gtk.AttachOptions)(4)); - w6.YOptions = ((global::Gtk.AttachOptions)(4)); - // Container child table2.Gtk.Table+TableChild - this.rbEquipNumeric = new global::Gtk.RadioButton(""); - this.rbEquipNumeric.CanFocus = true; - this.rbEquipNumeric.Name = "rbEquipNumeric"; - this.rbEquipNumeric.DrawIndicator = true; - this.rbEquipNumeric.UseUnderline = true; - this.rbEquipNumeric.Group = this.rbEquipAll.Group; - this.table2.Add(this.rbEquipNumeric); - global::Gtk.Table.TableChild w7 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipNumeric])); - w7.XOptions = ((global::Gtk.AttachOptions)(4)); - w7.YOptions = ((global::Gtk.AttachOptions)(4)); - // Container child table2.Gtk.Table+TableChild - this.rbEquipPercent = new global::Gtk.RadioButton(""); - this.rbEquipPercent.CanFocus = true; - this.rbEquipPercent.Name = "rbEquipPercent"; - this.rbEquipPercent.DrawIndicator = true; - this.rbEquipPercent.UseUnderline = true; - this.rbEquipPercent.Group = this.rbEquipAll.Group; - this.table2.Add(this.rbEquipPercent); - global::Gtk.Table.TableChild w8 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipPercent])); - w8.TopAttach = ((uint)(1)); - w8.BottomAttach = ((uint)(2)); - w8.XOptions = ((global::Gtk.AttachOptions)(4)); - w8.YOptions = ((global::Gtk.AttachOptions)(4)); - // Container child table2.Gtk.Table+TableChild this.numericAmount = new global::Gtk.SpinButton(0, 100, 1); this.numericAmount.CanFocus = true; this.numericAmount.Name = "numericAmount"; @@ -143,11 +106,11 @@ this.numericAmount.ClimbRate = 1; this.numericAmount.Numeric = true; this.table2.Add(this.numericAmount); - global::Gtk.Table.TableChild w9 = ((global::Gtk.Table.TableChild)(this.table2[this.numericAmount])); - w9.LeftAttach = ((uint)(1)); - w9.RightAttach = ((uint)(2)); - w9.XOptions = ((global::Gtk.AttachOptions)(0)); - w9.YOptions = ((global::Gtk.AttachOptions)(4)); + global::Gtk.Table.TableChild w6 = ((global::Gtk.Table.TableChild)(this.table2[this.numericAmount])); + w6.LeftAttach = ((uint)(1)); + w6.RightAttach = ((uint)(2)); + w6.XOptions = ((global::Gtk.AttachOptions)(0)); + w6.YOptions = ((global::Gtk.AttachOptions)(4)); // Container child table2.Gtk.Table+TableChild this.percentageAmount = new global::Gtk.SpinButton(0, 100, 1); this.percentageAmount.CanFocus = true; @@ -157,12 +120,49 @@ this.percentageAmount.Digits = ((uint)(1)); this.percentageAmount.Numeric = true; this.table2.Add(this.percentageAmount); - global::Gtk.Table.TableChild w10 = ((global::Gtk.Table.TableChild)(this.table2[this.percentageAmount])); + global::Gtk.Table.TableChild w7 = ((global::Gtk.Table.TableChild)(this.table2[this.percentageAmount])); + w7.TopAttach = ((uint)(1)); + w7.BottomAttach = ((uint)(2)); + w7.LeftAttach = ((uint)(1)); + w7.RightAttach = ((uint)(2)); + w7.XOptions = ((global::Gtk.AttachOptions)(0)); + w7.YOptions = ((global::Gtk.AttachOptions)(4)); + // Container child table2.Gtk.Table+TableChild + this.rbEquipAll = new global::Gtk.RadioButton(""); + this.rbEquipAll.CanFocus = true; + this.rbEquipAll.Name = "rbEquipAll"; + this.rbEquipAll.DrawIndicator = true; + this.rbEquipAll.UseUnderline = true; + this.rbEquipAll.Group = new global::GLib.SList(global::System.IntPtr.Zero); + this.table2.Add(this.rbEquipAll); + global::Gtk.Table.TableChild w8 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipAll])); + w8.TopAttach = ((uint)(2)); + w8.BottomAttach = ((uint)(3)); + w8.XOptions = ((global::Gtk.AttachOptions)(4)); + w8.YOptions = ((global::Gtk.AttachOptions)(4)); + // Container child table2.Gtk.Table+TableChild + this.rbEquipNumeric = new global::Gtk.RadioButton(""); + this.rbEquipNumeric.CanFocus = true; + this.rbEquipNumeric.Name = "rbEquipNumeric"; + this.rbEquipNumeric.DrawIndicator = true; + this.rbEquipNumeric.UseUnderline = true; + this.rbEquipNumeric.Group = this.rbEquipAll.Group; + this.table2.Add(this.rbEquipNumeric); + global::Gtk.Table.TableChild w9 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipNumeric])); + w9.XOptions = ((global::Gtk.AttachOptions)(4)); + w9.YOptions = ((global::Gtk.AttachOptions)(4)); + // Container child table2.Gtk.Table+TableChild + this.rbEquipPercent = new global::Gtk.RadioButton(""); + this.rbEquipPercent.CanFocus = true; + this.rbEquipPercent.Name = "rbEquipPercent"; + this.rbEquipPercent.DrawIndicator = true; + this.rbEquipPercent.UseUnderline = true; + this.rbEquipPercent.Group = this.rbEquipAll.Group; + this.table2.Add(this.rbEquipPercent); + global::Gtk.Table.TableChild w10 = ((global::Gtk.Table.TableChild)(this.table2[this.rbEquipPercent])); w10.TopAttach = ((uint)(1)); w10.BottomAttach = ((uint)(2)); - w10.LeftAttach = ((uint)(1)); - w10.RightAttach = ((uint)(2)); - w10.XOptions = ((global::Gtk.AttachOptions)(0)); + w10.XOptions = ((global::Gtk.AttachOptions)(4)); w10.YOptions = ((global::Gtk.AttachOptions)(4)); this.hbox2.Add(this.table2); global::Gtk.Box.BoxChild w11 = ((global::Gtk.Box.BoxChild)(this.hbox2[this.table2])); @@ -213,7 +213,6 @@ this.buttonCancel.UseStock = true; this.buttonCancel.UseUnderline = true; this.buttonCancel.Label = "gtk-cancel"; - this.buttonCancel.Clicked+= CancelButtonClicked; this.AddActionWidget(this.buttonCancel, -6); global::Gtk.ButtonBox.ButtonBoxChild w17 = ((global::Gtk.ButtonBox.ButtonBoxChild)(w16[this.buttonCancel])); w17.Expand = false; @@ -238,6 +237,11 @@ this.DefaultWidth = 400; this.DefaultHeight = 300; this.Show(); + this.rbEquipNumeric.Clicked += new global::System.EventHandler(this.RadioButtonClicked); + this.percentageAmount.ValueChanged += new global::System.EventHandler(this.SpinButtonValueChanged); + this.numericAmount.ValueChanged += new global::System.EventHandler(this.SpinButtonValueChanged); + this.buttonCancel.Clicked += new global::System.EventHandler(this.CancelButtonClicked); + this.buttonOk.Clicked += new global::System.EventHandler(this.OkayButtonClicked); } } }
--- a/gtk-gui/gui.stetic Fri Aug 27 14:44:48 2010 +0000 +++ b/gtk-gui/gui.stetic Sun Aug 29 15:09:34 2010 +0000 @@ -1074,6 +1074,7 @@ <property name="StepIncrement">1</property> <property name="ClimbRate">1</property> <property name="Numeric">True</property> + <signal name="ValueChanged" handler="SpinButtonValueChanged" /> </widget> <packing> <property name="LeftAttach">1</property> @@ -1099,6 +1100,7 @@ <property name="ClimbRate">1</property> <property name="Digits">1</property> <property name="Numeric">True</property> + <signal name="ValueChanged" handler="SpinButtonValueChanged" /> </widget> <packing> <property name="TopAttach">1</property> @@ -1121,6 +1123,7 @@ <property name="MemberName" /> <property name="CanFocus">True</property> <property name="Label" translatable="yes" /> + <property name="Active">True</property> <property name="DrawIndicator">True</property> <property name="HasLabel">True</property> <property name="UseUnderline">True</property> @@ -1149,6 +1152,7 @@ <property name="HasLabel">True</property> <property name="UseUnderline">True</property> <property name="Group">group1</property> + <signal name="Clicked" handler="RadioButtonClicked" /> </widget> <packing> <property name="AutoSize">False</property>
--- a/gtk-gui/objects.xml Fri Aug 27 14:44:48 2010 +0000 +++ b/gtk-gui/objects.xml Sun Aug 29 15:09:34 2010 +0000 @@ -1,6 +1,2 @@ <objects attr-sync="on"> - <object type="IBBoard.WarFoundry.GTK.Widgets.UnitDisplayWidget" palette-category="WarFoundry GTK# GUI" allow-children="false" base-type="Gtk.Bin"> - <itemgroups /> - <signals /> - </object> </objects> \ No newline at end of file