Skip to content

Commit bd40910

Browse files
authored
Merge pull request #205 from TeamWheelWizard/MiiImageHover
Add Mii image hover support to MiiBlock
2 parents 9bf06b1 + c51f1fe commit bd40910

4 files changed

Lines changed: 324 additions & 6 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<UserControl xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400"
6+
x:Class="WheelWizard.Views.BehaviorComponent.MiiImageLoaderWithHover"
7+
xmlns:behaviorComp="clr-namespace:WheelWizard.Views.BehaviorComponent"
8+
xmlns:components="clr-namespace:WheelWizard.Views.Components"
9+
xmlns:conv="clr-namespace:WheelWizard.Views.Converters"
10+
x:DataType="behaviorComp:MiiImageLoaderWithHover" x:Name="Self"
11+
VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
12+
<behaviorComp:AspectGrid x:Name="MiiImageContainer" ClipToBounds="True" ColumnDefinitions="*,3*,*"
13+
RowDefinitions="*,3*,*"
14+
VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
15+
<components:LoadingIcon HorizontalAlignment="Stretch" VerticalAlignment="Stretch" x:Name="MiiLoadingIcon"
16+
Foreground="{Binding LoadingColor, ElementName=Self}" Grid.Column="1" Grid.Row="1" >
17+
<components:LoadingIcon.IsVisible>
18+
<MultiBinding Converter="{x:Static BoolConverters.And}">
19+
<Binding Path="MiiLoaded" ElementName="Self"
20+
Converter="{x:Static BoolConverters.Not}"/>
21+
<Binding Path="GeneratedImages" ElementName="Self"
22+
Converter="{x:Static conv:CollectionConverters.FirstIsNull}" />
23+
</MultiBinding>
24+
</components:LoadingIcon.IsVisible>
25+
</components:LoadingIcon>
26+
27+
<Path HorizontalAlignment="Center" VerticalAlignment="Bottom" Stretch="Uniform"
28+
Grid.ColumnSpan="3" Grid.Column="0" Grid.RowSpan="2" Grid.Row="1"
29+
Fill="{Binding FallBackColor, ElementName=Self}" Data="{StaticResource User}">
30+
<Path.IsVisible>
31+
<MultiBinding Converter="{x:Static BoolConverters.And}">
32+
<Binding Path="GeneratedImages" ElementName="Self"
33+
Converter="{x:Static conv:CollectionConverters.FirstIsNull}" />
34+
<Binding Path="MiiLoaded" ElementName="Self"/>
35+
</MultiBinding>
36+
</Path.IsVisible>
37+
</Path>
38+
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
39+
Margin="{Binding ImageOnlyMargin, ElementName=Self}" Grid.ColumnSpan="3" Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
40+
41+
<!-- Normal expression -->
42+
<Image HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
43+
Source="{Binding GeneratedImages, ElementName=Self, Converter={x:Static conv:CollectionConverters.First }}"
44+
IsVisible="{Binding ShowNormalImage, ElementName=Self}"/>
45+
46+
<!-- Hover expression -->
47+
<Image HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
48+
Source="{Binding GeneratedImages, ElementName=Self, Converter={x:Static conv:CollectionConverters.Second }}"
49+
IsVisible="{Binding ShowHoverImage, ElementName=Self}"/>
50+
</Grid>
51+
52+
</behaviorComp:AspectGrid>
53+
</UserControl>
54+
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System.ComponentModel;
2+
using Avalonia;
3+
using Avalonia.Media;
4+
using WheelWizard.MiiImages;
5+
using WheelWizard.MiiImages.Domain;
6+
using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
7+
8+
namespace WheelWizard.Views.BehaviorComponent;
9+
10+
public partial class MiiImageLoaderWithHover : BaseMiiImage
11+
{
12+
public static readonly StyledProperty<bool> IsHoveredProperty = AvaloniaProperty.Register<MiiImageLoaderWithHover, bool>(
13+
nameof(IsHovered),
14+
false
15+
);
16+
17+
public bool IsHovered
18+
{
19+
get => GetValue(IsHoveredProperty);
20+
set => SetValue(IsHoveredProperty, value);
21+
}
22+
23+
public static readonly StyledProperty<bool> ShowNormalImageProperty = AvaloniaProperty.Register<MiiImageLoaderWithHover, bool>(
24+
nameof(ShowNormalImage),
25+
true
26+
);
27+
28+
public bool ShowNormalImage
29+
{
30+
get => GetValue(ShowNormalImageProperty);
31+
private set => SetValue(ShowNormalImageProperty, value);
32+
}
33+
34+
public static readonly StyledProperty<bool> ShowHoverImageProperty = AvaloniaProperty.Register<MiiImageLoaderWithHover, bool>(
35+
nameof(ShowHoverImage),
36+
false
37+
);
38+
39+
public bool ShowHoverImage
40+
{
41+
get => GetValue(ShowHoverImageProperty);
42+
private set => SetValue(ShowHoverImageProperty, value);
43+
}
44+
45+
private void UpdateImageVisibility()
46+
{
47+
var hasHoverImage = GeneratedImages.Count > 1 && GeneratedImages[1] != null;
48+
49+
if (IsHovered && hasHoverImage)
50+
{
51+
ShowNormalImage = false;
52+
ShowHoverImage = true;
53+
}
54+
else
55+
{
56+
ShowNormalImage = true;
57+
ShowHoverImage = false;
58+
}
59+
}
60+
61+
public static readonly StyledProperty<IBrush> LoadingColorProperty = AvaloniaProperty.Register<MiiImageLoaderWithHover, IBrush>(
62+
nameof(LoadingColor),
63+
new SolidColorBrush(ViewUtils.Colors.Neutral900)
64+
);
65+
66+
public IBrush LoadingColor
67+
{
68+
get => GetValue(LoadingColorProperty);
69+
set => SetValue(LoadingColorProperty, value);
70+
}
71+
72+
public static readonly StyledProperty<IBrush> FallBackColorProperty = AvaloniaProperty.Register<MiiImageLoaderWithHover, IBrush>(
73+
nameof(FallBackColor),
74+
new SolidColorBrush(ViewUtils.Colors.Neutral700)
75+
);
76+
77+
public IBrush FallBackColor
78+
{
79+
get => GetValue(FallBackColorProperty);
80+
set => SetValue(FallBackColorProperty, value);
81+
}
82+
83+
public static readonly StyledProperty<Thickness> ImageOnlyMarginProperty = AvaloniaProperty.Register<
84+
MiiImageLoaderWithHover,
85+
Thickness
86+
>(nameof(ImageOnlyMargin), enableDataValidation: true);
87+
88+
public Thickness ImageOnlyMargin
89+
{
90+
get => GetValue(ImageOnlyMarginProperty);
91+
set => SetValue(ImageOnlyMarginProperty, value);
92+
}
93+
94+
public static readonly StyledProperty<MiiImageSpecifications> ImageVariantProperty = AvaloniaProperty.Register<
95+
MiiImageLoaderWithHover,
96+
MiiImageSpecifications
97+
>(nameof(ImageVariant), MiiImageVariants.OnlinePlayerSmall, coerce: CoerceVariant);
98+
99+
public MiiImageSpecifications ImageVariant
100+
{
101+
get => GetValue(ImageVariantProperty);
102+
set => SetValue(ImageVariantProperty, value);
103+
}
104+
105+
public static readonly StyledProperty<MiiImageSpecifications?> HoverVariantProperty = AvaloniaProperty.Register<
106+
MiiImageLoaderWithHover,
107+
MiiImageSpecifications?
108+
>(nameof(HoverVariant), coerce: CoerceHoverVariant);
109+
110+
public MiiImageSpecifications? HoverVariant
111+
{
112+
get => GetValue(HoverVariantProperty);
113+
set => SetValue(HoverVariantProperty, value);
114+
}
115+
116+
private static MiiImageSpecifications? CoerceHoverVariant(AvaloniaObject o, MiiImageSpecifications? value)
117+
{
118+
var loader = (MiiImageLoaderWithHover)o;
119+
// Reload both variants when hover variant changes (if Mii is already set)
120+
// If Mii is not set yet, OnMiiChanged will handle the reload
121+
if (loader.Mii != null && value != null)
122+
{
123+
loader.ReloadBothVariants();
124+
}
125+
return value;
126+
}
127+
128+
private static MiiImageSpecifications CoerceVariant(AvaloniaObject o, MiiImageSpecifications value)
129+
{
130+
((MiiImageLoaderWithHover)o).OnVariantChanged(value);
131+
return value;
132+
}
133+
134+
public MiiImageLoaderWithHover()
135+
{
136+
InitializeComponent();
137+
PropertyChanged += MiiImageLoaderWithHover_PropertyChanged;
138+
GeneratedImages.CollectionChanged += (s, e) => UpdateImageVisibility();
139+
MiiImageLoaded += (s, e) => UpdateImageVisibility();
140+
}
141+
142+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
143+
{
144+
base.OnPropertyChanged(change);
145+
146+
if (change.Property == IsHoveredProperty)
147+
{
148+
UpdateImageVisibility();
149+
}
150+
}
151+
152+
private void MiiImageLoaderWithHover_PropertyChanged(object? sender, PropertyChangedEventArgs e)
153+
{
154+
if (e.PropertyName == nameof(GeneratedImages))
155+
{
156+
UpdateImageVisibility();
157+
}
158+
}
159+
160+
private void OnVariantChanged(MiiImageSpecifications newSpecifications)
161+
{
162+
ReloadBothVariants();
163+
}
164+
165+
protected override void OnMiiChanged(Mii? newMii)
166+
{
167+
// Always reload both variants when Mii changes
168+
// This ensures both images are loaded even if hover variant is set later
169+
ReloadBothVariants();
170+
}
171+
172+
public void ReloadBothVariants()
173+
{
174+
if (Mii == null)
175+
return;
176+
177+
var variants = new List<MiiImageSpecifications>();
178+
179+
// Always load the normal variant first
180+
variants.Add(ImageVariant);
181+
182+
// If hover variant is set, load it as the second image
183+
if (HoverVariant != null)
184+
{
185+
variants.Add(HoverVariant);
186+
}
187+
188+
ReloadImages(Mii, variants);
189+
}
190+
}

WheelWizard/Views/Components/WhWzLibrary/MiiBlock.axaml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@
4444
IsVisible="{TemplateBinding Mii, Converter={x:Static ObjectConverters.IsNull} }"
4545
Width="1000" Foreground="{StaticResource Neutral500}" />
4646

47-
<behaviorComponent:MiiImageLoader HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
48-
IsVisible="{TemplateBinding Mii, Converter={x:Static ObjectConverters.IsNotNull} }"
49-
Mii="{TemplateBinding Mii}" ImageOnlyMargin="-4,-9,-4,0"
50-
FallBackColor="{StaticResource Neutral500}"
51-
LoadingColor="{StaticResource Neutral950}"
52-
ImageVariant="{x:Static miiVars:MiiImageVariants.MiiBlockProfile}" />
47+
<behaviorComponent:MiiImageLoaderWithHover x:Name="PART_MiiImageLoader" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
48+
IsVisible="{TemplateBinding Mii, Converter={x:Static ObjectConverters.IsNotNull} }"
49+
Mii="{TemplateBinding Mii}" ImageOnlyMargin="-4,-9,-4,0"
50+
FallBackColor="{StaticResource Neutral500}"
51+
LoadingColor="{StaticResource Neutral950}"
52+
ImageVariant="{x:Static miiVars:MiiImageVariants.MiiBlockProfile}"
53+
ReloadMethod="KeepAllUntilNew" />
5354

5455
<PathIcon Data="{StaticResource StarIcon}" Height="17"
5556
IsVisible="{TemplateBinding IsFavorite}" HorizontalAlignment="Left" VerticalAlignment="Top"
@@ -85,6 +86,10 @@
8586
<Setter Property="Opacity" Value="1" />
8687
</Style>
8788

89+
<Style Selector="^:pointerover /template/ behaviorComponent|MiiImageLoaderWithHover#PART_MiiImageLoader">
90+
<Setter Property="IsHovered" Value="True" />
91+
</Style>
92+
8893
<Style Selector="^[IsFavorite=True] /template/ Border#PART_MainBorder">
8994
<Setter Property="BorderBrush" Value="{StaticResource Warning500}"></Setter>
9095
<Style Selector="^ PathIcon#PART_AddIcon">

WheelWizard/Views/Components/WhWzLibrary/MiiBlock.axaml.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
using Avalonia;
22
using Avalonia.Controls;
3+
using Avalonia.Controls.Primitives;
34
using Avalonia.Input;
45
using Avalonia.Interactivity;
6+
using WheelWizard.MiiImages;
7+
using WheelWizard.MiiImages.Domain;
8+
using WheelWizard.Views.BehaviorComponent;
59
using WheelWizard.WiiManagement;
610
using WheelWizard.WiiManagement.MiiManagement;
711
using WheelWizard.WiiManagement.MiiManagement.Domain.Mii;
@@ -11,6 +15,7 @@ namespace WheelWizard.Views.Components;
1115
public class MiiBlock : RadioButton
1216
{
1317
private static ContextMenu? s_oldMenu;
18+
private MiiImageLoaderWithHover? _miiImageLoader;
1419

1520
public static readonly StyledProperty<Mii?> MiiProperty = AvaloniaProperty.Register<MiiBlock, Mii?>(nameof(Mii));
1621

@@ -44,6 +49,41 @@ public bool IsGlobal
4449
private set => SetValue(IsGlobalProperty, value);
4550
}
4651

52+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
53+
{
54+
base.OnApplyTemplate(e);
55+
56+
_miiImageLoader = e.NameScope.Find<MiiImageLoaderWithHover>("PART_MiiImageLoader");
57+
58+
// Set hover variant immediately
59+
if (_miiImageLoader != null)
60+
{
61+
// Create hover variant with smile expression
62+
var hoverVariant = MiiImageVariants.MiiBlockProfile.Clone();
63+
hoverVariant.Name = "MiiBlockProfileHover";
64+
hoverVariant.Expression = MiiImageSpecifications.FaceExpression.smile;
65+
_miiImageLoader.HoverVariant = hoverVariant;
66+
67+
// If Mii is already set, trigger reload
68+
if (Mii != null)
69+
{
70+
_miiImageLoader.ReloadBothVariants();
71+
}
72+
}
73+
}
74+
75+
private void SetupHoverVariant()
76+
{
77+
if (_miiImageLoader != null)
78+
{
79+
// Create hover variant with smile expression
80+
var hoverVariant = MiiImageVariants.MiiBlockProfile.Clone();
81+
hoverVariant.Name = "MiiBlockProfileHover";
82+
hoverVariant.Expression = MiiImageSpecifications.FaceExpression.smile;
83+
_miiImageLoader.HoverVariant = hoverVariant;
84+
}
85+
}
86+
4787
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
4888
{
4989
base.OnPropertyChanged(change);
@@ -54,12 +94,41 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
5494
MiiName = mii?.Name.ToString();
5595
IsFavorite = mii?.IsFavorite ?? false;
5696
IsGlobal = mii?.IsGlobal() ?? false;
97+
98+
// Ensure hover variant is set when Mii changes
99+
if (_miiImageLoader != null)
100+
{
101+
SetupHoverVariant();
102+
// Trigger reload if Mii is set
103+
if (mii != null)
104+
{
105+
_miiImageLoader.ReloadBothVariants();
106+
}
107+
}
57108
}
58109

59110
Tag = MiiName ?? String.Empty;
60111
ClipToBounds = string.IsNullOrWhiteSpace(MiiName);
61112
}
62113

114+
protected override void OnPointerEntered(PointerEventArgs e)
115+
{
116+
base.OnPointerEntered(e);
117+
if (_miiImageLoader != null)
118+
{
119+
_miiImageLoader.IsHovered = true;
120+
}
121+
}
122+
123+
protected override void OnPointerExited(PointerEventArgs e)
124+
{
125+
base.OnPointerExited(e);
126+
if (_miiImageLoader != null)
127+
{
128+
_miiImageLoader.IsHovered = false;
129+
}
130+
}
131+
63132
protected override void OnPointerPressed(PointerPressedEventArgs e)
64133
{
65134
e.Handled = true;

0 commit comments

Comments
 (0)