From 874bf9e7c0b635a57c523ecfa6ab77469830be7c Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Sat, 31 Dec 2022 10:02:30 -0700 Subject: [PATCH] Improve classic and chardonnay rating editor simmilarity --- Source/ApplicationServices/LibraryCommands.cs | 4 +- Source/DataLayer/EfClasses/UserDefinedItem.cs | 4 +- ...tingBox.axaml => MyRatingCellEditor.axaml} | 58 ++++---- .../Controls/MyRatingCellEditor.axaml.cs | 108 +++++++++++++++ .../Controls/MyRatingGridColumn.axaml | 8 ++ .../Controls/MyRatingGridColumn.axaml.cs | 66 +++++++++ .../Controls/RatingBox.axaml.cs | 125 ------------------ .../LibationAvalonia/ViewModels/GridEntry.cs | 2 +- .../ViewModels/LibraryBookEntry.cs | 4 +- .../ViewModels/SeriesEntry.cs | 4 +- .../Views/ProductsDisplay.axaml | 13 +- Source/LibationSearchEngine/SearchEngine.cs | 8 +- .../GridView/MyRatingCellEditor.cs | 4 +- .../GridView/MyRatingGridViewColumn.cs | 3 +- 14 files changed, 233 insertions(+), 178 deletions(-) rename Source/LibationAvalonia/Controls/{RatingBox.axaml => MyRatingCellEditor.axaml} (59%) create mode 100644 Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs create mode 100644 Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml create mode 100644 Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml.cs delete mode 100644 Source/LibationAvalonia/Controls/RatingBox.axaml.cs diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 8d6f5a05..21123ec5 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -439,7 +439,7 @@ namespace ApplicationServices udi.SetPdfStatus(pdfStatus); if (rating is not null) - udi.Rating = rating; + udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating); }); public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus) @@ -497,7 +497,7 @@ namespace ApplicationServices context.Attach(book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified; } - var qtyChanges = context.SaveChanges(); + var qtyChanges = context.SaveChanges(); if (qtyChanges > 0) BookUserDefinedItemCommitted?.Invoke(null, books); diff --git a/Source/DataLayer/EfClasses/UserDefinedItem.cs b/Source/DataLayer/EfClasses/UserDefinedItem.cs index 177eb79b..e97387ae 100644 --- a/Source/DataLayer/EfClasses/UserDefinedItem.cs +++ b/Source/DataLayer/EfClasses/UserDefinedItem.cs @@ -95,9 +95,9 @@ namespace DataLayer #region Rating // owned: not an optional one-to-one /// The user's individual book rating - public Rating Rating { get; set; } = new Rating(0, 0, 0); + public Rating Rating { get; private set; } = new Rating(0, 0, 0); - public void UpdateRating(float overallRating, float performanceRating, float storyRating) + public void UpdateRating(float overallRating, float performanceRating, float storyRating) => Rating.Update(overallRating, performanceRating, storyRating); #endregion diff --git a/Source/LibationAvalonia/Controls/RatingBox.axaml b/Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml similarity index 59% rename from Source/LibationAvalonia/Controls/RatingBox.axaml rename to Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml index 4f3f6a03..27b2592d 100644 --- a/Source/LibationAvalonia/Controls/RatingBox.axaml +++ b/Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml @@ -3,49 +3,49 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="LibationAvalonia.Controls.RatingBox"> - + x:Class="LibationAvalonia.Controls.MyRatingCellEditor"> + + - - + + - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + diff --git a/Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs b/Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs new file mode 100644 index 00000000..89a5ec91 --- /dev/null +++ b/Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs @@ -0,0 +1,108 @@ +using Avalonia; +using Avalonia.Controls; +using DataLayer; +using System.Linq; + +namespace LibationAvalonia.Controls +{ + public partial class MyRatingCellEditor : UserControl + { + private const string SOLID_STAR = "★"; + private const string HOLLOW_STAR = "☆"; + + public static readonly StyledProperty RatingProperty = + AvaloniaProperty.Register(nameof(Rating)); + + public bool AllRatingsVisible { get; set; } + public Rating Rating + { + get { return GetValue(RatingProperty); } + set { SetValue(RatingProperty, value); } + } + public MyRatingCellEditor() + { + InitializeComponent(); + if (Design.IsDesignMode) + Rating = new Rating(5, 4, 3); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property.Name == nameof(Rating) && Rating is not null) + { + int rating = 0; + foreach (TextBlock star in panelOverall.Children) + star.Tag = star.Text = Rating.OverallRating > rating++ ? SOLID_STAR : HOLLOW_STAR; + + rating = 0; + foreach (TextBlock star in panelPerform.Children) + star.Tag = star.Text = Rating.PerformanceRating > rating++ ? SOLID_STAR : HOLLOW_STAR; + + rating = 0; + foreach (TextBlock star in panelStory.Children) + star.Tag = star.Text = Rating.StoryRating > rating++ ? SOLID_STAR : HOLLOW_STAR; + + SetVisible(AllRatingsVisible); + } + base.OnPropertyChanged(change); + } + + private void SetVisible(bool allVisible) + { + tblockOverall.IsVisible = panelOverall.IsVisible = allVisible || Rating?.OverallRating > 0; + tblockPerform.IsVisible = panelPerform.IsVisible = allVisible || Rating?.PerformanceRating > 0; + tblockStory.IsVisible = panelStory.IsVisible = allVisible || Rating?.StoryRating > 0; + } + + public void Panel_PointerExited(object sender, Avalonia.Input.PointerEventArgs e) + { + var panel = sender as Panel; + var stackPanel = panel.Children.OfType().Single(); + + //Restore defaults + foreach (TextBlock child in stackPanel.Children) + child.Text = (string)child.Tag; + } + + public void Star_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e) + { + var thisTbox = sender as TextBlock; + var stackPanel = thisTbox.Parent as StackPanel; + var star = SOLID_STAR; + + foreach (TextBlock child in stackPanel.Children) + { + child.Text = star; + if (child == thisTbox) star = HOLLOW_STAR; + } + } + + public void Star_Tapped(object sender, Avalonia.Input.TappedEventArgs e) + { + var overall = Rating.OverallRating; + var perform = Rating.PerformanceRating; + var story = Rating.StoryRating; + + var thisTbox = sender as TextBlock; + var stackPanel = thisTbox.Parent as StackPanel; + + int newRatingValue = 0; + foreach (var tbox in stackPanel.Children) + { + newRatingValue++; + if (tbox == thisTbox) break; + } + + if (stackPanel == panelOverall) + overall = newRatingValue; + else if (stackPanel == panelPerform) + perform = newRatingValue; + else if (stackPanel == panelStory) + story = newRatingValue; + + if (overall + perform + story == 0f) return; + + Rating = new Rating(overall, perform, story); + } + } +} diff --git a/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml b/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml new file mode 100644 index 00000000..3c77bb14 --- /dev/null +++ b/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml @@ -0,0 +1,8 @@ + + + diff --git a/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml.cs b/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml.cs new file mode 100644 index 00000000..c9e67099 --- /dev/null +++ b/Source/LibationAvalonia/Controls/MyRatingGridColumn.axaml.cs @@ -0,0 +1,66 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using DataLayer; + +namespace LibationAvalonia.Controls +{ + public partial class MyRatingGridColumn : DataGridBoundColumn + { + private static Rating DefaultRating => new Rating(0, 0, 0); + public MyRatingGridColumn() + { + AvaloniaXamlLoader.Load(this); + BindingTarget = MyRatingCellEditor.RatingProperty; + } + + protected override IControl GenerateElement(DataGridCell cell, object dataItem) + { + var myRatingElement = new MyRatingCellEditor + { + Name = "CellMyRatingDisplay", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + AllRatingsVisible = false, + Margin = new Thickness(3), + IsEnabled = false + }; + + if (Binding != null) + { + myRatingElement.Bind(BindingTarget, Binding); + } + return myRatingElement; + } + + protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem) + { + var myRatingElement = new MyRatingCellEditor + { + Name = "CellMyRatingCellEditor", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + AllRatingsVisible = true, + Margin = new Thickness(3) + }; + + return myRatingElement; + } + + protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) + => editingElement is MyRatingCellEditor myRating + ? myRating.Rating + : DefaultRating; + + protected override void CancelCellEdit(IControl editingElement, object uneditedValue) + { + if (editingElement is MyRatingCellEditor myRating) + { + var uneditedRating = uneditedValue as Rating; + myRating.Rating = uneditedRating ?? DefaultRating; + } + } + } +} diff --git a/Source/LibationAvalonia/Controls/RatingBox.axaml.cs b/Source/LibationAvalonia/Controls/RatingBox.axaml.cs deleted file mode 100644 index f517ec5c..00000000 --- a/Source/LibationAvalonia/Controls/RatingBox.axaml.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using DataLayer; -using NPOI.POIFS.Storage; -using System.Linq; - -namespace LibationAvalonia.Controls -{ - public partial class RatingBox : UserControl - { - private const string SOLID_STAR = "★"; - private const string HOLLOW_STAR = "☆"; - private static readonly char[] FIVE_STARS = { '★', '★', '★', '★', '★' }; - - public static readonly StyledProperty RatingProperty = - AvaloniaProperty.Register(nameof(Rating)); - - public Rating Rating - { - get { return GetValue(RatingProperty); } - set { SetValue(RatingProperty, value); } - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property.Name == nameof(Rating) && Rating is not null) - { - tblockOverallRating.Text = StarRating((int)Rating.OverallRating); - tblockPerformRating.Text = StarRating((int)Rating.PerformanceRating); - tblockStoryRating.Text = StarRating((int)Rating.StoryRating); - - if (this.IsPointerOver) - RatingBox_PointerEntered(this, null); - else - RatingBox_PointerExited(this, null); - } - base.OnPropertyChanged(change); - } - public RatingBox() - { - InitializeComponent(); - PointerEntered += RatingBox_PointerEntered; - PointerExited += RatingBox_PointerExited; - } - - private void RatingBox_PointerExited(object sender, Avalonia.Input.PointerEventArgs e) - { - tblockOverall.IsVisible = Rating?.OverallRating > 0; - tblockPerform.IsVisible = Rating?.PerformanceRating > 0; - tblockStory.IsVisible = Rating?.StoryRating > 0; - } - - private void RatingBox_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e) - { - tblockOverall.IsVisible = true; - tblockPerform.IsVisible = true; - tblockStory.IsVisible = true; - } - - private static string StarRating(int rating) => new string(FIVE_STARS, 0, rating); - public void Panel_PointerExited(object sender, Avalonia.Input.PointerEventArgs e) - { - var panel = sender as Panel; - var stackPanel = panel.Children.OfType().Single(); - - panel.Children.OfType().Single().IsVisible = true; - stackPanel.IsVisible = false; - - foreach (TextBlock child in stackPanel.Children) - child.Text = HOLLOW_STAR; - } - - public void Panel_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e) - { - var panel = sender as Panel; - - panel.Children.OfType().Single().IsVisible = false; - panel.Children.OfType().Single().IsVisible = true; - } - - public void Star_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e) - { - var thisTbox = sender as TextBlock; - var stackPanel = thisTbox.Parent as StackPanel; - - var star = SOLID_STAR; - - foreach (TextBlock child in stackPanel.Children) - { - child.Text = star; - if (child == thisTbox) star = HOLLOW_STAR; - } - } - - public void Star_Tapped(object sender, Avalonia.Input.TappedEventArgs e) - { - var overall = Rating.OverallRating; - var perform = Rating.PerformanceRating; - var story = Rating.StoryRating; - - var thisTbox = sender as TextBlock; - var stackPanel = thisTbox.Parent as StackPanel; - - int newRating = 0; - foreach (var tbox in stackPanel.Children) - { - newRating++; - if (tbox == thisTbox) break; - } - - var ratingName = ((Panel)stackPanel.Parent).Children.OfType().Single().Name; - - if (ratingName == tblockOverallRating.Name) - overall = newRating; - else if (ratingName == tblockPerformRating.Name) - perform = newRating; - else if (ratingName == tblockStoryRating.Name) - story = newRating; - - if (overall + perform + story == 0f) return; - - Rating = new Rating(overall, perform, story); - } - } -} diff --git a/Source/LibationAvalonia/ViewModels/GridEntry.cs b/Source/LibationAvalonia/ViewModels/GridEntry.cs index 71007456..bea4fadb 100644 --- a/Source/LibationAvalonia/ViewModels/GridEntry.cs +++ b/Source/LibationAvalonia/ViewModels/GridEntry.cs @@ -45,7 +45,7 @@ namespace LibationAvalonia.ViewModels public string Misc { get; protected set; } public string Description { get; protected set; } public string ProductRating { get; protected set; } - public string MyRatingString => Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); + public string MyRatingString => MyRating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); protected Rating _myRating; public Rating MyRating { diff --git a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs b/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs index a172e87b..c79f4cfd 100644 --- a/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs +++ b/Source/LibationAvalonia/ViewModels/LibraryBookEntry.cs @@ -65,7 +65,9 @@ namespace LibationAvalonia.ViewModels Title = Book.Title; Series = Book.SeriesNames(); Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min"; - _myRating = Book.UserDefinedItem.Rating; + //Ratings are changed using Update(), which is a problem for Avalonia data bindings because + //the reference doesn't change. Clone the rating so that it updates within Avalonia properly. + _myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating); PurchaseDate = libraryBook.DateAdded.ToString("d"); ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); Authors = Book.AuthorNames(); diff --git a/Source/LibationAvalonia/ViewModels/SeriesEntry.cs b/Source/LibationAvalonia/ViewModels/SeriesEntry.cs index 67a46db7..c0ca2965 100644 --- a/Source/LibationAvalonia/ViewModels/SeriesEntry.cs +++ b/Source/LibationAvalonia/ViewModels/SeriesEntry.cs @@ -68,7 +68,9 @@ namespace LibationAvalonia.ViewModels Title = Book.Title; Series = Book.SeriesNames(); - _myRating = Book.UserDefinedItem.Rating; + //Ratings are changed using Update(), which is a problem for Avalonia data bindings because + //the reference doesn't change. Clone the rating so that it updates within Avalonia properly. + _myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating); ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace(""); Authors = Book.AuthorNames(); Narrators = Book.NarratorNames(); diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml b/Source/LibationAvalonia/Views/ProductsDisplay.axaml index 7175106d..0a86d97f 100644 --- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml +++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml @@ -140,7 +140,7 @@ - + @@ -160,16 +160,9 @@ - - - - - - - - - + + diff --git a/Source/LibationSearchEngine/SearchEngine.cs b/Source/LibationSearchEngine/SearchEngine.cs index c4537d1f..52404903 100644 --- a/Source/LibationSearchEngine/SearchEngine.cs +++ b/Source/LibationSearchEngine/SearchEngine.cs @@ -90,8 +90,8 @@ namespace LibationSearchEngine ["ProductRating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(), ["Rating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(), - ["UserRating"] = lb => userRating(lb.Book), - ["MyRating"] = lb => userRating(lb.Book) + ["UserRating"] = lb => userOverallRating(lb.Book), + ["MyRating"] = lb => userOverallRating(lb.Book) } ); @@ -136,7 +136,7 @@ namespace LibationSearchEngine var narrators = lb.Book.Narrators.Select(a => a.Name).ToArray(); return authors.Intersect(narrators).Any(); } - private static string userRating(Book book) => book.UserDefinedItem.Rating.OverallRating.ToLuceneString(); + private static string userOverallRating(Book book) => book.UserDefinedItem.Rating.OverallRating.ToLuceneString(); private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated; private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error; @@ -300,7 +300,7 @@ namespace LibationSearchEngine // fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY. // ie: must remove old before adding new else will create unwanted duplicates. - var v1 = userRating(book); + var v1 = userOverallRating(book); d.RemoveField("UserRating"); d.AddNotAnalyzed("UserRating", v1); d.RemoveField("MyRating"); diff --git a/Source/LibationWinForms/GridView/MyRatingCellEditor.cs b/Source/LibationWinForms/GridView/MyRatingCellEditor.cs index 7691b32e..6230b0eb 100644 --- a/Source/LibationWinForms/GridView/MyRatingCellEditor.cs +++ b/Source/LibationWinForms/GridView/MyRatingCellEditor.cs @@ -39,10 +39,10 @@ namespace LibationWinForms.GridView private void Star_MouseEnter(object sender, EventArgs e) { var thisTbox = sender as Label; - var stackPanel = thisTbox.Parent as Panel; + var panel = thisTbox.Parent as Panel; var star = SOLID_STAR; - foreach (Label child in stackPanel.Controls) + foreach (Label child in panel.Controls) { child.Text = star; if (child == thisTbox) star = HOLLOW_STAR; diff --git a/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs b/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs index ea174443..331c6123 100644 --- a/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs +++ b/Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs @@ -26,7 +26,7 @@ namespace LibationWinForms.GridView internal class MyRatingGridViewCell : DataGridViewTextBoxCell { - private Rating DefaultRating => new Rating(0, 0, 0); + private static Rating DefaultRating => new Rating(0, 0, 0); public override object DefaultNewRowValue => DefaultRating; public override Type EditType => typeof(MyRatingCellEditor); public override Type ValueType => typeof(Rating); @@ -45,6 +45,7 @@ namespace LibationWinForms.GridView if (value is Rating rating) { var starString = rating.ToStarString(); + ToolTipText = starString; base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, starString, starString, errorText, cellStyle, advancedBorderStyle, paintParts); } else