using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace PriFormat { public struct SectionRef where T : Section { internal int SectionIndex; internal SectionRef (int sectionIndex) { SectionIndex = sectionIndex; } public override string ToString () { return "Section " + typeof (T).Name + " at index " + SectionIndex; } } public static class LocaleExt { // GetLocaleInfoW for LCID-based queries [DllImport ("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern int GetLocaleInfoW (int Locale, int LCType, [Out] StringBuilder lpLCData, int cchData); // GetLocaleInfoEx for locale name based queries [DllImport ("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern int GetLocaleInfoEx (string lpLocaleName, int LCType, [Out] StringBuilder lpLCData, int cchData); // LocaleNameToLCID - available on Vista+; fallback is to use CultureInfo [DllImport ("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern int LocaleNameToLCID (string lpName, uint dwFlags); // LCIDToLocaleName (Vista+) [DllImport ("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern int LCIDToLocaleName (int Locale, [Out] StringBuilder lpName, int cchName, uint dwFlags); // Current locale name like "en-US" public static string CurrentLocale { get { try { // prefer thread culture name which reflects user culture string name = CultureInfo.CurrentCulture.Name; if (string.IsNullOrEmpty (name)) name = CultureInfo.InstalledUICulture.Name; return name ?? string.Empty; } catch { return string.Empty; } } } // Current LCID (int) public static int CurrentLCID { get { try { return CultureInfo.CurrentCulture.LCID; } catch { return CultureInfo.InvariantCulture.LCID; } } } // Convert LCID -> locale name (e.g. 1033 -> "en-US") public static string ToLocaleName (int lcid) { try { // Try managed first var ci = new CultureInfo (lcid); if (!string.IsNullOrEmpty (ci.Name)) return ci.Name; } catch { // try Win32 LCIDToLocaleName (Vista+) try { StringBuilder sb = new StringBuilder (LOCALE_NAME_MAX_LENGTH); int res = LCIDToLocaleName (lcid, sb, sb.Capacity, 0); if (res > 0) return sb.ToString (); } catch { } } return string.Empty; } // Convert locale name -> LCID public static int ToLCID (string localeName) { if (string.IsNullOrEmpty (localeName)) return CultureInfo.InvariantCulture.LCID; try { // prefer managed creation var ci = new CultureInfo (localeName); return ci.LCID; } catch { // try Win32 LocaleNameToLCID (Vista+) try { int lcid = LocaleNameToLCID (localeName, 0); if (lcid != 0) return lcid; } catch { } } // fallback: invariant culture return CultureInfo.InvariantCulture.LCID; } // Return a locale info string for given LCID and LCTYPE. LCTYPE is the Win32 LOCALE_* constant. // Returns a string (or empty string on failure). public static object LocaleInfo (int lcid, int lctype) { try { // First try mapping common LCTYPE values to managed properties for better correctness // Some common LCTYPE values: // LOCALE_SISO639LANGNAME = 0x59 (89) -> Two-letter ISO language name // LOCALE_SISO3166CTRYNAME = 0x5A (90) -> Two-letter country/region name // LOCALE_SNAME = 0x5c (92) -> locale name like "en-US" (Vista+) // But we cannot rely on all values, so we fallback to native GetLocaleInfoW. if (lctype == 0x59) // LOCALE_SISO639LANGNAME { try { var ci = new CultureInfo (lcid); return ci.TwoLetterISOLanguageName ?? string.Empty; } catch { } } else if (lctype == 0x5A) // LOCALE_SISO3166CTRYNAME { try { var ci = new CultureInfo (lcid); try { var ri = new RegionInfo (ci.Name); return ri.TwoLetterISORegionName ?? string.Empty; } catch { // some cultures have no region; fallback to parsing name var name = ci.Name; if (!string.IsNullOrEmpty (name) && name.IndexOf ('-') >= 0) { return name.Split ('-') [1]; } } } catch { } } else if (lctype == 0x5c) // LOCALE_SNAME { try { var ci = new CultureInfo (lcid); return ci.Name ?? string.Empty; } catch { } } // Fallback to native GetLocaleInfoW StringBuilder sb = new StringBuilder (256); int ret = GetLocaleInfoW (lcid, lctype, sb, sb.Capacity); if (ret > 0) return sb.ToString (); return string.Empty; } catch { return string.Empty; } } // LocaleInfoEx: query by locale name string and LCTYPE // Returns string if available; otherwise returns the integer result code (as int) if string empty (mimic C++ behavior). public static object LocaleInfoEx (string localeName, int lctype) { if (string.IsNullOrEmpty (localeName)) { // fall back to current culture name localeName = CurrentLocale; if (string.IsNullOrEmpty (localeName)) return 0; } try { // Try managed shortcuts for common types if (lctype == 0x59) // LOCALE_SISO639LANGNAME { try { var ci = new CultureInfo (localeName); return ci.TwoLetterISOLanguageName ?? string.Empty; } catch { } } else if (lctype == 0x5A) // LOCALE_SISO3166CTRYNAME { try { var ci = new CultureInfo (localeName); var ri = new RegionInfo (ci.Name); return ri.TwoLetterISORegionName ?? string.Empty; } catch { // try to split var parts = localeName.Split (new char [] { '-', '_' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) return parts [1]; } } else if (lctype == 0x5c) // LOCALE_SNAME { // localeName is probably already the name return localeName; } // Fallback to GetLocaleInfoEx StringBuilder sb = new StringBuilder (LOCALE_NAME_MAX_LENGTH); int res = GetLocaleInfoEx (localeName, lctype, sb, sb.Capacity); if (res > 0) { string outStr = sb.ToString (); if (!string.IsNullOrEmpty (outStr)) return outStr; } // if nothing returned, return the result code return res; } catch { return 0; } } // Helpers similar to the C++: restricted (language) and elaborated (region) codes public static string GetLocaleRestrictedCode (string localeName) { if (string.IsNullOrEmpty (localeName)) localeName = CurrentLocale; try { var ci = new CultureInfo (localeName); return ci.TwoLetterISOLanguageName ?? string.Empty; } catch { // fallback: parse name var parts = localeName.Split (new char [] { '-', '_' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 1) return parts [0]; return string.Empty; } } public static string GetLocaleElaboratedCode (string localeName) { if (string.IsNullOrEmpty (localeName)) localeName = CurrentLocale; try { var ci = new CultureInfo (localeName); // Region part from RegionInfo try { var ri = new RegionInfo (ci.Name); return ri.TwoLetterISORegionName ?? string.Empty; } catch { // fallback: parse var parts = localeName.Split (new char [] { '-', '_' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) return parts [1]; } } catch { var parts = localeName.Split (new char [] { '-', '_' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) return parts [1]; } return string.Empty; } // LCID -> combined code like "en-US" (with configurable separator) public static string LcidToLocaleCode (int lcid) { try { var ci = new CultureInfo (lcid); if (!string.IsNullOrEmpty (ci.Name)) return ci.Name; } catch { try { var name = ToLocaleName (lcid); if (!string.IsNullOrEmpty (name)) return name; } catch { } } return string.Empty; } // Get the user default locale name public static string GetUserDefaultLocaleName () { try { // In .NET, CurrentCulture corresponds to user default string name = CultureInfo.CurrentCulture.Name; if (!string.IsNullOrEmpty (name)) return name; } catch { } return LcidToLocaleCode (CultureInfo.CurrentCulture.LCID); } // Get system default locale name (machine) public static string GetSystemDefaultLocaleName () { try { // InstalledUICulture / Invariant fallback string name = CultureInfo.InstalledUICulture.Name; if (!string.IsNullOrEmpty (name)) return name; } catch { } return LcidToLocaleCode (CultureInfo.InstalledUICulture.LCID); } // Get computer locale code similar to C++ approach public static string GetComputerLocaleCode () { try { // Thread culture -> user -> system string threadName = System.Threading.Thread.CurrentThread.CurrentCulture.Name; if (!string.IsNullOrEmpty (threadName)) return threadName; string user = GetUserDefaultLocaleName (); if (!string.IsNullOrEmpty (user)) return user; string system = GetSystemDefaultLocaleName (); if (!string.IsNullOrEmpty (system)) return system; } catch { } // fallback to invariant return CultureInfo.InvariantCulture.Name ?? string.Empty; } // Compare two locale names; returns true if equal by name or LCID public static bool LocaleNameCompare (string left, string right) { if (string.Equals (left, right, StringComparison.OrdinalIgnoreCase)) return true; try { int l = ToLCID (left); int r = ToLCID (right); return l == r && l != CultureInfo.InvariantCulture.LCID; } catch { return false; } } // Constants private const int LOCALE_NAME_MAX_LENGTH = 85; // defined by Windows } public static class UIExt { // GetDeviceCaps index for DPI X private const int LOGPIXELSX = 88; [DllImport ("user32.dll")] private static extern IntPtr GetDC (IntPtr hWnd); [DllImport ("user32.dll")] private static extern int ReleaseDC (IntPtr hWnd, IntPtr hDC); [DllImport ("gdi32.dll")] private static extern int GetDeviceCaps (IntPtr hdc, int nIndex); /// /// Gets system DPI as percentage (100 = 96 DPI, 125 = 120 DPI, etc.) /// public static int DPI { get { return GetDPI (); } } /// /// Gets system DPI as scale factor (1.0 = 100%, 1.25 = 125%) /// public static double DPIScale { get { return DPI * 0.01; } } /// /// Gets system DPI percentage based on 96 DPI baseline. /// public static int GetDPI () { IntPtr hdc = IntPtr.Zero; try { hdc = GetDC (IntPtr.Zero); if (hdc == IntPtr.Zero) return 100; // safe default int dpiX = GetDeviceCaps (hdc, LOGPIXELSX); if (dpiX <= 0) return 100; // 96 DPI == 100% return (int)Math.Round (dpiX * 100.0 / 96.0); } catch { return 100; } finally { if (hdc != IntPtr.Zero) ReleaseDC (IntPtr.Zero, hdc); } } } public static class MSRUriHelper { public const string MsResScheme = "ms-resource:"; public static readonly int MsResSchemeLength = MsResScheme.Length; /// /// Converts ms-resource URI or file path to path segments. /// public static int KeyToPath (string key, IList output) { output.Clear (); if (string.IsNullOrEmpty (key)) return 0; key = key.Trim (); try { // URI if (IsMsResourceUri (key)) { Uri uri = new Uri (key, UriKind.RelativeOrAbsolute); return UriToPath (uri, output); } // File path SplitPath (key, '\\', output); } catch { // fallback: treat as file path SplitPath (key, '\\', output); } return output.Count; } public static List KeyToPath (string key) { List ret = new List (); KeyToPath (key, ret); return ret; } /// /// Converts System.Uri to path segments. /// public static int UriToPath (Uri uri, IList output) { output.Clear (); if (uri == null) return 0; try { string path = uri.AbsolutePath; string [] parts = path.Split (new [] { '/' }, StringSplitOptions.RemoveEmptyEntries); foreach (string p in parts) output.Add (p); } catch { // ignored } return output.Count; } public static int UriToPath (string uristr, IList output) { var uri = new Uri (uristr); return UriToPath (uri, output); } public static List UriToPath (Uri uri) { List ret = new List (); UriToPath (uri, ret); return ret; } public static List UriToPath (string uristr) { var uri = new Uri (uristr); return UriToPath (uri); } /// /// Checks whether key starts with ms-resource: /// public static bool IsMsResourceUri (string key) { if (string.IsNullOrEmpty (key)) return false; return key.TrimStart ().StartsWith (MsResScheme, StringComparison.OrdinalIgnoreCase); } /// /// ms-resource://... (full uri) /// public static bool IsFullMsResourceUri (string key) { if (!IsMsResourceUri (key)) return false; return key.TrimStart ().StartsWith ( MsResScheme + "//", StringComparison.OrdinalIgnoreCase); } /// /// ms-resource:foo/bar (relative uri) /// public static bool IsRelativeMsResourceUri (string key) { return IsMsResourceUri (key) && !IsFullMsResourceUri (key); } private static void SplitPath (string value, char sep, IList output) { string [] parts = value.Split (new [] { sep }, StringSplitOptions.RemoveEmptyEntries); foreach (string p in parts) output.Add (p); } } public enum PriPathSeparator { Backslash, // "\" Slash // "/" } public sealed class PriPath: IList, IEquatable { private readonly List _segments; public bool IgnoreCase { get; } public PriPath (bool ignoreCase = true) { _segments = new List (); IgnoreCase = ignoreCase; } public PriPath (IEnumerable segments, bool ignoreCase = true) { _segments = new List (segments ?? Enumerable.Empty ()); IgnoreCase = ignoreCase; } public PriPath (Uri resuri, bool ignoreCase = true) : this (MSRUriHelper.UriToPath (resuri), ignoreCase) { } public PriPath (string resname, bool ignoreCase = true) : this (MSRUriHelper.KeyToPath (resname), ignoreCase) { } public int Count => _segments.Count; public bool IsReadOnly => false; public string this [int index] { get { return _segments [index]; } set { _segments [index] = value; } } public void Add (string item) => _segments.Add (item); public void Clear () => _segments.Clear (); public bool Contains (string item) => _segments.Contains (item, IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); public void CopyTo (string [] array, int arrayIndex) => _segments.CopyTo (array, arrayIndex); public IEnumerator GetEnumerator () => _segments.GetEnumerator (); IEnumerator IEnumerable.GetEnumerator () => _segments.GetEnumerator (); public int IndexOf (string item) => _segments.FindIndex (x => string.Equals (x, item, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); public void Insert (int index, string item) => _segments.Insert (index, item); public bool Remove (string item) => _segments.Remove (item); public void RemoveAt (int index) => _segments.RemoveAt (index); public override string ToString () { return ToString (PriPathSeparator.Backslash); } public string ToString (PriPathSeparator sep) { string s = sep == PriPathSeparator.Backslash ? "\\" : "/"; return string.Join (s, _segments); } public string ToUriString () { // ms-resource: URI style (relative) return "ms-resource:" + ToString (PriPathSeparator.Slash); } public static PriPath FromString (string path, bool ignoreCase = true) { if (path == null) return new PriPath (ignoreCase: ignoreCase); // detect URI if (path.StartsWith ("ms-resource:", StringComparison.OrdinalIgnoreCase)) { string rest = path.Substring ("ms-resource:".Length); rest = rest.TrimStart ('/'); var segs = rest.Split (new [] { '/' }, StringSplitOptions.RemoveEmptyEntries); return new PriPath (segs, ignoreCase); } // file path var parts = path.Split (new [] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); return new PriPath (parts, ignoreCase); } public override bool Equals (object obj) { return Equals (obj as PriPath); } public bool Equals (PriPath other) { if (ReferenceEquals (other, null)) return false; if (ReferenceEquals (this, other)) return true; if (Count != other.Count) return false; var comparer = IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; for (int i = 0; i < Count; i++) { if (!comparer.Equals (_segments [i], other._segments [i])) return false; } return true; } public override int GetHashCode () { var comparer = IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; int hash = 17; foreach (var seg in _segments) { hash = hash * 31 + comparer.GetHashCode (seg?.Trim () ?? ""); } return hash; } // Operators public static bool operator == (PriPath a, PriPath b) { if (ReferenceEquals (a, b)) return true; if (ReferenceEquals (a, null) || ReferenceEquals (b, null)) return false; return a.Equals (b); } public static bool operator != (PriPath a, PriPath b) => !(a == b); // Concat with another path public static PriPath operator + (PriPath a, PriPath b) { if (a == null) return b == null ? null : new PriPath (b, ignoreCase: true); if (b == null) return new PriPath (a, ignoreCase: a.IgnoreCase); var result = new PriPath (a, a.IgnoreCase); result._segments.AddRange (b._segments); return result; } // Append segment public static PriPath operator / (PriPath a, string segment) { if (a == null) return new PriPath (new [] { segment }); var result = new PriPath (a, a.IgnoreCase); result._segments.Add (segment); return result; } } public sealed class PriResourceIdentifier: IEquatable { public string Key { get; private set; } public int TaskType { get; private set; } // 0: string (ms-resource), 1: file path public PriPath Path { get; private set; } public PriResourceIdentifier () { Key = string.Empty; TaskType = 1; Path = new PriPath (); } public PriResourceIdentifier (string key, int type = -1) { SetKey (key, type); } public PriResourceIdentifier (Uri uri, int type = -1) { if (uri == null) { SetKey (string.Empty, type); return; } SetKey (uri.ToString (), type); } public void SetKey (string value, int type = -1) { Key = value ?? string.Empty; if (type < 0 || type > 1) { TaskType = MSRUriHelper.IsMsResourceUri (Key) ? 0 : 1; } else { TaskType = type; } var arr = MSRUriHelper.KeyToPath (Key); if (TaskType == 1) arr.Insert (0, "Files"); else if (TaskType == 0) { if (MSRUriHelper.IsRelativeMsResourceUri (Key)) arr.Insert (0, "resources"); } // build path segments Path = new PriPath (arr, ignoreCase: true); } public bool IsUri () { return TaskType == 0; } public bool IsFilePath () { return TaskType == 1; } public bool IsRelativeUri () { return MSRUriHelper.IsRelativeMsResourceUri (Key); } public bool IsFullUri () { return MSRUriHelper.IsFullMsResourceUri (Key); } public int GetPath (IList output) { if (output == null) throw new ArgumentNullException ("output"); output.Clear (); if (Path != null) { foreach (var seg in Path) output.Add (seg); } return output.Count; } public override string ToString () { return Key; } // Equals / HashCode public override bool Equals (object obj) { return Equals (obj as PriResourceIdentifier); } public bool Equals (PriResourceIdentifier other) { if (ReferenceEquals (other, null)) return false; if (ReferenceEquals (this, other)) return true; // Key and Path should be equivalent return string.Equals (Key, other.Key, StringComparison.OrdinalIgnoreCase) && ((Path == null && other.Path == null) || (Path != null && Path.Equals (other.Path))); } public override int GetHashCode () { int hash = 17; hash = hash * 31 + (Key ?? "").ToLowerInvariant ().GetHashCode (); hash = hash * 31 + (Path != null ? Path.GetHashCode () : 0); return hash; } public static bool operator == (PriResourceIdentifier a, PriResourceIdentifier b) { if (ReferenceEquals (a, b)) return true; if (ReferenceEquals (a, null) || ReferenceEquals (b, null)) return false; return a.Equals (b); } public static bool operator != (PriResourceIdentifier a, PriResourceIdentifier b) { return !(a == b); } } }