diff --git a/ModernWpf.Controls/Common/SharedHelpers.cs b/ModernWpf.Controls/Common/SharedHelpers.cs
index 2509f1b5..1aaf8c54 100644
--- a/ModernWpf.Controls/Common/SharedHelpers.cs
+++ b/ModernWpf.Controls/Common/SharedHelpers.cs
@@ -119,6 +119,11 @@ public static Window GetActiveWindow()
}
public static string SafeSubstring(this string s, int startIndex)
+ {
+ return s.SafeSubstring(startIndex, s.Length - startIndex);
+ }
+
+ public static string SafeSubstring(this string s, int startIndex, int length)
{
if (s is null)
{
@@ -130,7 +135,12 @@ public static string SafeSubstring(this string s, int startIndex)
return string.Empty;
}
- return s.Substring(startIndex);
+ if (length > s.Length - startIndex)
+ {
+ length = s.Length - startIndex;
+ }
+
+ return s.Substring(startIndex, length);
}
public static bool IndexOf(this UIElementCollection collection, UIElement element, out int index)
diff --git a/ModernWpf.Controls/DropDownButton/DropDownButtonAutomationPeer.cs b/ModernWpf.Controls/DropDownButton/DropDownButtonAutomationPeer.cs
index 845943a6..f55669d5 100644
--- a/ModernWpf.Controls/DropDownButton/DropDownButtonAutomationPeer.cs
+++ b/ModernWpf.Controls/DropDownButton/DropDownButtonAutomationPeer.cs
@@ -26,7 +26,7 @@ public override object GetPattern(PatternInterface patternInterface)
protected override string GetClassNameCore()
{
- return nameof(DropDownButton);
+ return typeof(DropDownButton).FullName;
}
private DropDownButton GetImpl()
diff --git a/ModernWpf.Controls/NumberBox/NumberBoxAutomationPeer.cs b/ModernWpf.Controls/NumberBox/NumberBoxAutomationPeer.cs
index 52d46057..10ee920e 100644
--- a/ModernWpf.Controls/NumberBox/NumberBoxAutomationPeer.cs
+++ b/ModernWpf.Controls/NumberBox/NumberBoxAutomationPeer.cs
@@ -1,4 +1,7 @@
-using System.Windows.Automation;
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Windows.Automation;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using ModernWpf.Controls;
@@ -23,7 +26,7 @@ public override object GetPattern(PatternInterface patternInterface)
protected override string GetClassNameCore()
{
- return nameof(NumberBox);
+ return typeof(NumberBox).FullName;
}
protected override AutomationControlType GetAutomationControlTypeCore()
diff --git a/ModernWpf.Controls/PersonPicture/InitialsGenerator.cs b/ModernWpf.Controls/PersonPicture/InitialsGenerator.cs
new file mode 100644
index 00000000..bf2cb167
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/InitialsGenerator.cs
@@ -0,0 +1,570 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Linq;
+
+namespace ModernWpf.Controls
+{
+ ///
+ /// Value indicating the general character set for a given character.
+ ///
+ enum CharacterType
+ {
+ ///
+ /// Indicates we could not match the character set.
+ ///
+ Other = 0,
+
+ ///
+ /// Member of the Latin character set.
+ ///
+ Standard = 1,
+
+ ///
+ /// Member of a symbolic character set.
+ ///
+ Symbolic = 2,
+
+ ///
+ /// Member of a character set which supports glyphs.
+ ///
+ Glyph = 3
+ };
+
+ ///
+ /// PersonPicture Control. Displays the Profile Picture, or in its absence Initials,
+ /// for a given Contact.
+ ///
+ class InitialsGenerator
+ {
+ ///
+ /// Helper function which takes a DisplayName, as generated by
+ /// Windows.ApplicationModel.Contacts, and returns a initials representation.
+ ///
+ ///
+ /// Helper function which indicates the type of characters in a given string
+ ///
+ /// String from which to detect character-set.
+ ///
+ /// Character set of the string: Latin, Symbolic, Glyph, or other.
+ ///
+ public static CharacterType GetCharacterType(string str)
+ {
+ // Since we're doing initials, we're really only interested in the first
+ // few characters. If the first three characters aren't a glyph then
+ // we don't need to count it as such because we won't be changing meaning
+ // by truncating to one or two.
+ CharacterType result = CharacterType.Other;
+
+ for (int i = 0; i < 3; i++)
+ {
+ // Break on null character. 0xFEFF is a terminating character which appears as null.
+ if ((i >= str.Length) || (str[i] == '\0') || (str[i] == 0xFEFF))
+ {
+ break;
+ }
+
+ char character = str[i];
+ CharacterType evaluationResult = GetCharacterType(character);
+
+ // In mix-match scenarios, we'll want to follow this order of precedence:
+ // Glyph > Symbolic > Roman
+ switch (evaluationResult)
+ {
+ case CharacterType.Glyph:
+ result = CharacterType.Glyph;
+ break;
+ case CharacterType.Symbolic:
+ // Don't override a Glyph state with a Symbolic State.
+ if (result != CharacterType.Glyph)
+ {
+ result = CharacterType.Symbolic;
+ }
+ break;
+ case CharacterType.Standard:
+ // Don't override a Glyph or Symbolic state with a Latin state.
+ if ((result != CharacterType.Glyph) && (result != CharacterType.Symbolic))
+ {
+ result = CharacterType.Standard;
+ }
+ break;
+ default:
+ // Preserve result's current state (if we never got data other
+ // than "Other", it'll be set to other anyway).
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Helper function which indicates the character-set of a given character.
+ ///
+ /// Character for which to detect character-set.
+ ///
+ /// Character set of the string: Latin, Symbolic, Glyph, or other.
+ ///
+ public static CharacterType GetCharacterType(char character)
+ {
+ // To ensure predictable behavior, we're currently operating on a whitelist of character sets.
+ //
+ // Each block below is a HEX range in the official Unicode spec, which defines a set
+ // of Unicode characters. Changes to the character sets would only be made by Unicode, and
+ // are highly unlikely (as it would break virtually every modern text parser).
+ // Definitions available here: http://www.unicode.org/charts/
+ //
+ // GLYPH
+ //
+ // IPA Extensions
+ if ((character >= 0x0250) && (character <= 0x02AF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Arabic
+ if ((character >= 0x0600) && (character <= 0x06FF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Arabic Supplement
+ if ((character >= 0x0750) && (character <= 0x077F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Arabic Extended-A
+ if ((character >= 0x08A0) && (character <= 0x08FF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Arabic Presentation Forms-A
+ if ((character >= 0xFB50) && (character <= 0xFDFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Arabic Presentation Forms-B
+ if ((character >= 0xFE70) && (character <= 0xFEFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Devanagari
+ if ((character >= 0x0900) && (character <= 0x097F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Devanagari Extended
+ if ((character >= 0xA8E0) && (character <= 0xA8FF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Bengali
+ if ((character >= 0x0980) && (character <= 0x09FF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Gurmukhi
+ if ((character >= 0x0A00) && (character <= 0x0A7F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Gujarati
+ if ((character >= 0x0A80) && (character <= 0x0AFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Oriya
+ if ((character >= 0x0B00) && (character <= 0x0B7F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Tamil
+ if ((character >= 0x0B80) && (character <= 0x0BFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Telugu
+ if ((character >= 0x0C00) && (character <= 0x0C7F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Kannada
+ if ((character >= 0x0C80) && (character <= 0x0CFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Malayalam
+ if ((character >= 0x0D00) && (character <= 0x0D7F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Sinhala
+ if ((character >= 0x0D80) && (character <= 0x0DFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Thai
+ if ((character >= 0x0E00) && (character <= 0x0E7F))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // Lao
+ if ((character >= 0x0E80) && (character <= 0x0EFF))
+ {
+ return CharacterType.Glyph;
+ }
+
+ // SYMBOLIC
+ //
+ // CJK Unified Ideographs
+ if ((character >= 0x4E00) && (character <= 0x9FFF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Unified Ideographs Extension
+ if ((character >= 0x3400) && (character <= 0x4DBF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Unified Ideographs Extension B
+ if ((character >= 0x20000) && (character <= 0x2A6DF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Unified Ideographs Extension C
+ if ((character >= 0x2A700) && (character <= 0x2B73F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Unified Ideographs Extension D
+ if ((character >= 0x2B740) && (character <= 0x2B81F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Radicals Supplement
+ if ((character >= 0x2E80) && (character <= 0x2EFF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Symbols and Punctuation
+ if ((character >= 0x3000) && (character <= 0x303F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Strokes
+ if ((character >= 0x31C0) && (character <= 0x31EF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // Enclosed CJK Letters and Months
+ if ((character >= 0x3200) && (character <= 0x32FF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Compatibility
+ if ((character >= 0x3300) && (character <= 0x33FF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Compatibility Ideographs
+ if ((character >= 0xF900) && (character <= 0xFAFF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Compatibility Forms
+ if ((character >= 0xFE30) && (character <= 0xFE4F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // CJK Compatibility Ideographs Supplement
+ if ((character >= 0x2F800) && (character <= 0x2FA1F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // Greek and Coptic
+ if ((character >= 0x0370) && (character <= 0x03FF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // Hebrew
+ if ((character >= 0x0590) && (character <= 0x05FF))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // Armenian
+ if ((character >= 0x0530) && (character <= 0x058F))
+ {
+ return CharacterType.Symbolic;
+ }
+
+ // LATIN
+ //
+ // Basic Latin
+ if ((character > 0x0000) && (character <= 0x007F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin-1 Supplement
+ if ((character >= 0x0080) && (character <= 0x00FF))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended-A
+ if ((character >= 0x0100) && (character <= 0x017F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended-B
+ if ((character >= 0x0180) && (character <= 0x024F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended-C
+ if ((character >= 0x2C60) && (character <= 0x2C7F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended-D
+ if ((character >= 0xA720) && (character <= 0xA7FF))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended-E
+ if ((character >= 0xAB30) && (character <= 0xAB6F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Latin Extended Additional
+ if ((character >= 0x1E00) && (character <= 0x1EFF))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Cyrillic
+ if ((character >= 0x0400) && (character <= 0x04FF))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Cyrillic Supplement
+ if ((character >= 0x0500) && (character <= 0x052F))
+ {
+ return CharacterType.Standard;
+ }
+
+ // Combining Diacritical Marks
+ if ((character >= 0x0300) && (character <= 0x036F))
+ {
+ return CharacterType.Standard;
+ }
+
+ return CharacterType.Other;
+ }
+
+ ///
+ /// Helper function which takes in a string and returns a vector of pieces, separated by delimiter.
+ ///
+ /// String on which to perform the split operation.
+ /// String on which to perform the split operation.
+ /// Maximum number of times to perform a getline
loop.
+ /// A vector of pieces from the source string, separated by delimiter
+ static string[] Split(string source, char delim, int maxIterations = 25)
+ {
+ return source.Split(new[] { delim }, maxIterations);
+ }
+
+ ///
+ /// Helper function to remove bracket qualifier from the end of a display name if present.
+ ///
+ /// String on which to perform the operation.
+ /// A string with the content within brackets removed.
+ static void StripTrailingBrackets(ref string source)
+ {
+ // Guidance from the world readiness team is that text within a final set of brackets
+ // can be removed for the purposes of calculating initials. ex. John Smith (OSG)
+ string[] delimiters = { "{}", "()", "[]" };
+
+ if (source.Length == 0)
+ {
+ return;
+ }
+
+ foreach (var delimiter in delimiters)
+ {
+ if (source[source.Length - 1] != delimiter[1])
+ {
+ continue;
+ }
+
+ var start = source.LastIndexOf(delimiter[0]);
+ if (start == -1)
+ {
+ continue;
+ }
+
+ source = source.Remove(start);
+ return;
+ }
+ }
+
+ ///
+ /// Extracts the first full character from a given string, including any diacritics or combining characters.
+ ///
+ /// String from which to extract the character.
+ /// A wstring which represents a given character.
+ static string GetFirstFullCharacter(string str)
+ {
+ // Index should begin at the first desireable character.
+ int start = 0;
+
+ while (start < str.Length)
+ {
+ char character = str[start];
+
+ // Omit ! " # $ % & ' ( ) * + , - . /
+ if ((character >= 0x0021) && (character <= 0x002F))
+ {
+ start++;
+ continue;
+ }
+
+ // Omit : ; < = > ? @
+ if ((character >= 0x003A) && (character <= 0x0040))
+ {
+ start++;
+ continue;
+ }
+
+ // Omit { | } ~
+ if ((character >= 0x007B) && (character <= 0x007E))
+ {
+ start++;
+ continue;
+ }
+
+ break;
+ }
+
+ // If no desireable characters exist, we'll start at index 1, as combining
+ // characters begin only after the first character.
+ if (start >= str.Length)
+ {
+ start = 0;
+ }
+
+ // Combining characters begin only after the first character, so we should start
+ // looking 1 after the start character.
+ int index = start + 1;
+
+ while (index < str.Length)
+ {
+ char character = str[index];
+
+ // Combining Diacritical Marks -- Official Unicode character block
+ if ((character < 0x0300) || (character > 0x036F))
+ {
+ break;
+ }
+
+ index++;
+ }
+
+ // Determine number of diacritics by adjusting for our initial offset.
+ int strLength = index - start;
+
+ string result = str.SafeSubstring(start, strLength);
+ return result;
+ }
+ };
+}
diff --git a/ModernWpf.Controls/PersonPicture/PersonPicture.cs b/ModernWpf.Controls/PersonPicture/PersonPicture.cs
new file mode 100644
index 00000000..3d692e4d
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/PersonPicture.cs
@@ -0,0 +1,524 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+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 ModernWpf.Automation.Peers;
+
+namespace ModernWpf.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 = GetTemplateChild("InitialsTextBlock") as TextBlock;
+
+ m_badgeNumberTextBlock = GetTemplateChild("BadgeNumberTextBlock") as TextBlock;
+ m_badgeGlyphIcon = GetTemplateChild("BadgeGlyphIcon") as FontIcon;
+ m_badgingEllipse = GetTemplateChild("BadgingEllipse") as Ellipse;
+ m_badgingBackgroundEllipse = GetTemplateChild("BadgingBackgroundEllipse") as Ellipse;
+
+ 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 = GetTemplateChild("BadgeImageBrush") as ImageBrush;
+ }
+
+ 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 = Strings.Resources.GroupName;
+ }
+ else if (!string.IsNullOrEmpty(DisplayName))
+ {
+ contactName = DisplayName;
+ }
+ else if (!string.IsNullOrEmpty(Initials))
+ {
+ contactName = Initials;
+ }
+ else
+ {
+ contactName = Strings.Resources.PersonName;
+ }
+
+ // 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(
+ Strings.Resources.BadgeItemTextOverride,
+ 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(
+ Strings.Resources.BadgeIconTextOverride,
+ contactName,
+ BadgeText);
+ }
+ else
+ {
+ automationName = string.Format(
+ Strings.Resources.BadgeIcon,
+ 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 = Strings.Resources.BadgeItemSingular;
+ }
+ else if (numericValue == 2) // 2
+ {
+ value = Strings.Resources.BadgeItemPlural7;
+ }
+ else if (numericValue == 3 || numericValue == 4) // 3,4
+ {
+ value = Strings.Resources.BadgeItemPlural2;
+ }
+ else if (numericValue >= 5 && numericValue <= 10) // 5-10
+ {
+ value = Strings.Resources.BadgeItemPlural5;
+ }
+ else if (numericValue >= 11 && numericValue <= 19) // 11-19
+ {
+ value = Strings.Resources.BadgeItemPlural6;
+ }
+ else if (valueMod10 == 1) // 21, 31, 41, etc.
+ {
+ value = Strings.Resources.BadgeItemPlural1;
+ }
+ else if (valueMod10 >= 2 && valueMod10 <= 4) // 22-24, 32-34, 42-44, etc.
+ {
+ value = Strings.Resources.BadgeItemPlural3;
+ }
+ else // Everything else... 0, 20, 25-30, 35-40, etc.
+ {
+ value = Strings.Resources.BadgeItemPlural4;
+ }
+
+ 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.
+ ///
+ TextBlock m_initialsTextBlock;
+
+ ///
+ /// XAML Element for the first TextBlock matching x:Name of BadgeNumberTextBlock.
+ ///
+ TextBlock m_badgeNumberTextBlock;
+
+ ///
+ /// XAML Element for the first TextBlock matching x:Name of BadgeGlyphIcon.
+ ///
+ FontIcon m_badgeGlyphIcon;
+
+ ///
+ /// XAML Element for the first ImageBrush matching x:Name of BadgeImageBrush.
+ ///
+ ImageBrush m_badgeImageBrush;
+
+ ///
+ /// XAML Element for the first Ellipse matching x:Name of BadgingBackgroundEllipse.
+ ///
+ Ellipse m_badgingEllipse;
+
+ ///
+ /// XAML Element for the first Ellipse matching x:Name of BadgingEllipse.
+ ///
+ Ellipse m_badgingBackgroundEllipse;
+
+ ///
+ /// The async operation object representing the loading and assignment of the Thumbnail.
+ ///
+ //tracker_ref> m_profilePictureReadAsync;
+
+ ///
+ /// The initials from the DisplayName property.
+ ///
+ string m_displayNameInitials;
+
+ ///
+ /// The initials from the Contact property.
+ ///
+ string m_contactDisplayNameInitials;
+
+ ///
+ /// The ImageSource from the Contact property.
+ ///
+ ImageSource m_contactImageSource;
+ }
+}
diff --git a/ModernWpf.Controls/PersonPicture/PersonPicture.properties.cs b/ModernWpf.Controls/PersonPicture/PersonPicture.properties.cs
new file mode 100644
index 00000000..44426374
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/PersonPicture.properties.cs
@@ -0,0 +1,226 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Windows;
+using System.Windows.Media;
+
+namespace ModernWpf.Controls
+{
+ partial class PersonPicture
+ {
+ #region BadgeGlyph
+
+ public static readonly DependencyProperty BadgeGlyphProperty =
+ DependencyProperty.Register(
+ nameof(BadgeGlyph),
+ typeof(string),
+ typeof(PersonPicture),
+ new PropertyMetadata(string.Empty, OnBadgeGlyphPropertyChanged, CoerceStringProperty));
+
+ public string BadgeGlyph
+ {
+ get => (string)GetValue(BadgeGlyphProperty);
+ set => SetValue(BadgeGlyphProperty, value);
+ }
+
+ private static void OnBadgeGlyphPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region BadgeImageSource
+
+ public static readonly DependencyProperty BadgeImageSourceProperty =
+ DependencyProperty.Register(
+ nameof(BadgeImageSource),
+ typeof(ImageSource),
+ typeof(PersonPicture),
+ new PropertyMetadata(null, OnBadgeImageSourcePropertyChanged));
+
+ public ImageSource BadgeImageSource
+ {
+ get => (ImageSource)GetValue(BadgeImageSourceProperty);
+ set => SetValue(BadgeImageSourceProperty, value);
+ }
+
+ private static void OnBadgeImageSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region BadgeNumber
+
+ public static readonly DependencyProperty BadgeNumberProperty =
+ DependencyProperty.Register(
+ nameof(BadgeNumber),
+ typeof(int),
+ typeof(PersonPicture),
+ new PropertyMetadata(0, OnBadgeNumberPropertyChanged));
+
+ public int BadgeNumber
+ {
+ get => (int)GetValue(BadgeNumberProperty);
+ set => SetValue(BadgeNumberProperty, value);
+ }
+
+ private static void OnBadgeNumberPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region BadgeText
+
+ public static readonly DependencyProperty BadgeTextProperty =
+ DependencyProperty.Register(
+ nameof(BadgeText),
+ typeof(string),
+ typeof(PersonPicture),
+ new PropertyMetadata(string.Empty, OnBadgeTextPropertyChanged, CoerceStringProperty));
+
+ public string BadgeText
+ {
+ get => (string)GetValue(BadgeTextProperty);
+ set => SetValue(BadgeTextProperty, value);
+ }
+
+ private static void OnBadgeTextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region DisplayName
+
+ public static readonly DependencyProperty DisplayNameProperty =
+ DependencyProperty.Register(
+ nameof(DisplayName),
+ typeof(string),
+ typeof(PersonPicture),
+ new PropertyMetadata(string.Empty, OnDisplayNamePropertyChanged, CoerceStringProperty));
+
+ public string DisplayName
+ {
+ get => (string)GetValue(DisplayNameProperty);
+ set => SetValue(DisplayNameProperty, value);
+ }
+
+ private static void OnDisplayNamePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region Initials
+
+ public static readonly DependencyProperty InitialsProperty =
+ DependencyProperty.Register(
+ nameof(Initials),
+ typeof(string),
+ typeof(PersonPicture),
+ new PropertyMetadata(string.Empty, OnInitialsPropertyChanged, CoerceStringProperty));
+
+ public string Initials
+ {
+ get => (string)GetValue(InitialsProperty);
+ set => SetValue(InitialsProperty, value);
+ }
+
+ private static void OnInitialsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region IsGroup
+
+ public static readonly DependencyProperty IsGroupProperty =
+ DependencyProperty.Register(
+ nameof(IsGroup),
+ typeof(bool),
+ typeof(PersonPicture),
+ new PropertyMetadata(false, OnIsGroupPropertyChanged));
+
+ public bool IsGroup
+ {
+ get => (bool)GetValue(IsGroupProperty);
+ set => SetValue(IsGroupProperty, value);
+ }
+
+ private static void OnIsGroupPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region ProfilePicture
+
+ public static readonly DependencyProperty ProfilePictureProperty =
+ DependencyProperty.Register(
+ nameof(ProfilePicture),
+ typeof(ImageSource),
+ typeof(PersonPicture),
+ new PropertyMetadata(null, OnProfilePicturePropertyChanged));
+
+ public ImageSource ProfilePicture
+ {
+ get => (ImageSource)GetValue(ProfilePictureProperty);
+ set => SetValue(ProfilePictureProperty, value);
+ }
+
+ private static void OnProfilePicturePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ #region TemplateSettings
+
+ private static readonly DependencyPropertyKey TemplateSettingsPropertyKey =
+ DependencyProperty.RegisterReadOnly(
+ nameof(TemplateSettings),
+ typeof(PersonPictureTemplateSettings),
+ typeof(PersonPicture),
+ new PropertyMetadata(null, OnTemplateSettingsPropertyChanged));
+
+ public static readonly DependencyProperty TemplateSettingsProperty =
+ TemplateSettingsPropertyKey.DependencyProperty;
+
+ public PersonPictureTemplateSettings TemplateSettings
+ {
+ get => (PersonPictureTemplateSettings)GetValue(TemplateSettingsProperty);
+ private set => SetValue(TemplateSettingsPropertyKey, value);
+ }
+
+ private static void OnTemplateSettingsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ var owner = (PersonPicture)sender;
+ owner.PrivateOnPropertyChanged(args);
+ }
+
+ #endregion
+
+ private static object CoerceStringProperty(DependencyObject d, object baseValue)
+ {
+ return baseValue ?? string.Empty;
+ }
+ }
+}
diff --git a/ModernWpf.Controls/PersonPicture/PersonPicture.xaml b/ModernWpf.Controls/PersonPicture/PersonPicture.xaml
new file mode 100644
index 00000000..23e35133
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/PersonPicture.xaml
@@ -0,0 +1,145 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ModernWpf.Controls/PersonPicture/PersonPictureAutomationPeer.cs b/ModernWpf.Controls/PersonPicture/PersonPictureAutomationPeer.cs
new file mode 100644
index 00000000..72c67673
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/PersonPictureAutomationPeer.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Windows.Automation.Peers;
+using ModernWpf.Controls;
+
+namespace ModernWpf.Automation.Peers
+{
+ public class PersonPictureAutomationPeer : FrameworkElementAutomationPeer
+ {
+ public PersonPictureAutomationPeer(PersonPicture owner) : base(owner)
+ {
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.Text;
+ }
+
+ protected override string GetClassNameCore()
+ {
+ return typeof(PersonPicture).FullName;
+ }
+ }
+}
diff --git a/ModernWpf.Controls/PersonPicture/PersonPictureTemplateSettings.cs b/ModernWpf.Controls/PersonPicture/PersonPictureTemplateSettings.cs
new file mode 100644
index 00000000..ebb4787f
--- /dev/null
+++ b/ModernWpf.Controls/PersonPicture/PersonPictureTemplateSettings.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Windows;
+using System.Windows.Media;
+
+namespace ModernWpf.Controls
+{
+ public sealed class PersonPictureTemplateSettings : DependencyObject
+ {
+ #region ActualImageBrush
+
+ private static readonly DependencyPropertyKey ActualImageBrushPropertyKey =
+ DependencyProperty.RegisterReadOnly(
+ nameof(ActualImageBrush),
+ typeof(ImageBrush),
+ typeof(PersonPictureTemplateSettings),
+ null);
+
+ public static readonly DependencyProperty ActualImageBrushProperty =
+ ActualImageBrushPropertyKey.DependencyProperty;
+
+ public ImageBrush ActualImageBrush
+ {
+ get => (ImageBrush)GetValue(ActualImageBrushProperty);
+ internal set => SetValue(ActualImageBrushPropertyKey, value);
+ }
+
+ #endregion
+
+ #region ActualInitials
+
+ private static readonly DependencyPropertyKey ActualInitialsPropertyKey =
+ DependencyProperty.RegisterReadOnly(
+ nameof(ActualInitials),
+ typeof(string),
+ typeof(PersonPictureTemplateSettings),
+ new PropertyMetadata(string.Empty));
+
+ public static readonly DependencyProperty ActualInitialsProperty =
+ ActualInitialsPropertyKey.DependencyProperty;
+
+ public string ActualInitials
+ {
+ get => (string)GetValue(ActualInitialsProperty);
+ internal set => SetValue(ActualInitialsPropertyKey, value);
+ }
+
+ #endregion
+ }
+}
diff --git a/ModernWpf.Controls/SplitButton/SplitButtonAutomationPeer.cs b/ModernWpf.Controls/SplitButton/SplitButtonAutomationPeer.cs
index ded0d75b..4660aea1 100644
--- a/ModernWpf.Controls/SplitButton/SplitButtonAutomationPeer.cs
+++ b/ModernWpf.Controls/SplitButton/SplitButtonAutomationPeer.cs
@@ -29,7 +29,7 @@ public override object GetPattern(PatternInterface patternInterface)
protected override string GetClassNameCore()
{
- return nameof(SplitButton);
+ return typeof(SplitButton).FullName;
}
protected override AutomationControlType GetAutomationControlTypeCore()
diff --git a/ModernWpf.Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs b/ModernWpf.Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs
index bfc8c482..14ab4451 100644
--- a/ModernWpf.Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs
+++ b/ModernWpf.Controls/SplitButton/ToggleSplitButtonAutomationPeer.cs
@@ -29,7 +29,7 @@ public override object GetPattern(PatternInterface patternInterface)
protected override string GetClassNameCore()
{
- return nameof(ToggleSplitButton);
+ return typeof(ToggleSplitButton).FullName;
}
protected override AutomationControlType GetAutomationControlTypeCore()
diff --git a/ModernWpf.Controls/Themes/Generic.xaml b/ModernWpf.Controls/Themes/Generic.xaml
index 8b26a851..195e99d3 100644
--- a/ModernWpf.Controls/Themes/Generic.xaml
+++ b/ModernWpf.Controls/Themes/Generic.xaml
@@ -16,6 +16,7 @@
+
diff --git a/ModernWpf.SampleApp/App.xaml b/ModernWpf.SampleApp/App.xaml
index 96bb6feb..097c411e 100644
--- a/ModernWpf.SampleApp/App.xaml
+++ b/ModernWpf.SampleApp/App.xaml
@@ -62,6 +62,7 @@
+