diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/App.xaml b/source/iNKORE.UI.WPF.Modern.Gallery/App.xaml index 02d35662..e5b116a1 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/App.xaml +++ b/source/iNKORE.UI.WPF.Modern.Gallery/App.xaml @@ -30,6 +30,7 @@ + @@ -47,6 +48,7 @@ + @@ -60,6 +62,7 @@ + diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Data/IconsDataSource.cs b/source/iNKORE.UI.WPF.Modern.Gallery/Data/IconsDataSource.cs new file mode 100644 index 00000000..e5a777dd --- /dev/null +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Data/IconsDataSource.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using iNKORE.UI.WPF.Modern.Gallery.DataModel; + +namespace iNKORE.UI.WPF.Modern.Gallery.Helpers; + +internal class IconsDataSource +{ + public static IconsDataSource Instance { get; } = new(); + + public static List Icons => Instance.icons; + + // Public list of available icon sets discovered via reflection + public List AvailableSets { get; } = new(); + + // Current active set name (null = all) + public string ActiveSet { get; private set; } + + private List icons = new(); + + private IconsDataSource() { } + + public object _lock = new(); + + public async Task> LoadIcons() + { + // Yield once to keep this method truly asynchronous without changing logic. + await Task.Yield(); + // If already loaded, return current list + lock (_lock) + { + if (icons.Count != 0) + { + return icons; + } + } + + // Try reflection-first: enumerate types in Common.IconKeys namespace + try + { + var assembly = typeof(iNKORE.UI.WPF.Modern.Common.IconKeys.FontDictionary).Assembly; + var types = assembly.GetTypes().Where(t => t.IsClass && t.IsSealed && t.IsAbstract && t.Namespace == "iNKORE.UI.WPF.Modern.Common.IconKeys"); + var discovered = new List(); + + foreach (var type in types) + { + // collect a set name for the class + var setName = type.Name; + // Try public static fields and properties of type FontIconData + var fields = type.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + foreach (var f in fields) + { + if (f.FieldType.FullName == "iNKORE.UI.WPF.Modern.Common.IconKeys.FontIconData") + { + try + { + var value = f.GetValue(null); + var glyphProp = value?.GetType().GetProperty("Glyph"); + var glyph = glyphProp?.GetValue(value) as string; + var familyProp = value?.GetType().GetProperty("FontFamily"); + var family = familyProp?.GetValue(value) as System.Windows.Media.FontFamily; + var name = f.Name; + var data = new IconData { Name = name, Glyph = glyph, Set = setName, FontFamily = family }; + discovered.Add(data); + } + catch { } + } + } + + var props = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + foreach (var p in props) + { + if (p.PropertyType.FullName == "iNKORE.UI.WPF.Modern.Common.IconKeys.FontIconData") + { + try + { + var value = p.GetValue(null); + var glyphProp = value?.GetType().GetProperty("Glyph"); + var glyph = glyphProp?.GetValue(value) as string; + var familyProp = value?.GetType().GetProperty("FontFamily"); + var family = familyProp?.GetValue(value) as System.Windows.Media.FontFamily; + var name = p.Name; + var data = new IconData { Name = name, Glyph = glyph, Set = setName, FontFamily = family }; + discovered.Add(data); + } + catch { } + } + } + + if (discovered.Any(d => d.Set == setName)) + { + AvailableSets.Add(setName); + } + } + + if (discovered.Count > 0) + { + lock (_lock) + { + icons = discovered.OrderBy(i => i.Name).ToList(); + } + // Ensure legacy/alias sets are present so the UI can show them + EnsureLegacySets(); + return icons; + } + } + catch + { + // reflection failed; no fallback + } + + return icons; + } + + //private static string ToCode(string glyph) + //{ + // if (string.IsNullOrEmpty(glyph)) return string.Empty; + // // glyph is a single-character string; convert to hex code (without leading 0x) + // var ch = glyph[0]; + // return ((int)ch).ToString("X4"); + //} + + // Set active set and return filtered icons + public List SetActiveSet(string setName) + { + // Normalize legacy aliases to concrete set names when possible + //if (string.Equals(setName, "SegoeMDL2Assets", StringComparison.OrdinalIgnoreCase) || + // string.Equals(setName, "Segoe MDL2 Assets", StringComparison.OrdinalIgnoreCase)) + //{ + // // These glyphs generally live in the JSON data under empty Set (or specific set names). + // // Treat this alias as a request to show all non-Fluent-only icons. + // ActiveSet = setName; + // return icons.Where(i => !i.IsSegoeFluentOnly).ToList(); + //} + + //if (string.Equals(setName, "SegoeIcons", StringComparison.OrdinalIgnoreCase) || + // string.Equals(setName, "Segoe Icons", StringComparison.OrdinalIgnoreCase)) + //{ + // // No dedicated SegoeIcons set in the built-in keys; treat as all icons (fallback). + // ActiveSet = setName; + // return icons; + //} + + ActiveSet = setName; + if (string.IsNullOrEmpty(setName)) return icons; + return icons.Where(i => i.Set == setName).ToList(); + } + + private void EnsureLegacySets() + { + // No-op: legacy set aliases are handled in SetActiveSet(). + } +} diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/Data/Controls.Foundation.json b/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/Data/Controls.Foundation.json index 45a7ec64..edcc9291 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/Data/Controls.Foundation.json +++ b/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/Data/Controls.Foundation.json @@ -24,6 +24,30 @@ "Description": "Design guidance and resources", "Items": [ + { + "UniqueId": "Iconography", + "Title": "Iconography", + "Subtitle": "Segoe Fluent Icons and MDL2 Assets", + "ImagePath": "ms-appx:///Assets/ControlIcons/DefaultIcon.png", + "ImageIconPath": "ms-appx:///Assets/ControlIcons/DefaultIcon.png", + "Description": "", + "Content": "", + "IncludedInBuild": true, + "IsNew": false, + "IsUpdated": false, + "Docs": + [ + { + "Title": "Iconography in Windows 11", + "Uri": "https://learn.microsoft.com/windows/apps/design/signature-experiences/iconography#system-icons" + }, + { + "Title": "Segoe Fluent Icons font", + "Uri": "https://learn.microsoft.com/windows/apps/design/style/segoe-fluent-icons-font" + } + ], + "RelatedControls": [] + }, { "UniqueId": "Typography", "Title": "Typography", @@ -68,4 +92,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/IconData.cs b/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/IconData.cs new file mode 100644 index 00000000..8d62757e --- /dev/null +++ b/source/iNKORE.UI.WPF.Modern.Gallery/DataModel/IconData.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Windows.Media; +using iNKORE.UI.WPF.Modern.Common.IconKeys; + +namespace iNKORE.UI.WPF.Modern.Gallery.DataModel +{ + public class IconData + { + public string Name { get; set; } + // Which icon set this icon came from (e.g. "SegoeFluentIcons", "FluentSystemIcons.Regular") + public string Set { get; set; } + public string[] Tags { get; set; } = Array.Empty(); + // The actual font to use for rendering this glyph (important for Fluent System Icons) + public FontFamily FontFamily { get; set; } + + public string Code { get; protected set; } + + + private string p_glyph; + public string Glyph + { + get => this.p_glyph; + set + { + this.p_glyph = value; + this.Code = ToCode(this.p_glyph); + } + } + + public string CodeGlyph => string.IsNullOrWhiteSpace(Code) ? string.Empty : "\\u" + Code; + public string TextGlyph => string.IsNullOrWhiteSpace(Code) ? string.Empty : "&#x" + Code + ";"; + + // WPF doesn't have Symbol enum like WinUI + public string SymbolName => null; + + + public static string ToCode(string glyph) + { + var codepoint = FontIconData.ToUtf32(glyph); + return $"{codepoint:X}"; + } + } +} diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Navigation/NavigationRootPage.xaml b/source/iNKORE.UI.WPF.Modern.Gallery/Navigation/NavigationRootPage.xaml index 61e9a267..32a6a7f9 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/Navigation/NavigationRootPage.xaml +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Navigation/NavigationRootPage.xaml @@ -121,6 +121,14 @@ + + + + + r.Groups) + .SelectMany(g => g.Items) + .FirstOrDefault(i => i.UniqueId == "Iconography"); + + if (iconographyItem != null) + { + rootFrame.Navigate(ItemPage.Create(iconographyItem)); + } + } else if (selectedItem?.Tag?.ToString() == "Typography") { // Handle Typography navigation var typographyId = "Typography"; if (_lastItem?.ToString() == typographyId) return; _lastItem = typographyId; - + // Find Typography item from the data source var typographyItem = ControlInfoDataSource.Instance.Realms .SelectMany(r => r.Groups) .SelectMany(g => g.Items) .FirstOrDefault(i => i.UniqueId == "Typography"); - + if (typographyItem != null) { rootFrame.Navigate(ItemPage.Create(typographyItem)); diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml new file mode 100644 index 00000000..7f9d3970 --- /dev/null +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml @@ -0,0 +1,910 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + iUWM provides a unified icon system with icon keys for SegoeFluentIcons and FluentSystemIcons. Segoe MDL2 is available via SymbolIcon or as a fallback through SymbolThemeFontFamily. We recommend Fluent System Icons as your primary choice. + + + Learn more about the Iconography in iUWM by visiting the + + + + + . + + + + + + + + + + Use ui:FontIcon with icon keys, such as SegoeFluentIcons.Delete or FluentSystemIcons.Edit_24_Regular (match FontSize to the icon’s designed size). When you set the Icon property, the correct FontFamily is applied automatically for the selected icon set. + + + If you don't specify a FontFamily, or you specify a FontFamily that isn't available on the system at runtime, FontIcon falls back to the default font family defined by the SymbolThemeFontFamily resource. + + + The default FontSize is 16 (roughly a 16x16 epx bitmap). For multi-size sets like Fluent System Icons, set FontSize to the icon’s designed size (e.g., 12, 16, 20, 24, 28, 32, 48) to keep pixel-perfect rendering. + + + All glyphs in Segoe Fluent Icons use a fixed width with a consistent height and left origin point. You can layer and colorize multiple glyphs by drawing them directly on top of each other. + + + + Avoid using raw Glyph strings directly; prefer icon keys for clarity, theme compatibility, and future-proofing. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml.cs b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml.cs new file mode 100644 index 00000000..fc6bc286 --- /dev/null +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/IconographyPage.xaml.cs @@ -0,0 +1,660 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using iNKORE.UI.WPF.Modern.Controls; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using iNKORE.UI.WPF.Modern.Gallery.Helpers; +using iNKORE.UI.WPF.Modern.Gallery.DataModel; +using iNKORE.UI.WPF.Modern; +using System.Windows.Media.Animation; +using System.Collections.ObjectModel; +using System.Windows.Media; +using System.Diagnostics; +using System.Windows.Navigation; + +namespace iNKORE.UI.WPF.Modern.Gallery.Pages.Controls.Foundation +{ + public partial class IconographyPage : iNKORE.UI.WPF.Modern.Controls.Page + { + public List FontSizes { get; } = new() + { + 16, + 24, + 32, + 48 + }; + + private string currentSearch = null; + + public IconData SelectedItem + { + get { return (IconData)GetValue(SelectedItemProperty); } + set + { + SetValue(SelectedItemProperty, value); + SetSampleCodePresenterCode(value); + } + } + + private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) + { + try + { + var uri = e.Uri?.AbsoluteUri ?? (sender as System.Windows.Documents.Hyperlink)?.NavigateUri?.AbsoluteUri; + if (!string.IsNullOrEmpty(uri)) + { + var psi = new ProcessStartInfo(uri) { UseShellExecute = true }; + Process.Start(psi); + } + } + catch + { + } + e.Handled = true; + } + + public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(IconData), typeof(IconographyPage), new PropertyMetadata(null, OnSelectedItemChanged)); + + private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is IconographyPage page) + { + try + { + page.UpdateSelectionVisuals(e.NewValue as IconData); + } + catch + { + } + } + } + + public IconographyPage() + { + InitializeComponent(); + Loaded += IconographyPage_Loaded; + DataContext = this; + } + + private void TogglePageTheme() + { + // Toggle requested theme only on the example container Border so + // only the interior region re-evaluates DynamicResource brushes. + try + { + var exampleBorder = FindName("ExampleContainerBorder") as FrameworkElement; + if (exampleBorder != null) + { + exampleBorder.ToggleTheme(); + return; + } + + // Fallback to toggling the whole page if the named Border isn't found + this.LayoutRoot.ToggleTheme(); + } + catch { } + } + + // Dependency properties for side panel sample text + public string FontIconXaml + { + get => (string)GetValue(FontIconXamlProperty); + set => SetValue(FontIconXamlProperty, value); + } + public static readonly DependencyProperty FontIconXamlProperty = DependencyProperty.Register(nameof(FontIconXaml), typeof(string), typeof(IconographyPage), new PropertyMetadata(string.Empty)); + + public string FontIconCSharp + { + get => (string)GetValue(FontIconCSharpProperty); + set => SetValue(FontIconCSharpProperty, value); + } + public static readonly DependencyProperty FontIconCSharpProperty = DependencyProperty.Register(nameof(FontIconCSharp), typeof(string), typeof(IconographyPage), new PropertyMetadata(string.Empty)); + + public string SymbolIconXaml + { + get => (string)GetValue(SymbolIconXamlProperty); + set => SetValue(SymbolIconXamlProperty, value); + } + public static readonly DependencyProperty SymbolIconXamlProperty = DependencyProperty.Register(nameof(SymbolIconXaml), typeof(string), typeof(IconographyPage), new PropertyMetadata(string.Empty)); + + public string SymbolIconCSharp + { + get => (string)GetValue(SymbolIconCSharpProperty); + set => SetValue(SymbolIconCSharpProperty, value); + } + public static readonly DependencyProperty SymbolIconCSharpProperty = DependencyProperty.Register(nameof(SymbolIconCSharp), typeof(string), typeof(IconographyPage), new PropertyMetadata(string.Empty)); + + private void IconographyPage_Loaded(object sender, RoutedEventArgs e) + { + if (NavigationRootPage.Current?.NavigationView != null) + { + NavigationRootPage.Current.NavigationView.Header = "Iconography"; + } + + // Register toggle theme action on page header so Theme toggle also affects this page + try + { + if (NavigationRootPage.Current?.PageHeader != null) + { + NavigationRootPage.Current.PageHeader.ToggleThemeAction = TogglePageTheme; + } + } + catch { } + + // Load icons on a background thread and assign them immediately to the repeater. + Task.Run(async delegate + { + var icons = await IconsDataSource.Instance.LoadIcons(); + + // Assign the full list to the ItemsRepeater on the UI thread. + await Dispatcher.BeginInvoke(new Action(() => + { + var iconsItemsView = FindName("IconsItemsView") as iNKORE.UI.WPF.Modern.Controls.ItemsRepeater; + if (iconsItemsView != null) + { + iconsItemsView.ItemsSource = icons; + + // Populate icon set ComboBox if present + var setCombo = FindName("IconSetComboBox") as ComboBox; + try + { + if (setCombo != null) + { + setCombo.ItemsSource = IconsDataSource.Instance.AvailableSets; + setCombo.SelectionChanged += IconSetComboBox_SelectionChanged; + if (IconsDataSource.Instance.AvailableSets.Count > 0) + { + // Prefer SegoeFluentIcons when present + var preferred = IconsDataSource.Instance.AvailableSets.FirstOrDefault(s => string.Equals(s, "SegoeFluentIcons", StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrEmpty(preferred)) + { + setCombo.SelectedItem = preferred; + } + else + { + setCombo.SelectedIndex = 0; + } + } + } + } + catch { } + + // Select the first item by default and show side panel + if (icons != null && icons.Count > 0) + { + var first = icons[0]; + SelectedItem = first; + var sidePanel = FindName("SidePanel") as Border; + if (sidePanel != null) + { + sidePanel.Visibility = Visibility.Visible; + } + UpdateSelectionVisuals(first); + } + } + })); + }); + } + + private void SetSampleCodePresenterCode(IconData value) + { + // Update presenters and side panel when selection changes + var sidePanel = FindName("SidePanel") as Border; + if (value == null) + { + if (sidePanel != null) + { + sidePanel.Visibility = Visibility.Collapsed; + } + return; + } + + // Ensure side panel visible + if (sidePanel != null) + { + sidePanel.Visibility = Visibility.Visible; + } + + // Populate code strings for TextBoxes + if (!string.IsNullOrEmpty(value.Set)) + { + // Use static icon class reference when available + // Example XAML: + FontIconXaml = $""; + // Use fully-qualified C# reference to the static property in Common.IconKeys + FontIconCSharp = "using iNKORE.UI.WPF.Modern.Common.IconKeys;" + Environment.NewLine + Environment.NewLine + $"FontIcon icon = new FontIcon();" + Environment.NewLine + $"icon.Icon = {value.Set}.{value.Name};"; + } + else + { + FontIconXaml = $""; + FontIconCSharp = "FontIcon icon = new FontIcon();" + Environment.NewLine + $"icon.Glyph = \"{value.CodeGlyph}\";"; + } + + if (!string.IsNullOrEmpty(value.SymbolName)) + { + SymbolIconXaml = $""; + SymbolIconCSharp = "SymbolIcon icon = new SymbolIcon();" + Environment.NewLine + $"icon.Symbol = Symbol.{value.SymbolName};"; + var symbolPanel = FindName("SymbolPanel") as StackPanel; + if (symbolPanel != null) + { + symbolPanel.Visibility = Visibility.Visible; + } + } + else + { + SymbolIconXaml = string.Empty; + SymbolIconCSharp = string.Empty; + var symbolPanel = FindName("SymbolPanel") as StackPanel; + if (symbolPanel != null) + { + symbolPanel.Visibility = Visibility.Collapsed; + } + } + + // Tags visibility + try + { + var tagsView = FindName("TagsItemsView") as iNKORE.UI.WPF.Modern.Controls.ItemsRepeater; + var noTags = FindName("NoTagsTextBlock") as TextBlock; + if (value.Tags == null || value.Tags.Length == 0 || value.Tags.All(t => string.IsNullOrWhiteSpace(t))) + { + if (tagsView != null) tagsView.Visibility = Visibility.Collapsed; + if (noTags != null) noTags.Visibility = Visibility.Collapsed; + TagsLabel.Visibility = Visibility.Collapsed; + } + else + { + if (tagsView != null) tagsView.Visibility = Visibility.Visible; + if (noTags != null) noTags.Visibility = Visibility.Collapsed; + TagsLabel.Visibility = Visibility.Visible; + } + } + catch { } + } + + private void SearchTextBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + var tb = FindName("IconsSearchBox"); + var text = tb?.GetType().GetProperty("Text")?.GetValue(tb) as string; + Filter(text); + } + + public void Filter(string search) + { + currentSearch = search; + string[] filter = search?.Split(' '); + + Task.Run(() => + { + var newItems = new List(); + foreach (var item in IconsDataSource.Icons) + { + var fitsFilter = filter == null || filter.All(entry => + item.Code.Contains(entry, StringComparison.CurrentCultureIgnoreCase) || + item.Name.Contains(entry, StringComparison.CurrentCultureIgnoreCase) || + item.Tags.Any(tag => !string.IsNullOrEmpty(tag) && tag.Contains(entry, StringComparison.CurrentCultureIgnoreCase))); + + if (fitsFilter) + { + newItems.Add(item); + } + } + + // Assign filtered list to repeater immediately on UI thread + Dispatcher.BeginInvoke(new Action(() => + { + var iconsItemsView = FindName("IconsItemsView") as iNKORE.UI.WPF.Modern.Controls.ItemsRepeater; + if (iconsItemsView != null) + { + iconsItemsView.ItemsSource = newItems; + if (newItems.Count > 0) + { + SelectedItem = newItems[0]; + UpdateSelectionVisuals(newItems[0]); + } + } + })); + }); + } + + private void IconsItemsView_SelectionChanged(object sender, EventArgs e) + { + try + { + // Try to read a SelectedItem property from the sender (GridView exposes this) + var view = FindName("IconsItemsView"); + if (view != null) + { + var selProp = view.GetType().GetProperty("SelectedItem"); + if (selProp != null) + { + var sel = selProp.GetValue(view) as IconData; + if (sel != null) SelectedItem = sel; + } + } + } + catch { } + + try + { + var argsType = e.GetType(); + var invokedProp = argsType.GetProperty("InvokedItem"); + if (invokedProp != null) + { + var tag = invokedProp.GetValue(e) as string; + if (!string.IsNullOrEmpty(tag)) + { + // Set text on the named search box if present + var searchBox = FindName("IconsSearchBox"); + if (searchBox != null) + { + var textProp = searchBox.GetType().GetProperty("Text"); + textProp?.SetValue(searchBox, tag); + } + } + } + } + catch { } + } + + private void IconsItemsView_ItemClicked(object sender, RoutedEventArgs e) + { + try + { + if (sender is Button btn && btn.DataContext is IconData data) + { + SelectedItem = data; + UpdateSelectionVisuals(data); + } + } + catch { } + } + + private void IconSetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + try + { + if (sender is ComboBox cb && cb.SelectedItem is string setName) + { + var items = IconsDataSource.Instance.SetActiveSet(setName); + Dispatcher.BeginInvoke(new Action(() => + { + var iconsItemsView = FindName("IconsItemsView") as iNKORE.UI.WPF.Modern.Controls.ItemsRepeater; + if (iconsItemsView != null) + { + iconsItemsView.ItemsSource = items; + if (items.Count > 0) + { + SelectedItem = items[0]; + UpdateSelectionVisuals(items[0]); + } + } + })); + } + } + catch { } + } + + private void TagButton_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is Button btn) + { + // The TextBlock is inside the Button's visual tree; find it + var tb = FindDescendants(btn).FirstOrDefault(); + var tag = tb?.Text; + if (!string.IsNullOrEmpty(tag)) + { + // Update search box text via the named control if available + var searchBoxField = this.GetType().GetField("IconsSearchBox", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); + if (searchBoxField != null) + { + var searchBox = searchBoxField.GetValue(this); + if (searchBox != null) + { + var textProp = searchBox.GetType().GetProperty("Text"); + textProp?.SetValue(searchBox, tag); + var focusMethod = searchBox.GetType().GetMethod("Focus", System.Type.EmptyTypes); + focusMethod?.Invoke(searchBox, null); + + // Try to open suggestion list if property exists + var isOpenProp = searchBox.GetType().GetProperty("IsSuggestionListOpen"); + if (isOpenProp != null) isOpenProp.SetValue(searchBox, true); + } + } + + // Also call Filter to update items immediately + Filter(tag); + } + } + } + catch { } + } + + // handler for Border-based tag chips. Extracts the tag text and + // performs the same actions as TagButton_Click (populate search box, + // focus, open suggestions, and filter). + private void TagChip_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + try + { + if (sender is Border outerBorder) + { + // The inner TextBlock is a descendant; find it + var tb = FindDescendants(outerBorder).FirstOrDefault(); + var tag = tb?.Text; + if (!string.IsNullOrEmpty(tag)) + { + // Reuse the same reflection-based logic to set the search box + var searchBoxField = this.GetType().GetField("IconsSearchBox", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); + if (searchBoxField != null) + { + var searchBox = searchBoxField.GetValue(this); + if (searchBox != null) + { + var textProp = searchBox.GetType().GetProperty("Text"); + textProp?.SetValue(searchBox, tag); + var focusMethod = searchBox.GetType().GetMethod("Focus", System.Type.EmptyTypes); + focusMethod?.Invoke(searchBox, null); + + // Try to open suggestion list if property exists + var isOpenProp = searchBox.GetType().GetProperty("IsSuggestionListOpen"); + if (isOpenProp != null) isOpenProp.SetValue(searchBox, true); + } + } + + // Also call Filter to update items immediately + Filter(tag); + } + } + } + catch { } + } + + private async void CopyValueButton_Click(object sender, RoutedEventArgs e) + { + string textToCopy = null; + try + { + if (sender is Button btn) + { + // Primary path: Tag is bound to the value to copy + textToCopy = btn.Tag as string; + + // Fallbacks for cases where the Tag binding isn't available or was modified + if (string.IsNullOrEmpty(textToCopy)) + { + switch (btn.Name) + { + case "CopyInlineExampleButton": + textToCopy = (FindName("InlineExampleBox") as TextBox)?.Text; + break; + case "CopyNameButton": + textToCopy = SelectedItem?.Name; + break; + case "CopyTextGlyphButton": + textToCopy = SelectedItem?.TextGlyph; + break; + case "CopyCodeGlyphButton": + textToCopy = SelectedItem?.CodeGlyph; + break; + case "CopyXamlButton": + textToCopy = FontIconXaml; + break; + case "CopyCSharpButton": + textToCopy = FontIconCSharp; + break; + case "CopySymbolXamlButton": + textToCopy = SymbolIconXaml; + break; + case "CopySymbolCSharpButton": + textToCopy = SymbolIconCSharp; + break; + } + } + + if (!string.IsNullOrEmpty(textToCopy)) + { + var copied = TrySetClipboardText(textToCopy, 3, TimeSpan.FromMilliseconds(150)); + if (!copied) + { + throw new InvalidOperationException("Unable to open clipboard after multiple attempts."); + } + } + } + } + catch (Exception ex) + { + iNKORE.UI.WPF.Modern.Controls.MessageBox.Show(ex.ToString(), "Unable to Perform Copy", MessageBoxButton.OK, MessageBoxImage.Error); + } + // Trigger the Geometry-style copy confirmation animation. + // The VisualStateGroups are declared on LayoutRoot; use GoToElementState + // so the states are found in the same namescope. + if (sender is Button) + { + try + { + var layoutRoot = FindName("LayoutRoot") as FrameworkElement; + if (layoutRoot != null) + { + // If the Button has a name like "CopyXamlButton" derive a per-button + // state name like "CopyXamlVisible" and "CopyXamlHidden" and try + // to trigger it. Fall back to the shared Control/Overlay groups. + if (sender is Button btn && !string.IsNullOrEmpty(btn.Name)) + { + // Strip trailing "Button" if present + var baseName = btn.Name.EndsWith("Button") ? btn.Name.Substring(0, btn.Name.Length - 6) : btn.Name; + var visibleState = baseName + "Visible"; + var hiddenState = baseName + "Hidden"; + + var started = VisualStateManager.GoToElementState(layoutRoot, visibleState, true); + if (!started) + { + // fall back to existing Control/Overlay groups + started = VisualStateManager.GoToElementState(layoutRoot, "ControlCornerRadiusCopyButtonVisible", true); + if (!started) + { + VisualStateManager.GoToElementState(layoutRoot, "OverlayCornerRadiusCopyButtonVisible", true); + } + } + + // Revert after a short delay so the success checkmark animates back. + await Task.Delay(900); + + // Try to revert per-button state first, then the shared states. + VisualStateManager.GoToElementState(layoutRoot, hiddenState, true); + VisualStateManager.GoToElementState(layoutRoot, "ControlCornerRadiusCopyButtonHidden", true); + VisualStateManager.GoToElementState(layoutRoot, "OverlayCornerRadiusCopyButtonHidden", true); + } + else + { + var started = VisualStateManager.GoToElementState(layoutRoot, "ControlCornerRadiusCopyButtonVisible", true); + if (!started) + { + VisualStateManager.GoToElementState(layoutRoot, "OverlayCornerRadiusCopyButtonVisible", true); + } + await Task.Delay(900); + VisualStateManager.GoToElementState(layoutRoot, "ControlCornerRadiusCopyButtonHidden", true); + VisualStateManager.GoToElementState(layoutRoot, "OverlayCornerRadiusCopyButtonHidden", true); + } + } + } + catch + { + // Swallow any VSM exceptions; copy already occurred. + } + } + } + + // Recursive descendant finder + private static IEnumerable FindDescendants(DependencyObject root) where T : DependencyObject + { + if (root == null) yield break; + + var queue = new Queue(); + queue.Enqueue(root); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + int count = VisualTreeHelper.GetChildrenCount(current); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(current, i); + if (child is T t) yield return t; + queue.Enqueue(child); + } + } + } + + // Robust clipboard setter: runs SetText on a new STA thread and retries when clipboard is busy. + private static bool TrySetClipboardText(string text, int maxAttempts, TimeSpan delayBetweenAttempts) + { + if (string.IsNullOrEmpty(text)) return false; + + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + var success = false; + var thread = new Thread(() => + { + try + { + // Use System.Windows.Clipboard on STA thread + System.Windows.Clipboard.SetText(text); + success = true; + } + catch + { + success = false; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + thread.Join(); + + if (success) return true; + + Thread.Sleep(delayBetweenAttempts); + } + + return false; + } + + //Keep for reference in the future + private void UpdateSelectionVisuals(IconData selected) + { + + } + } +} diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj b/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj index b26db0e3..565a6fc5 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj +++ b/source/iNKORE.UI.WPF.Modern.Gallery/iNKORE.UI.WPF.Modern.Gallery.csproj @@ -1,4 +1,4 @@ - + WinExe diff --git a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FluentSystemIcons.cs b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FluentSystemIcons.cs index 99a272db..b3a9abd1 100644 --- a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FluentSystemIcons.cs +++ b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FluentSystemIcons.cs @@ -29,15 +29,8 @@ public static FontIconData CreateIcon(string glyph, FluentSystemIconVariants var public static FontIconData CreateIcon(int chara, FluentSystemIconVariants variant) { - return CreateIcon(ToGlyph(chara), variant); + return CreateIcon(FontIconData.ToGlyph(chara), variant); } - - public static string ToGlyph(int chara) - { - return char.ConvertFromUtf32(chara); - } - - } public enum FluentSystemIconVariants diff --git a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FontDictionary.cs b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FontDictionary.cs index 7fe64a49..cfde05f0 100644 --- a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FontDictionary.cs +++ b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/FontDictionary.cs @@ -51,5 +51,30 @@ public FontIconData(string glyph, FontFamily family = null) _glyph = glyph; _fontFamily = family; } + + public static string ToGlyph(int chara) + { + return char.ConvertFromUtf32(chara); + } + + public static int ToUtf32(string glyph) + { + if (string.IsNullOrEmpty(glyph)) + throw new ArgumentException("Input glyph cannot be null or empty."); + + if (glyph.Length == 1) + { + return char.ConvertToUtf32(glyph, 0); + } + else if (glyph.Length == 2 && char.IsSurrogatePair(glyph[0], glyph[1])) + { + return char.ConvertToUtf32(glyph, 0); + } + else + { + throw new ArgumentException("Input glyph must be a single character or a valid surrogate pair."); + } + } + } } diff --git a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/SegoeFluentIcons.cs b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/SegoeFluentIcons.cs index 5b274057..613dc362 100644 --- a/source/iNKORE.UI.WPF.Modern/Common/IconKeys/SegoeFluentIcons.cs +++ b/source/iNKORE.UI.WPF.Modern/Common/IconKeys/SegoeFluentIcons.cs @@ -19,12 +19,7 @@ public static FontIconData CreateIcon(string glyph, bool forceFluent = false) public static FontIconData CreateIcon(int chara, bool forceFluent = false) { - return CreateIcon(ToGlyph(chara), forceFluent); - } - - public static string ToGlyph(int chara) - { - return char.ConvertFromUtf32(chara); + return CreateIcon(FontIconData.ToGlyph(chara), forceFluent); } } }