diff --git a/ImageSrc/BlueChevron.xcf b/ImageSrc/BlueChevron.xcf new file mode 100644 index 0000000..c1732ab Binary files /dev/null and b/ImageSrc/BlueChevron.xcf differ diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index caa7be4..31799d8 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -2179,11 +2179,35 @@ namespace SourceGen { newSet = null; } if (curVisSet != newSet) { + ChangeSet cs = new ChangeSet(1); + // New table, edited in place, or deleted. UndoableChange uc = UndoableChange.CreateVisualizationSetChange(offset, curVisSet, newSet); //Debug.WriteLine("Change " + curVisSet + " to " + newSet); - ChangeSet cs = new ChangeSet(uc); + cs.Add(uc); + + // And now the messy bit. If Visualizations were removed, we need to purge + // them from any animations that reference them. The edit dialog took care + // of this for animations in the same set, but we need to check other sets. + foreach (KeyValuePair kvp in mProject.VisualizationSets) { + if (kvp.Value == curVisSet) { + continue; + } + + VisualizationSet stripSet; + if (VisualizationSet.StripEntriesFromAnimations(kvp.Value, dlg.RemovedSerials, + out stripSet)) { + if (stripSet.Count == 0) { + stripSet = null; + } + uc = UndoableChange.CreateVisualizationSetChange(kvp.Key, + kvp.Value, stripSet); + cs.Add(uc); + Debug.WriteLine("Also updating visSet at +" + kvp.Key.ToString("x6")); + } + } + ApplyUndoableChanges(cs); } else { Debug.WriteLine("No change to VisualizationSet"); diff --git a/SourceGen/Res/BlueChevron.png b/SourceGen/Res/BlueChevron.png new file mode 100644 index 0000000..4c41364 Binary files /dev/null and b/SourceGen/Res/BlueChevron.png differ diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index e0b3755..79bbca7 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -430,5 +430,8 @@ + + + \ No newline at end of file diff --git a/SourceGen/Visualization.cs b/SourceGen/Visualization.cs index d67db5b..c6c85db 100644 --- a/SourceGen/Visualization.cs +++ b/SourceGen/Visualization.cs @@ -72,12 +72,24 @@ namespace SourceGen { /// public BitmapSource CachedImage { get; set; } + /// + /// True if CachedImage has something other than the default value. + /// + public bool HasImage { + get { + return CachedImage != BROKEN_IMAGE && CachedImage != ANIM_IMAGE; + } + } + /// /// Image to show when things are broken. /// public static readonly BitmapImage BROKEN_IMAGE = new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png")); + internal static readonly BitmapImage ANIM_IMAGE = + new BitmapImage(new Uri("pack://application:,,,/Res/BlueChevron.png")); + /// /// Serial number, for reference from other Visualization objects. Not serialized. /// diff --git a/SourceGen/VisualizationAnimation.cs b/SourceGen/VisualizationAnimation.cs index 237a0ca..1d68383 100644 --- a/SourceGen/VisualizationAnimation.cs +++ b/SourceGen/VisualizationAnimation.cs @@ -18,6 +18,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; namespace SourceGen { /// @@ -27,6 +30,8 @@ namespace SourceGen { /// References to Visualization objects (such as a 3D mesh or list of bitmaps) are held /// here. The VisGenParams property holds animation properties, such as frame rate and /// view angles. + /// + /// As with the base class, instances are generally immutable for the benefit of undo/redo. /// public class VisualizationAnimation : Visualization { /// @@ -64,11 +69,67 @@ namespace SourceGen { /// Parameters for visualization generator. /// Serial numbers of referenced Visualizations. public VisualizationAnimation(string tag, string visGenIdent, - ReadOnlyDictionary visGenParams, List visSerialNumbers) - : base(tag, visGenIdent, visGenParams) { + ReadOnlyDictionary visGenParams, List visSerialNumbers, + VisualizationAnimation oldObj) + : base(tag, visGenIdent, visGenParams, oldObj) { Debug.Assert(visSerialNumbers != null); - mSerialNumbers = visSerialNumbers; + // Make a copy of the list. + mSerialNumbers = new List(visSerialNumbers.Count); + foreach (int serial in visSerialNumbers) { + mSerialNumbers.Add(serial); + } + + CachedImage = ANIM_IMAGE; // default to this + } + + /// + /// The number of Visualizations linked from this animation. + /// + public int SerialCount { + get { return mSerialNumbers.Count; } + } + + public void GenerateImage(SortedList visSets) { + const int IMAGE_SIZE = 64; + + CachedImage = ANIM_IMAGE; + + if (mSerialNumbers.Count == 0) { + return; + } + Visualization vis = VisualizationSet.FindVisualizationBySerial(visSets, + mSerialNumbers[0]); + if (vis == null) { + return; + } + + double maxDim = Math.Max(vis.CachedImage.Width, vis.CachedImage.Height); + double dimMult = IMAGE_SIZE / maxDim; + double adjWidth = vis.CachedImage.Width * dimMult; + double adjHeight = vis.CachedImage.Height * dimMult; + Rect imgBounds = new Rect((IMAGE_SIZE - adjWidth) / 2, (IMAGE_SIZE - adjHeight) / 2, + adjWidth, adjHeight); + + DrawingVisual visual = new DrawingVisual(); + //RenderOptions.SetBitmapScalingMode(visual, BitmapScalingMode.NearestNeighbor); + DrawingContext dc = visual.RenderOpen(); + dc.DrawImage(vis.CachedImage, imgBounds); + dc.DrawImage(ANIM_IMAGE, new Rect(0, 0, IMAGE_SIZE, IMAGE_SIZE)); + dc.Close(); + + RenderTargetBitmap bmp = new RenderTargetBitmap(IMAGE_SIZE, IMAGE_SIZE, 96.0, 96.0, + PixelFormats.Pbgra32); + bmp.Render(visual); + CachedImage = bmp; + Debug.WriteLine("RENDERED " + Tag); + } + + /// + /// Returns a list of serial numbers. The caller must not modify the list. + /// + public List GetSerialNumbers() { + return mSerialNumbers; } /// @@ -83,5 +144,74 @@ namespace SourceGen { } return false; } + + /// + /// Strips serial numbers out of the list. + /// + /// Object to strip serial numbers from. + /// List of serial numbers to remove. + /// Object with changes, or null if nothing changed. + /// True if something was actually removed. + public static bool StripEntries(VisualizationAnimation visAnim, List removedSerials, + out VisualizationAnimation newAnim) { + bool somethingRemoved = false; + + // Both sets should be small, so not worried about O(m*n). + List newSerials = new List(visAnim.mSerialNumbers.Count); + foreach (int serial in visAnim.mSerialNumbers) { + if (removedSerials.Contains(serial)) { + Debug.WriteLine("Removing serial #" + serial + " from " + visAnim.Tag); + somethingRemoved = true; + continue; + } + newSerials.Add(serial); + } + + if (somethingRemoved) { + newAnim = new VisualizationAnimation(visAnim.Tag, visAnim.VisGenIdent, + visAnim.VisGenParams, newSerials, visAnim); + } else { + newAnim = null; + } + return somethingRemoved; + } + + + public static bool operator ==(VisualizationAnimation a, VisualizationAnimation b) { + if (ReferenceEquals(a, b)) { + return true; // same object, or both null + } + if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) { + return false; // one is null + } + return a.Equals(b); + } + public static bool operator !=(VisualizationAnimation a, VisualizationAnimation b) { + return !(a == b); + } + public override bool Equals(object obj) { + if (!(obj is VisualizationAnimation)) { + return false; + } + // Do base-class equality comparison and the ReferenceEquals check. + if (!base.Equals(obj)) { + return false; + } + Debug.WriteLine("Detailed: this=" + Tag + " other=" + Tag); + VisualizationAnimation other = (VisualizationAnimation)obj; + if (other.mSerialNumbers.Count != mSerialNumbers.Count) { + return false; + } + for (int i = 0; i < mSerialNumbers.Count; i++) { + if (other.mSerialNumbers[i] != mSerialNumbers[i]) { + return false; + } + } + Debug.WriteLine(" All serial numbers match"); + return true; + } + public override int GetHashCode() { + return base.GetHashCode() ^ mSerialNumbers.Count; // weak + } } } diff --git a/SourceGen/VisualizationSet.cs b/SourceGen/VisualizationSet.cs index 999d9db..dd21125 100644 --- a/SourceGen/VisualizationSet.cs +++ b/SourceGen/VisualizationSet.cs @@ -86,6 +86,56 @@ namespace SourceGen { return arr; } + /// + /// Finds a Visualization by serial number. + /// + /// List of sets of visualizations. + /// Serial number to search for. + /// Matching Visualization, or null if not found. + public static Visualization FindVisualizationBySerial( + SortedList visSets, int serial) { + foreach (KeyValuePair kvp in visSets) { + VisualizationSet visSet = kvp.Value; + foreach (Visualization vis in visSet) { + if (vis.SerialNumber == serial) { + return vis; + } + } + } + return null; + } + + /// + /// Strips a set of Visualizations out of animations in the set. The set is not + /// modified; rather, a new set is generated with updated entries. + /// + /// Input set. + /// Serial numbers of removed items. + /// Updated set. + /// True if entries were removed. + public static bool StripEntriesFromAnimations(VisualizationSet visSet, + List removedSerials, out VisualizationSet newSet) { + bool somethingRemoved = false; + newSet = new VisualizationSet(visSet.Count); + foreach (Visualization vis in visSet) { + if (!(vis is VisualizationAnimation)) { + newSet.Add(vis); + continue; + } + + if (VisualizationAnimation.StripEntries((VisualizationAnimation) vis, + removedSerials, out VisualizationAnimation newAnim)) { + somethingRemoved = true; + if (newAnim.SerialCount != 0) { + newSet.Add(newAnim); + } else { + Debug.WriteLine("Deleting empty animation " + vis.Tag); + } + } + } + return somethingRemoved; + } + #region Image generation private class ScriptSupport : MarshalByRefObject, PluginCommon.IApplication { @@ -126,14 +176,20 @@ namespace SourceGen { ScriptSupport iapp = null; List plugins = null; - foreach (KeyValuePair kvp in project.VisualizationSets) { + SortedList visSets = project.VisualizationSets; + + foreach (KeyValuePair kvp in visSets) { VisualizationSet visSet = kvp.Value; foreach (Visualization vis in visSet) { - if (vis.CachedImage != Visualization.BROKEN_IMAGE) { + if (vis.HasImage) { continue; } //Debug.WriteLine("Vis needs refresh: " + vis.Tag); + if (vis is VisualizationAnimation) { + continue; + } + if (iapp == null) { // Prep the plugins on first need. iapp = new ScriptSupport(); @@ -171,6 +227,19 @@ namespace SourceGen { if (iapp != null) { project.UnprepareScripts(); } + + // Now that we've generated images for the Visualizations, update any + // VisualizationAnimation thumbnails that may have been affected. + foreach (KeyValuePair kvp in visSets) { + VisualizationSet visSet = kvp.Value; + foreach (Visualization vis in visSet) { + if (!(vis is VisualizationAnimation)) { + continue; + } + VisualizationAnimation visAnim = (VisualizationAnimation)vis; + visAnim.GenerateImage(visSets); + } + } } /// @@ -215,9 +284,9 @@ namespace SourceGen { if (a.mList.Count != b.mList.Count) { return false; } - // Order matters. + // Order matters. Use Equals() rather than == to get polymorphism. for (int i = 0; i < a.mList.Count; i++) { - if (a.mList[i] != b.mList[i]) { + if (!a.mList[i].Equals(b.mList[i])) { return false; } } diff --git a/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs b/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs index a4d5d66..586a0b8 100644 --- a/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs +++ b/SourceGen/WpfGui/EditBitmapAnimation.xaml.cs @@ -158,15 +158,27 @@ namespace SourceGen.WpfGui { } private void PopulateItemLists() { + // Add the animation's visualizations, in order. + if (mOrigAnim != null) { + foreach (int serial in mOrigAnim.GetSerialNumbers()) { + Visualization vis = VisualizationSet.FindVisualizationBySerial(mEditedList, + serial); + if (vis != null) { + VisAnimItems.Add(vis); + } else { + Debug.Assert(false); + } + } + } + + // Add all remaining non-animation Visualizations to the "source" set. foreach (KeyValuePair kvp in mEditedList) { foreach (Visualization vis in kvp.Value) { if (vis is VisualizationAnimation) { - // disallow animations with animations + // disallow using animations as animation frames continue; } - if (mOrigAnim != null && mOrigAnim.ContainsSerial(vis.SerialNumber)) { - VisAnimItems.Add(vis); - } else { + if (!VisAnimItems.Contains(vis)) { VisSourceItems.Add(vis); } } @@ -194,7 +206,7 @@ namespace SourceGen.WpfGui { } NewAnim = new VisualizationAnimation(TagString, VisualizationAnimation.ANIM_VIS_GEN, - new ReadOnlyDictionary(visGenParams), serials); + new ReadOnlyDictionary(visGenParams), serials, mOrigAnim); DialogResult = true; } diff --git a/SourceGen/WpfGui/EditVisualizationSet.xaml.cs b/SourceGen/WpfGui/EditVisualizationSet.xaml.cs index 7d9490c..9a087b0 100644 --- a/SourceGen/WpfGui/EditVisualizationSet.xaml.cs +++ b/SourceGen/WpfGui/EditVisualizationSet.xaml.cs @@ -36,11 +36,24 @@ namespace SourceGen.WpfGui { /// public VisualizationSet NewVisSet { get; private set; } + /// + /// List of Visualization serial numbers that were removed from the set. The caller + /// can use this to update animations in other sets that referred to the removed items. + /// + /// + /// We have to use serial numbers because the user might have edited the Visualization + /// before removing it. + /// + public List RemovedSerials { get; private set; } + private DisasmProject mProject; private Formatter mFormatter; private VisualizationSet mOrigSet; private int mOffset; + /// + /// ItemsSource for visualizationGrid. + /// public ObservableCollection VisualizationList { get; private set; } = new ObservableCollection(); @@ -99,6 +112,8 @@ namespace SourceGen.WpfGui { mOrigSet = curSet; mOffset = offset; + RemovedSerials = new List(); + if (curSet != null) { // Populate the data grid ItemsSource. foreach (Visualization vis in curSet) { @@ -222,6 +237,35 @@ namespace SourceGen.WpfGui { if (index >= 0) { visualizationGrid.SelectedIndex = index; } + + RemovedSerials.Add(item.SerialNumber); + + // Update any animations in this set. Animations in other sets will be updated later. + // (This is a bit awkward because we can't modify VisualizationList while iterating + // through it, and there's no simple "replace entry" operation on an observable + // collection. Fortunately we don't do this often and the data sets are small.) + List needsUpdate = new List(); + foreach (Visualization vis in VisualizationList) { + if (vis is VisualizationAnimation) { + VisualizationAnimation visAnim = (VisualizationAnimation)vis; + if (visAnim.ContainsSerial(item.SerialNumber)) { + needsUpdate.Add(visAnim); + } + } + } + foreach (VisualizationAnimation visAnim in needsUpdate) { + VisualizationAnimation newAnim; + if (VisualizationAnimation.StripEntries(visAnim, + new List(1) { item.SerialNumber }, out newAnim)) { + if (newAnim.SerialCount == 0) { + VisualizationList.Remove(visAnim); + } else { + index = VisualizationList.IndexOf(visAnim); + VisualizationList.Remove(visAnim); + VisualizationList.Insert(index, newAnim); + } + } + } } private void UpButton_Click(object sender, RoutedEventArgs e) {