PageRenderTime 70ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/UtMisc.cs

https://bitbucket.org/rstarkov/tankiconmaker
C# | 669 lines | 562 code | 50 blank | 57 comment | 102 complexity | 92eb4aa0386ca2f29255b7ea4275e8e4 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Reflection;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using System.Windows;
  13. using Microsoft.Win32;
  14. using RT.Util;
  15. using RT.Util.Dialogs;
  16. using RT.Util.ExtensionMethods;
  17. using RT.Util.Lingo;
  18. using WotDataLib;
  19. namespace TankIconMaker
  20. {
  21. static partial class Ut
  22. {
  23. /// <summary>Shorthand for string.Format, with a more natural ordering (since formatting is typically an afterthought).</summary>
  24. public static string Fmt(this string formatString, params object[] args)
  25. {
  26. return string.Format(formatString, args);
  27. }
  28. /// <summary>Shorthand for comparing strings ignoring case. Suitable for things like filenames, but not address books.</summary>
  29. public static bool EqualsNoCase(this string string1, string string2)
  30. {
  31. return StringComparer.OrdinalIgnoreCase.Equals(string1, string2);
  32. }
  33. /// <summary>Returns one of the specified values based on which country this value represents.</summary>
  34. public static T Pick<T>(this Country country, T ussr, T germany, T usa, T france, T china, T uk, T japan, T czech, T sweden, T none)
  35. {
  36. switch (country)
  37. {
  38. case Country.USSR: return ussr;
  39. case Country.Germany: return germany;
  40. case Country.USA: return usa;
  41. case Country.France: return france;
  42. case Country.China: return china;
  43. case Country.UK: return uk;
  44. case Country.Japan: return japan;
  45. case Country.Czech: return czech;
  46. case Country.Sweden: return sweden;
  47. case Country.None: return none;
  48. default: throw new Exception();
  49. }
  50. }
  51. /// <summary>Returns one of the specified values based on which tank class this value represents.</summary>
  52. public static T Pick<T>(this Class class_, T light, T medium, T heavy, T destroyer, T artillery, T none)
  53. {
  54. switch (class_)
  55. {
  56. case Class.Light: return light;
  57. case Class.Medium: return medium;
  58. case Class.Heavy: return heavy;
  59. case Class.Destroyer: return destroyer;
  60. case Class.Artillery: return artillery;
  61. case Class.None: return none;
  62. default: throw new Exception();
  63. }
  64. }
  65. /// <summary>Returns one of the specified values based on which tank category this value represents.</summary>
  66. public static T Pick<T>(this Category class_, T normal, T premium, T special)
  67. {
  68. switch (class_)
  69. {
  70. case Category.Normal: return normal;
  71. case Category.Premium: return premium;
  72. case Category.Special: return special;
  73. default: throw new Exception();
  74. }
  75. }
  76. /// <summary>
  77. /// Enumerates the full paths to each installation of World of Tanks that can be found in the registry. Enumerates only those directories which exist,
  78. /// but does not verify that there is a valid WoT installation at that path.
  79. /// </summary>
  80. public static IEnumerable<string> EnumerateGameInstallations()
  81. {
  82. var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  83. try
  84. {
  85. using (var installs1 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", writable: false))
  86. using (var installs2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall", writable: false))
  87. {
  88. var keys = new[] { installs1, installs2 }.SelectMany(ins => ins.GetSubKeyNames().Select(name => new { Key = ins, Name = name }));
  89. foreach (var item in keys.Where(k => k.Name.StartsWith("{1EAC1D02-C6AC-4FA6-9A44-96258C37")))
  90. {
  91. try
  92. {
  93. using (var key = item.Key.OpenSubKey(item.Name))
  94. paths.Add(key.GetValue("InstallLocation") as string);
  95. }
  96. catch { }
  97. }
  98. }
  99. }
  100. catch { }
  101. paths.RemoveWhere(p => p == null || !Directory.Exists(p));
  102. return paths;
  103. }
  104. /// <summary>
  105. /// Returns the first item whose <paramref name="maxOf"/> selector is maximal in this collection, or null if the collection is empty.
  106. /// </summary>
  107. public static TItem MaxOrDefault<TItem, TSelector>(this IEnumerable<TItem> collection, Func<TItem, TSelector> maxOf)
  108. {
  109. return collection.MaxAll(maxOf).FirstOrDefault();
  110. }
  111. /// <summary>
  112. /// Enumerates all the items whose <paramref name="maxOf"/> selector is maximal in this collection.
  113. /// </summary>
  114. public static IEnumerable<TItem> MaxAll<TItem, TSelector>(this IEnumerable<TItem> collection, Func<TItem, TSelector> maxOf)
  115. {
  116. var comparer = Comparer<TSelector>.Default;
  117. var largest = default(TSelector);
  118. var result = new List<TItem>();
  119. bool any = false;
  120. foreach (var item in collection)
  121. {
  122. var current = maxOf(item);
  123. var compare = comparer.Compare(current, largest);
  124. if (!any || compare > 0)
  125. {
  126. any = true;
  127. largest = current;
  128. result.Clear();
  129. result.Add(item);
  130. }
  131. else if (compare == 0)
  132. result.Add(item);
  133. }
  134. return result;
  135. }
  136. /// <summary>
  137. /// Reduces the size of a stack trace by removing all lines which are outside this program's namespace and
  138. /// leaving only relative source file names.
  139. /// </summary>
  140. public static string CollapseStackTrace(string stackTrace)
  141. {
  142. var lines = stackTrace.Split('\n');
  143. var result = new StringBuilder();
  144. bool needEllipsis = true;
  145. string fileroot = null;
  146. try { fileroot = Path.GetDirectoryName(new StackFrame(true).GetFileName()) + @"\"; }
  147. catch { }
  148. foreach (var line in lines)
  149. {
  150. if (line.Contains(typeof(Ut).Namespace))
  151. {
  152. result.AppendLine(" " + (fileroot == null ? line : line.Replace(fileroot, "")).Trim());
  153. needEllipsis = true;
  154. }
  155. else if (needEllipsis)
  156. {
  157. result.AppendLine(" ...");
  158. needEllipsis = false;
  159. }
  160. }
  161. var str = result.ToString();
  162. return str.EndsWith("\r\n") ? str.Substring(0, str.Length - 2) : str;
  163. }
  164. public static int ModPositive(int value, int modulus)
  165. {
  166. int result = value % modulus;
  167. return result >= 0 ? result : (result + modulus);
  168. }
  169. /// <summary>
  170. /// Works correctly even if context is null; will simply skip trying to make the path relative to game directories.
  171. /// </summary>
  172. public static string MakeRelativePath(WotContext context, string path)
  173. {
  174. try
  175. {
  176. if (PathUtil.IsSubpathOfOrSame(path, PathUtil.AppPath))
  177. return PathUtil.ToggleRelative(PathUtil.AppPath, path);
  178. }
  179. catch { }
  180. try
  181. {
  182. if (PathUtil.IsSubpathOfOrSame(path, Path.Combine(context.Installation.Path, Ut.ExpandPath(context, context.VersionConfig.PathMods))))
  183. return PathUtil.ToggleRelative(Path.Combine(context.Installation.Path, Ut.ExpandPath(context, context.VersionConfig.PathMods)), path);
  184. }
  185. catch { }
  186. try
  187. {
  188. if (PathUtil.IsSubpathOfOrSame(path, context.Installation.Path))
  189. return PathUtil.ToggleRelative(context.Installation.Path, path);
  190. }
  191. catch { }
  192. return path;
  193. }
  194. /// <summary>
  195. /// Determines whether the specified file contains the specified text. The file doesn’t have to exist.
  196. /// </summary>
  197. public static bool FileContains(string fileName, string content)
  198. {
  199. if (content == null)
  200. return false;
  201. try
  202. {
  203. foreach (var line in File.ReadLines(fileName))
  204. if (line.Contains(content))
  205. return true;
  206. return false;
  207. }
  208. catch (FileNotFoundException) { return false; }
  209. catch (DirectoryNotFoundException) { return false; }
  210. }
  211. /// <summary>
  212. /// Generates a representation of the specified byte array as hexadecimal numbers (“hexdump”).
  213. /// </summary>
  214. public static string ToHex(this byte[] data)
  215. {
  216. if (data == null)
  217. throw new ArgumentNullException("data");
  218. char[] charArr = new char[data.Length * 2];
  219. var j = 0;
  220. for (int i = 0; i < data.Length; i++)
  221. {
  222. byte b = (byte) (data[i] >> 4);
  223. charArr[j] = (char) (b < 10 ? '0' + b : 'W' + b); // 'a'-10 = 'W'
  224. j++;
  225. b = (byte) (data[i] & 0xf);
  226. charArr[j] = (char) (b < 10 ? '0' + b : 'W' + b);
  227. j++;
  228. }
  229. return new string(charArr);
  230. }
  231. /// <summary>Copies <paramref name="len"/> bytes from one location to another. Works fastest if <paramref name="len"/> is divisible by 16.</summary>
  232. public static unsafe void MemSet(byte* dest, byte value, int len)
  233. {
  234. ushort ushort_ = (ushort) (value | (value << 8));
  235. uint uint_ = (uint) (ushort_ | (ushort_ << 16));
  236. ulong ulong_ = uint_ | ((ulong) uint_ << 32);
  237. if (len >= 16)
  238. {
  239. do
  240. {
  241. *(ulong*) dest = ulong_;
  242. *(ulong*) (dest + 8) = ulong_;
  243. dest += 16;
  244. }
  245. while ((len -= 16) >= 16);
  246. }
  247. if (len > 0)
  248. {
  249. if ((len & 8) != 0)
  250. {
  251. *(ulong*) dest = ulong_;
  252. dest += 8;
  253. }
  254. if ((len & 4) != 0)
  255. {
  256. *(uint*) dest = uint_;
  257. dest += 4;
  258. }
  259. if ((len & 2) != 0)
  260. {
  261. *(ushort*) dest = ushort_;
  262. dest += 2;
  263. }
  264. if ((len & 1) != 0)
  265. *dest = value;
  266. }
  267. }
  268. /// <summary>Copies <paramref name="len"/> bytes from one location to another. Works fastest if <paramref name="len"/> is divisible by 16.</summary>
  269. public static unsafe void MemCpy(byte* dest, byte* src, int len)
  270. {
  271. if (len >= 16)
  272. {
  273. do
  274. {
  275. *(long*) dest = *(long*) src;
  276. *(long*) (dest + 8) = *(long*) (src + 8);
  277. dest += 16;
  278. src += 16;
  279. }
  280. while ((len -= 16) >= 16);
  281. }
  282. if (len > 0)
  283. {
  284. if ((len & 8) != 0)
  285. {
  286. *(long*) dest = *(long*) src;
  287. dest += 8;
  288. src += 8;
  289. }
  290. if ((len & 4) != 0)
  291. {
  292. *(int*) dest = *(int*) src;
  293. dest += 4;
  294. src += 4;
  295. }
  296. if ((len & 2) != 0)
  297. {
  298. *(short*) dest = *(short*) src;
  299. dest += 2;
  300. src += 2;
  301. }
  302. if ((len & 1) != 0)
  303. *dest = *src;
  304. }
  305. }
  306. /// <summary>Copies <paramref name="len"/> bytes from one location to another. Works fastest if <paramref name="len"/> is divisible by 16.</summary>
  307. public static unsafe void MemCpy(byte[] dest, byte* src, int len)
  308. {
  309. if (len > dest.Length)
  310. throw new ArgumentOutOfRangeException("len");
  311. fixed (byte* destPtr = dest)
  312. MemCpy(destPtr, src, len);
  313. }
  314. /// <summary>Copies <paramref name="len"/> bytes from one location to another. Works fastest if <paramref name="len"/> is divisible by 16.</summary>
  315. public static unsafe void MemCpy(byte* dest, byte[] src, int len)
  316. {
  317. if (len > src.Length)
  318. throw new ArgumentOutOfRangeException("len");
  319. fixed (byte* srcPtr = src)
  320. MemCpy(dest, srcPtr, len);
  321. }
  322. public static Language GetOsLanguage()
  323. {
  324. try
  325. {
  326. var curUiLanguage = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName.ToLowerInvariant();
  327. // Special case for the languages we supply, so that if there are several with the same 2-letter code
  328. // we can pick the right one.
  329. switch (curUiLanguage)
  330. {
  331. case "en": return Language.EnglishUK;
  332. case "ru": return Language.Russian;
  333. case "de": return Language.German;
  334. }
  335. var t = typeof(Language);
  336. foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.Static))
  337. {
  338. var a = f.GetCustomAttributes<LanguageInfoAttribute>().FirstOrDefault();
  339. if (a != null && a.LanguageCode == curUiLanguage)
  340. return (Language) f.GetValue(null);
  341. }
  342. }
  343. catch { }
  344. return Language.EnglishUK;
  345. }
  346. /// <summary>Expands a Tank Icon Maker-style path, which may have expandable tokens like "VersionName".</summary>
  347. public static string ExpandPath(WotContext context, string path)
  348. {
  349. if (path == null)
  350. return null;
  351. path = path.Replace("\"VersionName\"", context.Installation.GameVersionName);
  352. if (path.Contains('"'))
  353. throw new Exception("The path “{0}” contains double-quote characters after expanding all known tokens. Did you mean one of: \"VersionName\"?".Fmt(path));
  354. return path;
  355. }
  356. /// <summary>Expands a Tank Icon Maker-style path, which may have expandable tokens like "VersionName".</summary>
  357. public static string ExpandIconPath(string path, WotContext context, Style style, Country country, Class class_, bool fragment = false)
  358. {
  359. return ExpandIconPath(path, context, style, country.Pick("ussr", "germany", "usa", "france", "china", "uk", "japan", "czech", "sweden", "none"), class_.Pick("light", "medium", "heavy", "destroyer", "artillery", "none"), fragment);
  360. }
  361. /// <summary>Expands a Tank Icon Maker-style path, which may have expandable tokens like "VersionName".</summary>
  362. public static string ExpandIconPath(string path, WotContext context, Style style, string country, string class_, bool fragment = false)
  363. {
  364. if (path == "")
  365. path = "{IconsPath}";
  366. path = path.Replace("{IconsPath}", Ut.ExpandPath(context, context.VersionConfig.PathDestination) + @"\");
  367. path = path.Replace("{TimPath}", PathUtil.AppPath + @"\");
  368. path = path.Replace("{GamePath}", context.Installation.Path + @"\");
  369. path = path.Replace("{GameVersion}", context.Installation.GameVersionName);
  370. if (class_ != null)
  371. path = path.Replace("{TankClass}", class_);
  372. if (country != null)
  373. path = path.Replace("{TankCountry}", country);
  374. path = path.Replace("{StyleName}", style.Name);
  375. path = path.Replace("{StyleAuthor}", style.Author);
  376. path = Environment.ExpandEnvironmentVariables(path);
  377. path = path.Replace(@"\\", @"\").Replace(@"\\", @"\").Replace(@"\\", @"\");
  378. if (path.EndsWith(@"\") && !path.EndsWith(@":\"))
  379. path = path.Substring(0, path.Length - 1);
  380. return fragment ? path : Path.GetFullPath(Path.Combine(context.Installation.Path, path));
  381. }
  382. public static void RemoveWhere<T>(this ICollection<T> collection, Func<T, bool> predicate)
  383. {
  384. foreach (var item in collection.Where(predicate).ToList())
  385. collection.Remove(item);
  386. }
  387. /// <summary>Selects a string from the specified dictionary based on the current application language.</summary>
  388. public static string StringForCurrentLanguage(IDictionary<string, string> dictionary)
  389. {
  390. if (dictionary.Count == 0)
  391. return "";
  392. string lang;
  393. if (App.Translation.Language == Language.EnglishUK || App.Translation.Language == Language.EnglishUS)
  394. lang = "En";
  395. else
  396. lang = App.Translation.Language.GetIsoLanguageCode();
  397. if (dictionary.ContainsKey(lang))
  398. return dictionary[lang];
  399. else if (dictionary.ContainsKey("En"))
  400. return dictionary["En"];
  401. else
  402. return dictionary.Values.First();
  403. }
  404. /// <summary>
  405. /// Attempts to copy the specified text to clipboard, correctly handling the case where the clipboard may be
  406. /// temporarily locked by another application (such as TeamViewer). Waits for the clipboard to become available
  407. /// for up to 1 second; shows a dialog on failure with the option to retry.
  408. /// </summary>
  409. public static bool ClipboardSet(string text)
  410. {
  411. while (true)
  412. {
  413. for (int retries = 0; retries < 10; retries++)
  414. {
  415. try { Clipboard.SetText(text, TextDataFormat.UnicodeText); return true; }
  416. catch { Thread.Sleep(100); }
  417. }
  418. if (1 == DlgMessage.ShowWarning(App.Translation.Error.ClipboardError, App.Translation.Error.ClipboardError_Retry, App.Translation.Prompt.Cancel))
  419. return false;
  420. }
  421. }
  422. /// <summary>
  423. /// Constructs a string describing an exception for the purpose of sending an error report to developers.
  424. /// </summary>
  425. public static string ExceptionToDebugString(object exception)
  426. {
  427. var errorInfo = new StringBuilder("Thread: " + Thread.CurrentThread.Name + "\n");
  428. while (exception != null)
  429. {
  430. errorInfo.AppendFormat("\nException: {0}", exception.GetType());
  431. var excp = exception as Exception;
  432. if (excp != null)
  433. {
  434. errorInfo.AppendFormat("\nMessage: {0}\n", excp.Message);
  435. errorInfo.AppendLine(Ut.CollapseStackTrace(excp.StackTrace));
  436. exception = excp.InnerException;
  437. }
  438. }
  439. return errorInfo.ToString();
  440. }
  441. }
  442. /// <summary>
  443. /// Enables scheduling tasks to execute in a thread of a different priority than Normal. Only non-critical tasks
  444. /// should be scheduled using this scheduler because any remaining queued tasks will be dropped on program exit.
  445. /// </summary>
  446. public class PriorityScheduler : TaskScheduler
  447. {
  448. // http://stackoverflow.com/a/9056702/33080
  449. public static PriorityScheduler AboveNormal = new PriorityScheduler(ThreadPriority.AboveNormal);
  450. public static PriorityScheduler BelowNormal = new PriorityScheduler(ThreadPriority.BelowNormal);
  451. public static PriorityScheduler Lowest = new PriorityScheduler(ThreadPriority.Lowest);
  452. private BlockingCollection<Task> _tasks = new BlockingCollection<Task>();
  453. private Thread[] _threads;
  454. private ThreadPriority _priority;
  455. private readonly int _maximumConcurrencyLevel = Math.Max(1, Environment.ProcessorCount);
  456. public PriorityScheduler(ThreadPriority priority)
  457. {
  458. _priority = priority;
  459. }
  460. public override int MaximumConcurrencyLevel
  461. {
  462. get { return _maximumConcurrencyLevel; }
  463. }
  464. protected override IEnumerable<Task> GetScheduledTasks()
  465. {
  466. return _tasks;
  467. }
  468. protected override void QueueTask(Task task)
  469. {
  470. _tasks.Add(task);
  471. if (_threads == null)
  472. {
  473. _threads = new Thread[_maximumConcurrencyLevel];
  474. for (int i = 0; i < _threads.Length; i++)
  475. {
  476. _threads[i] = new Thread(() =>
  477. {
  478. foreach (Task t in _tasks.GetConsumingEnumerable())
  479. base.TryExecuteTask(t);
  480. });
  481. _threads[i].Name = string.Format("PriorityScheduler: {0}", i);
  482. _threads[i].Priority = _priority;
  483. _threads[i].IsBackground = true;
  484. _threads[i].Start();
  485. }
  486. }
  487. }
  488. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
  489. {
  490. return false; // we might not want to execute task that should schedule as high or low priority inline
  491. }
  492. }
  493. sealed class TypeInfo<T>
  494. {
  495. public Type Type;
  496. public Func<T> Constructor;
  497. public string Name { get; set; }
  498. public string Description { get; set; }
  499. }
  500. interface IHasTypeNameDescription
  501. {
  502. string TypeName { get; }
  503. string TypeDescription { get; }
  504. }
  505. /// <summary>
  506. /// Encapsulates a file name which may refer to a file inside a container file, or just to a file directly. The parts
  507. /// are separated with a "|". Helps avoid issues with Path.Combine complaining about invalid filenames.
  508. /// </summary>
  509. struct CompositePath
  510. {
  511. /// <summary>The main path part.</summary>
  512. public string File { get; private set; }
  513. /// <summary>The inner path part, where present.</summary>
  514. public string InnerFile { get; private set; }
  515. public override string ToString() { return File + (InnerFile == null ? "" : ("|" + InnerFile)); }
  516. public CompositePath(WotContext context, params string[] path)
  517. : this()
  518. {
  519. var builder = new StringBuilder(256);
  520. string first = null;
  521. foreach (var part in path)
  522. {
  523. int colon = part.IndexOf(':');
  524. int pipe = part.IndexOf('|');
  525. if (colon != -1 && colon != 1)
  526. throw new StyleUserError("Invalid composite file path: \":\" is only allowed in a drive letter specification at the start of the path.");
  527. if (pipe != -1 && (colon > pipe || first != null))
  528. throw new StyleUserError("Invalid composite file path: \":\" is not allowed in the path anywhere after the \"|\".");
  529. // now the colon is either absent or at the right place, in the first half of the composite path
  530. if (pipe != -1 && first != null)
  531. throw new StyleUserError("Invalid composite file path: \"|\" must not occur more than once.");
  532. // now we know that the colon and pipe characters are used correctly in the path so far
  533. if (colon > 0 || part.StartsWith("/") || part.StartsWith("\\"))
  534. builder.Clear();
  535. else if (builder.Length > 0 && builder[builder.Length - 1] != '/' && builder[builder.Length - 1] != '\\')
  536. builder.Append('\\');
  537. if (pipe == -1)
  538. builder.Append(part);
  539. else
  540. {
  541. builder.Append(part.Substring(0, pipe));
  542. first = builder.ToString();
  543. builder.Clear();
  544. builder.Append(part.Substring(pipe + 1));
  545. }
  546. }
  547. var second = builder.ToString();
  548. if (context != null)
  549. {
  550. File = Ut.ExpandPath(context, first ?? second);
  551. InnerFile = first == null ? null : Ut.ExpandPath(context, second);
  552. }
  553. else
  554. {
  555. // Used for testing only
  556. File = first ?? second;
  557. InnerFile = first == null ? null : second;
  558. }
  559. }
  560. #region Tests
  561. internal static void Tests()
  562. {
  563. test(new CompositePath(null, @""), @"", null);
  564. test(new CompositePath(null, @"foo"), @"foo", null);
  565. test(new CompositePath(null, @"foo/bar"), @"foo/bar", null);
  566. test(new CompositePath(null, @"\foo\bar"), @"\foo\bar", null);
  567. test(new CompositePath(null, @"C:\foo\bar"), @"C:\foo\bar", null);
  568. test(new CompositePath(null, @"foo\bar", @"thingy\blah"), @"foo\bar\thingy\blah", null);
  569. test(new CompositePath(null, @"foo\bar\", @"thingy\blah"), @"foo\bar\thingy\blah", null);
  570. test(new CompositePath(null, @"foo\bar", @"thingy\blah", @"stuff"), @"foo\bar\thingy\blah\stuff", null);
  571. test(new CompositePath(null, @"foo\bar", @"thingy\blah", @"D:\stuff"), @"D:\stuff", null);
  572. test(new CompositePath(null, @"C:\foo\bar", @"thingy"), @"C:\foo\bar\thingy", null);
  573. test(new CompositePath(null, @"C:\foo\bar", @"thingy", @"stuff"), @"C:\foo\bar\thingy\stuff", null);
  574. test(new CompositePath(null, @"C:\foo\bar", @"thingy", @"D:\stuff"), @"D:\stuff", null);
  575. test(new CompositePath(null, @"|"), @"", @"");
  576. test(new CompositePath(null, @"fo|o"), @"fo", @"o");
  577. test(new CompositePath(null, @"fo|o/bar"), @"fo", @"o/bar");
  578. test(new CompositePath(null, @"foo/b|ar"), @"foo/b", @"ar");
  579. test(new CompositePath(null, @"C:\fo|o\bar"), @"C:\fo", @"o\bar");
  580. test(new CompositePath(null, @"C:\foo\b|ar"), @"C:\foo\b", @"ar");
  581. test(new CompositePath(null, @"foo\b|ar", @"thingy\blah"), @"foo\b", @"ar\thingy\blah");
  582. test(new CompositePath(null, @"foo\b|ar\", @"thingy\blah"), @"foo\b", @"ar\thingy\blah");
  583. test(new CompositePath(null, @"foo\b|ar", @"thingy\blah", @"stuff"), @"foo\b", @"ar\thingy\blah\stuff");
  584. test(new CompositePath(null, @"D:\foo\b|ar", @"thingy\blah", @"stuff"), @"D:\foo\b", @"ar\thingy\blah\stuff");
  585. test(new CompositePath(null, @"foo\bar", @"thin|gy\blah"), @"foo\bar\thin", @"gy\blah");
  586. test(new CompositePath(null, @"foo\bar\", @"thin|gy\blah"), @"foo\bar\thin", @"gy\blah");
  587. test(new CompositePath(null, @"foo\bar", @"thin|gy\blah", @"stuff"), @"foo\bar\thin", @"gy\blah\stuff");
  588. test(new CompositePath(null, @"foo\bar", @"D:\thin|gy\blah", @"stuff"), @"D:\thin", @"gy\blah\stuff");
  589. test(new CompositePath(null, @"foo\bar", @"thingy\blah", @"stu|ff"), @"foo\bar\thingy\blah\stu", @"ff");
  590. test(new CompositePath(null, @"foo\bar", @"thingy\blah", @"D:\stu|ff"), @"D:\stu", @"ff");
  591. test(new CompositePath(null, @"C:\fo|o\bar", @"thingy"), @"C:\fo", @"o\bar\thingy");
  592. test(new CompositePath(null, @"C:\foo\bar", @"thin|gy"), @"C:\foo\bar\thin", @"gy");
  593. test(new CompositePath(null, @"C:\fo|o\bar", @"thingy", @"stuff"), @"C:\fo", @"o\bar\thingy\stuff");
  594. test(new CompositePath(null, @"C:\foo\bar", @"thin|gy", @"stuff"), @"C:\foo\bar\thin", @"gy\stuff");
  595. test(new CompositePath(null, @"C:\foo\bar", @"thingy", @"stu|ff"), @"C:\foo\bar\thingy\stu", @"ff");
  596. test(new CompositePath(null, @"C:\foo\bar", @"thingy", @"D:\stu|ff"), @"D:\stu", @"ff");
  597. }
  598. private static void test(CompositePath cf, string expectedPath, string expectedInnerPath)
  599. {
  600. if (cf.File != expectedPath || cf.InnerFile != expectedInnerPath)
  601. throw new Exception("CompositePath test failed.");
  602. }
  603. #endregion
  604. }
  605. [TypeConverter(typeof(BoolWithPassthroughTranslation.Conv))]
  606. enum BoolWithPassthrough
  607. {
  608. No,
  609. Yes,
  610. Passthrough,
  611. }
  612. }