/MainWindow.xaml.cs
C# | 2391 lines | 2020 code | 230 blank | 141 comment | 416 complexity | 5ad727745d1d6eb8d26b9ddd96ed428e MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0
Large files files are truncated, but you can click here to view the full file
- using System;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.Diagnostics;
- using System.Globalization;
- using System.IO;
- using System.IO.Packaging;
- using System.Linq;
- using System.Reflection;
- using System.Text.RegularExpressions;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Data;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Media.Imaging;
- using System.Windows.Threading;
- using System.Xml.Linq;
- using ICSharpCode.SharpZipLib.Zip;
- using Ookii.Dialogs.Wpf;
- using RT.Util;
- using RT.Util.Dialogs;
- using RT.Util.ExtensionMethods;
- using RT.Util.Forms;
- using RT.Util.Lingo;
- using RT.Util.Serialization;
- using TankIconMaker.Layers;
- using WotDataLib;
- using WpfCrutches;
- using Xceed.Wpf.Toolkit.PropertyGrid;
-
- namespace TankIconMaker
- {
- partial class MainWindow : ManagedWindow
- {
- private DispatcherTimer _updateIconsTimer = new DispatcherTimer(DispatcherPriority.Background);
- private DispatcherTimer _updatePropertiesTimer = new DispatcherTimer(DispatcherPriority.Background);
- private CancellationTokenSource _cancelRender = new CancellationTokenSource();
- private Dictionary<string, RenderTask> _renderResults = new Dictionary<string, RenderTask>();
- private static BitmapImage _warningImage;
- private ObservableValue<bool> _rendering = new ObservableValue<bool>(false);
- private ObservableValue<bool> _dataMissing = new ObservableValue<bool>(false);
- private ObservableCollection<Warning> _warnings = new ObservableCollection<Warning>();
-
- private LanguageHelperWpfOld<Translation> _translationHelper;
-
- public MainWindow()
- : base(App.Settings.MainWindow)
- {
- InitializeComponent();
- UiZoom = App.Settings.UiZoom;
- GlobalStatusShow(App.Translation.Misc.GlobalStatus_Loading);
- ContentRendered += InitializeEverything;
- Closing += MainWindow_Closing;
- }
-
- /// <summary>
- /// Shows a message in large letters in an overlay in the middle of the window. Must be called on the UI thread
- /// and won't become visible until the UI thread returns (into the dispatcher).
- /// </summary>
- private void GlobalStatusShow(string message)
- {
- (ctGlobalStatusBox.Child as TextBlock).Text = message;
- ctGlobalStatusBox.Visibility = Visibility.Visible;
- IsEnabled = false;
- ctIconsPanel.Opacity = 0.6;
- }
-
- /// <summary>
- /// Hides the message shown using <see cref="GlobalStatusShow"/>.
- /// </summary>
- private void GlobalStatusHide()
- {
- IsEnabled = true;
- ctGlobalStatusBox.Visibility = Visibility.Collapsed;
- ctIconsPanel.Opacity = 1;
- }
-
- private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
- {
- if (_translationHelper != null && !_translationHelper.MayExitApplication())
- e.Cancel = true;
- }
-
- /// <summary>
- /// Performs most of the slow initializations. This method is only called after the UI becomes visible, to improve the
- /// perceived start-up performance.
- /// </summary>
- private void InitializeEverything(object ___, EventArgs ____)
- {
- ContentRendered -= InitializeEverything;
-
- OldFiles.DeleteOldFiles();
-
- RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.HighQuality);
-
- var mat = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
- App.DpiScaleX = mat.M11;
- App.DpiScaleY = mat.M22;
-
- var lingoTypeDescProvider = new LingoTypeDescriptionProvider<Translation>(() => App.Translation);
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(LayerBase));
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(EffectBase));
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<string>));
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<BoolWithPassthrough>));
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<Color>));
- System.ComponentModel.TypeDescriptor.AddProvider(lingoTypeDescProvider, typeof(SelectorBase<Filename>));
- #if DEBUG
- Lingo.AlsoSaveTranslationsTo = PathUtil.AppPathCombine(@"..\..\Resources\Translations");
- using (var translationFileGenerator = new Lingo.TranslationFileGenerator(PathUtil.AppPathCombine(@"..\..\Translation.g.cs")))
- {
- translationFileGenerator.TranslateWindow(this, App.Translation.MainWindow);
- var wnd = new PathTemplateWindow();
- translationFileGenerator.TranslateWindow(wnd, App.Translation.PathTemplateWindow);
- wnd.Close();
- var wnd2 = new BulkSaveSettingsWindow();
- translationFileGenerator.TranslateWindow(wnd2, App.Translation.BulkSaveSettingsWindow);
- wnd2.Close();
- }
- #endif
- using (var iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/TankIconMaker;component/Resources/Graphics/icon.ico")).Stream)
- _translationHelper = new LanguageHelperWpfOld<Translation>("Tank Icon Maker", "TankIconMaker", true,
- App.Settings.TranslationFormSettings, new System.Drawing.Icon(iconStream), () => App.Settings.Lingo);
- _translationHelper.TranslationChanged += TranslationChanged;
- Translate(first: true);
- Title += " (v{0:000} b{1})".Fmt(Assembly.GetExecutingAssembly().GetName().Version.Major, Assembly.GetExecutingAssembly().GetName().Version.Minor);
-
- CommandBindings.Add(new CommandBinding(TankLayerCommands.AddLayer, cmdLayer_AddLayer));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.AddEffect, cmdLayer_AddEffect, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.Rename, cmdLayer_Rename, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.Delete, cmdLayer_Delete, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.Copy, cmdLayer_Copy, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.CopyEffects, cmdLayer_CopyEffects, (_, a) => { a.CanExecute = isLayerSelected(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.Paste, cmdLayer_Paste, (_, a) => { a.CanExecute = isLayerOrEffectInClipboard(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.MoveUp, cmdLayer_MoveUp, (_, a) => { a.CanExecute = cmdLayer_MoveUp_IsAvailable(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.MoveDown, cmdLayer_MoveDown, (_, a) => { a.CanExecute = cmdLayer_MoveDown_IsAvailable(); }));
- CommandBindings.Add(new CommandBinding(TankLayerCommands.ToggleVisibility, cmdLayer_ToggleVisibility, (_, a) => { a.CanExecute = isLayerOrEffectSelected(); }));
-
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Add, cmdStyle_Add));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Delete, cmdStyle_Delete, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.ChangeName, cmdStyle_ChangeName, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.ChangeAuthor, cmdStyle_ChangeAuthor, (_, a) => { a.CanExecute = cmdStyle_UserStyleSelected(); }));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Duplicate, cmdStyle_Duplicate));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Import, cmdStyle_Import));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Export, cmdStyle_Export));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.IconWidth, cmdStyle_IconWidth));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.IconHeight, cmdStyle_IconHeight));
- CommandBindings.Add(new CommandBinding(TankStyleCommands.Centerable, cmdStyle_Centerable));
-
- _updateIconsTimer.Tick += UpdateIcons;
- _updateIconsTimer.Interval = TimeSpan.FromMilliseconds(100);
-
- if (App.Settings.LeftColumnWidth != null)
- ctLeftColumn.Width = new GridLength(App.Settings.LeftColumnWidth.Value);
- if (App.Settings.NameColumnWidth != null)
- ctLayerProperties.NameColumnWidth = App.Settings.NameColumnWidth.Value;
- ctDisplayMode.SelectedIndex = (int) App.Settings.DisplayFilter;
-
- ApplyBackground();
- ApplyBackgroundColors();
-
- _warningImage = new BitmapImage(new Uri(@"pack://application:,,,/Resources/Graphics/warning.png"));
-
- // Styles: build the combined built-in and user-defined styles collection
- var styles = new CompositeCollection<Style>();
- RecreateBuiltInStyles();
- styles.AddCollection(_builtinStyles);
- styles.AddCollection(App.Settings.Styles);
- // Styles: update the active style
- if (App.Settings.ActiveStyle == null)
- App.Settings.ActiveStyle = _builtinStyles.First();
- else if (!App.Settings.Styles.Contains(App.Settings.ActiveStyle))
- App.Settings.ActiveStyle = styles.FirstOrDefault(s => s.Name == App.Settings.ActiveStyle.Name && s.Author == App.Settings.ActiveStyle.Author) ?? _builtinStyles.First();
- // Styles: configure the UI control
- ctStyleDropdown.ItemsSource = styles;
- ctStyleDropdown.DisplayMemberPath = "Display";
- ctStyleDropdown.SelectedItem = App.Settings.ActiveStyle;
-
- // Game installations: find/add all installations if blank
- if (App.Settings.GameInstallations.Count == 0)
- AddGameInstallations();
- // Game installations: make sure one of the installations is the active one
- #pragma warning disable 0618 // ActiveInstallation should only be used for loading/saving the setting, which is what the code below does.
- if (!App.Settings.GameInstallations.Contains(App.Settings.ActiveInstallation)) // includes the "null" case
- App.Settings.ActiveInstallation = App.Settings.GameInstallations.FirstOrDefault();
- // Game installations: configure the UI control
- ctGamePath.ItemsSource = App.Settings.GameInstallations;
- ctGamePath.DisplayMemberPath = "DisplayName";
- ctGamePath.SelectedItem = App.Settings.ActiveInstallation;
- #pragma warning restore 0618
-
- ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ColorSelector), ExpandableObject = true });
- ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ValueSelector<>), ExpandableObject = true });
- ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(Filename), EditorType = typeof(FilenameEditor) });
- ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(ExtraPropertyId), EditorType = typeof(DataSourceEditor) });
- ctLayerProperties.EditorDefinitions.Add(new EditorDefinition { TargetType = typeof(Anchor), EditorType = typeof(AnchorEditor) });
-
- ReloadData(first: true);
-
- // Set WPF bindings now that all the data we need is loaded
- BindingOperations.SetBinding(ctRemoveGamePath, Button.IsEnabledProperty, LambdaBinding.New(
- new Binding { Source = ctGamePath, Path = new PropertyPath(ComboBox.SelectedIndexProperty) },
- (int index) => index >= 0
- ));
- BindingOperations.SetBinding(ctGamePath, ComboBox.IsEnabledProperty, LambdaBinding.New(
- new Binding { Source = App.Settings.GameInstallations, Path = new PropertyPath("Count") },
- (int count) => count > 0
- ));
- BindingOperations.SetBinding(ctWarning, Image.VisibilityProperty, LambdaBinding.New(
- new Binding { Source = _warnings, Path = new PropertyPath("Count") },
- (int warningCount) => warningCount == 0 ? Visibility.Collapsed : Visibility.Visible
- ));
- BindingOperations.SetBinding(ctSave, Button.IsEnabledProperty, LambdaBinding.New(
- new Binding { Source = _rendering, Path = new PropertyPath("Value") },
- new Binding { Source = _dataMissing, Path = new PropertyPath("Value") },
- (bool rendering, bool dataMissing) => !rendering && !dataMissing
- ));
- BindingOperations.SetBinding(ctLayersTree, TreeView.MaxHeightProperty, LambdaBinding.New(
- new Binding { Source = ctLeftBottomPane, Path = new PropertyPath(Grid.ActualHeightProperty) },
- (double paneHeight) => paneHeight * 0.4
- ));
- BindingOperations.SetBinding(ctUiZoomIn, Button.IsEnabledProperty, LambdaBinding.New(
- new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
- (double zoom) => zoom <= 2.5
- ));
- BindingOperations.SetBinding(ctUiZoomOut, Button.IsEnabledProperty, LambdaBinding.New(
- new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
- (double zoom) => zoom >= 0.5
- ));
- BindingOperations.SetBinding(ctPathTemplate, TextBlock.TextProperty, LambdaBinding.New(
- new Binding { Path = new PropertyPath("PathTemplate") },
- (string template) => string.IsNullOrEmpty(template) ? (string) App.Translation.MainWindow.PathTemplate_Standard : template
- ));
-
-
-
- // Another day, another WPF crutch... http://stackoverflow.com/questions/3921712
- ctLayersTree.PreviewMouseDown += (_, __) => { FocusManager.SetFocusedElement(this, ctLayersTree); };
-
- // Bind the events now that all the UI is set up as desired
- Closing += (_, __) => SaveSettings();
- this.SizeChanged += SaveSettingsDelayed;
- this.LocationChanged += SaveSettingsDelayed;
- ctStyleDropdown.SelectionChanged += ctStyleDropdown_SelectionChanged;
- ctLayerProperties.PropertyValueChanged += ctLayerProperties_PropertyValueChanged;
- ctDisplayMode.SelectionChanged += ctDisplayMode_SelectionChanged;
- ctGamePath.SelectionChanged += ctGamePath_SelectionChanged;
- ctGamePath.PreviewKeyDown += ctGamePath_PreviewKeyDown;
- ctLayersTree.SelectedItemChanged += (_, e) => { _updatePropertiesTimer.Stop(); _updatePropertiesTimer.Start(); };
- _updatePropertiesTimer.Tick += (_, __) => { _updatePropertiesTimer.Stop(); ctLayerProperties.SelectedObject = ctLayersTree.SelectedItem; };
- _updatePropertiesTimer.Interval = TimeSpan.FromMilliseconds(200);
-
- // Refresh all the commands because otherwise WPF doesn’t realise the states have changed.
- CommandManager.InvalidateRequerySuggested();
-
- // Fire off some GUI events manually now that everything's set up
- ctStyleDropdown_SelectionChanged();
-
- // Done
- GlobalStatusHide();
- _updateIconsTimer.Start();
- }
-
- private void TranslationChanged(Translation t)
- {
- App.Translation = t;
- App.Settings.Lingo = t.Language;
- Translate();
- }
-
- private void Translate(bool first = false)
- {
- App.LayerTypes = translateTypes(App.LayerTypes);
- App.EffectTypes = translateTypes(App.EffectTypes);
- Lingo.TranslateWindow(this, App.Translation.MainWindow);
- DlgMessage.Translate(App.Translation.DlgMessage.OK,
- App.Translation.DlgMessage.CaptionInfo,
- App.Translation.DlgMessage.CaptionQuestion,
- App.Translation.DlgMessage.CaptionWarning,
- App.Translation.DlgMessage.CaptionError);
-
- foreach (var style in ctStyleDropdown.Items.OfType<Style>()) // this includes the built-in styles too, unlike App.Settings.Styles
- style.TranslationChanged();
-
- if (!first)
- {
- var wasSelected = ctLayerProperties.SelectedObject;
- ctLayerProperties.SelectedObject = null;
- ctLayerProperties.SelectedObject = wasSelected;
-
- ctLayersTree.ItemsSource = null;
- ctPathTemplate.DataContext = null;
- ctStyleDropdown_SelectionChanged();
-
- ReloadData();
- UpdateIcons();
- }
- }
-
- private static IList<TypeInfo<T>> translateTypes<T>(IList<TypeInfo<T>> types) where T : IHasTypeNameDescription
- {
- return types.Select(type =>
- {
- var obj = type.Constructor();
- return new TypeInfo<T>
- {
- Type = type.Type,
- Constructor = type.Constructor,
- Name = obj.TypeName,
- Description = obj.TypeDescription,
- };
- }).OrderBy(ti => ti.Name).ToList().AsReadOnly();
- }
-
- private ObservableSortedList<Style> _builtinStyles = new ObservableSortedList<Style>();
- private void RecreateBuiltInStyles()
- {
- _builtinStyles.Clear();
-
- var assy = Assembly.GetExecutingAssembly();
- foreach (var resourceName in assy.GetManifestResourceNames().Where(n => n.Contains(".BuiltInStyles.")))
- {
- try
- {
- XDocument doc;
- using (var stream = assy.GetManifestResourceStream(resourceName))
- doc = XDocument.Load(stream);
- var style = ClassifyXml.Deserialize<Style>(doc.Root);
- style.Kind = style.Name == "Original" ? StyleKind.Original : style.Name == "Current" ? StyleKind.Current : StyleKind.BuiltIn;
- _builtinStyles.Add(style);
- }
- catch { } // should not happen, but if it does, pretend the style doesn’t exist.
- }
- }
-
- /// <summary>
- /// Gets the installation currently selected in the GUI, or null if none are available.
- /// </summary>
- private TimGameInstallation ActiveInstallation
- {
- get { return ctGamePath.Items.Count == 0 ? null : (TimGameInstallation) ctGamePath.SelectedItem; }
- }
-
- [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.
- private WotContext _context;
-
- /// <summary>
- /// Returns a WotContext based on the currently selected game installation and the last loaded game data. Null if there
- /// was a problem preventing a context being created. Do not reference this property off the GUI thread. Store the referenced
- /// instance in a local and pass _that_ to any background threads. The property will change if the user does certain things
- /// while the backround tasks are running, but the WotContext instance itself is immutable.
- /// </summary>
- public WotContext CurContext
- {
- get
- {
- #pragma warning disable 618
- if (_context != null && _context.Installation.GameVersionId != ActiveInstallation.GameVersionId)
- throw new Exception("CurContext used without a reload"); // this shouldn't be possible; this is just a bug-detecting assertion.
- return _context;
- #pragma warning restore 618
- }
- }
-
- /// <summary>
- /// Does a bunch of stuff necessary to reload all the data off disk and refresh the UI (except for drawing the icons:
- /// this must be done as a separate step).
- /// </summary>
- private void ReloadData(bool first = false)
- {
- _renderResults.Clear();
- ZipCache.Clear();
- ImageCache.Clear();
- _warnings.Clear();
- #pragma warning disable 618 // ReloadData is the only method allowed to modify _context
- _context = null;
- #pragma warning restore 618
-
- foreach (var gameInstallation in App.Settings.GameInstallations.ToList()) // grab a list of all items because the source auto-resorts on changes
- gameInstallation.Reload();
-
- // Disable parts of the UI if some of the data is unavailable, and show warnings as appropriate
- if (ActiveInstallation == null || ActiveInstallation.GameVersionId == null)
- {
- // This means we don't have a valid WoT installation available. So we still can't show the correct lists of properties
- // in the drop-downs, and also can't render tanks because we don't know which ones.
- _dataMissing.Value = true;
- if (ActiveInstallation == null)
- ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_NoInstallationSelected;
- else if (!Directory.Exists(ActiveInstallation.Path))
- ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_DirNotFound;
- else
- ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_NoWotInstallation;
- ctGameInstallationWarning.Tag = null;
- }
- else
- {
- // Attempt to load the data
- try
- {
- #pragma warning disable 618 // ReloadData is the only method allowed to modify _context
- _context = WotData.Load(PathUtil.AppPathCombine("Data"), ActiveInstallation, App.Settings.DefaultPropertyAuthor, PathUtil.AppPathCombine("Data", "Exported"));
- #pragma warning restore 618
- }
- catch (WotDataUserError e)
- {
- _dataMissing.Value = true;
- ctGameInstallationWarning.Text = e.Message;
- ctGameInstallationWarning.Tag = null;
- }
- #if !DEBUG
- catch (Exception e)
- {
- _dataMissing.Value = true;
- ctGameInstallationWarning.Text = "Error loading game data from this path. Click this message for details.";
- ctGameInstallationWarning.Tag = Ut.ExceptionToDebugString(e);
- }
- #endif
- }
-
- // CurContext is now set as appropriate: either null or a reloaded context
- if (CurContext != null)
- {
- // See how complete of a context we managed to get
- if (CurContext.VersionConfig == null)
- {
- // The WoT installation is valid, but we don't have a suitable version config. Can list the right properties, but can't really render.
- _dataMissing.Value = true;
- ctGameInstallationWarning.Text = App.Translation.Error.DataMissing_WotVersionTooOld.Fmt(ActiveInstallation.GameVersionName + " #" + ActiveInstallation.GameVersionId);
- ctGameInstallationWarning.Tag = null;
- }
- else
- {
- // Everything's fine.
- _dataMissing.Value = false;
- ctGameInstallationWarning.Text = "";
- ctGameInstallationWarning.Tag = null;
- // Show any non-fatal data loading warnings
- foreach (var warning in CurContext.Warnings)
- _warnings.Add(new Warning_DataLoadWarning(warning));
- }
- }
- FilenameEditor.LastContext = CurContext; // this is a bit of a hack to give FilenameEditor instances access to the context, see comment on the field.
-
- // Just some self-tests for any bugs in the above
- if (CurContext == null && !_dataMissing) throw new Exception();
- if (_dataMissing != (ctGameInstallationWarning.Text != "")) throw new Exception(); // must show installation warning iff UI set to the dataMissing state
- if (ctGameInstallationWarning.Text == null && ctGameInstallationWarning.Tag != null) throw new Exception();
-
- // Update the list of data sources currently available. This list is used by drop-downs which offer the user to select a property.
- foreach (var item in App.DataSources.Where(ds => ds.GetType() == typeof(DataSourceInfo)).ToArray())
- {
- var extra = CurContext == null ? null : CurContext.ExtraProperties.FirstOrDefault(df => df.PropertyId == item.PropertyId);
- if (extra == null)
- App.DataSources.Remove(item);
- else
- item.UpdateFrom(extra);
- }
- if (CurContext != null)
- foreach (var extra in CurContext.ExtraProperties)
- {
- if (!App.DataSources.Any(item => extra.PropertyId == item.PropertyId))
- App.DataSources.Add(new DataSourceInfo(extra));
- }
-
- if (_dataMissing)
- {
- // Clear the icons area. It will remain empty because icon rendering code exits if _dataMissing is true.
- // Various UI controls disable automatically whenever _dataMissing is true.
- ctIconsPanel.Children.Clear();
- }
- else
- {
- // Force a full re-render
- if (!first)
- {
- _renderResults.Clear();
- UpdateIcons();
- }
- }
- }
-
- /// <summary>
- /// Schedules an icon update to occur after a short timeout. If called again before the timeout, will re-set the timeout
- /// back to original value. If called during a render, the render is cancelled immediately. Call this if the event that
- /// invalidated the current icons can occur frequently. Call <see cref="UpdateIcons"/> for immediate response.
- /// </summary>
- private void ScheduleUpdateIcons()
- {
- _cancelRender.Cancel();
-
- _rendering.Value = true;
- foreach (var image in ctIconsPanel.Children.OfType<TankImageControl>())
- image.Opacity = 0.7;
-
- _updateIconsTimer.Stop();
- _updateIconsTimer.Start();
- }
-
- /// <summary>
- /// Begins an icon update immediately. The icons are rendered in the background without blocking the UI. If called during
- /// a previous render, the render is cancelled immediately. Call this if the event that invalidated the current icons occurs
- /// infrequently, to ensure immediate response to user action. For very frequent updates, use <see cref="ScheduleUpdateIcons"/>.
- /// Only the icons not in the render cache are re-rendered; remove some or all to force a re-render of the icon.
- /// </summary>
- private void UpdateIcons(object _ = null, EventArgs __ = null)
- {
- _rendering.Value = true;
- foreach (var image in ctIconsPanel.Children.OfType<TankImageControl>())
- image.Opacity = 0.7;
- _warnings.RemoveWhere(w => w is Warning_RenderedWithErrWarn);
-
- _updateIconsTimer.Stop();
- _cancelRender.Cancel();
- _cancelRender = new CancellationTokenSource();
- 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
-
- if (_dataMissing)
- return;
-
- var context = CurContext;
- var images = ctIconsPanel.Children.OfType<TankImageControl>().ToList();
- var style = App.Settings.ActiveStyle;
- var renderTasks = ListRenderTasks(context, style);
-
- foreach (var layer in style.Layers)
- TestLayer(style, layer);
-
- var tasks = new List<Action>();
- for (int i = 0; i < renderTasks.Count; i++)
- {
- if (i >= images.Count)
- images.Add(CreateTankImageControl(style));
- var renderTask = renderTasks[i];
- var image = images[i];
-
- image.ToolTip = renderTasks[i].TankId;
- if (_renderResults.ContainsKey(renderTask.TankId))
- {
- image.Source = _renderResults[renderTask.TankId].Image;
- image.RenderTask = _renderResults[renderTask.TankId];
- image.Opacity = 1;
- }
- else
- tasks.Add(() =>
- {
- try
- {
- if (cancelToken.IsCancellationRequested) return;
- renderTask.Render();
- if (cancelToken.IsCancellationRequested) return;
- Dispatcher.Invoke(new Action(() =>
- {
- if (cancelToken.IsCancellationRequested) return;
- _renderResults[renderTask.TankId] = renderTask;
- image.Source = renderTask.Image;
- image.RenderTask = renderTask;
- image.Opacity = 1;
- if (ctIconsPanel.Children.OfType<TankImageControl>().All(c => c.Opacity == 1))
- UpdateIconsCompleted();
- }));
- }
- catch { }
- });
- }
- foreach (var task in tasks)
- Task.Factory.StartNew(task, cancelToken, TaskCreationOptions.None, PriorityScheduler.Lowest);
-
- // Remove unused images
- foreach (var image in images.Skip(renderTasks.Count))
- ctIconsPanel.Children.Remove(image);
- if (ctIconsPanel.Children.OfType<TankImageControl>().All(c => c.Opacity == 1))
- UpdateIconsCompleted();
- }
-
- /// <summary>
- /// Called on the GUI thread whenever all the icon renders are completed.
- /// </summary>
- private void UpdateIconsCompleted()
- {
- _rendering.Value = false;
-
- // Update the warning messages
- if (_renderResults.Values.Any(rr => rr.Exception != null))
- _warnings.Add(new Warning_RenderedWithErrWarn(App.Translation.Error.RenderWithErrors));
- else if (_renderResults.Values.Any(rr => rr.WarningsCount > 0))
- _warnings.Add(new Warning_RenderedWithErrWarn(App.Translation.Error.RenderWithWarnings));
-
- // Clean up all those temporary images we've just created and won't be doing again for a while.
- // (this keeps "private bytes" when idle 10-15 MB lower)
- GC.Collect();
- }
-
- /// <summary>
- /// Tests the specified layer instance for its handling of missing extra properties (and possibly other problems). Adds an
- /// appropriate warning message if a problem is detected.
- /// </summary>
- private void TestLayer(Style style, LayerBase layer)
- {
- // Test missing extra properties
- _warnings.RemoveWhere(w => w is Warning_LayerTest_MissingExtra);
- var context = CurContext;
- if (context == null)
- return;
- try
- {
- var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
- tank.LoadedImage = new BitmapRam(style.IconWidth, style.IconHeight);
- layer.Draw(tank);
- }
- catch (Exception e)
- {
- if (!(e is StyleUserError))
- _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)));
- // The maker must not throw when properties are missing: firstly, for configurable properties the user could select "None"
- // from the drop-down, and secondly, hard-coded properties could simply be missing altogether.
- // (although this could, of course, be a bug in TankIconMaker itself)
- }
-
- // Test unexpected property values
- _warnings.RemoveWhere(w => w is Warning_LayerTest_UnexpectedProperty);
- try
- {
- var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
- tank.PropertyValue = "z"; // very short, so substring/indexing can fail, also not parseable as integer. Hopefully "unexpected enough".
- tank.LoadedImage = new BitmapRam(style.IconWidth, style.IconHeight);
- layer.Draw(tank);
- }
- catch (Exception e)
- {
- if (!(e is StyleUserError))
- _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)));
- // The maker must not throw for unexpected property values: it could issue a warning using tank.AddWarning.
- // (although this could, of course, be a bug in TankIconMaker itself)
- }
-
- // Test missing images
- _warnings.RemoveWhere(w => w is Warning_LayerTest_MissingImage);
- try
- {
- var tank = new TestTank("test", 5, Country.USSR, Class.Medium, Category.Normal, context);
- tank.PropertyValue = "test";
- layer.Draw(tank);
- }
- catch (Exception e)
- {
- if (!(e is StyleUserError))
- _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)));
- // The maker must not throw if the images are missing: it could issue a warning using tank.AddWarning though.
- // (although this could, of course, be a bug in TankIconMaker itself)
- }
- }
-
-
- /// <summary>
- /// Creates a TankImageControl and adds it to the scrollable tank image area. This involves a bunch of properties,
- /// event handlers, and bindings, and is hence abstracted into a method.
- /// </summary>
- private TankImageControl CreateTankImageControl(Style style)
- {
- var img = new TankImageControl
- {
- SnapsToDevicePixels = true,
- Margin = new Thickness { Right = 15 },
- Cursor = Cursors.Hand,
- Opacity = 0.7,
- };
- img.MouseLeftButtonDown += TankImage_MouseLeftButtonDown;
- img.MouseLeftButtonUp += TankImage_MouseLeftButtonUp;
- BindingOperations.SetBinding(img, TankImageControl.WidthProperty, LambdaBinding.New(
- new Binding { Source = ctZoomCheckbox, Path = new PropertyPath(CheckBox.IsCheckedProperty) },
- new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
- (bool check, double uiZoom) => (double) style.IconWidth * (check ? App.Settings.IconScaleZoomed : App.Settings.IconScaleNormal) / App.DpiScaleX / uiZoom
- ));
- BindingOperations.SetBinding(img, TankImageControl.HeightProperty, LambdaBinding.New(
- new Binding { Source = ctZoomCheckbox, Path = new PropertyPath(CheckBox.IsCheckedProperty) },
- new Binding { Source = UiZoomObservable, Path = new PropertyPath("Value") },
- (bool check, double uiZoom) => (double) style.IconHeight * (check ? App.Settings.IconScaleZoomed : App.Settings.IconScaleNormal) / App.DpiScaleY / uiZoom
- ));
- img.HorizontalAlignment = HorizontalAlignment.Left;
- ctIconsPanel.Children.Add(img);
- return img;
- }
-
- private object _lastTankImageDown;
-
- void TankImage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- _lastTankImageDown = sender;
- }
-
- private void TankImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
- {
- bool skip = _lastTankImageDown != sender;
- _lastTankImageDown = null;
- if (skip)
- return;
-
- var image = sender as TankImageControl;
- if (image == null)
- return;
- var renderResult = image.RenderTask;
- if (renderResult == null)
- return;
-
- if (renderResult.Exception == null && renderResult.WarningsCount == 0)
- {
- DlgMessage.ShowInfo(App.Translation.Error.RenderIconOK);
- return;
- }
-
- var warnings = renderResult.WarningsCount == 0 ? new List<object>() : renderResult.Warnings.Cast<object>().ToList();
- var warningsText = RT.Util.Ut.Lambda(() =>
- {
- if (warnings.Count == 1)
- return warnings[0].ToString();
- return new EggsTag(warnings.Select(w => new EggsTag('[', new[] { w is string ? new EggsText((string) w) : (EggsNode) w })).InsertBetween<EggsNode>(new EggsText("\n"))).ToString();
- });
-
- if (renderResult.Exception != null && !(renderResult.Exception is StyleUserError))
- {
- string details = "";
- if (renderResult.Exception is InvalidOperationException && renderResult.Exception.Message.Contains("belongs to a different thread than its parent Freezable"))
- details = "Possible cause: a layer or effect reuses a WPF drawing primitive (like Brush) for different tanks without calling Freeze() on it.\n";
-
- details += Ut.ExceptionToDebugString(renderResult.Exception);
-
- warnings.Add((string) App.Translation.Error.ExceptionInRender);
-
- bool copy = DlgMessage.Show(warningsText(), null, DlgType.Warning, DlgMessageFormat.EggsML, App.Translation.Error.ErrorToClipboard_Copy, App.Translation.Error.ErrorToClipboard_OK) == 0;
- if (copy)
- if (Ut.ClipboardSet(details))
- DlgMessage.ShowInfo(App.Translation.Error.ErrorToClipboard_Copied);
- }
- else
- {
- if (renderResult.Exception != null)
- {
- if (!(renderResult.Exception as StyleUserError).Formatted)
- warnings.Add(App.Translation.Error.RenderIconFail.Fmt(renderResult.Exception.Message));
- else
- 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()))));
- }
-
- DlgMessage.Show(warningsText(), null, DlgType.Warning, DlgMessageFormat.EggsML);
- }
- }
-
- private void SaveSettings()
- {
- _saveSettingsTimer.Stop();
- App.Settings.LeftColumnWidth = ctLeftColumn.Width.Value;
- App.Settings.NameColumnWidth = ctLayerProperties.NameColumnWidth;
- App.Settings.SaveThreaded();
- }
-
- private void SaveSettings(object _, EventArgs __)
- {
- SaveSettings();
- }
-
- private DispatcherTimer _saveSettingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5), IsEnabled = false };
-
- private void SaveSettingsDelayed(object _ = null, EventArgs __ = null)
- {
- _saveSettingsTimer.Stop();
- _saveSettingsTimer.Tick -= SaveSettings;
- _saveSettingsTimer.Tick += SaveSettings;
- _saveSettingsTimer.Start();
- }
-
- private void ctStyleDropdown_SelectionChanged(object sender = null, SelectionChangedEventArgs __ = null)
- {
- _renderResults.Clear();
- ctIconsPanel.Children.Clear();
- App.Settings.ActiveStyle = (Style) ctStyleDropdown.SelectedItem;
- ctUpvote.Visibility = App.Settings.ActiveStyle.Kind == StyleKind.BuiltIn ? Visibility.Visible : Visibility.Collapsed;
- SaveSettings();
- ctLayersTree.ItemsSource = App.Settings.ActiveStyle.Layers;
- ctPathTemplate.DataContext = App.Settings.ActiveStyle;
- if (App.Settings.ActiveStyle.Layers.Count > 0)
- {
- App.Settings.ActiveStyle.Layers[0].TreeViewItem.IsSelected = true;
- ctLayerProperties.SelectedObject = App.Settings.ActiveStyle.Layers[0];
- }
- else
- ctLayerProperties.SelectedObject = null;
- UpdateIcons();
- }
-
- private static bool isMedHighTier(WotTank t)
- {
- switch (t.Class)
- {
- case Class.Light: return t.Tier >= 4;
- case Class.Artillery: return t.Tier >= 5;
- case Class.Destroyer: return t.Tier >= 5;
- case Class.Medium: return t.Tier >= 6;
- case Class.Heavy: return t.Tier >= 6;
- default: return false;
- }
- }
-
- private static bool isHighTier(WotTank t)
- {
- switch (t.Class)
- {
- case Class.Light: return t.Tier >= 7;
- case Class.Artillery: return t.Tier >= 8;
- case Class.Destroyer: return t.Tier >= 8;
- case Class.Medium: return t.Tier >= 9;
- case Class.Heavy: return t.Tier >= 9;
- default: return false;
- }
- }
-
- /// <summary>
- /// Constructs a list of render tasks based on the current settings in the GUI. Will enumerate only some
- /// of the tanks if the user chose a smaller subset in the GUI.
- /// </summary>
- /// <param name="all">Forces the method to enumerate all tanks regardless of the GUI setting.</param>
- private static List<RenderTask> ListRenderTasks(WotContext context, Style style, bool all = false)
- {
- if (context.Tanks.Count == 0)
- return new List<RenderTask>(); // happens when there are no built-in data files
-
- IEnumerable<WotTank> selection = null;
- switch (all ? DisplayFilter.All : App.Settings.DisplayFilter)
- {
- case DisplayFilter.All: selection = context.Tanks; break;
- case DisplayFilter.OneOfEach:
- selection = context.Tanks.Select(t => new { t.Category, t.Class, t.Country }).Distinct()
- .SelectMany(p => SelectTiers(context.Tanks.Where(t => t.Category == p.Category && t.Class == p.Class && t.Country == p.Country)));
- break;
-
- case DisplayFilter.China: selection = context.Tanks.Where(t => t.Country == Country.China); break;
- case DisplayFilter.Czech: selection = context.Tanks.Where(t => t.Country == Country.Czech); break;
- case DisplayFilter.France: selection = context.Tanks.Where(t => t.Country == Country.France); break;
- case DisplayFilter.Germany: selection = context.Tanks.Where(t => t.Country == Country.Germany); break;
- case DisplayFilter.Japan: selection = context.Tanks.Where(t => t.Country == Country.Japan); break;
- case DisplayFilter.Sweden: selection = context.Tanks.Where(t => t.Country == Country.Sweden); break;
- case DisplayFilter.UK: selection = context.Tanks.Where(t => t.Country == Country.UK); break;
- case DisplayFilter.USA: selection = context.Tanks.Where(t => t.Country == Country.USA); break;
- case DisplayFilter.USSR: selection = context.Tanks.Where(t => t.Country == Country.USSR); break;
-
- case DisplayFilter.Light: selection = context.Tanks.Where(t => t.Class == Class.Light); break;
- case DisplayFilter.Medium: selection = context.Tanks.Where(t => t.Class == Class.Medium); break;
- case DisplayFilter.Heavy: selection = context.Tanks.Where(t => t.Class == Class.Heavy); break;
- case DisplayFilter.Artillery: selection = context.Tanks.Where(t => t.Class == Class.Artillery); break;
- case DisplayFilter.Destroyer: selection = context.Tanks.Where(t => t.Class == Class.Destroyer); break;
-
- case DisplayFilter.Normal: selection = context.Tanks.Where(t => t.Category == Category.Normal); break;
- case DisplayFilter.Premium: selection = context.Tanks.Where(t => t.Category == Category.Premium); break;
- case DisplayFilter.Special: selection = context.Tanks.Where(t => t.Category == Category.Special); break;
-
- case DisplayFilter.TierLow: selection = context.Tanks.Where(t => !isMedHighTier(t)); break;
- case DisplayFilter.TierMedHigh: selection = context.Tanks.Where(t => isMedHighTier(t)); break;
- case DisplayFilter.TierHigh: selection = context.Tanks.Where(t => isHighTier(t)); break;
- }
-
- return selection.OrderBy(t => t.Country).ThenBy(t => t.Class).ThenBy(t => t.Tier).ThenBy(t => t.Category).ThenBy(t => t.TankId)
- .Select(tank =>
- {
- var task = new RenderTask(style);
- task.TankId = tank.TankId;
- task.Tank = new Tank(
- tank,
- addWarning: task.AddWarning
- );
- return task;
- }).ToList();
- }
-
- /// <summary>
- /// Enumerates up to three tanks with tiers as different as possible. Ideally enumerates one tier 1, one tier 5 and one tier 10 tank.
- /// </summary>
- private static IEnumerable<WotTank> SelectTiers(IEnumerable<WotTank> tanks)
- {
- WotTank min = null;
- WotTank mid = null;
- WotTank max = null;
- foreach (var tank in tanks)
- {
- if (min == null || tank.Tier < min.Tier)
- min = tank;
- if (mid == null || Math.Abs(tank.Tier - 5) < Math.Abs(mid.Tier - 5))
- mid = tank;
- if (max == null || tank.Tier > max.Tier)
- max = tank;
- }
- if (Math.Abs((mid == null ? 999 : mid.Tier) - (min == null ? 999 : min.Tier)) < 3)
- mid = null;
- if (Math.Abs((mid == null ? 999 : mid.Tier) - (max == null ? 999 : max.Tier)) < 3)
- mid = null;
- if (Math.Abs((min == null ? 999 : min.Tier) - (max == null ? 999 : max.Tier)) < 5)
- max = null;
- if (min != null)
- yield return min;
- if (mid != null)
- yield return mid;
- if (max != null)
- yield return max;
- }
-
- private void ctGamePath_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- #pragma warning disable 0618 // ActiveInstallation should only be used for loading/saving the setting, which is what the code below does.
- App.Settings.ActiveInstallation = ctGamePath.SelectedItem as TimGameInstallation;
- #pragma warning restore 0618
-
- ReloadData();
- SaveSettings();
- }
-
- private void ctGamePath_PreviewKeyDown(object sender, KeyEventArgs e)
- {
- if (ctGamePath.IsKeyboardFocusWithin && ctGamePath.IsDropDownOpen && e.Key == Key.Delete)
- {
- RemoveGamePath();
- e.Handled = true;
- }
- }
-
- private void ctGameInstallationWarning_MouseDown(object sender, M…
Large files files are truncated, but you can click here to view the full file