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