PageRenderTime 59ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/MainWindow.xaml.cs

https://bitbucket.org/rstarkov/tankiconmaker
C# | 2391 lines | 2020 code | 230 blank | 141 comment | 416 complexity | 5ad727745d1d6eb8d26b9ddd96ed428e MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0

Large files files are truncated, but you can click here to view the full file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.IO.Packaging;
  8. using System.Linq;
  9. using System.Reflection;
  10. using System.Text.RegularExpressions;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. using System.Windows;
  14. using System.Windows.Controls;
  15. using System.Windows.Data;
  16. using System.Windows.Input;
  17. using System.Windows.Media;
  18. using System.Windows.Media.Imaging;
  19. using System.Windows.Threading;
  20. using System.Xml.Linq;
  21. using ICSharpCode.SharpZipLib.Zip;
  22. using Ookii.Dialogs.Wpf;
  23. using RT.Util;
  24. using RT.Util.Dialogs;
  25. using RT.Util.ExtensionMethods;
  26. using RT.Util.Forms;
  27. using RT.Util.Lingo;
  28. using RT.Util.Serialization;
  29. using TankIconMaker.Layers;
  30. using WotDataLib;
  31. using WpfCrutches;
  32. using Xceed.Wpf.Toolkit.PropertyGrid;
  33. namespace TankIconMaker
  34. {
  35. partial class MainWindow : ManagedWindow
  36. {
  37. private DispatcherTimer _updateIconsTimer = new DispatcherTimer(DispatcherPriority.Background);
  38. private DispatcherTimer _updatePropertiesTimer = new DispatcherTimer(DispatcherPriority.Background);
  39. private CancellationTokenSource _cancelRender = new CancellationTokenSource();
  40. private Dictionary<string, RenderTask> _renderResults = new Dictionary<string, RenderTask>();
  41. private static BitmapImage _warningImage;
  42. private ObservableValue<bool> _rendering = new ObservableValue<bool>(false);
  43. private ObservableValue<bool> _dataMissing = new ObservableValue<bool>(false);
  44. private ObservableCollection<Warning> _warnings = new ObservableCollection<Warning>();
  45. private LanguageHelperWpfOld<Translation> _translationHelper;
  46. public MainWindow()
  47. : base(App.Settings.MainWindow)
  48. {
  49. InitializeComponent();
  50. UiZoom = App.Settings.UiZoom;
  51. GlobalStatusShow(App.Translation.Misc.GlobalStatus_Loading);
  52. ContentRendered += InitializeEverything;
  53. Closing += MainWindow_Closing;
  54. }
  55. /// <summary>
  56. /// Shows a message in large letters in an overlay in the middle of the window. Must be called on the UI thread
  57. /// and won't become visible until the UI thread returns (into the dispatcher).
  58. /// </summary>
  59. private void GlobalStatusShow(string message)
  60. {
  61. (ctGlobalStatusBox.Child as TextBlock).Text = message;
  62. ctGlobalStatusBox.Visibility = Visibility.Visible;
  63. IsEnabled = false;
  64. ctIconsPanel.Opacity = 0.6;
  65. }
  66. /// <summary>
  67. /// Hides the message shown using <see cref="GlobalStatusShow"/>.
  68. /// </summary>
  69. private void GlobalStatusHide()
  70. {
  71. IsEnabled = true;
  72. ctGlobalStatusBox.Visibility = Visibility.Collapsed;
  73. ctIconsPanel.Opacity = 1;
  74. }
  75. private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
  76. {
  77. if (_translationHelper != null && !_translationHelper.MayExitApplication())
  78. e.Cancel = true;
  79. }
  80. /// <summary>
  81. /// Performs most of the slow initializations. This method is only called after the UI becomes visible, to improve the
  82. /// perceived start-up performance.
  83. /// </summary>
  84. private void InitializeEverything(object ___, EventArgs ____)
  85. {
  86. ContentRendered -= InitializeEverything;
  87. OldFiles.DeleteOldFiles();
  88. RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.HighQuality);
  89. var mat = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
  90. App.DpiScaleX = mat.M11;
  91. App.DpiScaleY = mat.M22;
  92. var lingoTypeDescProvider = new LingoTypeDescriptionProvider<Translation>(() => App.Translation);
  93. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(LayerBase));
  94. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(EffectBase));
  95. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<string>));
  96. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<BoolWithPassthrough>));
  97. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<Color>));
  98. System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<Filename>));
  99. #if DEBUG
  100. Lingo.AlsoSaveTranslationsTo = PathUtil.AppPathCombine(@"..\..\Resources\Translations");
  101. using (var translationFileGenerator = new Lingo.TranslationFileGenerator(PathUtil.AppPathCombine(@"..\..\Translation.g.cs")))
  102. {
  103. translationFileGenerator.TranslateWindow(this, App.Translation.MainWindow);
  104. var wnd = new PathTemplateWindow();
  105. translationFileGenerator.TranslateWindow(wnd, App.Translation.PathTemplateWindow);
  106. wnd.Close();
  107. var wnd2 = new BulkSaveSettingsWindow();
  108. translationFileGenerator.TranslateWindow(wnd2, App.Translation.BulkSaveSettingsWindow);
  109. wnd2.Close();
  110. }
  111. #endif
  112. using (var iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/TankIconMaker;component/Resources/Graphics/icon.ico")).Stream)
  113. _translationHelper = new LanguageHelperWpfOld<Translation>("Tank Icon Maker", "TankIconMaker", true,
  114. App.Settings.TranslationFormSettings, new System.Drawing.Icon(iconStream), () => App.Settings.Lingo);
  115. _translationHelper.TranslationChanged += TranslationChanged;
  116. Translate(first: true);
  117. Title += " (v{0:000} b{1})".Fmt(Assembly.GetExecutingAssembly().GetName().Version.Major, Assembly.GetExecutingAssembly().GetName().Version.Minor);
  118. CommandBindings.Add(new CommandBinding(TankLayerCommands.AddLayer, cmdLayer_AddLayer));
  119. CommandBindings.Add(new CommandBinding(TankLayerCommands.AddEffect, cmdLayer_AddEffect, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
  120. CommandBindings.Add(new CommandBinding(TankLayerCommands.Rename, cmdLayer_Rename, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
  121. CommandBindings.Add(new CommandBinding(TankLayerCommands.Delete, cmdLayer_Delete, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
  122. CommandBindings.Add(new CommandBinding(TankLayerCommands.Copy, cmdLayer_Copy, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
  123. CommandBindings.Add(new CommandBinding(TankLayerCommands.CopyEffects, cmdLayer_CopyEffects, (_, a) => { a.CanExecute = isLayerSelected(); }));
  124. CommandBindings.Add(new CommandBinding(TankLayerCommands.Paste, cmdLayer_Paste, (_, a) => { a.CanExecute = isLayerOrEffectInClipboard(); }));
  125. CommandBindings.Add(new CommandBinding(TankLayerCommands.MoveUp, cmdLayer_MoveUp, (_, a) => { a.CanExecute = cmdLayer_MoveUp_IsAvailable(); }));
  126. CommandBindings.Add(new CommandBinding(TankLayerCommands.MoveDown, cmdLayer_MoveDown, (_, a) => { a.CanExecute = cmdLayer_MoveDown_IsAvailable(); }));
  127. CommandBindings.Add(new CommandBinding(TankLayerCommands.ToggleVisibility, cmdLayer_ToggleVisibility, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
  128. CommandBindings.Add(new CommandBinding(TankStyleCommands.Add, cmdStyle_Add));
  129. CommandBindings.Add(new CommandBinding(TankStyleCommands.Delete, cmdStyle_Delete, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
  130. CommandBindings.Add(new CommandBinding(TankStyleCommands.ChangeName, cmdStyle_ChangeName, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
  131. CommandBindings.Add(new CommandBinding(TankStyleCommands.ChangeAuthor, cmdStyle_ChangeAuthor, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
  132. CommandBindings.Add(new CommandBinding(TankStyleCommands.Duplicate, cmdStyle_Duplicate));
  133. CommandBindings.Add(new CommandBinding(TankStyleCommands.Import, cmdStyle_Import));
  134. CommandBindings.Add(new CommandBinding(TankStyleCommands.Export, cmdStyle_Export));
  135. CommandBindings.Add(new CommandBinding(TankStyleCommands.IconWidth, cmdStyle_IconWidth));
  136. CommandBindings.Add(new CommandBinding(TankStyleCommands.IconHeight, cmdStyle_IconHeight));
  137. CommandBindings.Add(new CommandBinding(TankStyleCommands.Centerable, cmdStyle_Centerable));
  138. _updateIconsTimer.Tick += UpdateIcons;
  139. _updateIconsTimer.Interval = TimeSpan.FromMilliseconds(100);
  140. if (App.Settings.LeftColumnWidth != null)
  141. ctLeftColumn.Width = new GridLength(App.Settings.LeftColumnWidth.Value);
  142. if (App.Settings.NameColumnWidth != null)
  143. ctLayerProperties.NameColumnWidth = App.Settings.NameColumnWidth.Value;
  144. ctDisplayMode.SelectedIndex = (int) App.Settings.DisplayFilter;
  145. ApplyBackground();
  146. ApplyBackgroundColors();
  147. _warningImage = new BitmapImage(new Uri(@"pack://application:,,,/Resources/Graphics/warning.png"));
  148. // Styles: build the combined built-in and user-defined styles collection
  149. var styles = new CompositeCollection<Style>();
  150. RecreateBuiltInStyles();
  151. styles.AddCollection(_builtinStyles);
  152. styles.AddCollection(App.Settings.Styles);
  153. // Styles: update the active style
  154. if (App.Settings.ActiveStyle == null)
  155. App.Settings.ActiveStyle = _builtinStyles.First();
  156. else if (!App.Settings.Styles.Contains(App.Settings.ActiveStyle))
  157. App.Settings.ActiveStyle = styles.FirstOrDefault(s => s.Name == App.Settings.ActiveStyle.Name && s.Author == App.Settings.ActiveStyle.Author) ?? _builtinStyles.First();
  158. // Styles: configure the UI control
  159. ctStyleDropdown.ItemsSource = styles;
  160. ctStyleDropdown.DisplayMemberPath = "Display";
  161. ctStyleDropdown.SelectedItem = App.Settings.ActiveStyle;
  162. // Game installations: find/add all installations if blank
  163. if (App.Settings.GameInstallations.Count == 0)
  164. AddGameInstallations();
  165. // Game installations: make sure one of the installations is the active one
  166. #pragma warning disable 0618 // ActiveInstallation should only be used for loading/saving the setting, which is what the code below does.
  167. if (!App.Settings.GameInstallations.Contains(App.Settings.ActiveInstallation)) // includes the "null" case
  168. App.Settings.ActiveInstallation = App.Settings.GameInstallations.FirstOrDefault();
  169. // Game installations: configure the UI control
  170. ctGamePath.ItemsSource = App.Settings.GameInstallations;
  171. ctGamePath.DisplayMemberPath = "DisplayName";
  172. ctGamePath.SelectedItem = App.Settings.ActiveInstallation;
  173. #pragma warning restore 0618
  174. ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ColorSelector), ExpandableObject = true });
  175. ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ValueSelector<>), ExpandableObject = true });
  176. ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(Filename), EditorType = typeof(FilenameEditor) });
  177. ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ExtraPropertyId), EditorType = typeof(DataSourceEditor) });
  178. ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(Anchor), EditorType = typeof(AnchorEditor) });
  179. ReloadData(first: true);
  180. // Set WPF bindings now that all the data we need is loaded
  181. BindingOperations.SetBinding(ctRemoveGamePath, Button.IsEnabledProperty, LambdaBinding.New(
  182. new Binding { Source = ctGamePath, Path = new PropertyPath(ComboBox.SelectedIndexProperty) },
  183. (int index) => index >= 0
  184. ));
  185. BindingOperations.SetBinding(ctGamePath, ComboBox.IsEnabledProperty, LambdaBinding.New(
  186. new Binding { Source = App.Settings.GameInstallations, Path = new PropertyPath("Count") },
  187. (int count) => count > 0
  188. ));
  189. BindingOperations.SetBinding(ctWarning, Image.VisibilityProperty, LambdaBinding.New(
  190. new Binding { Source = _warnings, Path = new PropertyPath("Count") },
  191. (int warningCount) => warningCount == 0 ? Visibility.Collapsed : Visibility.Visible
  192. ));
  193. BindingOperations.SetBinding(ctSave, Button.IsEnabledProperty, LambdaBinding.New(
  194. new Binding { Source = _rendering, Path = new PropertyPath("Value") },
  195. new Binding { Source = _dataMissing, Path = new PropertyPath("Value") },
  196. (bool rendering, bool dataMissing) => !rendering && !dataMissing
  197. ));
  198. BindingOperations.SetBinding(ctLayersTree, TreeView.MaxHeightProperty, LambdaBinding.New(
  199. new Binding { Source = ctLeftBottomPane, Path = new PropertyPath(Grid.ActualHeightProperty) },
  200. (double paneHeight) => paneHeight * 0.4
  201. ));
  202. BindingOperations.SetBinding(ctUiZoomIn, Button.IsEnabledProperty, LambdaBinding.New(
  203. new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
  204. (double zoom) => zoom <= 2.5
  205. ));
  206. BindingOperations.SetBinding(ctUiZoomOut, Button.IsEnabledProperty, LambdaBinding.New(
  207. new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
  208. (double zoom) => zoom >= 0.5
  209. ));
  210. BindingOperations.SetBinding(ctPathTemplate, TextBlock.TextProperty, LambdaBinding.New(
  211. new Binding { Path = new PropertyPath("PathTemplate") },
  212. (string template) => string.IsNullOrEmpty(template) ? (string) App.Translation.MainWindow.PathTemplate_Standard : template
  213. ));
  214. // Another day, another WPF crutch... http://stackoverflow.com/questions/3921712
  215. ctLayersTree.PreviewMouseDown += (_, __) => { FocusManager.SetFocusedElement(this, ctLayersTree); };
  216. // Bind the events now that all the UI is set up as desired
  217. Closing += (_, __) => SaveSettings();
  218. this.SizeChanged += SaveSettingsDelayed;
  219. this.LocationChanged += SaveSettingsDelayed;
  220. ctStyleDropdown.SelectionChanged += ctStyleDropdown_SelectionChanged;
  221. ctLayerProperties.PropertyValueChanged += ctLayerProperties_PropertyValueChanged;
  222. ctDisplayMode.SelectionChanged += ctDisplayMode_SelectionChanged;
  223. ctGamePath.SelectionChanged += ctGamePath_SelectionChanged;
  224. ctGamePath.PreviewKeyDown += ctGamePath_PreviewKeyDown;
  225. ctLayersTree.SelectedItemChanged += (_, e) => { _updatePropertiesTimer.Stop(); _updatePropertiesTimer.Start(); };
  226. _updatePropertiesTimer.Tick += (_, __) => { _updatePropertiesTimer.Stop(); ctLayerProperties.SelectedObject = ctLayersTree.SelectedItem; };
  227. _updatePropertiesTimer.Interval = TimeSpan.FromMilliseconds(200);
  228. // Refresh all the commands because otherwise WPF doesn’t realise the states have changed.
  229. CommandManager.InvalidateRequerySuggested();
  230. // Fire off some GUI events manually now that everything's set up
  231. ctStyleDropdown_SelectionChanged();
  232. // Done
  233. GlobalStatusHide();
  234. _updateIconsTimer.Start();
  235. }
  236. private void TranslationChanged(Translation t)
  237. {
  238. App.Translation = t;
  239. App.Settings.Lingo = t.Language;
  240. Translate();
  241. }
  242. private void Translate(bool first = false)
  243. {
  244. App.LayerTypes = translateTypes(App.LayerTypes);
  245. App.EffectTypes = translateTypes(App.EffectTypes);
  246. Lingo.TranslateWindow(this, App.Translation.MainWindow);
  247. DlgMessage.Translate(App.Translation.DlgMessage.OK,
  248. App.Translation.DlgMessage.CaptionInfo,
  249. App.Translation.DlgMessage.CaptionQuestion,
  250. App.Translation.DlgMessage.CaptionWarning,
  251. App.Translation.DlgMessage.CaptionError);
  252. foreach (var style in ctStyleDropdown.Items.OfType<Style>()) // this includes the built-in styles too, unlike App.Settings.Styles
  253. style.TranslationChanged();
  254. if (!first)
  255. {
  256. var wasSelected = ctLayerProperties.SelectedObject;
  257. ctLayerProperties.SelectedObject = null;
  258. ctLayerProperties.SelectedObject = wasSelected;
  259. ctLayersTree.ItemsSource = null;
  260. ctPathTemplate.DataContext = null;
  261. ctStyleDropdown_SelectionChanged();
  262. ReloadData();
  263. UpdateIcons();
  264. }
  265. }
  266. private static IList<TypeInfo<T>> translateTypes<T>(IList<TypeInfo<T>> types) where T : IHasTypeNameDescription
  267. {
  268. return types.Select(type =>
  269. {
  270. var obj = type.Constructor();
  271. return new TypeInfo<T>
  272. {
  273. Type = type.Type,
  274. Constructor = type.Constructor,
  275. Name = obj.TypeName,
  276. Description = obj.TypeDescription,
  277. };
  278. }).OrderBy(ti => ti.Name).ToList().AsReadOnly();
  279. }
  280. private ObservableSortedList<Style> _builtinStyles = new ObservableSortedList<Style>();
  281. private void RecreateBuiltInStyles()
  282. {
  283. _builtinStyles.Clear();
  284. var assy = Assembly.GetExecutingAssembly();
  285. foreach (var resourceName in assy.GetManifestResourceNames().Where(n => n.Contains(".BuiltInStyles.")))
  286. {
  287. try
  288. {
  289. XDocument doc;
  290. using (var stream = assy.GetManifestResourceStream(resourceName))
  291. doc = XDocument.Load(stream);
  292. var style = ClassifyXml.Deserialize<Style>(doc.Root);
  293. style.Kind = style.Name == "Original" ? StyleKind.Original : style.Name == "Current" ? StyleKind.Current : StyleKind.BuiltIn;
  294. _builtinStyles.Add(style);
  295. }
  296. catch { } // should not happen, but if it does, pretend the style doesn’t exist.
  297. }
  298. }
  299. /// <summary>
  300. /// Gets the installation currently selected in the GUI, or null if none are available.
  301. /// </summary>
  302. private TimGameInstallation ActiveInstallation
  303. {
  304. get { return ctGamePath.Items.Count == 0 ? null : (TimGameInstallation) ctGamePath.SelectedItem; }
  305. }
  306. [Obsolete("Use CurContext instead!")] // this warning ensures that CurContext is never directly modified by accident. Only ReloadData is allowed to do that, because it's the one that displays all the warnings to the user if the context cannot be loaded.
  307. private WotContext _context;
  308. /// <summary>
  309. /// Returns a WotContext based on the currently selected game installation and the last loaded game data. Null if there
  310. /// was a problem preventing a context being created. Do not reference this property off the GUI thread. Store the referenced
  311. /// instance in a local and pass _that_ to any background threads. The property will change if the user does certain things
  312. /// while the backround tasks are running, but the WotContext instance itself is immutable.
  313. /// </summary>
  314. public WotContext CurContext
  315. {
  316. get
  317. {
  318. #pragma warning disable 618
  319. if (_context != null && _context.Installation.GameVersionId != ActiveInstallation.GameVersionId)
  320. throw new Exception("CurContext used without a reload"); // this shouldn't be possible; this is just a bug-detecting assertion.
  321. return _context;
  322. #pragma warning restore 618
  323. }
  324. }
  325. /// <summary>
  326. /// Does a bunch of stuff necessary to reload all the data off disk and refresh the UI (except for drawing the icons:
  327. /// this must be done as a separate step).
  328. /// </summary>
  329. private void ReloadData(bool first = false)
  330. {
  331. _renderResults.Clear();
  332. ZipCache.Clear();
  333. ImageCache.Clear();
  334. _warnings.Clear();
  335. #pragma warning disable 618 // ReloadData is the only method allowed to modify _context
  336. _context = null;
  337. #pragma warning restore 618
  338. foreach (var gameInstallation in App.Settings.GameInstallations.ToList()) // grab a list of all items because the source auto-resorts on changes
  339. gameInstallation.Reload();
  340. // Disable parts of the UI if some of the data is unavailable, and show warnings as appropriate
  341. if (ActiveInstallation == null || ActiveInstallation.GameVersionId == null)
  342. {
  343. // This means we don't have a valid WoT installation available. So we still can't show the correct lists of properties
  344. // in the drop-downs, and also can't render tanks because we don't know which ones.
  345. _dataMissing.Value = true;
  346. if (ActiveInstallation == null)
  347. ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_NoInstallationSelected;
  348. else if (!Directory.Exists(ActiveInstallation.Path))
  349. ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_DirNotFound;
  350. else
  351. ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_NoWotInstallation;
  352. ctGameInstallationWarning.Tag = null;
  353. }
  354. else
  355. {
  356. // Attempt to load the data
  357. try
  358. {
  359. #pragma warning disable 618 // ReloadData is the only method allowed to modify _context
  360. _context = WotData.Load(PathUtil.AppPathCombine("Data"), ActiveInstallation, App.Settings.DefaultPropertyAuthor, PathUtil.AppPathCombine("Data", "Exported"));
  361. #pragma warning restore 618
  362. }
  363. catch (WotDataUserError e)
  364. {
  365. _dataMissing.Value = true;
  366. ctGameInstallationWarning.Text = e.Message;
  367. ctGameInstallationWarning.Tag = null;
  368. }
  369. #if !DEBUG
  370. catch (Exception e)
  371. {
  372. _dataMissing.Value = true;
  373. ctGameInstallationWarning.Text = "Error loading game data from this path. Click this message for details.";
  374. ctGameInstallationWarning.Tag = Ut.ExceptionToDebugString(e);
  375. }
  376. #endif
  377. }
  378. // CurContext is now set as appropriate: either null or a reloaded context
  379. if (CurContext != null)
  380. {
  381. // See how complete of a context we managed to get
  382. if (CurContext.VersionConfig == null)
  383. {
  384. // The WoT installation is valid, but we don't have a suitable version config. Can list the right properties, but can't really render.
  385. _dataMissing.Value = true;
  386. ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_WotVersionTooOld.Fmt(ActiveInstallation.GameVersionName + " #" + ActiveInstallation.GameVersionId);
  387. ctGameInstallationWarning.Tag = null;
  388. }
  389. else
  390. {
  391. // Everything's fine.
  392. _dataMissing.Value = false;
  393. ctGameInstallationWarning.Text = "";
  394. ctGameInstallationWarning.Tag = null;
  395. // Show any non-fatal data loading warnings
  396. foreach (var warning in CurContext.Warnings)
  397. _warnings.Add(new Warning_DataLoadWarning(warning));
  398. }
  399. }
  400. FilenameEditor.LastContext = CurContext; // this is a bit of a hack to give FilenameEditor instances access to the context, see comment on the field.
  401. // Just some self-tests for any bugs in the above
  402. if (CurContext == null && !_dataMissing) throw new Exception();
  403. if (_dataMissing != (ctGameInstallationWarning.Text != "")) throw new Exception(); // must show installation warning iff UI set to the dataMissing state
  404. if (ctGameInstallationWarning.Text == null && ctGameInstallationWarning.Tag != null) throw new Exception();
  405. // Update the list of data sources currently available. This list is used by drop-downs which offer the user to select a property.
  406. foreach (var item in App.DataSources.Where(ds => ds.GetType() == typeof(DataSourceInfo)).ToArray())
  407. {
  408. var extra = CurContext == null ? null : CurContext.ExtraProperties.FirstOrDefault(df => df.PropertyId == item.PropertyId);
  409. if (extra == null)
  410. App.DataSources.Remove(item);
  411. else
  412. item.UpdateFrom(extra);
  413. }
  414. if (CurContext != null)
  415. foreach (var extra in CurContext.ExtraProperties)
  416. {
  417. if (!App.DataSources.Any(item => extra.PropertyId == item.PropertyId))
  418. App.DataSources.Add(new DataSourceInfo(extra));
  419. }
  420. if (_dataMissing)
  421. {
  422. // Clear the icons area. It will remain empty because icon rendering code exits if _dataMissing is true.
  423. // Various UI controls disable automatically whenever _dataMissing is true.
  424. ctIconsPanel.Children.Clear();
  425. }
  426. else
  427. {
  428. // Force a full re-render
  429. if (!first)
  430. {
  431. _renderResults.Clear();
  432. UpdateIcons();
  433. }
  434. }
  435. }
  436. /// <summary>
  437. /// Schedules an icon update to occur after a short timeout. If called again before the timeout, will re-set the timeout
  438. /// back to original value. If called during a render, the render is cancelled immediately. Call this if the event that
  439. /// invalidated the current icons can occur frequently. Call <see cref="UpdateIcons"/> for immediate response.
  440. /// </summary>
  441. private void ScheduleUpdateIcons()
  442. {
  443. _cancelRender.Cancel();
  444. _rendering.Value = true;
  445. foreach (var image in ctIconsPanel.Children.OfType<TankImageControl>())
  446. image.Opacity = 0.7;
  447. _updateIconsTimer.Stop();
  448. _updateIconsTimer.Start();
  449. }
  450. /// <summary>
  451. /// Begins an icon update immediately. The icons are rendered in the background without blocking the UI. If called during
  452. /// a previous render, the render is cancelled immediately. Call this if the event that invalidated the current icons occurs
  453. /// infrequently, to ensure immediate response to user action. For very frequent updates, use <see cref="ScheduleUpdateIcons"/>.
  454. /// Only the icons not in the render cache are re-rendered; remove some or all to force a re-render of the icon.
  455. /// </summary>
  456. private void UpdateIcons(object _ = null, EventArgs __ = null)
  457. {
  458. _rendering.Value = true;
  459. foreach (var image in ctIconsPanel.Children.OfType<TankImageControl>())
  460. image.Opacity = 0.7;
  461. _warnings.RemoveWhere(w => w is Warning_RenderedWithErrWarn);
  462. _updateIconsTimer.Stop();
  463. _cancelRender.Cancel();
  464. _cancelRender = new CancellationTokenSource();
  465. var cancelToken = _cancelRender.Token; // must be a local so that the task lambda captures it; _cancelRender could get reassigned before a task gets to check for cancellation of the old one
  466. if (_dataMissing)
  467. return;
  468. var context = CurContext;
  469. var images = ctIconsPanel.Children.OfType<TankImageControl>().ToList();
  470. var style = App.Settings.ActiveStyle;
  471. var renderTasks = ListRenderTasks(context, style);
  472. foreach (var layer in style.Layers)
  473. TestLayer(style, layer);
  474. var tasks = new List<Action>();
  475. for (int i = 0; i < renderTasks.Count; i++)
  476. {
  477. if (i >= images.Count)
  478. images.Add(CreateTankImageControl(style));
  479. var renderTask = renderTasks[i];
  480. var image = images[i];
  481. image.ToolTip = renderTasks[i].TankId;
  482. if (_renderResults.ContainsKey(renderTask.TankId))
  483. {
  484. image.Source = _renderResults[renderTask.TankId].Image;
  485. image.RenderTask = _renderResults[renderTask.TankId];
  486. image.Opacity = 1;
  487. }
  488. else
  489. tasks.Add(() =>
  490. {
  491. try
  492. {
  493. if (cancelToken.IsCancellationRequested) return;
  494. renderTask.Render();
  495. if (cancelToken.IsCancellationRequested) return;
  496. Dispatcher.Invoke(new Action(() =>
  497. {
  498. if (cancelToken.IsCancellationRequested) return;
  499. _renderResults[renderTask.TankId] = renderTask;
  500. image.Source = renderTask.Image;
  501. image.RenderTask = renderTask;
  502. image.Opacity = 1;
  503. if (ctIconsPanel.Children.OfType<TankImageControl>().All(c => c.Opacity == 1))
  504. UpdateIconsCompleted();
  505. }));
  506. }
  507. catch { }
  508. });
  509. }
  510. foreach (var task in tasks)
  511. Task.Factory.StartNew(task, cancelToken, TaskCreationOptions.None, PriorityScheduler.Lowest);
  512. // Remove unused images
  513. foreach (var image in images.Skip(renderTasks.Count))
  514. ctIconsPanel.Children.Remove(image);
  515. if (ctIconsPanel.Children.OfType<TankImageControl>().All(c => c.Opacity == 1))
  516. UpdateIconsCompleted();
  517. }
  518. /// <summary>
  519. /// Called on the GUI thread whenever all the icon renders are completed.
  520. /// </summary>
  521. private void UpdateIconsCompleted()
  522. {
  523. _rendering.Value = false;
  524. // Update the warning messages
  525. if (_renderResults.Values.Any(rr => rr.Exception != null))
  526. _warnings.Add(new Warning_RenderedWithErrWarn(App.Translation.Error.RenderWithErrors));
  527. else if (_renderResults.Values.Any(rr => rr.WarningsCount > 0))
  528. _warnings.Add(new Warning_RenderedWithErrWarn(App.Translation.Error.RenderWithWarnings));
  529. // Clean up all those temporary images we've just created and won't be doing again for a while.
  530. // (this keeps "private bytes" when idle 10-15 MB lower)
  531. GC.Collect();
  532. }
  533. /// <summary>
  534. /// Tests the specified layer instance for its handling of missing extra properties (and possibly other problems). Adds an
  535. /// appropriate warning message if a problem is detected.
  536. /// </summary>
  537. private void TestLayer(Style style, LayerBase layer)
  538. {
  539. // Test missing extra properties
  540. _warnings.RemoveWhere(w => w is Warning_LayerTest_MissingExtra);
  541. var context = CurContext;
  542. if (context == null)
  543. return;
  544. try
  545. {
  546. var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
  547. tank.LoadedImage = new BitmapRam(style.IconWidth, style.IconHeight);
  548. layer.Draw(tank);
  549. }
  550. catch (Exception e)
  551. {
  552. if (!(e is StyleUserError))
  553. _warnings.Add(new Warning_LayerTest_MissingExtra(("The layer {0} is buggy: it throws a {1} when presented with a tank that is missing some \"extra\" properties. Please report this to the developer.").Fmt(layer.GetType().Name, e.GetType().Name)));
  554. // The maker must not throw when properties are missing: firstly, for configurable properties the user could select "None"
  555. // from the drop-down, and secondly, hard-coded properties could simply be missing altogether.
  556. // (although this could, of course, be a bug in TankIconMaker itself)
  557. }
  558. // Test unexpected property values
  559. _warnings.RemoveWhere(w => w is Warning_LayerTest_UnexpectedProperty);
  560. try
  561. {
  562. var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
  563. tank.PropertyValue = "z"; // very short, so substring/indexing can fail, also not parseable as integer. Hopefully "unexpected enough".
  564. tank.LoadedImage = new BitmapRam(style.IconWidth, style.IconHeight);
  565. layer.Draw(tank);
  566. }
  567. catch (Exception e)
  568. {
  569. if (!(e is StyleUserError))
  570. _warnings.Add(new Warning_LayerTest_UnexpectedProperty(("The layer {0} is buggy: it throws a {1} possibly due to a property value it didn't expect. Please report this to the developer.").Fmt(layer.GetType().Name, e.GetType().Name)));
  571. // The maker must not throw for unexpected property values: it could issue a warning using tank.AddWarning.
  572. // (although this could, of course, be a bug in TankIconMaker itself)
  573. }
  574. // Test missing images
  575. _warnings.RemoveWhere(w => w is Warning_LayerTest_MissingImage);
  576. try
  577. {
  578. var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
  579. tank.PropertyValue = "test";
  580. layer.Draw(tank);
  581. }
  582. catch (Exception e)
  583. {
  584. if (!(e is StyleUserError))
  585. _warnings.Add(new Warning_LayerTest_MissingImage(("The layer {0} is buggy: it throws a {1} when some of the standard images cannot be found. Please report this to the developer.").Fmt(layer.GetType().Name, e.GetType().Name)));
  586. // The maker must not throw if the images are missing: it could issue a warning using tank.AddWarning though.
  587. // (although this could, of course, be a bug in TankIconMaker itself)
  588. }
  589. }
  590. /// <summary>
  591. /// Creates a TankImageControl and adds it to the scrollable tank image area. This involves a bunch of properties,
  592. /// event handlers, and bindings, and is hence abstracted into a method.
  593. /// </summary>
  594. private TankImageControl CreateTankImageControl(Style style)
  595. {
  596. var img = new TankImageControl
  597. {
  598. SnapsToDevicePixels = true,
  599. Margin = new Thickness { Right = 15 },
  600. Cursor = Cursors.Hand,
  601. Opacity = 0.7,
  602. };
  603. img.MouseLeftButtonDown += TankImage_MouseLeftButtonDown;
  604. img.MouseLeftButtonUp += TankImage_MouseLeftButtonUp;
  605. BindingOperations.SetBinding(img, TankImageControl.WidthProperty, LambdaBinding.New(
  606. new Binding { Source = ctZoomCheckbox, Path = new PropertyPath(CheckBox.IsCheckedProperty) },
  607. new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
  608. (bool check, double uiZoom) => (double) style.IconWidth * (check ? App.Settings.IconScaleZoomed : App.Settings.IconScaleNormal) / App.DpiScaleX / uiZoom
  609. ));
  610. BindingOperations.SetBinding(img, TankImageControl.HeightProperty, LambdaBinding.New(
  611. new Binding { Source = ctZoomCheckbox, Path = new PropertyPath(CheckBox.IsCheckedProperty) },
  612. new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
  613. (bool check, double uiZoom) => (double) style.IconHeight * (check ? App.Settings.IconScaleZoomed : App.Settings.IconScaleNormal) / App.DpiScaleY / uiZoom
  614. ));
  615. img.HorizontalAlignment = HorizontalAlignment.Left;
  616. ctIconsPanel.Children.Add(img);
  617. return img;
  618. }
  619. private object _lastTankImageDown;
  620. void TankImage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  621. {
  622. _lastTankImageDown = sender;
  623. }
  624. private void TankImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  625. {
  626. bool skip = _lastTankImageDown != sender;
  627. _lastTankImageDown = null;
  628. if (skip)
  629. return;
  630. var image = sender as TankImageControl;
  631. if (image == null)
  632. return;
  633. var renderResult = image.RenderTask;
  634. if (renderResult == null)
  635. return;
  636. if (renderResult.Exception == null && renderResult.WarningsCount == 0)
  637. {
  638. DlgMessage.ShowInfo(App.Translation.Error.RenderIconOK);
  639. return;
  640. }
  641. var warnings = renderResult.WarningsCount == 0 ? new List<object>() : renderResult.Warnings.Cast<object>().ToList();
  642. var warningsText = RT.Util.Ut.Lambda(() =>
  643. {
  644. if (warnings.Count == 1)
  645. return warnings[0].ToString();
  646. return new EggsTag(warnings.Select(w => new EggsTag('[', new[] { w is string ? new EggsText((string) w) : (EggsNode) w })).InsertBetween<EggsNode>(new EggsText("\n"))).ToString();
  647. });
  648. if (renderResult.Exception != null && !(renderResult.Exception is StyleUserError))
  649. {
  650. string details = "";
  651. if (renderResult.Exception is InvalidOperationException && renderResult.Exception.Message.Contains("belongs to a different thread than its parent Freezable"))
  652. details = "Possible cause: a layer or effect reuses a WPF drawing primitive (like Brush) for different tanks without calling Freeze() on it.\n";
  653. details += Ut.ExceptionToDebugString(renderResult.Exception);
  654. warnings.Add((string) App.Translation.Error.ExceptionInRender);
  655. bool copy = DlgMessage.Show(warningsText(), null, DlgType.Warning, DlgMessageFormat.EggsML, App.Translation.Error.ErrorToClipboard_Copy, App.Translation.Error.ErrorToClipboard_OK) == 0;
  656. if (copy)
  657. if (Ut.ClipboardSet(details))
  658. DlgMessage.ShowInfo(App.Translation.Error.ErrorToClipboard_Copied);
  659. }
  660. else
  661. {
  662. if (renderResult.Exception != null)
  663. {
  664. if (!(renderResult.Exception as StyleUserError).Formatted)
  665. warnings.Add(App.Translation.Error.RenderIconFail.Fmt(renderResult.Exception.Message));
  666. else
  667. warnings.Add(new EggsTag(App.Translation.Error.RenderIconFail.FmtEnumerable(EggsML.Parse(renderResult.Exception.Message)).Select(v => (v is EggsNode) ? (EggsNode) v : new EggsText(v.ToString()))));
  668. }
  669. DlgMessage.Show(warningsText(), null, DlgType.Warning, DlgMessageFormat.EggsML);
  670. }
  671. }
  672. private void SaveSettings()
  673. {
  674. _saveSettingsTimer.Stop();
  675. App.Settings.LeftColumnWidth = ctLeftColumn.Width.Value;
  676. App.Settings.NameColumnWidth = ctLayerProperties.NameColumnWidth;
  677. App.Settings.SaveThreaded();
  678. }
  679. private void SaveSettings(object _, EventArgs __)
  680. {
  681. SaveSettings();
  682. }
  683. private DispatcherTimer _saveSettingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5), IsEnabled = false };
  684. private void SaveSettingsDelayed(object _ = null, EventArgs __ = null)
  685. {
  686. _saveSettingsTimer.Stop();
  687. _saveSettingsTimer.Tick -= SaveSettings;
  688. _saveSettingsTimer.Tick += SaveSettings;
  689. _saveSettingsTimer.Start();
  690. }
  691. private void ctStyleDropdown_SelectionChanged(object sender = null, SelectionChangedEventArgs __ = null)
  692. {
  693. _renderResults.Clear();
  694. ctIconsPanel.Children.Clear();
  695. App.Settings.ActiveStyle = (Style) ctStyleDropdown.SelectedItem;
  696. ctUpvote.Visibility = App.Settings.ActiveStyle.Kind == StyleKind.BuiltIn ? Visibility.Visible : Visibility.Collapsed;
  697. SaveSettings();
  698. ctLayersTree.ItemsSource = App.Settings.ActiveStyle.Layers;
  699. ctPathTemplate.DataContext = App.Settings.ActiveStyle;
  700. if (App.Settings.ActiveStyle.Layers.Count > 0)
  701. {
  702. App.Settings.ActiveStyle.Layers[0].TreeViewItem.IsSelected = true;
  703. ctLayerProperties.SelectedObject = App.Settings.ActiveStyle.Layers[0];
  704. }
  705. else
  706. ctLayerProperties.SelectedObject = null;
  707. UpdateIcons();
  708. }
  709. private static bool isMedHighTier(WotTank t)
  710. {
  711. switch (t.Class)
  712. {
  713. case Class.Light: return t.Tier >= 4;
  714. case Class.Artillery: return t.Tier >= 5;
  715. case Class.Destroyer: return t.Tier >= 5;
  716. case Class.Medium: return t.Tier >= 6;
  717. case Class.Heavy: return t.Tier >= 6;
  718. default: return false;
  719. }
  720. }
  721. private static bool isHighTier(WotTank t)
  722. {
  723. switch (t.Class)
  724. {
  725. case Class.Light: return t.Tier >= 7;
  726. case Class.Artillery: return t.Tier >= 8;
  727. case Class.Destroyer: return t.Tier >= 8;
  728. case Class.Medium: return t.Tier >= 9;
  729. case Class.Heavy: return t.Tier >= 9;
  730. default: return false;
  731. }
  732. }
  733. /// <summary>
  734. /// Constructs a list of render tasks based on the current settings in the GUI. Will enumerate only some
  735. /// of the tanks if the user chose a smaller subset in the GUI.
  736. /// </summary>
  737. /// <param name="all">Forces the method to enumerate all tanks regardless of the GUI setting.</param>
  738. private static List<RenderTask> ListRenderTasks(WotContext context, Style style, bool all = false)
  739. {
  740. if (context.Tanks.Count == 0)
  741. return new List<RenderTask>(); // happens when there are no built-in data files
  742. IEnumerable<WotTank> selection = null;
  743. switch (all ? DisplayFilter.All : App.Settings.DisplayFilter)
  744. {
  745. case DisplayFilter.All: selection = context.Tanks; break;
  746. case DisplayFilter.OneOfEach:
  747. selection = context.Tanks.Select(t => new { t.Category, t.Class, t.Country }).Distinct()
  748. .SelectMany(p => SelectTiers(context.Tanks.Where(t => t.Category == p.Category && t.Class == p.Class && t.Country == p.Country)));
  749. break;
  750. case DisplayFilter.China: selection = context.Tanks.Where(t => t.Country == Country.China); break;
  751. case DisplayFilter.Czech: selection = context.Tanks.Where(t => t.Country == Country.Czech); break;
  752. case DisplayFilter.France: selection = context.Tanks.Where(t => t.Country == Country.France); break;
  753. case DisplayFilter.Germany: selection = context.Tanks.Where(t => t.Country == Country.Germany); break;
  754. case DisplayFilter.Japan: selection = context.Tanks.Where(t => t.Country == Country.Japan); break;
  755. case DisplayFilter.Sweden: selection = context.Tanks.Where(t => t.Country == Country.Sweden); break;
  756. case DisplayFilter.UK: selection = context.Tanks.Where(t => t.Country == Country.UK); break;
  757. case DisplayFilter.USA: selection = context.Tanks.Where(t => t.Country == Country.USA); break;
  758. case DisplayFilter.USSR: selection = context.Tanks.Where(t => t.Country == Country.USSR); break;
  759. case DisplayFilter.Light: selection = context.Tanks.Where(t => t.Class == Class.Light); break;
  760. case DisplayFilter.Medium: selection = context.Tanks.Where(t => t.Class == Class.Medium); break;
  761. case DisplayFilter.Heavy: selection = context.Tanks.Where(t => t.Class == Class.Heavy); break;
  762. case DisplayFilter.Artillery: selection = context.Tanks.Where(t => t.Class == Class.Artillery); break;
  763. case DisplayFilter.Destroyer: selection = context.Tanks.Where(t => t.Class == Class.Destroyer); break;
  764. case DisplayFilter.Normal: selection = context.Tanks.Where(t => t.Category == Category.Normal); break;
  765. case DisplayFilter.Premium: selection = context.Tanks.Where(t => t.Category == Category.Premium); break;
  766. case DisplayFilter.Special: selection = context.Tanks.Where(t => t.Category == Category.Special); break;
  767. case DisplayFilter.TierLow: selection = context.Tanks.Where(t => !isMedHighTier(t)); break;
  768. case DisplayFilter.TierMedHigh: selection = context.Tanks.Where(t => isMedHighTier(t)); break;
  769. case DisplayFilter.TierHigh: selection = context.Tanks.Where(t => isHighTier(t)); break;
  770. }
  771. return selection.OrderBy(t => t.Country).ThenBy(t => t.Class).ThenBy(t => t.Tier).ThenBy(t => t.Category).ThenBy(t => t.TankId)
  772. .Select(tank =>
  773. {
  774. var task = new RenderTask(style);
  775. task.TankId = tank.TankId;
  776. task.Tank = new Tank(
  777. tank,
  778. addWarning: task.AddWarning
  779. );
  780. return task;
  781. }).ToList();
  782. }
  783. /// <summary>
  784. /// Enumerates up to three tanks with tiers as different as possible. Ideally enumerates one tier 1, one tier 5 and one tier 10 tank.
  785. /// </summary>
  786. private static IEnumerable<WotTank> SelectTiers(IEnumerable<WotTank> tanks)
  787. {
  788. WotTank min = null;
  789. WotTank mid = null;
  790. WotTank max = null;
  791. foreach (var tank in tanks)
  792. {
  793. if (min == null || tank.Tier < min.Tier)
  794. min = tank;
  795. if (mid == null || Math.Abs(tank.Tier - 5) < Math.Abs(mid.Tier - 5))
  796. mid = tank;
  797. if (max == null || tank.Tier > max.Tier)
  798. max = tank;
  799. }
  800. if (Math.Abs((mid == null ? 999 : mid.Tier) - (min == null ? 999 : min.Tier)) < 3)
  801. mid = null;
  802. if (Math.Abs((mid == null ? 999 : mid.Tier) - (max == null ? 999 : max.Tier)) < 3)
  803. mid = null;
  804. if (Math.Abs((min == null ? 999 : min.Tier) - (max == null ? 999 : max.Tier)) < 5)
  805. max = null;
  806. if (min != null)
  807. yield return min;
  808. if (mid != null)
  809. yield return mid;
  810. if (max != null)
  811. yield return max;
  812. }
  813. private void ctGamePath_SelectionChanged(object sender, SelectionChangedEventArgs e)
  814. {
  815. #pragma warning disable 0618 // ActiveInstallation should only be used for loading/saving the setting, which is what the code below does.
  816. App.Settings.ActiveInstallation = ctGamePath.SelectedItem as TimGameInstallation;
  817. #pragma warning restore 0618
  818. ReloadData();
  819. SaveSettings();
  820. }
  821. private void ctGamePath_PreviewKeyDown(object sender, KeyEventArgs e)
  822. {
  823. if (ctGamePath.IsKeyboardFocusWithin && ctGamePath.IsDropDownOpen && e.Key == Key.Delete)
  824. {
  825. RemoveGamePath();
  826. e.Handled = true;
  827. }
  828. }
  829. private void ctGameInstallationWarning_MouseDown(object sender, M

Large files files are truncated, but you can click here to view the full file