From f3974807a83d4ad5f7c8eab420b41f19d12217dc Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 6 Mar 2020 20:41:01 +0100 Subject: [PATCH] Creating and updating seasons with an editor. --- .../Controls/BindableListControl.cs | 87 +++++--- EstusShots.Gtk/Dialogs/DialogBase.cs | 81 +++++++ .../Dialogs/Glade/SeasonEditor.glade | 210 ++++++++++++++++++ EstusShots.Gtk/Dialogs/PlayerEditor.cs | 13 -- EstusShots.Gtk/Dialogs/SeasonEditor.cs | 57 +++++ EstusShots.Gtk/EstusShots.Gtk.csproj | 4 + EstusShots.Gtk/MainWindow.glade | 64 +++--- EstusShots.Gtk/Pages/BaseDataPage.cs | 5 +- EstusShots.Gtk/Pages/SeasonsPage.cs | 53 +++-- EstusShots.Server/Services/PlayersService.cs | 2 +- EstusShots.Server/Services/SeasonsService.cs | 19 +- .../Extensions/StringExtensions.cs | 38 ++++ 12 files changed, 525 insertions(+), 108 deletions(-) create mode 100644 EstusShots.Gtk/Dialogs/DialogBase.cs create mode 100644 EstusShots.Gtk/Dialogs/Glade/SeasonEditor.glade create mode 100644 EstusShots.Gtk/Dialogs/SeasonEditor.cs create mode 100644 EstusShots.Shared/Extensions/StringExtensions.cs diff --git a/EstusShots.Gtk/Controls/BindableListControl.cs b/EstusShots.Gtk/Controls/BindableListControl.cs index d5f2a97..6fec306 100644 --- a/EstusShots.Gtk/Controls/BindableListControl.cs +++ b/EstusShots.Gtk/Controls/BindableListControl.cs @@ -7,39 +7,22 @@ using DateTime = GLib.DateTime; namespace EstusShots.Gtk.Controls { - public class SelectionChangedEventArgs : EventArgs + public class SelectionChangedEventArgs : EventArgs { - public SelectionChangedEventArgs(object selection) + public SelectionChangedEventArgs(T selection) { Selection = selection; } - public object Selection { get; } + public T Selection { get; } } - public delegate void SelectionChangedEventHandler(object o, SelectionChangedEventArgs args); + public delegate void SelectionChangedEventHandler(object o, SelectionChangedEventArgs args); + + public delegate void ItemActivatedEventHandler(T item); public class BindableListControl { - /// - /// Initialize a new BindableListView with an existing TreeView Widget - /// - /// The columns of the grid - /// Unique key field in the item sources type - /// An instance of an existing TreeView Widget - public BindableListControl(List columns, string keyField, TreeView treeView = null) - { - TreeView = treeView ?? new TreeView(); - Columns = columns; - KeyField = keyField; - InitTreeViewColumns(); - InitListStore(); - TreeView.Model = ListStore; - Items = new List(); - - TreeView.RowActivated += TreeViewOnRowActivated; - } - /// The GTK ListStore that is managed by this . public ListStore ListStore { get; internal set; } @@ -61,7 +44,32 @@ namespace EstusShots.Gtk.Controls /// /// Event will be invoked when the selected item in the has changed. /// - public event SelectionChangedEventHandler OnSelectionChanged; + public event SelectionChangedEventHandler SelectionChanged; + + /// + /// Will be invoked when a row in the view has ben acitvated (e.g. double clicked) + /// + public event ItemActivatedEventHandler ItemActivated; + + /// + /// Initialize a new BindableListView with an existing TreeView Widget + /// + /// The columns of the grid + /// Unique key field in the item sources type + /// An instance of an existing TreeView Widget + public BindableListControl(List columns, string keyField, TreeView treeView = null) + { + TreeView = treeView ?? new TreeView(); + Columns = columns; + KeyField = keyField; + InitTreeViewColumns(); + InitListStore(); + TreeView.Model = ListStore; + Items = new List(); + + TreeView.RowActivated += TreeViewOnRowActivated; + TreeView.Selection.Changed += TreeViewSelectionOnChanged; + } /// /// Set elements from the property in the . @@ -99,8 +107,8 @@ namespace EstusShots.Gtk.Controls Console.WriteLine(e); } } - - private void TreeViewOnRowActivated(object o, RowActivatedArgs args) + + private void TreeViewOnRowActivated(object o, RowActivatedArgs args) { if (!(o is TreeView tree)) return; var selection = tree.Selection; @@ -120,7 +128,29 @@ namespace EstusShots.Gtk.Controls } SelectedItem = item; - OnSelectionChanged?.Invoke(this, new SelectionChangedEventArgs(SelectedItem)); + ItemActivated?.Invoke(SelectedItem); + } + + private void TreeViewSelectionOnChanged(object sender, EventArgs e) + { + if (!(sender is TreeSelection selection)) return; + selection.GetSelected(out var model, out var iter); + var key = model.GetValue(iter, 0); + var item = Items.FirstOrDefault(x => + { + var prop = x.GetType().GetProperty(KeyField); + var value = prop?.GetValue(x); + return value != null && value.Equals(key); + }); + + if (item == null) + { + Console.WriteLine($"No item for key '{key}' found in data store"); + return; + } + + SelectedItem = item; + SelectionChanged?.Invoke(selection, new SelectionChangedEventArgs(SelectedItem)); } private void InitTreeViewColumns() @@ -130,10 +160,11 @@ namespace EstusShots.Gtk.Controls // Offset by one, because the first column in the data store is fixed to the key value of the row var valueIndex = Columns.IndexOf(dataColumn) + 1; dataColumn.AddAttribute(dataColumn.Cell, dataColumn.ValueAttribute, valueIndex); + dataColumn.SortColumnId = valueIndex; TreeView.AppendColumn(dataColumn); } } - + private void InitListStore() { var types = Columns diff --git a/EstusShots.Gtk/Dialogs/DialogBase.cs b/EstusShots.Gtk/Dialogs/DialogBase.cs new file mode 100644 index 0000000..3b75924 --- /dev/null +++ b/EstusShots.Gtk/Dialogs/DialogBase.cs @@ -0,0 +1,81 @@ +using System; +using EstusShots.Shared.Models; +using Gtk; +using UI = Gtk.Builder.ObjectAttribute; + +namespace EstusShots.Gtk.Dialogs +{ + public class DialogClosedEventArgs : EventArgs where T: class, new() + { + public bool Ok { get; } + public T Model { get; } + + public DialogClosedEventArgs(bool ok, T model) + { + Ok = ok; + Model = model; + } + } + public delegate void DialogClosedEventHandler(object o, DialogClosedEventArgs args) where T: class, new(); + + // TODO remove non-generic version + public class DialogClosedEventArgs : EventArgs + { + public bool Ok { get; } + public object Model { get; } + + public DialogClosedEventArgs(bool ok, object model) + { + Ok = ok; + Model = model; + } + } + public delegate void DialogClosedEventHandler(object o, DialogClosedEventArgs args); + + public abstract class DialogBase where T: class, new() + { + protected T EditObject { get; set; } + + [UI] private readonly Dialog _editorDialog = null; + [UI] private readonly Button _saveButton = null; + [UI] private readonly Button _cancelButton = null; + + public event DialogClosedEventHandler OnDialogClosed; + + protected DialogBase(Window parent, Builder builder) + { + builder.Autoconnect(this); + + _saveButton.Clicked += OnSaveButtonClicked; + _cancelButton.Clicked += (sender, args) => + { + OnDialogClosed?.Invoke(this, new DialogClosedEventArgs(false, new T())); + _editorDialog.Dispose(); + }; + _editorDialog.TransientFor = parent; + } + + public void Show() + { + LoadFromModel(); + _editorDialog.Show(); + } + + private void OnSaveButtonClicked(object sender, EventArgs e) + { + try + { + LoadToModel(); + OnDialogClosed?.Invoke(this, new DialogClosedEventArgs(true, EditObject)); + _editorDialog.Dispose(); + } + catch (Exception exception) + { + ErrorDialog.Show(new OperationResult(exception)); + } + } + + protected abstract void LoadToModel(); + protected abstract void LoadFromModel(); + } +} \ No newline at end of file diff --git a/EstusShots.Gtk/Dialogs/Glade/SeasonEditor.glade b/EstusShots.Gtk/Dialogs/Glade/SeasonEditor.glade new file mode 100644 index 0000000..7b5a25f --- /dev/null +++ b/EstusShots.Gtk/Dialogs/Glade/SeasonEditor.glade @@ -0,0 +1,210 @@ + + + + + + False + Season + False + True + center-on-parent + 400 + True + dialog + center + + + + + + False + vertical + 2 + + + False + end + + + gtk-save + True + True + True + True + + + + True + True + 0 + + + + + gtk-cancel + True + True + True + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + 10 + 10 + 5 + 7 + + + True + False + end + Number + + + 0 + 0 + + + + + True + False + end + Game + + + 0 + 1 + + + + + True + True + + + 1 + 1 + + + + + True + False + end + Start + + + 0 + 2 + + + + + True + False + end + End + + + 0 + 3 + + + + + True + False + end + start + Description + + + 0 + 4 + + + + + True + True + True + in + 150 + True + True + + + True + True + True + natural + natural + word + + + + + 1 + 4 + + + + + True + True + + + 1 + 2 + + + + + True + True + gtk-clear + Ongoing + + + 1 + 3 + + + + + True + True + digits + + + 1 + 0 + + + + + True + True + 1 + + + + + + diff --git a/EstusShots.Gtk/Dialogs/PlayerEditor.cs b/EstusShots.Gtk/Dialogs/PlayerEditor.cs index 1e6e92a..bd41dd0 100644 --- a/EstusShots.Gtk/Dialogs/PlayerEditor.cs +++ b/EstusShots.Gtk/Dialogs/PlayerEditor.cs @@ -5,19 +5,6 @@ using UI = Gtk.Builder.ObjectAttribute; namespace EstusShots.Gtk.Dialogs { - public class DialogClosedEventArgs : EventArgs - { - public bool Ok { get; } - public object Model { get; } - - public DialogClosedEventArgs(bool ok, object model) - { - Ok = ok; - Model = model; - } - } - public delegate void DialogClosedEventHandler(object o, DialogClosedEventArgs args); - public class PlayerEditor { private readonly Player _player; diff --git a/EstusShots.Gtk/Dialogs/SeasonEditor.cs b/EstusShots.Gtk/Dialogs/SeasonEditor.cs new file mode 100644 index 0000000..95fecd2 --- /dev/null +++ b/EstusShots.Gtk/Dialogs/SeasonEditor.cs @@ -0,0 +1,57 @@ +using System; +using EstusShots.Shared.Dto; +using EstusShots.Shared.Extensions; +using Gtk; +using UI = Gtk.Builder.ObjectAttribute; + +namespace EstusShots.Gtk.Dialogs +{ + public class SeasonEditor : DialogBase + { + [UI] private readonly Entry _numberEntry = null; + [UI] private readonly Entry _gameEntry = null; + [UI] private readonly Entry _startEntry = null; + [UI] private readonly Entry _endEntry = null; + [UI] private readonly TextView _descriptionTextView = null; + + public SeasonEditor(Window parent, Season season) : base(parent, new Builder("SeasonEditor.glade")) + { + EditObject = season; + _startEntry.FocusOutEvent += (o, args) => + { + if (!(o is Entry entry)) return; + entry.Text = entry.Text.DateMask(); + }; + _endEntry.FocusOutEvent += (o, args) => + { + if (!(o is Entry entry)) return; + if (entry.Text.IsNullOrWhiteSpace()) return; + entry.Text = entry.Text.DateMask(); + }; + _endEntry.IconPress += (o, args) => + { + if (!(o is Entry entry)) return; + entry.Text = ""; + }; + } + + protected override void LoadFromModel() + { + if (EditObject.SeasonId.IsEmpty()) return; + _numberEntry.Text = EditObject.Number.ToString(); + _gameEntry.Text = EditObject.Game; + _startEntry.Text = EditObject.Start.ToString("yyyy-MM-dd"); + _endEntry.Text = EditObject.End?.ToString("yyyy-MM-dd") ?? ""; + _descriptionTextView.Buffer = new TextBuffer(new TextTagTable()) {Text = EditObject.Description}; + } + + protected override void LoadToModel() + { + EditObject.Number = _numberEntry.Text.ToInt32OrDefault(); + EditObject.Game = _gameEntry.Text; + EditObject.Start = _startEntry.Text.ToDateTime(); + EditObject.End = _endEntry.Text.IsNullOrWhiteSpace() ? null :_endEntry.Text.ToNullableDateTime(); + EditObject.Description = _descriptionTextView.Buffer.Text; + } + } +} \ No newline at end of file diff --git a/EstusShots.Gtk/EstusShots.Gtk.csproj b/EstusShots.Gtk/EstusShots.Gtk.csproj index 90bcc60..0025cd3 100644 --- a/EstusShots.Gtk/EstusShots.Gtk.csproj +++ b/EstusShots.Gtk/EstusShots.Gtk.csproj @@ -25,4 +25,8 @@ + + + + diff --git a/EstusShots.Gtk/MainWindow.glade b/EstusShots.Gtk/MainWindow.glade index e7ceb7b..068ce85 100644 --- a/EstusShots.Gtk/MainWindow.glade +++ b/EstusShots.Gtk/MainWindow.glade @@ -62,37 +62,6 @@ True False vertical - - - True - False - - - True - True - in - - - True - True - horizontal - - - - - - - - -1 - - - - - True - True - 0 - - True @@ -132,7 +101,38 @@ False True 2 - 2 + 0 + + + + + True + False + + + True + True + in + + + True + True + horizontal + + + + + + + + -1 + + + + + True + True + 1 diff --git a/EstusShots.Gtk/Pages/BaseDataPage.cs b/EstusShots.Gtk/Pages/BaseDataPage.cs index 82cc046..45e79e4 100644 --- a/EstusShots.Gtk/Pages/BaseDataPage.cs +++ b/EstusShots.Gtk/Pages/BaseDataPage.cs @@ -35,7 +35,7 @@ namespace EstusShots.Gtk new DataColumnText(nameof(Player.HexId)) {Title = "Hex ID"}, }; _playersControl = new BindableListControl(playerColumns, nameof(Player.PlayerId), PlayersTreeView); - _playersControl.OnSelectionChanged += PlayersControlOnOnSelectionChanged; + _playersControl.ItemActivated += PlayersControlActivated; var drinkColumns = new List @@ -53,9 +53,8 @@ namespace EstusShots.Gtk // Events - private void PlayersControlOnOnSelectionChanged(object o, SelectionChangedEventArgs args) + private void PlayersControlActivated(Player player) { - if (!(args.Selection is Player player)) return; var dialog = new PlayerEditor(this, player); dialog.OnDialogClosed += PlayerEditorClosed; } diff --git a/EstusShots.Gtk/Pages/SeasonsPage.cs b/EstusShots.Gtk/Pages/SeasonsPage.cs index 989ea13..bdfc720 100644 --- a/EstusShots.Gtk/Pages/SeasonsPage.cs +++ b/EstusShots.Gtk/Pages/SeasonsPage.cs @@ -40,18 +40,20 @@ namespace EstusShots.Gtk await ReloadSeasons(); } - private async void NewSeasonButtonOnClicked(object sender, EventArgs e) + private void NewSeasonButtonOnClicked(object sender, EventArgs e) { + var dialog = new SeasonEditor(this, new Season()); + dialog.OnDialogClosed += SeasonEditorClosed; + dialog.Show(); + } + + private async void SeasonEditorClosed(object o, DialogClosedEventArgs args) + { + if (!args.Ok) return; + using var _ = new LoadingMode(this); - // TODO real season edit control - var season = new Season - { - Game = "Test Game", - Number = SeasonsControl.Items.Any() ? SeasonsControl.Items.Max(x => x.Number) + 1 : 1, - Start = DateTime.Now, - Description = "This is a demo description!" - }; - var parameter = new SaveSeasonParameter(season); + + var parameter = new SaveSeasonParameter(args.Model); var res = await Client.Seasons.SaveSeason(parameter); if (!res.OperationResult.Success) { @@ -61,24 +63,25 @@ namespace EstusShots.Gtk } await ReloadSeasons(); - Info("Created new Season"); + Info($"Season {args.Model.DisplayName}"); } - private async void SeasonsControlOnSelectionChanged(object sender, SelectionChangedEventArgs e) + private async void SeasonsControlSelectionChanged(object sender, SelectionChangedEventArgs e) { - if (!(e.Selection is Season season)) return; - using var _ = new LoadingMode(this); - EpisodesPage.Show(); - var parameter = new GetEpisodesParameter(season.SeasonId); + var parameter = new GetEpisodesParameter(e.Selection.SeasonId); var res = await Client.Episodes.GetEpisodes(parameter); EpisodesControl.Items = res.Data.Episodes; EpisodesControl.DataBind(); - UpdateTitle(); - Navigation.Page = EpisodesPageNumber; - - Info($"{season.DisplayName}: {res.Data.Episodes.Count} episodes"); + Info($"{e.Selection.DisplayName}: {res.Data.Episodes.Count} episodes"); + } + + private void SeasonsControlItemActivated(Season item) + { + var dialog = new SeasonEditor(this, item); + dialog.OnDialogClosed += SeasonEditorClosed; + dialog.Show(); } // Private Methods @@ -93,8 +96,9 @@ namespace EstusShots.Gtk ErrorDialog.Show(res.OperationResult); return; } - - SeasonsControl.Items = res.Data.Seasons; + + // TODO Initial ordering should be done by the control + SeasonsControl.Items = res.Data.Seasons.OrderBy(x => x.DisplayName).ToList(); SeasonsControl.DataBind(); Info("Seasons Refreshed"); } @@ -103,7 +107,7 @@ namespace EstusShots.Gtk { var columns = new List { - new DataColumnText(nameof(Season.DisplayName)) {Title = "Name"}, + new DataColumnText(nameof(Season.DisplayName)) {Title = "Name", SortOrder = SortType.Ascending}, new DataColumnText(nameof(Season.Description)), new DataColumnText(nameof(Season.Start)) { @@ -115,7 +119,8 @@ namespace EstusShots.Gtk } }; SeasonsControl = new BindableListControl(columns, nameof(Season.SeasonId), SeasonsView); - SeasonsControl.OnSelectionChanged += SeasonsControlOnSelectionChanged; + SeasonsControl.SelectionChanged += SeasonsControlSelectionChanged; + SeasonsControl.ItemActivated += SeasonsControlItemActivated; } } } \ No newline at end of file diff --git a/EstusShots.Server/Services/PlayersService.cs b/EstusShots.Server/Services/PlayersService.cs index 7dda1cb..75e5640 100644 --- a/EstusShots.Server/Services/PlayersService.cs +++ b/EstusShots.Server/Services/PlayersService.cs @@ -60,7 +60,7 @@ namespace EstusShots.Server.Services _context.Players.Update(player); _mapper.Map(parameter.Player, player); var count = await _context.SaveChangesAsync(); - _logger.LogInformation($"Updated player '{player.PlayerId}'"); + _logger.LogInformation($"Updated player '{player.PlayerId}' ({count} rows)"); return new ApiResponse(new SavePlayerResponse(player.PlayerId)); } } diff --git a/EstusShots.Server/Services/SeasonsService.cs b/EstusShots.Server/Services/SeasonsService.cs index efddb7f..3313ce8 100644 --- a/EstusShots.Server/Services/SeasonsService.cs +++ b/EstusShots.Server/Services/SeasonsService.cs @@ -46,18 +46,23 @@ namespace EstusShots.Server.Services public async Task> SaveSeason(SaveSeasonParameter parameter) { - var season = _mapper.Map(parameter.Season); - var existing = await _context.Seasons.FindAsync(season.SeasonId); - if (existing == null) + if (parameter.Season.SeasonId.IsEmpty()) { - _context.Seasons.Add(season);await _context.SaveChangesAsync(); - _logger.LogInformation($"New season created: '{season.SeasonId}'"); + var season = _mapper.Map(parameter.Season); + _context.Seasons.Add(season); + var count = await _context.SaveChangesAsync(); + _logger.LogInformation($"Season created: '{season.SeasonId}' ({count} rows updated)"); + return new ApiResponse(new SaveSeasonResponse(season.SeasonId)); } else { - throw new NotImplementedException(); + var season = await _context.Seasons.FindAsync(parameter.Season.SeasonId); + _context.Seasons.Update(season); + _mapper.Map(parameter.Season, season); + var count = await _context.SaveChangesAsync(); + _logger.LogInformation($"Season '{season.SeasonId}' updated ({count} rows updated)"); + return new ApiResponse(new SaveSeasonResponse(season.SeasonId)); } - return new ApiResponse(new SaveSeasonResponse(season.SeasonId)); } } } \ No newline at end of file diff --git a/EstusShots.Shared/Extensions/StringExtensions.cs b/EstusShots.Shared/Extensions/StringExtensions.cs new file mode 100644 index 0000000..4a255ef --- /dev/null +++ b/EstusShots.Shared/Extensions/StringExtensions.cs @@ -0,0 +1,38 @@ +using System.Text.RegularExpressions; + +namespace EstusShots.Shared.Extensions +{ + public static class StringExtensions + { + /// + /// Forces a string into the "yyyy-mm-dd" format + /// + /// + /// + public static string DateMask(this string @this) + { + // Remove all non-numbers + @this = Regex.Replace(@this, "[^0-9.]", ""); + if (@this.Length < 8) @this += "????????"; + return string.Format("{0}-{1}-{2}", + @this.Substring(0, 4), // The year, + @this.Substring(4, 2), // The month, + @this.Substring(6, 2)); // The day); + } + + /// + /// Forces a string into the "HH:MM" format + /// + /// + /// + public static string HourMinuteMask(this string @this) + { + // Remove all non-numbers + @this = Regex.Replace(@this, "[^0-9.]", ""); + if (@this.Length < 4) @this += "0000"; + return string.Format("{0}:{1}", + @this.Substring(0, 2), // The hours, + @this.Substring(2, 4)); // The minutes + } + } +} \ No newline at end of file