PageRenderTime 43ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/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
  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, MouseButtonEventArgs e)
  830. {
  831. var str = ctGameInstallationWarning.Tag as string;
  832. if (string.IsNullOrEmpty(str))
  833. return;
  834. bool copy = DlgMessage.ShowWarning(App.Translation.Error.ExceptionLoadingGameData,
  835. App.Translation.Error.ErrorToClipboard_Copy, App.Translation.Error.ErrorToClipboard_OK) == 0;
  836. if (copy)
  837. if (Ut.ClipboardSet(str))
  838. DlgMessage.ShowInfo(App.Translation.Error.ErrorToClipboard_Copied);
  839. }
  840. private void ctLayerProperties_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
  841. {
  842. if (App.Settings.ActiveStyle.Kind != StyleKind.User)
  843. {
  844. GetEditableStyle(); // duplicate the style
  845. RecreateBuiltInStyles();
  846. }
  847. _renderResults.Clear();
  848. ScheduleUpdateIcons();
  849. SaveSettings();
  850. }
  851. private void ctDisplayMode_SelectionChanged(object _, SelectionChangedEventArgs __)
  852. {
  853. App.Settings.DisplayFilter = (DisplayFilter) ctDisplayMode.SelectedIndex;
  854. UpdateIcons();
  855. SaveSettings();
  856. }
  857. private void ctBackground_Click(object _, EventArgs __)
  858. {
  859. var menu = ctBackground.ContextMenu;
  860. menu.PlacementTarget = ctBackground;
  861. menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
  862. menu.Items.Clear();
  863. Directory.CreateDirectory(PathUtil.AppPathCombine("Backgrounds"));
  864. foreach (var file in new DirectoryInfo(PathUtil.AppPathCombine("Backgrounds")).GetFiles("*.jpg").Where(f => f.Extension == ".jpg")) /* GetFiles has a bug whereby "blah.jpg2" is also matched */
  865. menu.Items.Add(new MenuItem { Header = Path.GetFileNameWithoutExtension(file.Name), Tag = file.Name });
  866. menu.Items.Add(new Separator());
  867. menu.Items.Add(new MenuItem { Header = App.Translation.MainWindow.BackgroundCheckered.ToString(), Tag = ":checkered" });
  868. menu.Items.Add(new MenuItem { Header = App.Translation.MainWindow.BackgroundSolidColor.ToString(), Tag = ":solid" });
  869. if (App.Settings.Background == ":checkered")
  870. {
  871. menu.Items.Add(new Separator());
  872. var menuitem = new MenuItem { Header = App.Translation.MainWindow.BackgroundChangeCheckered1.ToString() };
  873. menuitem.Click += delegate { ChangeColor(ref App.Settings.BackgroundCheckeredColor1); ApplyBackgroundColors(); };
  874. menu.Items.Add(menuitem);
  875. menuitem = new MenuItem { Header = App.Translation.MainWindow.BackgroundChangeCheckered2.ToString() };
  876. menuitem.Click += delegate { ChangeColor(ref App.Settings.BackgroundCheckeredColor2); ApplyBackgroundColors(); };
  877. menu.Items.Add(menuitem);
  878. menuitem = new MenuItem { Header = App.Translation.MainWindow.BackgroundRestoreDefaults.ToString() };
  879. menuitem.Click += delegate
  880. {
  881. App.Settings.BackgroundCheckeredColor1 = Color.FromRgb(0xc0, 0xc0, 0xc0);
  882. App.Settings.BackgroundCheckeredColor2 = Color.FromRgb(0xa0, 0xa0, 0xa0);
  883. SaveSettings();
  884. ApplyBackgroundColors();
  885. };
  886. menu.Items.Add(menuitem);
  887. }
  888. else if (App.Settings.Background == ":solid")
  889. {
  890. menu.Items.Add(new Separator());
  891. var menuitem = new MenuItem { Header = App.Translation.MainWindow.BackgroundChangeSolid.ToString() };
  892. menuitem.Click += delegate { ChangeColor(ref App.Settings.BackgroundSolidColor); ApplyBackgroundColors(); };
  893. menu.Items.Add(menuitem);
  894. menuitem = new MenuItem { Header = App.Translation.MainWindow.BackgroundRestoreDefaults.ToString() };
  895. menuitem.Click += delegate
  896. {
  897. App.Settings.BackgroundSolidColor = Color.FromRgb(0x80, 0xc0, 0xff);
  898. SaveSettings();
  899. ApplyBackgroundColors();
  900. };
  901. menu.Items.Add(menuitem);
  902. }
  903. foreach (var itemForeach in menu.Items.OfType<MenuItem>().Where(i => i.Tag != null))
  904. {
  905. var item = itemForeach; // C# 5 fixed this but it's still causing issues for some
  906. item.IsChecked = App.Settings.Background.EqualsNoCase(item.Tag as string);
  907. item.Click += delegate { App.Settings.Background = item.Tag as string; ApplyBackground(); };
  908. }
  909. menu.IsOpen = true;
  910. }
  911. private void ApplyBackground()
  912. {
  913. if (App.Settings.Background == ":checkered")
  914. {
  915. ctOuterGrid.Background = (Brush) Resources["bkgCheckered"];
  916. }
  917. else if (App.Settings.Background == ":solid")
  918. {
  919. ctOuterGrid.Background = (Brush) Resources["bkgSolidBrush"];
  920. }
  921. else
  922. {
  923. try
  924. {
  925. var path = Path.Combine(PathUtil.AppPath, "Backgrounds", App.Settings.Background);
  926. if (File.Exists(path))
  927. {
  928. var img = new BitmapImage();
  929. img.BeginInit();
  930. img.StreamSource = new MemoryStream(File.ReadAllBytes(path));
  931. img.EndInit();
  932. ctOuterGrid.Background = new ImageBrush
  933. {
  934. ImageSource = img,
  935. Stretch = Stretch.UniformToFill,
  936. };
  937. }
  938. else
  939. {
  940. // This will occur pretty much only at startup, when the image has been removed after the user selected it
  941. App.Settings.Background = ":checkered";
  942. ApplyBackground();
  943. }
  944. }
  945. catch
  946. {
  947. // The file was either corrupt or could not be opened
  948. App.Settings.Background = ":checkered";
  949. ApplyBackground();
  950. }
  951. }
  952. }
  953. private void ApplyBackgroundColors()
  954. {
  955. ((SolidColorBrush) Resources["bkgCheckeredBrush1"]).Color = App.Settings.BackgroundCheckeredColor1;
  956. ((SolidColorBrush) Resources["bkgCheckeredBrush2"]).Color = App.Settings.BackgroundCheckeredColor2;
  957. ((SolidColorBrush) Resources["bkgSolidBrush"]).Color = App.Settings.BackgroundSolidColor;
  958. }
  959. private void ChangeColor(ref Color color)
  960. {
  961. var dlg = new System.Windows.Forms.ColorDialog();
  962. dlg.Color = color.ToColorGdi();
  963. dlg.CustomColors = App.Settings.CustomColors;
  964. dlg.FullOpen = true;
  965. if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK)
  966. return;
  967. color = dlg.Color.ToColorWpf();
  968. App.Settings.CustomColors = dlg.CustomColors;
  969. SaveSettings();
  970. }
  971. private void ctLanguage_Click(object _, EventArgs __)
  972. {
  973. var menu = ctLanguage.ContextMenu;
  974. menu.PlacementTarget = ctLanguage;
  975. menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
  976. menu.Items.Clear();
  977. _translationHelper.PopulateMenuItems(menu.Items);
  978. menu.IsOpen = true;
  979. }
  980. private void ctWarning_MouseUp(object sender, MouseButtonEventArgs e)
  981. {
  982. DlgMessage.ShowWarning(string.Join("\n\n", _warnings.Select(w => "• " + w.Text)));
  983. }
  984. private void ctReload_Click(object sender, RoutedEventArgs e)
  985. {
  986. ReloadData();
  987. }
  988. HashSet<string> _overwriteAccepted = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // icon path for which the user has confirmed that overwriting is OK
  989. private void saveToAtlas(string pathTemplate, string nameAtlas, bool custom = false)
  990. {
  991. var context = CurContext;
  992. var style = App.Settings.ActiveStyle; // capture it in case the user selects a different one while the background task is running
  993. var pathPartial = Ut.ExpandIconPath(pathTemplate, context, style, null, null);
  994. try
  995. {
  996. if (Directory.Exists(pathPartial) &&
  997. Directory.GetFileSystemEntries(pathPartial)
  998. .Any(x => nameAtlas.EqualsNoCase(Path.GetFileNameWithoutExtension(x))))
  999. {
  1000. if (DlgMessage.ShowQuestion(App.Translation.Prompt.OverwriteIcons_Prompt
  1001. .Fmt(pathPartial, context.VersionConfig.TankIconExtension),
  1002. App.Translation.Prompt.OverwriteIcons_Yes, App.Translation.Prompt.Cancel) == 1)
  1003. {
  1004. return;
  1005. }
  1006. }
  1007. GlobalStatusShow(App.Translation.Misc.GlobalStatus_Saving);
  1008. var renderTasks = ListRenderTasks(context, style, all: true);
  1009. var renders = _renderResults.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
  1010. // The rest of the save process occurs off the GUI thread, while this method returns.
  1011. Task.Factory.StartNew(() =>
  1012. {
  1013. Exception exception = null;
  1014. try
  1015. {
  1016. foreach (var renderTask in renderTasks)
  1017. if (!renders.ContainsKey(renderTask.TankId))
  1018. {
  1019. renders[renderTask.TankId] = renderTask;
  1020. renderTask.Render();
  1021. }
  1022. var atlasBuilder = new AtlasBuilder(context);
  1023. atlasBuilder.SaveAtlas(pathTemplate, nameAtlas, renders.Values, custom);
  1024. }
  1025. catch (Exception e)
  1026. {
  1027. exception = e;
  1028. }
  1029. finally
  1030. {
  1031. Dispatcher.Invoke((Action)(() =>
  1032. {
  1033. GlobalStatusHide();
  1034. // Cache any new renders that we don't already have
  1035. foreach (var kvp in renders)
  1036. if (!_renderResults.ContainsKey(kvp.Key))
  1037. _renderResults[kvp.Key] = kvp.Value;
  1038. // Inform the user of what happened
  1039. if (exception == null)
  1040. {
  1041. int skipped = renders.Values.Count(rr => rr.Exception != null);
  1042. int choice;
  1043. choice = new DlgMessage
  1044. {
  1045. Message = App.Translation.Prompt.IconsSaved.Fmt(pathPartial) +
  1046. (skipped == 0 ? "" : ("\n\n" + App.Translation.Prompt.IconsSaveSkipped.Fmt(App.Translation, skipped))),
  1047. Type = skipped == 0 ? DlgType.Info : DlgType.Warning,
  1048. Buttons = new string[] { App.Translation.DlgMessage.OK, App.Translation.Prompt.IconsSavedGoToForum },
  1049. AcceptButton = 0,
  1050. CancelButton = 0,
  1051. }.Show();
  1052. if (choice == 1)
  1053. visitProjectWebsite("savehelp");
  1054. }
  1055. else
  1056. {
  1057. DlgMessage.ShowError(App.Translation.Prompt.IconsSaveError.Fmt(exception.Message));
  1058. }
  1059. }));
  1060. }
  1061. });
  1062. }
  1063. catch (Exception e)
  1064. {
  1065. DlgMessage.ShowError(App.Translation.Prompt.IconsSaveError.Fmt(e.Message));
  1066. }
  1067. }
  1068. private void saveIcons(string pathTemplate)
  1069. {
  1070. var context = CurContext;
  1071. var style = App.Settings.ActiveStyle; // capture it in case the user selects a different one while the background task is running
  1072. var pathPartial = Ut.ExpandIconPath(pathTemplate, context, style, null, null);
  1073. try
  1074. {
  1075. if (!_overwriteAccepted.Contains(pathPartial) && (!pathPartial.Contains("{TankCountry}") && Directory.Exists(pathPartial) && Directory.GetFileSystemEntries(pathPartial).Any()))
  1076. if (DlgMessage.ShowQuestion(App.Translation.Prompt.OverwriteIcons_Prompt
  1077. .Fmt(pathPartial, context.VersionConfig.TankIconExtension), App.Translation.Prompt.OverwriteIcons_Yes, App.Translation.Prompt.Cancel) == 1)
  1078. return;
  1079. _overwriteAccepted.Add(pathPartial);
  1080. GlobalStatusShow(App.Translation.Misc.GlobalStatus_Saving);
  1081. var renderTasks = ListRenderTasks(context, style, all: true);
  1082. var renders = _renderResults.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
  1083. // The rest of the save process occurs off the GUI thread, while this method returns.
  1084. Task.Factory.StartNew(() =>
  1085. {
  1086. Exception exception = null;
  1087. try
  1088. {
  1089. foreach (var renderTask in renderTasks)
  1090. if (!renders.ContainsKey(renderTask.TankId))
  1091. {
  1092. renders[renderTask.TankId] = renderTask;
  1093. renderTask.Render();
  1094. }
  1095. foreach (var renderTask in renderTasks)
  1096. {
  1097. var render = renders[renderTask.TankId];
  1098. if (render.Exception == null)
  1099. {
  1100. var path = Ut.ExpandIconPath(pathTemplate, context, style, renderTask.Tank.Country, renderTask.Tank.Class);
  1101. Directory.CreateDirectory(path);
  1102. Ut.SaveImage(render.Image, Path.Combine(path, renderTask.TankId + context.VersionConfig.TankIconExtension), context.VersionConfig.TankIconExtension);
  1103. }
  1104. }
  1105. }
  1106. catch (Exception e)
  1107. {
  1108. exception = e;
  1109. }
  1110. finally
  1111. {
  1112. Dispatcher.Invoke((Action) (() =>
  1113. {
  1114. GlobalStatusHide();
  1115. // Cache any new renders that we don't already have
  1116. foreach (var kvp in renders)
  1117. if (!_renderResults.ContainsKey(kvp.Key))
  1118. _renderResults[kvp.Key] = kvp.Value;
  1119. // Inform the user of what happened
  1120. if (exception == null)
  1121. {
  1122. int skipped = renders.Values.Count(rr => rr.Exception != null);
  1123. int choice;
  1124. choice = new DlgMessage
  1125. {
  1126. Message = App.Translation.Prompt.IconsSaved.Fmt(pathPartial) +
  1127. (skipped == 0 ? "" : ("\n\n" + App.Translation.Prompt.IconsSaveSkipped.Fmt(App.Translation, skipped))),
  1128. Type = skipped == 0 ? DlgType.Info : DlgType.Warning,
  1129. Buttons = new string[] { App.Translation.DlgMessage.OK, App.Translation.Prompt.IconsSavedGoToForum },
  1130. AcceptButton = 0,
  1131. CancelButton = 0,
  1132. }.Show();
  1133. if (choice == 1)
  1134. visitProjectWebsite("savehelp");
  1135. }
  1136. else
  1137. {
  1138. DlgMessage.ShowError(App.Translation.Prompt.IconsSaveError.Fmt(exception.Message));
  1139. }
  1140. }));
  1141. }
  1142. });
  1143. }
  1144. catch (Exception e)
  1145. {
  1146. DlgMessage.ShowError(App.Translation.Prompt.IconsSaveError.Fmt(e.Message));
  1147. }
  1148. }
  1149. private void bulkSaveIcons(IEnumerable<Style> stylesToSave, string overridePathTemplate = null)
  1150. {
  1151. _rendering.Value = true;
  1152. GlobalStatusShow(App.Translation.Prompt.BulkSave_Progress);
  1153. var lastGuiUpdate = DateTime.UtcNow;
  1154. var tasks = new List<Task>();
  1155. var context = CurContext;
  1156. var stylesCount = stylesToSave.Count();
  1157. int tasksRemaining = stylesCount;
  1158. var atlasBuilder = new AtlasBuilder(context);
  1159. foreach (var styleF in stylesToSave)
  1160. {
  1161. var style = styleF; // foreach variable scope fix
  1162. if (!style.IconsBulkSaveEnabled && !style.BattleAtlasBulkSaveEnabled && !style.VehicleMarkersAtlasBulkSaveEnabled)
  1163. {
  1164. continue;
  1165. }
  1166. var styleActions = new List<Action>();
  1167. var styleTasks = new List<Task>();
  1168. var renderTasks = ListRenderTasks(context, style, true);
  1169. foreach (var renderTaskF in renderTasks)
  1170. {
  1171. var renderTask = renderTaskF; // foreach variable scope fix
  1172. styleActions.Add(() =>
  1173. {
  1174. try
  1175. {
  1176. var path = Ut.ExpandIconPath(overridePathTemplate ?? style.PathTemplate, context, style,
  1177. renderTask.Tank.Country, renderTask.Tank.Class);
  1178. renderTask.Render();
  1179. Directory.CreateDirectory(path);
  1180. if (style.IconsBulkSaveEnabled)
  1181. {
  1182. Ut.SaveImage(renderTask.Image,
  1183. Path.Combine(path, renderTask.TankId + context.VersionConfig.TankIconExtension),
  1184. context.VersionConfig.TankIconExtension);
  1185. }
  1186. }
  1187. finally
  1188. {
  1189. }
  1190. });
  1191. }
  1192. foreach (var task in styleActions)
  1193. {
  1194. styleTasks.Add(Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None,
  1195. PriorityScheduler.Lowest));
  1196. }
  1197. var atlasTask = Task.Factory.ContinueWhenAll(styleTasks.ToArray(), renders =>
  1198. {
  1199. if (style.BattleAtlasBulkSaveEnabled)
  1200. {
  1201. var path = Ut.ExpandIconPath(overridePathTemplate ?? style.BattleAtlasPathTemplate, context, style, null, null);
  1202. atlasBuilder.SaveAtlas(path, battleAtlas, renderTasks);
  1203. }
  1204. if (style.VehicleMarkersAtlasBulkSaveEnabled)
  1205. {
  1206. var path = Ut.ExpandIconPath(overridePathTemplate ?? style.VehicleMarkersAtlasPathTemplate, context, style, null, null);
  1207. atlasBuilder.SaveAtlas(path, vehicleMarkerAtlas, renderTasks);
  1208. }
  1209. Interlocked.Decrement(ref tasksRemaining);
  1210. if ((DateTime.UtcNow - lastGuiUpdate).TotalMilliseconds > 50)
  1211. {
  1212. lastGuiUpdate = DateTime.UtcNow;
  1213. Dispatcher.Invoke(
  1214. new Action(
  1215. () =>
  1216. GlobalStatusShow(App.Translation.Prompt.BulkSave_Progress +
  1217. "\n{0:0}%".Fmt(100 -
  1218. tasksRemaining / (double)stylesCount * 100))));
  1219. }
  1220. });
  1221. tasks.Add(atlasTask);
  1222. }
  1223. Task.Factory.ContinueWhenAll(tasks.ToArray(), renders =>
  1224. {
  1225. Dispatcher.Invoke(new Action(() =>
  1226. {
  1227. _rendering.Value = false;
  1228. GlobalStatusHide();
  1229. GC.Collect();
  1230. }));
  1231. });
  1232. }
  1233. private void ctSave_Click(object _, RoutedEventArgs __)
  1234. {
  1235. saveIcons(App.Settings.ActiveStyle.PathTemplate);
  1236. }
  1237. private void ctSaveAs_Click(object _, RoutedEventArgs __)
  1238. {
  1239. var menu = ctSaveAs.ContextMenu;
  1240. menu.PlacementTarget = ctSaveAs;
  1241. menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
  1242. menu.IsOpen = true;
  1243. }
  1244. private void ctSaveIconsToGameFolder_Click(object _ = null, RoutedEventArgs __ = null)
  1245. {
  1246. saveIcons("");
  1247. }
  1248. private void ctSaveIconsToSpecifiedFolder_Click(object _ = null, RoutedEventArgs __ = null)
  1249. {
  1250. var dlg = new VistaFolderBrowserDialog();
  1251. dlg.ShowNewFolderButton = true; // argh, the dialog requires the path to exist
  1252. if (App.Settings.SaveToFolderPath != null && Directory.Exists(App.Settings.SaveToFolderPath))
  1253. dlg.SelectedPath = App.Settings.SaveToFolderPath;
  1254. if (dlg.ShowDialog() != true)
  1255. return;
  1256. _overwriteAccepted.Remove(dlg.SelectedPath); // force the prompt
  1257. App.Settings.SaveToFolderPath = dlg.SelectedPath;
  1258. SaveSettings();
  1259. saveIcons(App.Settings.SaveToFolderPath);
  1260. }
  1261. private void ctSaveIconsToBattleAtlas_Click(object _ = null, RoutedEventArgs __ = null)
  1262. {
  1263. var dlg = new VistaFolderBrowserDialog();
  1264. dlg.ShowNewFolderButton = true; // argh, the dialog requires the path to exist
  1265. if (App.Settings.SaveToFolderPath != null && Directory.Exists(App.Settings.SaveToFolderPath))
  1266. dlg.SelectedPath = App.Settings.SaveToFolderPath;
  1267. if (dlg.ShowDialog() != true)
  1268. return;
  1269. _overwriteAccepted.Remove(dlg.SelectedPath); // force the prompt
  1270. App.Settings.SaveToFolderPath = dlg.SelectedPath;
  1271. SaveSettings();
  1272. saveToAtlas(App.Settings.SaveToFolderPath, battleAtlas);
  1273. }
  1274. private void ctSaveIconsToVehicleMarkerAtlas_Click(object sender, RoutedEventArgs e)
  1275. {
  1276. var dlg = new VistaFolderBrowserDialog();
  1277. dlg.ShowNewFolderButton = true; // argh, the dialog requires the path to exist
  1278. if (App.Settings.SaveToFolderPath != null && Directory.Exists(App.Settings.SaveToFolderPath))
  1279. dlg.SelectedPath = App.Settings.SaveToFolderPath;
  1280. if (dlg.ShowDialog() != true)
  1281. return;
  1282. _overwriteAccepted.Remove(dlg.SelectedPath); // force the prompt
  1283. App.Settings.SaveToFolderPath = dlg.SelectedPath;
  1284. SaveSettings();
  1285. saveToAtlas(App.Settings.SaveToFolderPath, vehicleMarkerAtlas);
  1286. }
  1287. private void ctSaveToAtlas_Click(object _ = null, RoutedEventArgs __ = null)
  1288. {
  1289. var dlg = new VistaSaveFileDialog();
  1290. dlg.AddExtension = true;
  1291. dlg.FileName = "IconsAtlas"; // Default file name
  1292. dlg.DefaultExt = ".png"; // Default file extension
  1293. dlg.Filter = "PNG (.png)|*.png"; // Filter files by extension
  1294. //dlg.ShowNewFolderButton = true; // argh, the dialog requires the path to exist
  1295. if (App.Settings.SaveToFolderPath != null && Directory.Exists(App.Settings.SaveToFolderPath))
  1296. dlg.InitialDirectory = App.Settings.SaveToFolderPath;
  1297. if (dlg.ShowDialog() != true)
  1298. return;
  1299. _overwriteAccepted.Remove(dlg.InitialDirectory); // force the prompt
  1300. App.Settings.SaveToAtlas = dlg.FileName;
  1301. SaveSettings();
  1302. saveToAtlas(Path.GetDirectoryName(App.Settings.SaveToAtlas), Path.GetFileNameWithoutExtension(App.Settings.SaveToAtlas), true);
  1303. }
  1304. private List<Style> getBulkSaveStyles(string overridePathTemplate = null)
  1305. {
  1306. var context = CurContext;
  1307. var allStyles = App.Settings.Styles
  1308. .Select(style => new CheckListItem<Style>
  1309. {
  1310. Item = style,
  1311. Column1 = string.Format("{0} ({1})", style.Name, style.Author),
  1312. Column2 = Ut.ExpandIconPath(overridePathTemplate ?? style.PathTemplate, context, style, null, null),
  1313. IsChecked = style == App.Settings.ActiveStyle ? true : false
  1314. })
  1315. .ToList();
  1316. var tr = App.Translation.Prompt;
  1317. return CheckListWindow.ShowCheckList(this, allStyles, tr.BulkSave_Prompt, tr.BulkSave_Yes, new string[] { tr.BulkStyles_ColumnTitle, tr.BulkStyles_PathColumn }).ToList();
  1318. }
  1319. private void ctBulkSaveIcons_Click(object sender, RoutedEventArgs e)
  1320. {
  1321. var stylesToSave = getBulkSaveStyles();
  1322. if (stylesToSave.Count == 0)
  1323. return;
  1324. bulkSaveIcons(stylesToSave);
  1325. }
  1326. private void ctBulkSaveIconsToFolder_Click(object sender, RoutedEventArgs e)
  1327. {
  1328. var dlg = new VistaFolderBrowserDialog();
  1329. dlg.ShowNewFolderButton = true; // argh, the dialog requires the path to exist
  1330. if (App.Settings.BulkSaveToFolderPath != null && Directory.Exists(App.Settings.BulkSaveToFolderPath))
  1331. dlg.SelectedPath = App.Settings.BulkSaveToFolderPath;
  1332. if (dlg.ShowDialog() != true)
  1333. return;
  1334. var overridePathTemplate = dlg.SelectedPath + "\\{StyleName} ({StyleAuthor})";
  1335. var stylesToSave = getBulkSaveStyles(overridePathTemplate);
  1336. if (stylesToSave.Count == 0)
  1337. return;
  1338. App.Settings.BulkSaveToFolderPath = dlg.SelectedPath;
  1339. SaveSettings();
  1340. bulkSaveIcons(stylesToSave, overridePathTemplate);
  1341. }
  1342. private void ctEditPathTemplate_Click(object _, RoutedEventArgs __)
  1343. {
  1344. var wnd = BulkSaveSettingsWindow.Show(this, App.Settings.ActiveStyle.PathTemplate, CurContext,
  1345. App.Settings.ActiveStyle);
  1346. if (wnd.DialogResult ?? false)
  1347. {
  1348. var style = GetEditableStyle();
  1349. style.PathTemplate = wnd.PathTemplate;
  1350. style.BattleAtlasPathTemplate = wnd.BattleAtlasPathTemplate;
  1351. style.VehicleMarkersAtlasPathTemplate = wnd.VehicleMarkersAtlasPathTemplate;
  1352. style.IconsBulkSaveEnabled = wnd.IconsBulkSaveEnabled;
  1353. style.BattleAtlasBulkSaveEnabled = wnd.BattleAtlasBulkSaveEnabled;
  1354. style.VehicleMarkersAtlasBulkSaveEnabled = wnd.VehicleMarkersAtlasBulkSaveEnabled;
  1355. }
  1356. }
  1357. private ObservableValue<double> UiZoomObservable = new ObservableValue<double>(1);
  1358. private double UiZoom
  1359. {
  1360. get { return App.Settings.UiZoom; }
  1361. set
  1362. {
  1363. var val = value;
  1364. if (val >= 0.95 && val <= 1.05) // snap to 100%
  1365. val = 1;
  1366. App.Settings.UiZoom = UiZoomObservable.Value = val;
  1367. ApplyUiZoom(this);
  1368. SaveSettingsDelayed();
  1369. }
  1370. }
  1371. public static void ApplyUiZoom(Window wnd)
  1372. {
  1373. // not the best location for this method... but not completely awful either since this is where most of the UI zoom code resides already...
  1374. TextOptions.SetTextFormattingMode(wnd, App.Settings.UiZoom == 1 ? TextFormattingMode.Display : TextFormattingMode.Ideal);
  1375. var scale = wnd.Resources["UiZoomer"] as ScaleTransform;
  1376. scale.ScaleX = scale.ScaleY = App.Settings.UiZoom;
  1377. }
  1378. private void ctUiZoomIn_Click(object _, EventArgs __)
  1379. {
  1380. UiZoom *= 1.1;
  1381. }
  1382. private void ctUiZoomOut_Click(object _, EventArgs __)
  1383. {
  1384. UiZoom /= 1.1;
  1385. }
  1386. private void ctAbout_Click(object sender, RoutedEventArgs e)
  1387. {
  1388. var assembly = Assembly.GetExecutingAssembly();
  1389. string version = assembly.GetName().Version.Major.ToString().PadLeft(3, '0');
  1390. string build = assembly.GetName().Version.Minor.ToString();
  1391. string copyright = assembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false).OfType<AssemblyCopyrightAttribute>().Select(c => c.Copyright).FirstOrDefault();
  1392. var icon = Icon as BitmapSource;
  1393. var choice = new DlgMessage()
  1394. {
  1395. Message = "Tank Icon Maker\n" + App.Translation.Misc.ProgramVersion.Fmt(version, build) + "\nBy Roman Starkov\n\n" + copyright
  1396. + (App.Translation.Language == RT.Util.Lingo.Language.EnglishUK ? "" : ("\n\n" + App.Translation.TranslationCredits)),
  1397. Caption = "Tank Icon Maker",
  1398. Buttons = new string[] { App.Translation.DlgMessage.OK, App.Translation.Prompt.VisitWebsiteBtn },
  1399. AcceptButton = 0,
  1400. CancelButton = 0,
  1401. Image = icon == null ? null : icon.ToBitmapGdi().GetBitmapCopy()
  1402. }.Show();
  1403. if (choice == 1)
  1404. visitProjectWebsite("about");
  1405. }
  1406. private static void visitProjectWebsite(string what)
  1407. {
  1408. Process.Start(new ProcessStartInfo("http://roman.st/TankIconMaker/go/{0}?lang={1}".Fmt(
  1409. what, App.Translation.Language.GetIsoLanguageCode().SubstringSafe(0, 2))) { UseShellExecute = true });
  1410. }
  1411. private void AddGamePath(object _, RoutedEventArgs __)
  1412. {
  1413. // Add the very first path differently: by guessing where the game is installed
  1414. if (App.Settings.GameInstallations.Count == 0)
  1415. {
  1416. AddGameInstallations();
  1417. if (App.Settings.GameInstallations.Count > 0)
  1418. {
  1419. ctGamePath.SelectedItem = App.Settings.GameInstallations.First(); // this triggers all the necessary work, like updating ActiveInstallation and re-rendering
  1420. return;
  1421. }
  1422. }
  1423. var dlg = new VistaFolderBrowserDialog();
  1424. if (ActiveInstallation != null && Directory.Exists(ActiveInstallation.Path))
  1425. dlg.SelectedPath = ActiveInstallation.Path;
  1426. if (dlg.ShowDialog() != true)
  1427. return;
  1428. var gameInstallation = new TimGameInstallation(dlg.SelectedPath);
  1429. if (gameInstallation.GameVersionId == null)
  1430. {
  1431. if (DlgMessage.ShowWarning(App.Translation.Prompt.GameNotFound_Prompt,
  1432. App.Translation.Prompt.GameNotFound_Ignore, App.Translation.Prompt.Cancel) == 1)
  1433. return;
  1434. }
  1435. App.Settings.GameInstallations.Add(gameInstallation);
  1436. SaveSettings();
  1437. ctGamePath.SelectedItem = gameInstallation; // this triggers all the necessary work, like updating ActiveInstallation and re-rendering
  1438. }
  1439. private void RemoveGamePath(object _ = null, RoutedEventArgs __ = null)
  1440. {
  1441. // Looks rather hacky but seems to do the job correctly even when called with the drop-down visible.
  1442. var index = ctGamePath.SelectedIndex;
  1443. App.Settings.GameInstallations.RemoveAt(ctGamePath.SelectedIndex);
  1444. ctGamePath.ItemsSource = null;
  1445. ctGamePath.ItemsSource = App.Settings.GameInstallations;
  1446. ctGamePath.SelectedIndex = Math.Min(index, App.Settings.GameInstallations.Count - 1);
  1447. SaveSettings();
  1448. }
  1449. /// <summary>
  1450. /// Finds all installations of World of Tanks on the user's computer and adds them to the list of installations.
  1451. /// </summary>
  1452. private void AddGameInstallations()
  1453. {
  1454. foreach (var path in Ut.EnumerateGameInstallations())
  1455. App.Settings.GameInstallations.Add(new TimGameInstallation(path));
  1456. SaveSettings();
  1457. }
  1458. private void ctStyleMore_Click(object sender, RoutedEventArgs e)
  1459. {
  1460. ctStyleMore.ContextMenu.PlacementTarget = ctStyleMore;
  1461. ctStyleMore.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
  1462. ctStyleMore.ContextMenu.IsOpen = true;
  1463. }
  1464. private void ctUpvote_Click(object sender, RoutedEventArgs e)
  1465. {
  1466. var style = App.Settings.ActiveStyle;
  1467. if (style.Kind != StyleKind.BuiltIn)
  1468. {
  1469. DlgMessage.ShowInfo(App.Translation.Prompt.Upvote_BuiltInOnly);
  1470. return;
  1471. }
  1472. if (string.IsNullOrWhiteSpace(style.ForumLink) || (!style.ForumLink.StartsWith("http://") && !style.ForumLink.StartsWith("https://")))
  1473. {
  1474. DlgMessage.ShowInfo(App.Translation.Prompt.Upvote_NotAvailable);
  1475. return;
  1476. }
  1477. if (DlgMessage.ShowInfo(App.Translation.Prompt.Upvote_Prompt
  1478. .Fmt(style.Author, style.ForumLink.UrlUnescape()), App.Translation.Prompt.Upvote_Open, App.Translation.Prompt.Cancel) == 1)
  1479. return;
  1480. Process.Start(new ProcessStartInfo(style.ForumLink) { UseShellExecute = true });
  1481. }
  1482. private void ctLayersTree_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
  1483. {
  1484. // ARGH: WPF does not select the item right-clicked on, nor does it make it easy to make this happen.
  1485. var item = WpfUtil.VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject);
  1486. if (item != null)
  1487. {
  1488. item.Focus();
  1489. e.Handled = true;
  1490. }
  1491. }
  1492. /// <summary>
  1493. /// Gets the currently selected style or, if it’s built-in, duplicates it first, selects the duplicate,
  1494. /// and returns that.
  1495. /// </summary>
  1496. private Style GetEditableStyle()
  1497. {
  1498. if (App.Settings.ActiveStyle.Kind == StyleKind.User)
  1499. return App.Settings.ActiveStyle;
  1500. // Otherwise the active style is not editable. We must clone it, make the clone the active style, and cause as few changes
  1501. // in the UI as possible.
  1502. // Remember what was expanded and selected
  1503. var layer = ctLayersTree.SelectedItem as LayerBase;
  1504. var effect = ctLayersTree.SelectedItem as EffectBase;
  1505. int selectedLayerIndex = layer == null && effect == null ? -1 : App.Settings.ActiveStyle.Layers.IndexOf(layer ?? effect.Layer);
  1506. int selectedEffectIndex = effect == null ? -1 : effect.Layer.Effects.IndexOf(effect);
  1507. var expandedIndexes = App.Settings.ActiveStyle.Layers.Select((l, i) => l.TreeViewItem.IsExpanded ? i : -1).Where(i => i >= 0).ToArray();
  1508. // Duplicate
  1509. var style = App.Settings.ActiveStyle.Clone();
  1510. style.Kind = StyleKind.User;
  1511. style.Name = App.Translation.Misc.NameOfCopied.Fmt(style.Name);
  1512. App.Settings.Styles.Add(style);
  1513. ctStyleDropdown.SelectedItem = style;
  1514. SaveSettings();
  1515. // Re-select/expand
  1516. foreach (var i in expandedIndexes)
  1517. style.Layers[i].TreeViewItem.IsExpanded = true;
  1518. Dispatcher.Invoke((Action) delegate // must let the TreeView think about this before we can set IsSelected
  1519. {
  1520. layer = selectedLayerIndex < 0 ? null : style.Layers[selectedLayerIndex];
  1521. effect = selectedEffectIndex < 0 ? null : layer.Effects[selectedEffectIndex];
  1522. var tvi = effect != null ? effect.TreeViewItem : layer != null ? layer.TreeViewItem : null;
  1523. if (tvi != null)
  1524. {
  1525. tvi.IsSelected = true;
  1526. tvi.BringIntoView();
  1527. }
  1528. }, DispatcherPriority.Background);
  1529. return style;
  1530. }
  1531. private void cmdLayer_AddLayer(object sender, ExecutedRoutedEventArgs e)
  1532. {
  1533. var newLayer = AddWindow.ShowAddLayer(this);
  1534. if (newLayer == null)
  1535. return;
  1536. var style = GetEditableStyle();
  1537. var curLayer = ctLayersTree.SelectedItem as LayerBase;
  1538. var curEffect = ctLayersTree.SelectedItem as EffectBase;
  1539. if (curEffect != null)
  1540. curLayer = curEffect.Layer;
  1541. if (curLayer != null)
  1542. style.Layers.Insert(style.Layers.IndexOf(curLayer) + 1, newLayer);
  1543. else
  1544. style.Layers.Add(newLayer);
  1545. newLayer.TreeViewItem.IsSelected = true;
  1546. newLayer.TreeViewItem.BringIntoView();
  1547. _renderResults.Clear();
  1548. UpdateIcons();
  1549. SaveSettings();
  1550. }
  1551. private bool isLayerOrEffectInClipboard()
  1552. {
  1553. IDataObject iData = Clipboard.GetDataObject();
  1554. // Determines whether the data is in a format you can use.
  1555. if (!iData.GetDataPresent(DataFormats.Text))
  1556. return false;
  1557. string clipboardData = (string) iData.GetData(DataFormats.Text);
  1558. return Regex.IsMatch(clipboardData, @"^<({0}|{1}|{2})\b".Fmt(clipboard_LayerRoot, clipboard_EffectRoot, clipboard_EffectListRoot));
  1559. }
  1560. private bool isLayerOrEffectSelected()
  1561. {
  1562. return ctLayersTree.SelectedItem is LayerBase || ctLayersTree.SelectedItem is EffectBase;
  1563. }
  1564. private bool isLayerSelected()
  1565. {
  1566. return ctLayersTree.SelectedItem is LayerBase;
  1567. }
  1568. private bool isEffectSelected()
  1569. {
  1570. return ctLayersTree.SelectedItem is EffectBase;
  1571. }
  1572. private void cmdLayer_AddEffect(object sender, ExecutedRoutedEventArgs e)
  1573. {
  1574. var newEffect = AddWindow.ShowAddEffect(this);
  1575. if (newEffect == null)
  1576. return;
  1577. var style = GetEditableStyle();
  1578. var curLayer = ctLayersTree.SelectedItem as LayerBase;
  1579. var curEffect = ctLayersTree.SelectedItem as EffectBase;
  1580. if (curLayer != null)
  1581. curLayer.Effects.Add(newEffect);
  1582. else if (curEffect != null)
  1583. curEffect.Layer.Effects.Insert(curEffect.Layer.Effects.IndexOf(curEffect) + 1, newEffect);
  1584. else
  1585. return;
  1586. if (!newEffect.Layer.TreeViewItem.IsExpanded)
  1587. newEffect.Layer.TreeViewItem.IsExpanded = true;
  1588. _renderResults.Clear();
  1589. ScheduleUpdateIcons(); // schedule immediately so that they go semi-transparent instantly; then force the update later
  1590. SaveSettings();
  1591. Dispatcher.BeginInvoke((Action) delegate
  1592. {
  1593. newEffect.TreeViewItem.IsSelected = true;
  1594. newEffect.TreeViewItem.BringIntoView();
  1595. UpdateIcons();
  1596. }, DispatcherPriority.Background);
  1597. }
  1598. private void cmdLayer_Rename(object sender, ExecutedRoutedEventArgs e)
  1599. {
  1600. var layer = ctLayersTree.SelectedItem as LayerBase;
  1601. var effect = ctLayersTree.SelectedItem as EffectBase;
  1602. var newName = layer != null
  1603. ? PromptWindow.ShowPrompt(this, layer.Name, App.Translation.Prompt.RenameLayer_Title, App.Translation.Prompt.RenameLayer_Label)
  1604. : PromptWindow.ShowPrompt(this, effect.Name, App.Translation.Prompt.RenameEffect_Title, App.Translation.Prompt.RenameEffect_Label);
  1605. if (newName == null)
  1606. return;
  1607. var style = GetEditableStyle();
  1608. if (layer != null)
  1609. {
  1610. layer = ctLayersTree.SelectedItem as LayerBase;
  1611. layer.Name = newName;
  1612. }
  1613. else
  1614. {
  1615. effect = ctLayersTree.SelectedItem as EffectBase;
  1616. effect.Name = newName;
  1617. }
  1618. SaveSettings();
  1619. }
  1620. private const string clipboard_LayerRoot = "TankIconMaker_Layer";
  1621. private const string clipboard_EffectRoot = "TankIconMaker_Effect";
  1622. private const string clipboard_EffectListRoot = "TankIconMaker_EffectList";
  1623. private const string battleAtlas = "BattleAtlas";
  1624. private const string vehicleMarkerAtlas = "vehicleMarkerAtlas";
  1625. private void cmdLayer_Copy(object sender, ExecutedRoutedEventArgs e)
  1626. {
  1627. var fmt = ClassifyXmlFormat.Create(ctLayersTree.SelectedItem is LayerBase ? clipboard_LayerRoot : clipboard_EffectRoot);
  1628. XElement element = ClassifyXml.Serialize(ctLayersTree.SelectedItem, format: fmt);
  1629. Ut.ClipboardSet(element.ToString());
  1630. }
  1631. private void cmdLayer_CopyEffects(object sender, ExecutedRoutedEventArgs e)
  1632. {
  1633. var style = App.Settings.ActiveStyle;
  1634. LayerBase layer = ctLayersTree.SelectedItem as LayerBase;
  1635. XElement element = ClassifyXml.Serialize(layer.Effects.ToList(), format: ClassifyXmlFormat.Create(clipboard_EffectListRoot));
  1636. Ut.ClipboardSet(element.ToString());
  1637. }
  1638. private void cmdLayer_Paste(object sender, ExecutedRoutedEventArgs e)
  1639. {
  1640. Style style = GetEditableStyle();
  1641. LayerBase curLayer = ctLayersTree.SelectedItem as LayerBase;
  1642. EffectBase curEffect = ctLayersTree.SelectedItem as EffectBase;
  1643. if (curEffect != null)
  1644. curLayer = curEffect.Layer;
  1645. try
  1646. {
  1647. IDataObject iData = Clipboard.GetDataObject();
  1648. string clipboardData = (string) iData.GetData(DataFormats.Text);
  1649. if (Regex.IsMatch(clipboardData, @"^<{0}\b".Fmt(clipboard_LayerRoot)))
  1650. {
  1651. LayerBase layer = (LayerBase) ClassifyXml.Deserialize<LayerBase>(XElement.Parse(clipboardData));
  1652. if (curLayer != null)
  1653. style.Layers.Insert(style.Layers.IndexOf(curLayer) + 1, layer);
  1654. else
  1655. style.Layers.Add(layer);
  1656. layer.TreeViewItem.IsSelected = true;
  1657. layer.TreeViewItem.BringIntoView();
  1658. }
  1659. else
  1660. {
  1661. List<EffectBase> effects;
  1662. if (Regex.IsMatch(clipboardData, @"^<{0}\b".Fmt(clipboard_EffectRoot)))
  1663. effects = new List<EffectBase> { ClassifyXml.Deserialize<EffectBase>(XElement.Parse(clipboardData)) };
  1664. else if (Regex.IsMatch(clipboardData, @"^<{0}\b".Fmt(clipboard_EffectListRoot)))
  1665. effects = ClassifyXml.Deserialize<List<EffectBase>>(XElement.Parse(clipboardData));
  1666. else
  1667. throw new Exception(); // caught by the generic "cannot paste" handler below
  1668. EffectBase insertBefore = null;
  1669. if (curEffect != null && curEffect != curLayer.Effects.Last())
  1670. insertBefore = curLayer.Effects[curLayer.Effects.IndexOf(curEffect) + 1];
  1671. foreach (var effect in effects)
  1672. {
  1673. if (insertBefore != null)
  1674. curEffect.Layer.Effects.Insert(curEffect.Layer.Effects.IndexOf(insertBefore), effect);
  1675. else
  1676. curLayer.Effects.Add(effect);
  1677. }
  1678. curLayer.TreeViewItem.IsExpanded = true;
  1679. Dispatcher.BeginInvoke((Action) delegate
  1680. {
  1681. effects.Last().TreeViewItem.IsSelected = true;
  1682. effects.Last().TreeViewItem.BringIntoView();
  1683. }, DispatcherPriority.Background);
  1684. }
  1685. }
  1686. catch
  1687. {
  1688. DlgMessage.ShowError(App.Translation.Prompt.PasteLayerEffect_Error, App.Translation.DlgMessage.OK);
  1689. }
  1690. _renderResults.Clear();
  1691. UpdateIcons();
  1692. SaveSettings();
  1693. }
  1694. private void cmdLayer_Delete(object sender, ExecutedRoutedEventArgs e)
  1695. {
  1696. if (DlgMessage.ShowQuestion(App.Translation.Prompt.DeleteLayerEffect_Prompt, App.Translation.Prompt.DeleteLayerEffect_Yes, App.Translation.Prompt.Cancel) == 1)
  1697. return;
  1698. var style = GetEditableStyle();
  1699. var layer = ctLayersTree.SelectedItem as LayerBase;
  1700. var effect = ctLayersTree.SelectedItem as EffectBase;
  1701. if (layer != null)
  1702. {
  1703. int index = style.Layers.IndexOf(layer);
  1704. safeReselect(style.Layers, null, index);
  1705. style.Layers.RemoveAt(index);
  1706. }
  1707. else
  1708. {
  1709. int index = effect.Layer.Effects.IndexOf(effect);
  1710. safeReselect(effect.Layer.Effects, effect.Layer, index);
  1711. effect.Layer.Effects.RemoveAt(index);
  1712. }
  1713. _renderResults.Clear();
  1714. UpdateIcons();
  1715. SaveSettings();
  1716. }
  1717. private void safeReselect<T>(ObservableCollection<T> items, IHasTreeViewItem parent, int index) where T : IHasTreeViewItem
  1718. {
  1719. TreeViewItem item = null;
  1720. if (items.Count > index + 1)
  1721. item = items[index + 1].TreeViewItem;
  1722. else if (items.Count >= 2)
  1723. item = items[items.Count - 2].TreeViewItem;
  1724. else if (parent != null)
  1725. item = parent.TreeViewItem;
  1726. if (item != null)
  1727. {
  1728. item.IsSelected = true;
  1729. item.BringIntoView();
  1730. item.Focus();
  1731. }
  1732. }
  1733. private bool cmdLayer_MoveUp_IsAvailable()
  1734. {
  1735. return moveEffectOrLayer_IsAvailable((index, count) => index > 0);
  1736. }
  1737. private void cmdLayer_MoveUp(object sender, ExecutedRoutedEventArgs e)
  1738. {
  1739. moveEffectOrLayer(-1);
  1740. }
  1741. private bool cmdLayer_MoveDown_IsAvailable()
  1742. {
  1743. return moveEffectOrLayer_IsAvailable((index, count) => index < count - 1);
  1744. }
  1745. private void cmdLayer_MoveDown(object sender, ExecutedRoutedEventArgs e)
  1746. {
  1747. moveEffectOrLayer(1);
  1748. }
  1749. private bool moveEffectOrLayer_IsAvailable(Func<int, int, bool> check)
  1750. {
  1751. if (ctLayersTree.SelectedItem == null)
  1752. return false;
  1753. var style = App.Settings.ActiveStyle;
  1754. var layer = ctLayersTree.SelectedItem as LayerBase;
  1755. var effect = ctLayersTree.SelectedItem as EffectBase;
  1756. if (layer != null)
  1757. return check(style.Layers.IndexOf(layer), style.Layers.Count);
  1758. else
  1759. return check(effect.Layer.Effects.IndexOf(effect), effect.Layer.Effects.Count);
  1760. }
  1761. private void moveEffectOrLayer(int direction)
  1762. {
  1763. var style = GetEditableStyle();
  1764. var layer = ctLayersTree.SelectedItem as LayerBase;
  1765. var effect = ctLayersTree.SelectedItem as EffectBase;
  1766. if (layer != null)
  1767. {
  1768. int index = style.Layers.IndexOf(layer);
  1769. style.Layers.Move(index, index + direction);
  1770. layer.TreeViewItem.BringIntoView();
  1771. }
  1772. else
  1773. {
  1774. int index = effect.Layer.Effects.IndexOf(effect);
  1775. effect.Layer.Effects.Move(index, index + direction);
  1776. effect.TreeViewItem.BringIntoView();
  1777. }
  1778. _renderResults.Clear();
  1779. ScheduleUpdateIcons();
  1780. SaveSettings();
  1781. }
  1782. private void cmdLayer_ToggleVisibility(object sender, ExecutedRoutedEventArgs e)
  1783. {
  1784. var style = GetEditableStyle();
  1785. var layer = ctLayersTree.SelectedItem as LayerBase;
  1786. var effect = ctLayersTree.SelectedItem as EffectBase;
  1787. if (layer != null)
  1788. layer.Visible = !layer.Visible;
  1789. if (effect != null)
  1790. effect.Visible = !effect.Visible;
  1791. _renderResults.Clear();
  1792. UpdateIcons();
  1793. SaveSettings();
  1794. }
  1795. private void cmdStyle_Add(object sender, ExecutedRoutedEventArgs e)
  1796. {
  1797. var name = PromptWindow.ShowPrompt(this, App.Translation.Misc.NameOfNewStyle, App.Translation.Prompt.CreateStyle_Title, App.Translation.Prompt.CreateStyle_Label);
  1798. if (name == null)
  1799. return;
  1800. var style = new Style();
  1801. style.Name = name;
  1802. style.Author = App.Translation.Misc.NameOfNewStyleAuthor;
  1803. style.PathTemplate = "";
  1804. style.IconWidth = 80;
  1805. style.IconHeight = 24;
  1806. style.Centerable = true;
  1807. style.Layers.Add(new TankImageLayer { Name = App.Translation.Misc.NameOfTankImageLayer });
  1808. App.Settings.Styles.Add(style);
  1809. ctStyleDropdown.SelectedItem = style;
  1810. SaveSettings();
  1811. }
  1812. private bool cmdStyle_UserStyleSelected()
  1813. {
  1814. return App.Settings.ActiveStyle.Kind == StyleKind.User;
  1815. }
  1816. private void cmdStyle_Delete(object sender, ExecutedRoutedEventArgs e)
  1817. {
  1818. var allStyles = App.Settings.Styles
  1819. .Select(style => new CheckListItem<Style> { Item = style, Column1 = string.Format("{0} ({1})", style.Name, style.Author), IsChecked = style == App.Settings.ActiveStyle ? true : false })
  1820. .ToList();
  1821. var tr = App.Translation.Prompt;
  1822. var stylesToDelete = CheckListWindow.ShowCheckList(this, allStyles, tr.DeleteStyle_Prompt, tr.DeleteStyle_Yes, new string[] { tr.BulkStyles_ColumnTitle }, tr.DeleteStyle_PromptSure).ToHashSet();
  1823. if (stylesToDelete.Count == 0)
  1824. return;
  1825. if (stylesToDelete.Contains(App.Settings.ActiveStyle))
  1826. ctStyleDropdown.SelectedIndex = 0;
  1827. App.Settings.Styles.RemoveWhere(style => stylesToDelete.Contains(style));
  1828. SaveSettings();
  1829. DlgMessage.ShowInfo(tr.DeleteStyle_Success.Fmt(App.Translation.Language, stylesToDelete.Count));
  1830. }
  1831. private void cmdStyle_ChangeName(object sender, ExecutedRoutedEventArgs e)
  1832. {
  1833. var name = PromptWindow.ShowPrompt(this, App.Settings.ActiveStyle.Name, App.Translation.Prompt.RenameStyle_Title, App.Translation.Prompt.RenameStyle_Label);
  1834. if (name == null)
  1835. return;
  1836. App.Settings.ActiveStyle.Name = name;
  1837. SaveSettings();
  1838. }
  1839. private void cmdStyle_ChangeAuthor(object sender, ExecutedRoutedEventArgs e)
  1840. {
  1841. var author = PromptWindow.ShowPrompt(this, App.Settings.ActiveStyle.Author, App.Translation.Prompt.ChangeAuthor_Title, App.Translation.Prompt.ChangeAuthor_Label);
  1842. if (author == null)
  1843. return;
  1844. App.Settings.ActiveStyle.Author = author;
  1845. SaveSettings();
  1846. }
  1847. private void cmdStyle_Duplicate(object sender, ExecutedRoutedEventArgs e)
  1848. {
  1849. var name = PromptWindow.ShowPrompt(this, App.Translation.Misc.NameOfCopied.Fmt(App.Settings.ActiveStyle.Name),
  1850. App.Translation.Prompt.DuplicateStyle_Title, App.Translation.Prompt.DuplicateStyle_Label);
  1851. if (name == null)
  1852. return;
  1853. var style = App.Settings.ActiveStyle.Clone();
  1854. style.Kind = StyleKind.User;
  1855. style.Name = name;
  1856. App.Settings.Styles.Add(style);
  1857. ctStyleDropdown.SelectedItem = style;
  1858. SaveSettings();
  1859. }
  1860. private void cmdStyle_Import(object sender, ExecutedRoutedEventArgs e)
  1861. {
  1862. var dlg = new VistaOpenFileDialog();
  1863. dlg.Filter = App.Translation.Misc.Filter_ImportExportStyle;
  1864. dlg.FilterIndex = 0;
  1865. dlg.Multiselect = true;
  1866. dlg.CheckFileExists = true;
  1867. if (dlg.ShowDialog() != true)
  1868. return;
  1869. Style style = null;
  1870. foreach (string fileName in dlg.FileNames)
  1871. {
  1872. try
  1873. {
  1874. style = ClassifyXml.DeserializeFile<Style>(fileName);
  1875. style.Kind = StyleKind.User;
  1876. }
  1877. catch
  1878. {
  1879. DlgMessage.ShowWarning(App.Translation.Prompt.StyleImport_Fail);
  1880. return;
  1881. }
  1882. App.Settings.Styles.Add(style);
  1883. }
  1884. ctStyleDropdown.SelectedItem = style;
  1885. SaveSettings();
  1886. }
  1887. private void cmdStyle_Export(object sender, ExecutedRoutedEventArgs e)
  1888. {
  1889. var allStyles = App.Settings.Styles
  1890. .Select(style => new CheckListItem<Style> { Item = style, Column1 = string.Format("{0} ({1})", style.Name, style.Author), IsChecked = style == App.Settings.ActiveStyle ? true : false })
  1891. .ToList();
  1892. var tr = App.Translation.Prompt;
  1893. var stylesToExport = CheckListWindow.ShowCheckList(this, allStyles, tr.StyleExport_Prompt, tr.StyleExport_Yes, new string[] { tr.BulkStyles_ColumnTitle }).ToHashSet();
  1894. if (stylesToExport.Count == 0)
  1895. return;
  1896. else if (stylesToExport.Count == 1)
  1897. {
  1898. var dlg = new VistaSaveFileDialog();
  1899. dlg.Filter = App.Translation.Misc.Filter_ImportExportStyle;
  1900. dlg.FilterIndex = 0;
  1901. dlg.CheckPathExists = true;
  1902. if (dlg.ShowDialog() != true)
  1903. return;
  1904. var filename = dlg.FileName;
  1905. if (!filename.ToLower().EndsWith(".xml"))
  1906. filename += ".xml";
  1907. ClassifyXml.SerializeToFile(stylesToExport.First(), filename);
  1908. DlgMessage.ShowInfo(tr.StyleExport_Success.Fmt(App.Translation.Language, 1));
  1909. }
  1910. else
  1911. {
  1912. var dlg = new VistaFolderBrowserDialog();
  1913. dlg.ShowNewFolderButton = true;
  1914. if (dlg.ShowDialog() != true)
  1915. return;
  1916. string format = PromptWindow.ShowPrompt(this, "{Name} ({Author}).xml", App.Translation.Prompt.ExportFormat_Title, App.Translation.Prompt.ExportFormat_Label);
  1917. if (format == null)
  1918. return;
  1919. var path = format.Replace('/', '\\').Split('\\');
  1920. foreach (var style in stylesToExport)
  1921. ClassifyXml.SerializeToFile(style, Path.Combine(dlg.SelectedPath, Path.Combine(path.Select(p => p.Replace("{Name}", style.Name).Replace("{Author}", style.Author).FilenameCharactersEscape()).ToArray())));
  1922. DlgMessage.ShowInfo(tr.StyleExport_Success.Fmt(App.Translation.Language, stylesToExport.Count));
  1923. }
  1924. }
  1925. private void cmdStyle_IconWidth(object sender, ExecutedRoutedEventArgs e)
  1926. {
  1927. // note: most of this code is duplicated below
  1928. again: ;
  1929. var widthStr = InputBox.GetLine(App.Translation.Prompt.IconDims_Width, App.Settings.ActiveStyle.IconWidth.ToString(), App.Translation.Prompt.IconDims_Title, App.Translation.DlgMessage.OK, App.Translation.Prompt.Cancel);
  1930. if (widthStr == null)
  1931. return;
  1932. int width;
  1933. if (!int.TryParse(widthStr, out width) || width <= 0)
  1934. {
  1935. DlgMessage.ShowError(App.Translation.Prompt.IconDims_NumberError);
  1936. goto again;
  1937. }
  1938. if (App.Settings.ActiveStyle.IconWidth == width)
  1939. return;
  1940. var style = GetEditableStyle();
  1941. style.IconWidth = width;
  1942. SaveSettings();
  1943. _renderResults.Clear();
  1944. ctIconsPanel.Children.Clear(); // they need to be recreated with a new size
  1945. UpdateIcons();
  1946. }
  1947. private void cmdStyle_IconHeight(object sender, ExecutedRoutedEventArgs e)
  1948. {
  1949. // note: most of this code is duplicated above
  1950. again: ;
  1951. var heightStr = InputBox.GetLine(App.Translation.Prompt.IconDims_Height, App.Settings.ActiveStyle.IconHeight.ToString(), App.Translation.Prompt.IconDims_Title, App.Translation.DlgMessage.OK, App.Translation.Prompt.Cancel);
  1952. if (heightStr == null)
  1953. return;
  1954. int height;
  1955. if (!int.TryParse(heightStr, out height) || height <= 0)
  1956. {
  1957. DlgMessage.ShowError(App.Translation.Prompt.IconDims_NumberError);
  1958. goto again;
  1959. }
  1960. if (App.Settings.ActiveStyle.IconHeight == height)
  1961. return;
  1962. var style = GetEditableStyle();
  1963. style.IconHeight = height;
  1964. SaveSettings();
  1965. _renderResults.Clear();
  1966. ctIconsPanel.Children.Clear(); // they need to be recreated with a new size
  1967. UpdateIcons();
  1968. }
  1969. private void cmdStyle_Centerable(object sender, ExecutedRoutedEventArgs e)
  1970. {
  1971. var choice = DlgMessage.ShowQuestion(App.Translation.Prompt.Centerable_Prompt.Fmt(App.Settings.ActiveStyle.Centerable ? App.Translation.Prompt.Centerable_Yes : App.Translation.Prompt.Centerable_No),
  1972. App.Translation.Prompt.Centerable_Yes, App.Translation.Prompt.Centerable_No, App.Translation.Prompt.Cancel);
  1973. if (choice == 2)
  1974. return;
  1975. if (App.Settings.ActiveStyle.Centerable == (choice == 0))
  1976. return;
  1977. var style = GetEditableStyle();
  1978. style.Centerable = (choice == 0);
  1979. SaveSettings();
  1980. _renderResults.Clear();
  1981. UpdateIcons();
  1982. }
  1983. private abstract class Warning
  1984. {
  1985. public string Text { get; protected set; }
  1986. public override string ToString() { return Text; }
  1987. }
  1988. private sealed class Warning_LayerTest_MissingExtra : Warning { public Warning_LayerTest_MissingExtra(string text) { Text = text; } }
  1989. private sealed class Warning_LayerTest_UnexpectedProperty : Warning { public Warning_LayerTest_UnexpectedProperty(string text) { Text = text; } }
  1990. private sealed class Warning_LayerTest_MissingImage : Warning { public Warning_LayerTest_MissingImage(string text) { Text = text; } }
  1991. private sealed class Warning_RenderedWithErrWarn : Warning { public Warning_RenderedWithErrWarn(string text) { Text = text; } }
  1992. private sealed class Warning_DataLoadWarning : Warning { public Warning_DataLoadWarning(string text) { Text = text; } }
  1993. }
  1994. static class TankLayerCommands
  1995. {
  1996. public static RoutedCommand AddLayer = new RoutedCommand();
  1997. public static RoutedCommand AddEffect = new RoutedCommand();
  1998. public static RoutedCommand Rename = new RoutedCommand();
  1999. public static RoutedCommand Copy = new RoutedCommand();
  2000. public static RoutedCommand CopyEffects = new RoutedCommand();
  2001. public static RoutedCommand Paste = new RoutedCommand();
  2002. public static RoutedCommand Delete = new RoutedCommand();
  2003. public static RoutedCommand MoveUp = new RoutedCommand();
  2004. public static RoutedCommand MoveDown = new RoutedCommand();
  2005. public static RoutedCommand ToggleVisibility = new RoutedCommand();
  2006. }
  2007. static class TankStyleCommands
  2008. {
  2009. public static RoutedCommand Add = new RoutedCommand();
  2010. public static RoutedCommand Delete = new RoutedCommand();
  2011. public static RoutedCommand ChangeName = new RoutedCommand();
  2012. public static RoutedCommand ChangeAuthor = new RoutedCommand();
  2013. public static RoutedCommand Duplicate = new RoutedCommand();
  2014. public static RoutedCommand Import = new RoutedCommand();
  2015. public static RoutedCommand Export = new RoutedCommand();
  2016. public static RoutedCommand IconWidth = new RoutedCommand();
  2017. public static RoutedCommand IconHeight = new RoutedCommand();
  2018. public static RoutedCommand Centerable = new RoutedCommand();
  2019. }
  2020. /// <summary>
  2021. /// Holds all the information needed to render one tank image during the rendering stage, and the render results afterwards.
  2022. /// </summary>
  2023. sealed class RenderTask
  2024. {
  2025. /// <summary>Current style.</summary>
  2026. public Style Style { get; private set; }
  2027. /// <summary>System Id of the tank that this task is for.</summary>
  2028. public string TankId;
  2029. /// <summary>All the tank data pertaining to this render task.</summary>
  2030. public Tank Tank;
  2031. /// <summary>Image rendered by the maker for a tank.</summary>
  2032. public BitmapSource Image;
  2033. /// <summary>Any warnings generated by the maker while rendering this image.</summary>
  2034. public List<string> Warnings;
  2035. /// <summary>Exception that occurred while rendering this image, or null if none.</summary>
  2036. public Exception Exception;
  2037. /// <summary>Rendered layers with Id != "" used in Mask Layer.</summary>
  2038. private Dictionary<string, BitmapBase> _renderedLayers;
  2039. /// <summary>Layers referenced while rendering a single layer. Used to detect recursive references.</summary>
  2040. private HashSet<LayerBase> _referencedLayers = new HashSet<LayerBase>();
  2041. public RenderTask(Style style)
  2042. {
  2043. Style = style;
  2044. }
  2045. public void AddWarning(string warning)
  2046. {
  2047. if (Warnings == null) Warnings = new List<string>();
  2048. Warnings.Add(warning);
  2049. }
  2050. public int WarningsCount { get { return Warnings == null ? 0 : Warnings.Count; } }
  2051. public bool IsLayerAlreadyReferenced(LayerBase layer)
  2052. {
  2053. return _referencedLayers.Contains(layer);
  2054. }
  2055. public BitmapBase RenderLayer(LayerBase layer)
  2056. {
  2057. if (!string.IsNullOrEmpty(layer.Id) && _renderedLayers.ContainsKey(layer.Id))
  2058. return _renderedLayers[layer.Id];
  2059. _referencedLayers.Add(layer);
  2060. var img = layer.Draw(this.Tank);
  2061. if (img == null)
  2062. return null;
  2063. foreach (var effect in layer.Effects.Where(e => e.Visible && e.VisibleFor.GetValue(this.Tank) == BoolWithPassthrough.Yes))
  2064. {
  2065. img = effect.Apply(this, img.AsWritable());
  2066. if (effect is Effects.SizePosEffect)
  2067. if (img.Width < Style.IconWidth || img.Height < Style.IconHeight)
  2068. {
  2069. var imgOrig = img;
  2070. img = new BitmapRam(Math.Max(Style.IconWidth, img.Width), Math.Max(Style.IconHeight, img.Height));
  2071. img.DrawImage(imgOrig);
  2072. }
  2073. if (!string.IsNullOrEmpty(layer.Id))
  2074. _renderedLayers[layer.Id] = img;
  2075. }
  2076. _referencedLayers.Remove(layer);
  2077. return img;
  2078. }
  2079. /// <summary>
  2080. /// Executes this render task. Will handle any exceptions in the maker and draw an appropriate substitute image
  2081. /// to draw the user's attention to the problem.
  2082. /// </summary>
  2083. public void Render()
  2084. {
  2085. _renderedLayers = new Dictionary<string, BitmapBase>();
  2086. try
  2087. {
  2088. var conflict = Style.Layers.Where(x => !string.IsNullOrEmpty(x.Id)).GroupBy(x => x.Id).FirstOrDefault(x => x.Count() > 1);
  2089. if (conflict != null)
  2090. throw new StyleUserError(App.Translation.MainWindow.ErrorConflictingId.Fmt(conflict.Key));
  2091. var result = new BitmapWpf(Style.IconWidth, Style.IconHeight);
  2092. using (result.UseWrite())
  2093. {
  2094. foreach (var layer in Style.Layers.Where(l => l.Visible && l.VisibleFor.GetValue(this.Tank) == BoolWithPassthrough.Yes))
  2095. {
  2096. _referencedLayers.Clear();
  2097. var img = RenderLayer(layer);
  2098. if (img != null)
  2099. result.DrawImage(img);
  2100. }
  2101. }
  2102. if (Style.Centerable)
  2103. {
  2104. var width = result.PreciseWidth();
  2105. var wantedWidth = Math.Min(width.Right + width.Left + 1, Style.IconWidth);
  2106. var img = result;
  2107. result = new BitmapWpf(wantedWidth, img.Height);
  2108. result.CopyPixelsFrom(img);
  2109. }
  2110. result.MarkReadOnly();
  2111. this.Image = result.UnderlyingImage;
  2112. }
  2113. catch (Exception e)
  2114. {
  2115. this.Image = Ut.NewBitmapWpf(Style.IconWidth, Style.IconHeight, dc =>
  2116. {
  2117. dc.DrawRectangle(new SolidColorBrush(Color.FromArgb(180, 255, 255, 255)), null, new Rect(0.5, 1.5, Style.IconWidth - 1, Style.IconHeight - 3));
  2118. var pen = new Pen(e is StyleUserError ? Brushes.Green : Brushes.Red, 2);
  2119. dc.DrawLine(pen, new Point(1, 2), new Point(Style.IconWidth - 1, Style.IconHeight - 3));
  2120. dc.DrawLine(pen, new Point(Style.IconWidth - 1, 2), new Point(1, Style.IconHeight - 3));
  2121. dc.DrawRectangle(null, new Pen(Brushes.Black, 1), new Rect(0.5, 1.5, Style.IconWidth - 1, Style.IconHeight - 3));
  2122. });
  2123. this.Exception = e;
  2124. }
  2125. }
  2126. }
  2127. /// <summary>
  2128. /// Represents a tank's icon on screen.
  2129. /// </summary>
  2130. sealed class TankImageControl : Image
  2131. {
  2132. public RenderTask RenderTask;
  2133. static Geometry _triangle;
  2134. static TankImageControl()
  2135. {
  2136. _triangle = new PathGeometry(new[] {
  2137. new PathFigure(new Point(0, -50), new[] {
  2138. new PolyLineSegment(new[] { new Point(58, 50), new Point(-58, 50) }, isStroked: true)
  2139. }, closed: true)
  2140. });
  2141. _triangle.Freeze();
  2142. }
  2143. /// <summary>Renders the image, optionally with a warning triangle overlay (if there are warnings to be seen).</summary>
  2144. protected override void OnRender(DrawingContext dc)
  2145. {
  2146. base.OnRender(dc);
  2147. if (RenderTask == null) return;
  2148. if (RenderTask.Exception == null && RenderTask.WarningsCount == 0) return;
  2149. double cy = ActualHeight / 2;
  2150. double scale = 0.6 * ActualHeight / 100;
  2151. dc.PushTransform(new TranslateTransform(50 * scale - 7 / App.DpiScaleX, cy));
  2152. dc.PushTransform(new ScaleTransform(scale, scale));
  2153. dc.DrawGeometry(Brushes.Black, null, _triangle);
  2154. dc.PushTransform(new ScaleTransform(0.83, 0.83)); dc.PushTransform(new TranslateTransform(0, 3)); dc.DrawGeometry(Brushes.Red, null, _triangle); dc.Pop(); dc.Pop();
  2155. dc.PushTransform(new ScaleTransform(0.5, 0.5)); dc.PushTransform(new TranslateTransform(0, 16)); dc.DrawGeometry(Brushes.White, null, _triangle); dc.Pop(); dc.Pop();
  2156. var exclamation = new FormattedText("!", System.Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Arial Black"), 55, Brushes.Black);
  2157. dc.DrawText(exclamation, new Point(-exclamation.Width / 2, 11 - exclamation.Height / 2));
  2158. dc.Pop(); dc.Pop();
  2159. }
  2160. }
  2161. }