Creating and updating seasons with an editor.

This commit is contained in:
2020-03-06 20:41:01 +01:00
parent 118c15f6c9
commit f3974807a8
12 changed files with 525 additions and 108 deletions

View File

@@ -7,39 +7,22 @@ using DateTime = GLib.DateTime;
namespace EstusShots.Gtk.Controls
{
public class SelectionChangedEventArgs : EventArgs
public class SelectionChangedEventArgs<T> : 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<T>(object o, SelectionChangedEventArgs<T> args);
public delegate void ItemActivatedEventHandler<T>(T item);
public class BindableListControl<T>
{
/// <summary>
/// Initialize a new BindableListView with an existing TreeView Widget
/// </summary>
/// <param name="columns">The columns of the grid</param>
/// <param name="keyField">Unique key field in the item sources type</param>
/// <param name="treeView">An instance of an existing TreeView Widget</param>
public BindableListControl(List<DataColumn> columns, string keyField, TreeView treeView = null)
{
TreeView = treeView ?? new TreeView();
Columns = columns;
KeyField = keyField;
InitTreeViewColumns();
InitListStore();
TreeView.Model = ListStore;
Items = new List<T>();
TreeView.RowActivated += TreeViewOnRowActivated;
}
/// <summary> The GTK ListStore that is managed by this <see cref="BindableListControl{T}" />. </summary>
public ListStore ListStore { get; internal set; }
@@ -61,7 +44,32 @@ namespace EstusShots.Gtk.Controls
/// <summary>
/// Event will be invoked when the selected item in the <see cref="TreeView" /> has changed.
/// </summary>
public event SelectionChangedEventHandler OnSelectionChanged;
public event SelectionChangedEventHandler<T> SelectionChanged;
/// <summary>
/// Will be invoked when a row in the view has ben acitvated (e.g. double clicked)
/// </summary>
public event ItemActivatedEventHandler<T> ItemActivated;
/// <summary>
/// Initialize a new BindableListView with an existing TreeView Widget
/// </summary>
/// <param name="columns">The columns of the grid</param>
/// <param name="keyField">Unique key field in the item sources type</param>
/// <param name="treeView">An instance of an existing TreeView Widget</param>
public BindableListControl(List<DataColumn> columns, string keyField, TreeView treeView = null)
{
TreeView = treeView ?? new TreeView();
Columns = columns;
KeyField = keyField;
InitTreeViewColumns();
InitListStore();
TreeView.Model = ListStore;
Items = new List<T>();
TreeView.RowActivated += TreeViewOnRowActivated;
TreeView.Selection.Changed += TreeViewSelectionOnChanged;
}
/// <summary>
/// Set elements from the <see cref="Items" /> property in the <see cref="ListStore" />.
@@ -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<T>(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

View File

@@ -0,0 +1,81 @@
using System;
using EstusShots.Shared.Models;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;
namespace EstusShots.Gtk.Dialogs
{
public class DialogClosedEventArgs<T> : 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<T>(object o, DialogClosedEventArgs<T> 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<T> 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<T> OnDialogClosed;
protected DialogBase(Window parent, Builder builder)
{
builder.Autoconnect(this);
_saveButton.Clicked += OnSaveButtonClicked;
_cancelButton.Clicked += (sender, args) =>
{
OnDialogClosed?.Invoke(this, new DialogClosedEventArgs<T>(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<T>(true, EditObject));
_editorDialog.Dispose();
}
catch (Exception exception)
{
ErrorDialog.Show(new OperationResult(exception));
}
}
protected abstract void LoadToModel();
protected abstract void LoadFromModel();
}
}

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkDialog" id="_editorDialog">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Season</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">400</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="_saveButton">
<property name="label">gtk-save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<accelerator key="Return" signal="clicked"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_cancelButton">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="row_spacing">5</property>
<property name="column_spacing">7</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Number</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Game</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_gameEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Start</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">End</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Description</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">150</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkTextView" id="_descriptionTextView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="hscroll_policy">natural</property>
<property name="vscroll_policy">natural</property>
<property name="wrap_mode">word</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_startEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_endEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="secondary_icon_stock">gtk-clear</property>
<property name="placeholder_text" translatable="yes">Ongoing</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_numberEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="input_purpose">digits</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -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;

View File

@@ -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<Season>
{
[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;
}
}
}

View File

@@ -25,4 +25,8 @@
<ProjectReference Include="..\EstusShots.Shared\EstusShots.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Dialogs\Glade" />
</ItemGroup>
</Project>

View File

@@ -62,37 +62,6 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkOverlay" id="SeasonsOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="SeasonsView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="enable_grid_lines">horizontal</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
@@ -132,7 +101,38 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkOverlay" id="SeasonsOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="SeasonsView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="enable_grid_lines">horizontal</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>

View File

@@ -35,7 +35,7 @@ namespace EstusShots.Gtk
new DataColumnText(nameof(Player.HexId)) {Title = "Hex ID"},
};
_playersControl = new BindableListControl<Player>(playerColumns, nameof(Player.PlayerId), PlayersTreeView);
_playersControl.OnSelectionChanged += PlayersControlOnOnSelectionChanged;
_playersControl.ItemActivated += PlayersControlActivated;
var drinkColumns = new List<DataColumn>
@@ -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;
}

View File

@@ -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<Season> 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<Season> 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<DataColumn>
{
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<Season>(columns, nameof(Season.SeasonId), SeasonsView);
SeasonsControl.OnSelectionChanged += SeasonsControlOnSelectionChanged;
SeasonsControl.SelectionChanged += SeasonsControlSelectionChanged;
SeasonsControl.ItemActivated += SeasonsControlItemActivated;
}
}
}

View File

@@ -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<SavePlayerResponse>(new SavePlayerResponse(player.PlayerId));
}
}

View File

@@ -46,18 +46,23 @@ namespace EstusShots.Server.Services
public async Task<ApiResponse<SaveSeasonResponse>> SaveSeason(SaveSeasonParameter parameter)
{
var season = _mapper.Map<Season>(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<Season>(parameter.Season);
_context.Seasons.Add(season);
var count = await _context.SaveChangesAsync();
_logger.LogInformation($"Season created: '{season.SeasonId}' ({count} rows updated)");
return new ApiResponse<SaveSeasonResponse>(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<SaveSeasonResponse>(new SaveSeasonResponse(season.SeasonId));
}
return new ApiResponse<SaveSeasonResponse>(new SaveSeasonResponse(season.SeasonId));
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Text.RegularExpressions;
namespace EstusShots.Shared.Extensions
{
public static class StringExtensions
{
/// <summary>
/// Forces a string into the "yyyy-mm-dd" format
/// </summary>
/// <param name="this"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Forces a string into the "HH:MM" format
/// </summary>
/// <param name="this"></param>
/// <returns></returns>
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
}
}
}