using System; using System.Windows; using System.Windows.Automation; using System.Windows.Automation.Peers; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using TextBlock = System.Windows.Controls.TextBlock; namespace WPFluent.Controls; public partial class PersonPicture : Control { static PersonPicture() { DefaultStyleKeyProperty.OverrideMetadata(typeof(PersonPicture), new FrameworkPropertyMetadata(typeof(PersonPicture))); } public PersonPicture() { TemplateSettings = new PersonPictureTemplateSettings(); Unloaded += OnUnloaded; SizeChanged += OnSizeChanged; } protected override AutomationPeer OnCreateAutomationPeer() { return new PersonPictureAutomationPeer(this); } public override void OnApplyTemplate() { base.OnApplyTemplate(); m_initialsTextBlock = (TextBlock)GetTemplateChild("InitialsTextBlock"); m_badgeNumberTextBlock = (TextBlock)GetTemplateChild("BadgeNumberTextBlock"); m_badgeGlyphIcon = (FontIcon)GetTemplateChild("BadgeGlyphIcon"); m_badgingEllipse = (Ellipse)GetTemplateChild("BadgingEllipse"); m_badgingBackgroundEllipse = (Ellipse)GetTemplateChild("BadgingBackgroundEllipse"); UpdateBadge(); UpdateIfReady(); } /// /// Helper to determine the initials that should be shown. /// string GetInitials() { if (!string.IsNullOrEmpty(Initials)) { return Initials; } else if (!string.IsNullOrEmpty(m_displayNameInitials)) { return m_displayNameInitials; } else { return m_contactDisplayNameInitials; } } /// /// Helper to determine the image source that should be shown. /// ImageSource GetImageSource() { if (ProfilePicture != null) { return ProfilePicture; } else { return m_contactImageSource; } } /// /// Updates Control elements, if available, with the latest values. /// void UpdateIfReady() { string initials = GetInitials(); ImageSource imageSrc = GetImageSource(); var templateSettings = TemplateSettings; templateSettings.ActualInitials = initials; if (imageSrc != null) { var imageBrush = templateSettings.ActualImageBrush; if (imageBrush == null) { imageBrush = new ImageBrush(); imageBrush.Stretch = Stretch.UniformToFill; templateSettings.ActualImageBrush = imageBrush; } imageBrush.ImageSource = imageSrc; } else { templateSettings.ActualImageBrush = null!; } // If the control is converted to 'Group-mode', we'll clear individual-specific information. // When IsGroup evaluates to false, we will restore state. if (IsGroup) { VisualStateManager.GoToState(this, "Group", false); } else { if (imageSrc != null) { VisualStateManager.GoToState(this, "Photo", false); } else if (!string.IsNullOrEmpty(initials)) { VisualStateManager.GoToState(this, "Initials", false); } else { VisualStateManager.GoToState(this, "NoPhotoOrInitials", false); } } UpdateAutomationName(); } /// /// Updates the state of the Badging element. /// void UpdateBadge() { if (BadgeImageSource != null) { UpdateBadgeImageSource(); } else if (BadgeNumber != 0) { UpdateBadgeNumber(); } else if (!string.IsNullOrEmpty(BadgeGlyph)) { UpdateBadgeGlyph(); } // No badge properties set, so clear the badge XAML else { VisualStateManager.GoToState(this, "NoBadge", false); var badgeNumberTextBlock = m_badgeNumberTextBlock; if (badgeNumberTextBlock != null) { badgeNumberTextBlock.Text = ""; } var badgeGlyphIcon = m_badgeGlyphIcon; if (badgeGlyphIcon != null) { badgeGlyphIcon.Glyph = ""; } } UpdateAutomationName(); } /// /// Updates Badging Number text element. /// void UpdateBadgeNumber() { if (m_badgingEllipse == null || m_badgeNumberTextBlock == null) { return; } int badgeNumber = BadgeNumber; if (badgeNumber <= 0) { VisualStateManager.GoToState(this, "NoBadge", false); m_badgeNumberTextBlock.Text = ""; return; } // should have badging number to show if we are here VisualStateManager.GoToState(this, "BadgeWithoutImageSource", false); if (badgeNumber <= 99) { m_badgeNumberTextBlock.Text = badgeNumber.ToString(); } else { m_badgeNumberTextBlock.Text = "99+"; } } /// /// Updates Badging Glyph element. /// void UpdateBadgeGlyph() { if (m_badgingEllipse == null || m_badgeGlyphIcon == null) { return; } string badgeGlyph = BadgeGlyph; if (string.IsNullOrEmpty(badgeGlyph)) { VisualStateManager.GoToState(this, "NoBadge", false); m_badgeGlyphIcon.Glyph = ""; return; } // should have badging Glyph to show if we are here VisualStateManager.GoToState(this, "BadgeWithoutImageSource", false); m_badgeGlyphIcon.Glyph = badgeGlyph; } /// /// Updates Badging Image element. /// void UpdateBadgeImageSource() { if (m_badgeImageBrush == null) { m_badgeImageBrush = (ImageBrush)GetTemplateChild("BadgeImageBrush"); } if (m_badgingEllipse == null || m_badgeImageBrush == null) { return; } m_badgeImageBrush.ImageSource = BadgeImageSource; if (BadgeImageSource != null) { VisualStateManager.GoToState(this, "BadgeWithImageSource", false); } else { VisualStateManager.GoToState(this, "NoBadge", false); } } /// /// Sets the UI Automation name for the control based on contact name and badge state. /// void UpdateAutomationName() { string automationName; string contactName; // The AutomationName for the control is in the format: PersonName, BadgeInformation. // PersonName is set based on the name / initial properties in the order below. // if none exist, it defaults to "Person" if (IsGroup) { contactName = "Group"; } else if (!string.IsNullOrEmpty(DisplayName)) { contactName = DisplayName; } else if (!string.IsNullOrEmpty(Initials)) { contactName = Initials; } else { contactName = "Person"; } // BadgeInformation portion of the AutomationName is set to 'n items' if there is a BadgeNumber, // or 'icon' for BadgeGlyph or BadgeImageSource. If BadgeText is specified, it will override // the string 'items' or 'icon' if (BadgeNumber > 0) { if (!string.IsNullOrEmpty(BadgeText)) { automationName = string.Format( "{0}, {1}", contactName, BadgeNumber, BadgeText); } else { automationName = string.Format( GetLocalizedPluralBadgeItemStringResource(BadgeNumber), contactName, BadgeNumber); } } else if (!string.IsNullOrEmpty(BadgeGlyph) || BadgeImageSource != null) { if (!string.IsNullOrEmpty(BadgeText)) { automationName = string.Format( "{0}, {1}", contactName, BadgeText); } else { automationName = string.Format( "{0}, icon", contactName); } } else { automationName = contactName; } AutomationProperties.SetName(this, automationName); } // Helper functions string GetLocalizedPluralBadgeItemStringResource(int numericValue) { int valueMod10 = numericValue % 10; string value; if (numericValue == 1) // Singular { value = "{0}, {1:d} item"; } else if (numericValue == 2) // 2 { value = "{0}, {1:d} items"; } else if (numericValue == 3 || numericValue == 4) // 3,4 { value = "{0}, {1:d} items"; } else if (numericValue >= 5 && numericValue <= 10) // 5-10 { value = "{0}, {1:d} items"; } else if (numericValue >= 11 && numericValue <= 19) // 11-19 { value = "{0}, {1:d} items"; } else if (valueMod10 == 1) // 21, 31, 41, etc. { value = "{0}, {1:d} items"; } else if (valueMod10 >= 2 && valueMod10 <= 4) // 22-24, 32-34, 42-44, etc. { value = "{0}, {1:d} items"; } else // Everything else... 0, 20, 25-30, 35-40, etc. { value = "{0}, {1:d} items"; } return value; } void PrivateOnPropertyChanged(DependencyPropertyChangedEventArgs args) { DependencyProperty property = args.Property; if (property == BadgeNumberProperty || property == BadgeGlyphProperty || property == BadgeImageSourceProperty) { UpdateBadge(); } else if (property == BadgeTextProperty) { UpdateAutomationName(); } else if (property == DisplayNameProperty) { OnDisplayNameChanged(args); } else if (property == ProfilePictureProperty || property == InitialsProperty || property == IsGroupProperty) { UpdateIfReady(); } // No additional action required for s_PreferSmallImageProperty } // DependencyProperty changed event handlers void OnDisplayNameChanged(DependencyPropertyChangedEventArgs args) { m_displayNameInitials = InitialsGenerator.InitialsFromDisplayName(DisplayName); UpdateIfReady(); } // Event handlers void OnSizeChanged(object? sender, SizeChangedEventArgs args) { { bool widthChanged = args.NewSize.Width != args.PreviousSize.Width; bool heightChanged = args.NewSize.Height != args.PreviousSize.Height; double newSize; if (widthChanged && heightChanged) { // Maintain circle by enforcing the new size on both Width and Height. // To do so, we will use the minimum value. newSize = (args.NewSize.Width < args.NewSize.Height) ? args.NewSize.Width : args.NewSize.Height; } else if (widthChanged) { newSize = args.NewSize.Width; } else if (heightChanged) { newSize = args.NewSize.Height; } else { return; } Height = newSize; Width = newSize; } // Calculate the FontSize of the control's text. Design guidelines have specified the // font size to be 42% of the container. Since it's circular, 42% of either Width or Height. // Note that we cap it to a minimum of 1, since a font size of less than 1 is an invalid value // that will result in a failure. double fontSize = Math.Max(1.0, Width * .42); var initialsTextBlock = m_initialsTextBlock; if (initialsTextBlock != null) { initialsTextBlock.FontSize = fontSize; } if (m_badgingEllipse != null && m_badgingBackgroundEllipse != null && m_badgeNumberTextBlock != null && m_badgeGlyphIcon != null) { // Maintain badging circle and font size by enforcing the new size on both Width and Height. // Design guidelines have specified the font size to be 60% of the badging plate, and we want to keep // badging plate to be about 50% of the control so that don't block the initial/profile picture. double newSize = (args.NewSize.Width < args.NewSize.Height) ? args.NewSize.Width : args.NewSize.Height; m_badgingEllipse.Height = newSize * 0.5; m_badgingEllipse.Width = newSize * 0.5; m_badgingBackgroundEllipse.Height = newSize * 0.5; m_badgingBackgroundEllipse.Width = newSize * 0.5; m_badgeNumberTextBlock.FontSize = Math.Max(1.0, m_badgingEllipse.Height * 0.6); m_badgeGlyphIcon.FontSize = Math.Max(1.0, m_badgingEllipse.Height * 0.6); } } void OnUnloaded(object? sender, RoutedEventArgs e) { //if (auto profilePictureReadAsync = m_profilePictureReadAsync.get()) //{ // profilePictureReadAsync.Cancel(); //} } /// /// XAML Element for the first TextBlock matching x:Name of InitialsTextBlock. /// private TextBlock m_initialsTextBlock = null!; /// /// XAML Element for the first TextBlock matching x:Name of BadgeNumberTextBlock. /// private TextBlock m_badgeNumberTextBlock = null!; /// /// XAML Element for the first TextBlock matching x:Name of BadgeGlyphIcon. /// private FontIcon m_badgeGlyphIcon = null!; /// /// XAML Element for the first ImageBrush matching x:Name of BadgeImageBrush. /// private ImageBrush m_badgeImageBrush = null!; /// /// XAML Element for the first Ellipse matching x:Name of BadgingBackgroundEllipse. /// private Ellipse m_badgingEllipse = null!; /// /// XAML Element for the first Ellipse matching x:Name of BadgingEllipse. /// private Ellipse m_badgingBackgroundEllipse = null!; /// /// The async operation object representing the loading and assignment of the Thumbnail. /// //tracker_ref> m_profilePictureReadAsync; /// /// The initials from the DisplayName property. /// private string m_displayNameInitials = null!; /// /// The initials from the Contact property. /// private string m_contactDisplayNameInitials = null!; /// /// The ImageSource from the Contact property. /// private ImageSource m_contactImageSource = null!; }