<|start_filename|>Source/Plugins/EditorBase/EditorBasePluginSettings.cs<|end_filename|>
﻿using Duality.Editor.Plugins.Base.Forms;

namespace Duality.Editor.Plugins.Base
{
	/// <summary>
	/// Contains all user settings related to the editor base plugin.
	/// </summary>
	public class EditorBasePluginSettings
	{
		private PixmapSlicerSettings pixmapSlicer = new PixmapSlicerSettings();

		/// <summary>
		/// Settings related to the <see cref="PixmapSlicerForm"/> tool window.
		/// </summary>
		public PixmapSlicerSettings PixmapSlicer
		{
			get { return this.pixmapSlicer; }
			set { this.pixmapSlicer = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/ObjectInspector/Modules/ObjectInspectorSettings.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Xml.Linq;

namespace Duality.Editor.Plugins.ObjectInspector
{
	/// <summary>
	/// Contains all user settings for a single object inspector window.
	/// </summary>
	public class ObjectInspectorSettings
	{
		private bool     autoRefresh = true;
		private bool     locked      = false;
		private string   titleText   = "Object Inspector";
		private bool     debugMode   = false;
		private bool     sortByName  = true;
		private XElement expandState = null;

		/// <summary>
		/// Whether the inspector will automatically refresh its values periodically while the 
		/// Sandbox mode is active.
		/// </summary>
		public bool AutoRefresh
		{
			get { return this.autoRefresh; }
			set { this.autoRefresh = value; }
		}
		/// <summary>
		/// Whether the inspector window is locked to its current target objects. While true, the
		/// inspector will not change targets as global editor selection changes.
		/// </summary>
		public bool Locked
		{
			get { return this.locked; }
			set { this.locked = value; }
		}
		/// <summary>
		/// The window title of this inspector window.
		/// </summary>
		public string TitleText
		{
			get { return this.titleText; }
			set { this.titleText = value; }
		}
		/// <summary>
		/// Whether the inspector will show internal and private values.
		/// </summary>
		public bool DebugMode
		{
			get { return this.debugMode; }
			set { this.debugMode = value; }
		}
		/// <summary>
		/// Whether all displayed members will be sorted by name.
		/// </summary>
		public bool SortByName
		{
			get { return this.sortByName; }
			set { this.sortByName = value; }
		}
		/// <summary>
		/// Stores the last used expand / collapse state of all UI nodes, so it can be preserved
		/// between selection changes and editor restarts.
		/// </summary>
		public XElement ExpandState
		{
			get { return this.expandState; }
			set { this.expandState = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/LogView/Modules/LogView.Designer.cs<|end_filename|>
﻿namespace Duality.Editor.Plugins.LogView
{
	partial class LogView
	{
		/// <summary>
		/// Required designer variable.
		/// </summary>
		private System.ComponentModel.IContainer components = null;

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				components.Dispose();
			}
			base.Dispose(disposing);
		}

		#region Windows Form Designer generated code

		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{
			System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LogView));
			this.toolStrip = new System.Windows.Forms.ToolStrip();
			this.labelVisibility = new System.Windows.Forms.ToolStripLabel();
			this.buttonErrors = new System.Windows.Forms.ToolStripButton();
			this.buttonWarnings = new System.Windows.Forms.ToolStripButton();
			this.buttonMessages = new System.Windows.Forms.ToolStripButton();
			this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
			this.buttonCore = new System.Windows.Forms.ToolStripButton();
			this.buttonEditor = new System.Windows.Forms.ToolStripButton();
			this.buttonGame = new System.Windows.Forms.ToolStripButton();
			this.actionClear = new System.Windows.Forms.ToolStripSplitButton();
			this.checkAutoClear = new System.Windows.Forms.ToolStripMenuItem();
			this.buttonPauseOnError = new System.Windows.Forms.ToolStripButton();
			this.textBoxEntry = new System.Windows.Forms.TextBox();
			this.splitContainer = new System.Windows.Forms.SplitContainer();
			this.logEntryList = new Duality.Editor.Plugins.LogView.LogEntryList();
			this.toolStrip.SuspendLayout();
			((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
			this.splitContainer.Panel1.SuspendLayout();
			this.splitContainer.Panel2.SuspendLayout();
			this.splitContainer.SuspendLayout();
			this.SuspendLayout();
			// 
			// toolStrip
			// 
			this.toolStrip.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(212)))), ((int)(((byte)(212)))), ((int)(((byte)(212)))));
			this.toolStrip.GripMargin = new System.Windows.Forms.Padding(0);
			this.toolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
			this.toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.labelVisibility,
            this.buttonErrors,
            this.buttonWarnings,
            this.buttonMessages,
            this.toolStripSeparator1,
            this.buttonCore,
            this.buttonEditor,
            this.buttonGame,
            this.actionClear,
            this.buttonPauseOnError});
			this.toolStrip.Location = new System.Drawing.Point(0, 0);
			this.toolStrip.Name = "toolStrip";
			this.toolStrip.Size = new System.Drawing.Size(683, 25);
			this.toolStrip.TabIndex = 1;
			this.toolStrip.Text = "toolStrip";
			// 
			// labelVisibility
			// 
			this.labelVisibility.Name = "labelVisibility";
			this.labelVisibility.Size = new System.Drawing.Size(48, 22);
			this.labelVisibility.Text = "Display:";
			// 
			// buttonErrors
			// 
			this.buttonErrors.Checked = true;
			this.buttonErrors.CheckOnClick = true;
			this.buttonErrors.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonErrors.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonErrors.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_error;
			this.buttonErrors.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonErrors.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonErrors.Name = "buttonErrors";
			this.buttonErrors.Size = new System.Drawing.Size(23, 22);
			this.buttonErrors.Text = "Errors";
			this.buttonErrors.CheckedChanged += new System.EventHandler(this.buttonErrors_CheckedChanged);
			// 
			// buttonWarnings
			// 
			this.buttonWarnings.Checked = true;
			this.buttonWarnings.CheckOnClick = true;
			this.buttonWarnings.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonWarnings.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonWarnings.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_warning;
			this.buttonWarnings.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonWarnings.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonWarnings.Name = "buttonWarnings";
			this.buttonWarnings.Size = new System.Drawing.Size(23, 22);
			this.buttonWarnings.Text = "Warnings";
			this.buttonWarnings.CheckedChanged += new System.EventHandler(this.buttonWarnings_CheckedChanged);
			// 
			// buttonMessages
			// 
			this.buttonMessages.Checked = true;
			this.buttonMessages.CheckOnClick = true;
			this.buttonMessages.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonMessages.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonMessages.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_message;
			this.buttonMessages.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonMessages.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonMessages.Name = "buttonMessages";
			this.buttonMessages.Size = new System.Drawing.Size(23, 22);
			this.buttonMessages.Text = "Messages";
			this.buttonMessages.CheckedChanged += new System.EventHandler(this.buttonMessages_CheckedChanged);
			// 
			// toolStripSeparator1
			// 
			this.toolStripSeparator1.Name = "toolStripSeparator1";
			this.toolStripSeparator1.Size = new System.Drawing.Size(6, 25);
			// 
			// buttonCore
			// 
			this.buttonCore.Checked = true;
			this.buttonCore.CheckOnClick = true;
			this.buttonCore.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonCore.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonCore.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_core;
			this.buttonCore.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonCore.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonCore.Name = "buttonCore";
			this.buttonCore.Size = new System.Drawing.Size(23, 22);
			this.buttonCore.Text = "Core";
			this.buttonCore.CheckedChanged += new System.EventHandler(this.sourceFilterButton_CheckedChanged);
			// 
			// buttonEditor
			// 
			this.buttonEditor.Checked = true;
			this.buttonEditor.CheckOnClick = true;
			this.buttonEditor.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonEditor.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonEditor.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_editor;
			this.buttonEditor.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonEditor.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonEditor.Name = "buttonEditor";
			this.buttonEditor.Size = new System.Drawing.Size(23, 22);
			this.buttonEditor.Text = "Editor";
			this.buttonEditor.CheckedChanged += new System.EventHandler(this.sourceFilterButton_CheckedChanged);
			// 
			// buttonGame
			// 
			this.buttonGame.Checked = true;
			this.buttonGame.CheckOnClick = true;
			this.buttonGame.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonGame.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonGame.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.log_game;
			this.buttonGame.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonGame.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonGame.Name = "buttonGame";
			this.buttonGame.Size = new System.Drawing.Size(23, 22);
			this.buttonGame.Text = "Game";
			this.buttonGame.CheckedChanged += new System.EventHandler(this.sourceFilterButton_CheckedChanged);
			// 
			// actionClear
			// 
			this.actionClear.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
			this.actionClear.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.actionClear.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.checkAutoClear});
			this.actionClear.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.cross;
			this.actionClear.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.actionClear.Name = "actionClear";
			this.actionClear.Size = new System.Drawing.Size(32, 22);
			this.actionClear.Text = "Clear View";
			this.actionClear.ButtonClick += new System.EventHandler(this.actionClear_ButtonClick);
			// 
			// checkAutoClear
			// 
			this.checkAutoClear.Checked = true;
			this.checkAutoClear.CheckOnClick = true;
			this.checkAutoClear.CheckState = System.Windows.Forms.CheckState.Checked;
			this.checkAutoClear.Name = "checkAutoClear";
			this.checkAutoClear.Size = new System.Drawing.Size(143, 22);
			this.checkAutoClear.Text = "Clear on Play";
			this.checkAutoClear.CheckedChanged += new System.EventHandler(this.checkAutoClear_CheckedChanged);
			// 
			// buttonPauseOnError
			// 
			this.buttonPauseOnError.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
			this.buttonPauseOnError.Checked = true;
			this.buttonPauseOnError.CheckOnClick = true;
			this.buttonPauseOnError.CheckState = System.Windows.Forms.CheckState.Checked;
			this.buttonPauseOnError.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
			this.buttonPauseOnError.Image = global::Duality.Editor.Plugins.LogView.Properties.Resources.bell;
			this.buttonPauseOnError.ImageTransparentColor = System.Drawing.Color.Magenta;
			this.buttonPauseOnError.Margin = new System.Windows.Forms.Padding(1, 1, 1, 2);
			this.buttonPauseOnError.Name = "buttonPauseOnError";
			this.buttonPauseOnError.Size = new System.Drawing.Size(23, 22);
			this.buttonPauseOnError.Text = "Pause on Error";
			this.buttonPauseOnError.CheckedChanged += new System.EventHandler(this.buttonPauseOnError_CheckedChanged);
			// 
			// textBoxEntry
			// 
			this.textBoxEntry.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(92)))), ((int)(((byte)(92)))), ((int)(((byte)(92)))));
			this.textBoxEntry.BorderStyle = System.Windows.Forms.BorderStyle.None;
			this.textBoxEntry.Dock = System.Windows.Forms.DockStyle.Fill;
			this.textBoxEntry.Font = new System.Drawing.Font("Lucida Console", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
			this.textBoxEntry.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(212)))), ((int)(((byte)(212)))), ((int)(((byte)(212)))));
			this.textBoxEntry.Location = new System.Drawing.Point(0, 0);
			this.textBoxEntry.Multiline = true;
			this.textBoxEntry.Name = "textBoxEntry";
			this.textBoxEntry.ReadOnly = true;
			this.textBoxEntry.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
			this.textBoxEntry.Size = new System.Drawing.Size(681, 79);
			this.textBoxEntry.TabIndex = 3;
			this.textBoxEntry.WordWrap = false;
			this.textBoxEntry.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textBoxEntry_KeyDown);
			// 
			// splitContainer
			// 
			this.splitContainer.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(162)))), ((int)(((byte)(162)))), ((int)(((byte)(162)))));
			this.splitContainer.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
			this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
			this.splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
			this.splitContainer.Location = new System.Drawing.Point(0, 25);
			this.splitContainer.Name = "splitContainer";
			this.splitContainer.Orientation = System.Windows.Forms.Orientation.Horizontal;
			// 
			// splitContainer.Panel1
			// 
			this.splitContainer.Panel1.Controls.Add(this.logEntryList);
			this.splitContainer.Panel1MinSize = 50;
			// 
			// splitContainer.Panel2
			// 
			this.splitContainer.Panel2.Controls.Add(this.textBoxEntry);
			this.splitContainer.Panel2MinSize = 20;
			this.splitContainer.Size = new System.Drawing.Size(683, 170);
			this.splitContainer.SplitterDistance = 85;
			this.splitContainer.TabIndex = 4;
			// 
			// logEntryList
			// 
			this.logEntryList.AutoScroll = true;
			this.logEntryList.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(196)))), ((int)(((byte)(196)))), ((int)(((byte)(196)))));
			this.logEntryList.BaseColor = System.Drawing.Color.FromArgb(((int)(((byte)(162)))), ((int)(((byte)(162)))), ((int)(((byte)(162)))));
			this.logEntryList.Dock = System.Windows.Forms.DockStyle.Fill;
			this.logEntryList.Font = new System.Drawing.Font("Lucida Console", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
			this.logEntryList.Location = new System.Drawing.Point(0, 0);
			this.logEntryList.Name = "logEntryList";
			this.logEntryList.ScrollOffset = 0;
			this.logEntryList.SelectedEntry = null;
			this.logEntryList.Size = new System.Drawing.Size(681, 83);
			this.logEntryList.TabIndex = 2;
			this.logEntryList.SelectionChanged += new System.EventHandler(this.logEntryList_SelectionChanged);
			this.logEntryList.LogEntriesAdded += new System.EventHandler<Duality.Editor.Plugins.LogView.LogEntryList.ViewEntryEventArgs>(this.logEntryList_LogEntriesAdded);
			this.logEntryList.Enter += new System.EventHandler(this.logEntryList_Enter);
			// 
			// LogView
			// 
			this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
			this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
			this.ClientSize = new System.Drawing.Size(683, 195);
			this.Controls.Add(this.splitContainer);
			this.Controls.Add(this.toolStrip);
			this.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F);
			this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
			this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
			this.Name = "LogView";
			this.ShowHint = WeifenLuo.WinFormsUI.Docking.DockState.DockBottomAutoHide;
			this.Text = "Log View";
			this.toolStrip.ResumeLayout(false);
			this.toolStrip.PerformLayout();
			this.splitContainer.Panel1.ResumeLayout(false);
			this.splitContainer.Panel2.ResumeLayout(false);
			this.splitContainer.Panel2.PerformLayout();
			((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
			this.splitContainer.ResumeLayout(false);
			this.ResumeLayout(false);
			this.PerformLayout();

		}

		#endregion

		private System.Windows.Forms.ToolStrip toolStrip;
		private System.Windows.Forms.ToolStripButton buttonCore;
		private System.Windows.Forms.ToolStripButton buttonEditor;
		private System.Windows.Forms.ToolStripButton buttonGame;
		private System.Windows.Forms.ToolStripButton buttonMessages;
		private System.Windows.Forms.ToolStripButton buttonErrors;
		private System.Windows.Forms.ToolStripButton buttonWarnings;
		private System.Windows.Forms.ToolStripLabel labelVisibility;
		private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
		private System.Windows.Forms.ToolStripSplitButton actionClear;
		private System.Windows.Forms.ToolStripMenuItem checkAutoClear;
		private LogEntryList logEntryList;
		private System.Windows.Forms.ToolStripButton buttonPauseOnError;
		private System.Windows.Forms.TextBox textBoxEntry;
		private System.Windows.Forms.SplitContainer splitContainer;
	}
}

<|start_filename|>Test/Core/Drawing/CanvasTest.cs<|end_filename|>
﻿using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Diagnostics;

using Duality;
using Duality.Resources;
using Duality.Drawing;
using Duality.Tests.Properties;

using NUnit.Framework;

namespace Duality.Tests.Drawing
{
	[TestFixture(Category = TestCategory.Rendering)]
	public class CanvasTest
	{
		private Texture texCoordUV;

		[SetUp] public void FixtureSetup()
		{
			this.texCoordUV = new Texture(new Pixmap(BitmapToPixelData(TestRes.TexCoordUV)));
		}
		[TearDown] public void FixtureTearDown()
		{
			this.texCoordUV.Dispose();
			this.texCoordUV = null;
		}

		[Test] public void RenderFail()
		{
			// Perform the RedSquare test, but with a one pixel offset. This should fail.
			Assert.IsFalse(this.AreImagesEqual(TestRes.CanvasTestRedSquare, c => 
			{
				c.State.ColorTint = ColorRgba.Red;
				c.FillRect(1, 0, c.Width, c.Height);
			}));
		}
		[Test] public void RenderNothing()
		{
			this.TestImagesEqual(TestRes.CanvasTestNothing, c => {});
		}
		[Test] public void RenderRedSquare()
		{
			this.TestImagesEqual(TestRes.CanvasTestRedSquare, c => 
			{
				c.State.ColorTint = ColorRgba.Red;
				c.FillRect(0, 0, c.Width, c.Height);
			});
		}
		[Test] public void RenderDiagonalLine()
		{
			this.TestImagesEqual(TestRes.CanvasTestDiagonalLine, c => 
			{
				c.DrawLine(0, 0, c.Width, c.Height);
			});
		}
		[Test] public void RenderAllShapes()
		{
			this.TestImagesEqual(TestRes.CanvasTestAllShapes, c => 
			{
				// Background
				c.PushState();
				c.State.ColorTint = new ColorRgba(128, 192, 255);
				c.State.DepthOffset += 100.0f;
				c.FillRect(0, 0, c.Width, c.Height);
				c.PopState();

				// White shapes
				this.DrawTestImageRow(c, 100, 100);

				// Textured shapes
				c.State.SetMaterial(new BatchInfo(DrawTechnique.Mask, this.texCoordUV));
				this.DrawTestImageRow(c, 100, 300);
			});
		}
		[Test] public void RenderAllShapesTransformed()
		{
			this.TestImagesEqual(TestRes.CanvasTestAllShapesTransformed, c => 
			{
				// Background
				c.PushState();
				c.State.ColorTint = new ColorRgba(128, 192, 255);
				c.State.DepthOffset += 100.0f;
				c.FillRect(0, 0, c.Width, c.Height);
				c.PopState();

				c.State.TransformHandle = new Vector2(5, 5);
				c.State.TransformScale = new Vector2(0.75f, 0.75f);
				c.State.TransformAngle = MathF.RadAngle30;

				// White shapes
				this.DrawTestImageRow(c, 100, 100);

				// Textured shapes
				c.PushState();
				c.State.SetMaterial(new BatchInfo(DrawTechnique.Mask, this.texCoordUV));
				this.DrawTestImageRow(c, 100, 300);
				c.PopState();
			});
		}
		[Test] public void DrawImage()
		{
			this.TestImagesEqual(TestRes.CanvasTestDrawImage, c => 
			{
				// Background
				c.PushState();
				c.State.ColorTint = new ColorRgba(128, 192, 255);
				c.State.DepthOffset += 100.0f;
				c.FillRect(0, 0, c.Width, c.Height);
				c.PopState();

				// Draw a few test images
				c.State.SetMaterial(DrawTechnique.Alpha);
				c.DrawImage(Texture.Checkerboard, 10, 10);
				c.DrawImage(Texture.DualityLogoMedium, 10, 300);

				// Draw it with color modifications
				c.State.ColorTint = ColorRgba.Red;
				c.DrawImage(Texture.DualityLogoMedium, 300, 10);

				// Draw it with transformed vertices
				c.State.ColorTint = ColorRgba.White;
				c.State.TransformHandle = new Vector2(5, 5);
				c.State.TransformScale = new Vector2(1.5f, 0.75f);
				c.State.TransformAngle = MathF.RadAngle30;
				c.DrawImage(Texture.DualityLogoMedium, 300, 150);
			});
		}
		
		private void DrawTestImageRow(Canvas c, int baseX, int baseY)
		{
			Vector2[] polygon = new Vector2[]
			{ 
				new Vector2(0.0f, 0.0f), 
				new Vector2(50.0f, 0.0f), 
				new Vector2(50.0f, 45.0f), 
				new Vector2(5.0f, 50.0f), 
			};

			int x = baseX;
			int y = baseY;

			// Outline Shapes
			c.PushState();
			{
				c.DrawCircle(x, y, 25);
				x += 100;

				c.DrawCircleSegment(x, y, 25, 0.0f, MathF.RadAngle30 * 4, true);
				x += 100;

				c.DrawLine(x, y , x + 50, y + 25);
				c.DrawDashLine(x, y + 25, x + 50, y + 50);
				c.DrawThickLine(x, y + 50, x + 50, y + 75, 3);
				x += 100;

				c.DrawOval(x, y, 50, 50);
				x += 100;

				c.DrawOvalSegment(x, y, 50, 50, 0.0f, MathF.RadAngle30 * 4, true);
				x += 100;

				c.DrawPolygon(polygon, x, y);
				x += 100;

				c.DrawRect(x, y, 50, 50);
				x += 100;

				c.DrawText("<NAME>", x, y, drawBackground: true);
				x = baseX;
				y += 100;
			}
			c.PopState();

			// Filled Shapes
			c.PushState();
			{
				c.FillCircle(x, y, 25);
				x += 100;

				c.FillCircleSegment(x, y, 0, 25, MathF.RadAngle30 * 0, MathF.RadAngle30 * 4);
				c.FillCircleSegment(x, y, 0, 25, MathF.RadAngle30 * 5, MathF.RadAngle30 * 9, 10);
				x += 100;

				c.FillThickLine(x, y + 25, x + 50, y + 50, 3);
				x += 100;

				c.FillOval(x, y, 50, 50);
				x += 100;

				c.FillOvalSegment(x, y, 0, 50, 50, MathF.RadAngle30 * 0, MathF.RadAngle30 * 4);
				c.FillOvalSegment(x, y, 0, 50, 50, MathF.RadAngle30 * 5, MathF.RadAngle30 * 9, 10);
				x += 100;

				c.FillPolygon(polygon, x, y);
				x += 100;

				c.FillRect(x, y, 50, 50);
				x = baseX;
				y += 100;
			}
			c.PopState();
		}

		private void TestImagesEqual(Bitmap referenceImage, Action<Canvas> renderMethod)
		{
			PixelData image = this.RenderToTexture(referenceImage.Width, referenceImage.Height, renderMethod);
			PixelData reference = BitmapToPixelData(referenceImage);
			bool equal = this.AreImagesEqual(reference, image);
			if (!equal && Debugger.IsAttached)
			{
				// If the debugger is attached, create a nice diff image for the programmer to view.
				PixelData diff = this.CreateDiffImage(image, reference);
			}
			Assert.IsTrue(equal);
		}
		private bool AreImagesEqual(Bitmap referenceImage, Action<Canvas> renderMethod)
		{
			PixelData image = this.RenderToTexture(referenceImage.Width, referenceImage.Height, renderMethod);
			PixelData reference = BitmapToPixelData(referenceImage);
			return this.AreImagesEqual(reference, image);
		}
		private bool AreImagesEqual(PixelData first, PixelData second)
		{
			if (first == second) return true;
			if (first.Width != second.Width) return false;
			if (first.Height != second.Height) return false;

			ColorRgba[] firstData = first.Data;
			ColorRgba[] secondData = second.Data;
			float error = 0;
			float maxError = firstData.Length / 10.0f; // (1/255) off per ten pixels is probably okay.
			for (int i = 0; i < firstData.Length; i++)
			{
				error += MathF.Abs(firstData[i].R * (firstData[i].A / 255.0f) - secondData[i].R * (secondData[i].A / 255.0f));
				error += MathF.Abs(firstData[i].G * (firstData[i].A / 255.0f) - secondData[i].G * (secondData[i].A / 255.0f));
				error += MathF.Abs(firstData[i].B * (firstData[i].A / 255.0f) - secondData[i].B * (secondData[i].A / 255.0f));
				error += MathF.Abs(firstData[i].A - secondData[i].A);
				if (error >= maxError) return false;
			}

			return true;
		}
		
		private void UpdateReferenceImage(Expression<Func<Bitmap>> oldReferenceImageGetter, Action<Canvas> renderMethod)
		{
			var resourceMember = oldReferenceImageGetter.Body as MemberExpression;
			if (resourceMember == null) throw new ArgumentException("Only lambda methods of the form '() => Property' are accepted");

			string name = resourceMember.Member.Name;
			Bitmap oldRef = oldReferenceImageGetter.Compile()();

			this.CreateReferenceImage(name, oldRef.Width, oldRef.Height, renderMethod);
		}
		private void CreateReferenceImage(string name, int width, int height, Action<Canvas> renderMethod)
		{
			PixelData image = this.RenderToTexture(width, height, renderMethod);

			string formatId = ImageCodec.FormatPng;
			IImageCodec codec = ImageCodec.GetWrite(formatId);
			using (Stream stream = File.Open(TestHelper.GetEmbeddedResourcePath(name, ".png"), FileMode.Create))
			{
				codec.Write(stream, image, formatId);
			}
		}

		private PixelData CreateDiffImage(PixelData first, PixelData second)
		{
			if (first == second) return new PixelData(first.Width, first.Height);
			if (first.Width != second.Width) return new PixelData(1, 1);
			if (first.Height != second.Height) return new PixelData(1, 1);

			PixelData diff = new PixelData(first.Width, first.Height);
			ColorRgba[] firstData = first.Data;
			ColorRgba[] secondData = second.Data;
			ColorRgba[] diffData = diff.Data;
			for (int i = 0; i < firstData.Length; i++)
			{
				diffData[i].R = (byte)MathF.Abs(firstData[i].R - secondData[i].R);
				diffData[i].G = (byte)MathF.Abs(firstData[i].G - secondData[i].G);
				diffData[i].B = (byte)MathF.Abs(firstData[i].B - secondData[i].B);
				diffData[i].A = (byte)MathF.Abs(firstData[i].A - secondData[i].A);
			}

			return diff;
		}
		private PixelData RenderToTexture(int width, int height, Action<Canvas> renderMethod)
		{
			PixelData pixelData;

			using (Texture texture = new Texture(width, height, TextureSizeMode.NonPowerOfTwo, TextureMagFilter.Nearest, TextureMinFilter.Nearest))
			using (RenderTarget renderTarget = new RenderTarget(AAQuality.Off, true, texture))
			using (DrawDevice device = new DrawDevice())
			{
				device.Projection = ProjectionMode.Screen;
				device.VisibilityMask = VisibilityFlag.AllGroups | VisibilityFlag.ScreenOverlay;
				device.Target = renderTarget;
				device.TargetSize = renderTarget.Size;
				device.ViewportRect = new Rect(renderTarget.Size);

				device.PrepareForDrawcalls();
				{
					Canvas canvas = new Canvas();
					canvas.Begin(device);
					renderMethod(canvas);
					canvas.End();
				}
				device.Render();

				pixelData = texture.GetPixelData();
			}

			return pixelData;
		}
		
		private static PixelData BitmapToPixelData(Bitmap bitmap)
		{
			PixelData pixelData = new PixelData();
			pixelData.FromBitmap(bitmap);
			return pixelData;
		}
	}
}


<|start_filename|>Source/Editor/DualityEditor/Utility/PluginSettings.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Duality.Resources;

namespace Duality.Editor
{
	/// <summary>
	/// Container class for <see cref="DualityEditorUserData"/> that is defined by editor plugins.
	/// </summary>
	public class PluginSettings
	{
		private XElement oldStyleSettings = XElement.Parse("<Plugins></Plugins>");
		private List<object> data = new List<object>();

		/// <summary>
		/// Serialized XML blob that provides access to user data that is still serialized manually, as opposed to
		/// the new way via Duality serializers.
		/// </summary>
		[Obsolete("Use the model based api")]
		public XElement OldStyleSettings
		{
			get { return this.oldStyleSettings; }
			set { this.oldStyleSettings = value; }
		}

		/// <summary>
		/// Returns existing editor plugin settings of type <typeparamref name="T"/>, or creates and registers a new 
		/// instance if none existed yet.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <returns></returns>
		public T Get<T>() where T : class, new()
		{
			T setting = this.data.OfType<T>().FirstOrDefault();
			if (setting == null)
			{
				setting = new T();
				this.data.Add(setting);
			}

			return setting;
		}
		/// <summary>
		/// Adds or replaces the stored editor plugin settings instance of type <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="settings"></param>
		public void Set<T>(T settings) where T : class
		{
			this.data.RemoveAll(obj => obj is T);
			this.data.Add(settings);
		}
		/// <summary>
		/// Clears all editor plugin settings from this container, and resets it to an empty state.
		/// </summary>
		internal void Clear()
		{
			this.data.Clear();
			this.oldStyleSettings = XElement.Parse("<Plugins></Plugins>");
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/ObjectInspector/ObjectInspectorPlugin.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Windows.Forms;

using Duality.Editor.Forms;
using Duality.Editor.Properties;
using Duality.Editor.Plugins.ObjectInspector.Properties;

using WeifenLuo.WinFormsUI.Docking;

using AdamsLair.WinForms.ItemModels;


namespace Duality.Editor.Plugins.ObjectInspector
{
	public class ObjectInspectorPlugin : EditorPlugin
	{
		private static ObjectInspectorPlugin instance = null;
		internal static ObjectInspectorPlugin Instance
		{
			get { return instance; }
		}


		private List<ObjectInspector>   objViews     = new List<ObjectInspector>();
		private bool                    isLoading    = false;
		private ObjectInspectorPluginSettings userSettings = new ObjectInspectorPluginSettings();


		public override string Id
		{
			get { return "ObjectInspector"; }
		}
		public IEnumerable<ObjectInspector>	ObjViews
		{
			get { return this.objViews; }
		}


		public ObjectInspectorPlugin()
		{
			instance = this;
		}

		public ObjectInspector RequestObjView(bool dontShow = false)
		{
			ObjectInspector objView = new ObjectInspector(this.objViews.Count);
			this.objViews.Add(objView);
			objView.FormClosed += delegate (object sender, FormClosedEventArgs e) { this.objViews.Remove(sender as ObjectInspector); };

			if (!this.isLoading && !dontShow)
			{
				objView.Show(DualityEditorApp.MainForm.MainDockPanel);
				if (objView.Pane != null)
				{
					objView.Pane.Activate();
					objView.Focus();
				}
			}
			return objView;
		}

		protected override IDockContent DeserializeDockContent(Type dockContentType)
		{
			this.isLoading = true;
			IDockContent result;
			if (dockContentType == typeof(ObjectInspector))
				result = this.RequestObjView();
			else
				result = base.DeserializeDockContent(dockContentType);
			this.isLoading = false;
			return result;
		}
		protected override void InitPlugin(MainForm main)
		{
			base.InitPlugin(main);

			// Request menu
			MenuModelItem viewItem = main.MainMenu.RequestItem(GeneralRes.MenuName_View);
			viewItem.AddItem(new MenuModelItem
			{
				Name = ObjectInspectorRes.MenuItemName_ObjView,
				Icon = ObjectInspectorResCache.IconObjView.ToBitmap(),
				ActionHandler = this.menuItemObjView_Click
			});
		}

		protected override void SaveUserData(PluginSettings pluginSettings)
		{
			this.userSettings.InspectorSettingsById.Clear();
			foreach (ObjectInspector inspector in this.objViews)
			{
				inspector.SaveGridExpandState();
				this.userSettings.InspectorSettingsById[inspector.Id] = inspector.UserSettings;
			}
			pluginSettings.Set(this.userSettings);
		}
		protected override void LoadUserData(PluginSettings pluginSettings)
		{
			this.isLoading = true;
			this.userSettings = pluginSettings.Get<ObjectInspectorPluginSettings>();
			foreach (var pair in this.userSettings.InspectorSettingsById)
			{
				if (pair.Key < 0 || pair.Key >= this.objViews.Count) continue;
				this.objViews[pair.Key].UserSettings = pair.Value;
				this.objViews[pair.Key].ApplyUserSettings();
			}
			this.isLoading = false;
		}

		private void menuItemObjView_Click(object sender, EventArgs e)
		{
			ObjectInspector objView = this.RequestObjView();
		}
	}
}


<|start_filename|>Source/DualityTemplates/Templates/SolutionTemplate/Source/Launchers/DualityGame/Program.cs<|end_filename|>
﻿using System;
using Duality.Launcher;

namespace DualityGame
{
	internal static class Program
	{
		[STAThread]
		public static void Main(string[] args)
		{
			LauncherArgs launcherArgs = new LauncherArgs(args);

			if (launcherArgs.IsDebugging|| launcherArgs.IsRunFromEditor)
				ShowConsole();

			using (DualityLauncher launcher = new DualityLauncher(launcherArgs))
			{
				launcher.Run();
			}
		}

		private static bool hasConsole = false;
		private static void ShowConsole()
		{
			if (hasConsole) return;
			SafeNativeMethods.AllocConsole();
			hasConsole = true;
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/ObjectInspector/Modules/ObjectInspector.cs<|end_filename|>
﻿using System;
using System.Drawing;
using System.Linq;
using System.Xml.Linq;
using System.Windows.Forms;

using WeifenLuo.WinFormsUI.Docking;
using AdamsLair.WinForms.PropertyEditing;

using Duality.Resources;
using Duality.Editor.AssetManagement;

namespace Duality.Editor.Plugins.ObjectInspector
{
	public partial class ObjectInspector : DockContent
	{
		private int                      runtimeId           = 0;
		private TimeSpan                 lastRefreshTime     = TimeSpan.MinValue;
		private TimeSpan                 lastRefreshGameTime = TimeSpan.MinValue;
		private ObjectSelection.Category selSchedMouseCat    = ObjectSelection.Category.None;
		private ObjectSelection          selSchedMouse       = null;
		private ObjectSelection          displaySel          = null;
		private ObjectSelection.Category displayCat          = ObjectSelection.Category.None;

		private ExpandState              gridExpandState     = new ExpandState();
		private ObjectInspectorSettings  userSettings        = new ObjectInspectorSettings();
		

		public int Id
		{
			get { return this.runtimeId; }
		}
		public ObjectInspectorSettings UserSettings
		{
			get { return this.userSettings; }
			set { this.userSettings = value; }
		}
		public bool LockedSelection
		{
			get { return this.buttonLock.Checked; }
			set { this.buttonLock.Checked = value; }
		}
		public ObjectSelection DisplayedSelection
		{
			get { return (this.timerSelectSched.Enabled || this.selSchedMouse != null) ? this.selSchedMouse : this.displaySel; }
			set { this.UpdateSelection(value, value.Categories); }
		}
		public ObjectSelection.Category DisplayedCategory
		{
			get { return (this.timerSelectSched.Enabled || this.selSchedMouse != null) ? this.selSchedMouseCat : this.displayCat; }
		}
		public bool EmptySelection
		{
			get { return (this.displaySel == null || this.displaySel.Empty) && (!this.timerSelectSched.Enabled || this.selSchedMouse == null); }
		}


		public ObjectInspector(int runtimeId)
		{
			this.InitializeComponent();
			this.runtimeId = runtimeId;
			this.toolStrip.Renderer = new Duality.Editor.Controls.ToolStrip.DualitorToolStripProfessionalRenderer();
		}
		
		public void ApplyUserSettings()
		{
			this.buttonAutoRefresh.Checked = this.userSettings.AutoRefresh;
			this.buttonLock.Checked = this.userSettings.Locked;
			this.buttonDebug.Checked = this.userSettings.DebugMode;
			this.buttonSortByName.Checked = this.userSettings.SortByName;
			this.Text = this.userSettings.TitleText;

			if (this.userSettings.ExpandState != null)
			{
				this.gridExpandState.LoadFromXml(this.userSettings.ExpandState);
			}
		}
		internal void SaveGridExpandState()
		{
			// gridExpandState is normally only updated when the current selection changes.
			// Make sure we have the latest information when saving UserData.
			this.gridExpandState.UpdateFrom(this.propertyGrid.MainEditor);

			XElement expandStateNode = new XElement("ExpandState");
			this.gridExpandState.SaveToXml(expandStateNode);
			this.userSettings.ExpandState = expandStateNode;
		}
		
		public void CopyTo(ObjectInspector other)
		{
			this.gridExpandState.UpdateFrom(this.propertyGrid.MainEditor);

			other.buttonAutoRefresh.Checked = this.buttonAutoRefresh.Checked;
			other.buttonLock.Checked = this.buttonLock.Checked;
			other.buttonDebug.Checked = this.buttonDebug.Checked;
			other.buttonSortByName.Checked = this.buttonSortByName.Checked;

			this.gridExpandState.CopyTo(other.gridExpandState);
		}

		public bool AcceptsSelection(ObjectSelection sel)
		{
			if (this.LockedSelection) return false;
			return true;
		}

		protected override void OnShown(EventArgs e)
		{
			base.OnShown(e);

			this.ApplyUserSettings();
			this.UpdateButtons();

			// Add the global selection event once
			DualityEditorApp.SelectionChanged -= GlobalUpdateSelection;
			DualityEditorApp.SelectionChanged += GlobalUpdateSelection;

			DualityEditorApp.UpdatingEngine += this.DualityEditorApp_AfterUpdateDualityApp;
			DualityEditorApp.ObjectPropertyChanged += this.DualityEditorApp_ObjectPropertyChanged;
			DualityEditorApp.Terminating += this.DualityEditorApp_Terminating;
			AssetManager.ImportFinished += this.AssetManager_ImportFinished;

			// Select something initially, if not done yet
			if (this.propertyGrid.Selection.Count() == 0)
				this.UpdateSelection(DualityEditorApp.Selection, DualityEditorApp.SelectionActiveCategory);
		}
		protected override void OnClosed(EventArgs e)
		{
			base.OnClosed(e);

			DualityEditorApp.UpdatingEngine -= this.DualityEditorApp_AfterUpdateDualityApp;
			DualityEditorApp.ObjectPropertyChanged -= this.DualityEditorApp_ObjectPropertyChanged;
			DualityEditorApp.Terminating -= this.DualityEditorApp_Terminating;
			AssetManager.ImportFinished -= this.AssetManager_ImportFinished;
		}
		protected override void OnGotFocus(EventArgs e)
		{
			base.OnGotFocus(e);
			this.propertyGrid.Focus();
		}

		private void UpdateButtons()
		{
			this.buttonClone.Enabled = this.propertyGrid.Selection.Any();
			this.buttonLock.Enabled = true;
		}
		private void UpdateSelection(ObjectSelection sel, ObjectSelection.Category showCat)
		{
			this.selSchedMouse = null;
			this.selSchedMouseCat = ObjectSelection.Category.None;
			this.displaySel = sel;
			this.displayCat = showCat;

			if (showCat == ObjectSelection.Category.None) return;
			this.gridExpandState.UpdateFrom(this.propertyGrid.MainEditor);

			// Selection update may change MainEditor, unsubscribe from events
			if (this.propertyGrid.MainEditor is GroupedPropertyEditor)
			{
				GroupedPropertyEditor groupedMainEditor = this.propertyGrid.MainEditor as GroupedPropertyEditor;
				groupedMainEditor.EditorAdded -= this.MainEditor_EditorAdded;
			}

			if ((showCat & ObjectSelection.Category.GameObjCmp) != ObjectSelection.Category.None)
			{
				this.displaySel = new ObjectSelection(sel.GameObjects.Union(sel.Components.GameObject()));
				this.displayCat = ObjectSelection.Category.GameObjCmp;
				this.propertyGrid.SelectObjects(this.displaySel, false);
			}
			else if ((showCat & ObjectSelection.Category.Resource) != ObjectSelection.Category.None)
			{
				this.displaySel = new ObjectSelection(sel.Resources);
				this.displayCat = ObjectSelection.Category.Resource;
				this.propertyGrid.SelectObjects(this.displaySel, this.displaySel.Resources.Any(r => r.IsDefaultContent));
			}
			else if ((showCat & ObjectSelection.Category.Other) != ObjectSelection.Category.None)
			{
				this.displaySel = new ObjectSelection(sel.OtherObjects);
				this.displayCat = ObjectSelection.Category.Other;
				this.propertyGrid.SelectObjects(this.displaySel, false);
			}

			if (this.propertyGrid.MainEditor is GroupedPropertyEditor)
			{
				GroupedPropertyEditor groupedMainEditor = this.propertyGrid.MainEditor as GroupedPropertyEditor;
				groupedMainEditor.EditorAdded += this.MainEditor_EditorAdded;
			}

			this.gridExpandState.ApplyTo(this.propertyGrid.MainEditor);
			this.buttonClone.Enabled = this.propertyGrid.Selection.Any();
		}
		private void UpdateDisplayedValues(bool forceFullUpdate)
		{
			if (forceFullUpdate)
			{
				// Force a full update by re-selecting internally
				object[] obj = this.propertyGrid.Selection.ToArray();
				this.propertyGrid.SelectObject(null);
				this.propertyGrid.SelectObjects(obj, false, 100);
			}
			else
			{
				// A (selective) regular update - deferred to make sure we don't do it redundantly
				this.propertyGrid.UpdateFromObjects(100);
			}
		}

		private void MainEditor_EditorAdded(object sender, PropertyEditorEventArgs e)
		{
			// Make sure new editors start in the correct expand state
			GroupedPropertyEditor groupedEditor = e.Editor as GroupedPropertyEditor;
			if (groupedEditor != null)
				groupedEditor.Expanded = this.gridExpandState.IsEditorExpanded(groupedEditor);
		}
		private void DualityEditorApp_AfterUpdateDualityApp(object sender, EventArgs e)
		{
			// Perform auto-refresh as the game state changes
			if (this.buttonAutoRefresh.Checked)
			{
				// Continuous updates during play mode
				if (Sandbox.State == SandboxState.Playing)
				{
					// Sandbox was restarted - MainTimer got reset
					if (this.lastRefreshTime > Time.MainTimer)
						this.lastRefreshTime = Time.MainTimer;

					if ((Time.MainTimer - this.lastRefreshTime).TotalMilliseconds > 100.0d)
					{
						this.lastRefreshTime = Time.MainTimer;
						this.lastRefreshGameTime = Time.GameTimer;
						this.propertyGrid.UpdateFromObjects();
					}
				}
				// Distinct updates on simulation time changes / single-step
				else if (Sandbox.State == SandboxState.Paused)
				{
					if (this.lastRefreshGameTime != Time.GameTimer)
					{
						this.lastRefreshTime = Time.MainTimer;
						this.lastRefreshGameTime = Time.GameTimer;
						this.propertyGrid.UpdateFromObjects();
					}
				}
			}
		}
		private void DualityEditorApp_ObjectPropertyChanged(object sender, ObjectPropertyChangedEventArgs e)
		{
			if (!(e is PrefabAppliedEventArgs) && (sender is PropertyEditor) && (sender as PropertyEditor).ParentGrid == this.propertyGrid) return;
			if (!(e is PrefabAppliedEventArgs) && sender == this.propertyGrid) return;

			// Update values if anything changed that relates to the grids current selection
			if (e.Objects.Components.GameObject().Any(o => this.propertyGrid.Selection.Contains(o)) ||
				e.Objects.Any(o => this.propertyGrid.Selection.Contains(o)) ||
				(e.Objects.Contains(Scene.Current) && this.propertyGrid.Selection.Any(o => o is GameObject || o is Component)))
				this.propertyGrid.UpdateFromObjects(100);
		}
		private void AssetManager_ImportFinished(object sender, AssetImportFinishedEventArgs e)
		{
			if (!e.IsSuccessful) return;
			if (!e.IsReImport) return;

			// Force updating all potentially generated previews when we display a resource that was modified externally
			bool forceFullUpdate = false;
			foreach (AssetImportOutput item in e.Output)
			{
				if (this.propertyGrid.Selection.Contains(item.Resource))
				{
					forceFullUpdate = true;
					break;
				}
			}

			this.UpdateDisplayedValues(forceFullUpdate);
		}
		private void DualityEditorApp_Terminating(object sender, EventArgs e)
		{
			if (this.runtimeId == -1) this.Close();
		}
		private static void GlobalUpdateSelection(object sender, SelectionChangedEventArgs e)
		{
			ObjectInspector target = null;
			
			foreach (ObjectSelection.Category cat in ObjectSelection.EnumerateCategories(e.AffectedCategories))
			{
				var objViews = 
					from v in ObjectInspectorPlugin.Instance.ObjViews
					where v.AcceptsSelection(e.Current)
					select new { 
						View = v, 
						Empty = v.EmptySelection,
						PerfectFit = v.EmptySelection || (cat & v.DisplayedCategory) != ObjectSelection.Category.None,
						TypeShare = ObjectSelection.GetTypeShareLevel(e.Current.Exclusive(cat), v.DisplayedSelection),
						NumSameCatViews = ObjectInspectorPlugin.Instance.ObjViews.Count(o => o.AcceptsSelection(e.Current) && o.DisplayedCategory == v.DisplayedCategory) };
				var sortedQuery = 
					from o in objViews
					orderby o.PerfectFit descending, o.Empty ascending, o.NumSameCatViews descending, o.TypeShare ascending
					select o;
				var targetItem = sortedQuery.FirstOrDefault();
				if (targetItem == null) return;
				target = targetItem.View;

				// If a mouse button is pressed, reschedule the selection for later - there might be a drag in progress
				if (Control.MouseButtons != System.Windows.Forms.MouseButtons.None)
				{
					target.selSchedMouse = e.Current;
					target.selSchedMouseCat = cat;
					target.timerSelectSched.Enabled = true;
				}
				else
				{
					target.UpdateSelection(e.Current, cat);
				}
			}
			
			//  Make sure disposed objects are deselected in non-target views (locked, etc.)
			foreach (ObjectInspector v in ObjectInspectorPlugin.Instance.ObjViews)
			{
				if (v.EmptySelection) continue;
				if (v == target) continue;

				var disposedObj = e.Removed.OfType<IManageableObject>().Where(o => o.Disposed);
				if (disposedObj.Any())
				{
					ObjectSelection disposedSel = new ObjectSelection(disposedObj);
					v.UpdateSelection(v.DisplayedSelection - disposedSel, v.DisplayedCategory);
				}
			}
		}

		private void timerSelectSched_Tick(object sender, EventArgs e)
		{
			if (this.selSchedMouse == null)
			{
				this.timerSelectSched.Enabled = false;
			}
			else if (Control.MouseButtons == System.Windows.Forms.MouseButtons.None)
			{
				// If no more mouse buttons are currently pressed, process previously scheduled selection change...
				// .. but only if the cursor isn't located on this Control. That might mean something has been dragged here.
				Point curRelPos = this.PointToClient(Cursor.Position);
				if (curRelPos.X < 0 || curRelPos.Y < 0 || curRelPos.X > this.Bounds.Width || curRelPos.Y > this.Bounds.Height)
				{
					this.UpdateSelection(this.selSchedMouse, this.selSchedMouseCat);
				}
				this.timerSelectSched.Enabled = false;
			}
		}
		private void buttonClone_Click(object sender, EventArgs e)
		{
			ObjectInspector objView = ObjectInspectorPlugin.Instance.RequestObjView(true);
			this.CopyTo(objView);
			objView.buttonLock.Checked = true;

			DockPanel mainDoc = DualityEditorApp.MainForm.MainDockPanel;
			if (this.DockHandler.DockState.IsAutoHide())
				objView.Show(this.DockPanel, this.DockHandler.DockState);
			else
				objView.Show(this.DockHandler.Pane, DockAlignment.Bottom, 0.5d);
			
			// Need it before showing because of instant-selection
			objView.propertyGrid.SelectObjects(this.propertyGrid.Selection);
			objView.gridExpandState.ApplyTo(objView.propertyGrid.MainEditor);
		}
		private void buttonAutoRefresh_CheckedChanged(object sender, EventArgs e)
		{
			this.UserSettings.AutoRefresh = this.buttonAutoRefresh.Checked;
			if (this.buttonAutoRefresh.Checked)
			{
				this.lastRefreshTime = Time.MainTimer;
				this.lastRefreshGameTime = Time.GameTimer;
				this.propertyGrid.UpdateFromObjects(100);
			}
		}
		private void buttonDebug_CheckedChanged(object sender, EventArgs e)
		{
			this.UserSettings.DebugMode = this.buttonDebug.Checked;
			this.propertyGrid.ShowNonPublic = this.buttonDebug.Checked;
		}
		private void buttonSortByName_CheckedChanged(object sender, EventArgs e)
		{
			this.UserSettings.SortByName = this.buttonSortByName.Checked;
			this.propertyGrid.SortEditorsByName = this.buttonSortByName.Checked;
		}
		private void buttonLock_CheckedChanged(object sender, EventArgs e)
		{
			this.UserSettings.Locked = this.buttonLock.Checked;
		}
	}
}


<|start_filename|>Source/Core/Duality/Utility/SettingsContainer.cs<|end_filename|>
﻿using System;
using Duality.Serialization;

namespace Duality
{
	/// <summary>
	/// A container class to abstract all serialization logic away from settings classes.
	/// </summary>
	/// <typeparam name="TSettings"></typeparam>
	public class SettingsContainer<TSettings> : ISettingsContainer where TSettings : class, new()
	{
		private Func<TSettings> loadDelegate = null;
		private Action<TSettings> saveDelegate = null;


		/// <summary>
		/// The path of the file where the settings are loaded from and saved persistently.
		/// </summary>
		public string Path { get; }
		/// <summary>
		/// The settings data. Will equal null until <see cref="Load"/> is called for the first time, 
		/// after which it can never be null again.
		/// </summary>
		public TSettings Instance { get; private set; }


		/// <summary>
		/// Fired when the settings <see cref="Instance"/> is being applied after loading or modification.
		/// </summary>
		public event EventHandler Applying;
		/// <summary>
		/// Fired when the settings <see cref="Instance"/> is about to be saved, allowing subscribers to
		/// extend it with previously missing or updated information.
		/// </summary>
		public event EventHandler Saving;


		/// <summary>
		/// Creates a new settings container where the data for the settings will be saved and loaded 
		/// from <paramref name="path"/>.
		/// </summary>
		/// <param name="path"></param>
		public SettingsContainer(string path)
		{
			this.Path = path;
		}
		/// <summary>
		/// Creates a new settings container with specialized load and save routines.
		/// </summary>
		/// <param name="path"></param>
		/// <param name="loadDelegate"></param>
		/// <param name="saveDelegate"></param>
		public SettingsContainer(string path, Func<TSettings> loadDelegate, Action<TSettings> saveDelegate)
		{
			this.Path = path;
			this.loadDelegate = loadDelegate;
			this.saveDelegate = saveDelegate;
		}

		/// <summary>
		/// Loads the data of <typeparamref name="TSettings"/> from file.
		/// </summary>
		public void Load()
		{
			if (this.loadDelegate != null)
			{
				this.Instance = this.loadDelegate() ?? new TSettings();
			}
			else
			{
				this.Instance = Serializer.TryReadObject<TSettings>(this.Path, typeof(XmlSerializer)) ?? new TSettings();
			}
			this.Applying?.Invoke(this, EventArgs.Empty);
		}
		/// <summary>
		/// Saves the data of <typeparamref name="TSettings"/> to file.
		/// </summary>
		public void Save()
		{
			this.Saving?.Invoke(this, EventArgs.Empty);
			if (this.saveDelegate != null)
			{
				this.saveDelegate(this.Instance);
			}
			else
			{
				Serializer.WriteObject(this.Instance, this.Path, typeof(XmlSerializer));
			}
		}
		/// <summary>
		/// Ensures that any modifications to the settings <see cref="Instance"/> are applied to 
		/// all systems relying on them. 
		/// 
		/// Note that trivial settings values such as audio volume or similar may be applied directly
		/// as values are changed even before <see cref="Apply"/> is called.
		/// </summary>
		public void Apply()
		{
			this.Applying?.Invoke(this, EventArgs.Empty);
		}
	}
}


<|start_filename|>Source/Editor/DualityEditor/Forms/PublishGameDialog.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.IO.Compression;

using Duality.IO;

using Duality.Editor.Properties;

namespace Duality.Editor.Forms
{
	public partial class PublishGameDialog : Form
	{
		private static readonly string[] TemporaryProjectFiles = new string[] {
			@"*\.git",
			@"*\.svn",
			@".\Backup",
			@".\Temp",
			$@".\{EditorHelper.ImportDirectory}",
			@".\logfile*",
			@".\perflog*",
			@"*.vshost.*",
			@".\OpenAL32.dll" };
		private static readonly string[] SourcePathBlacklist = new string[] {
			$@".\{EditorHelper.SourceDirectory}",
			@"*.pdb",
			@"*.sln",
			@".\FarseerDuality.xml",
			@".\NVorbis.xml" };
		private static readonly string[] EditorPathBlacklist = new string[] {
			@".\Plugins\*.xml",
			@".\Plugins\*.editor.dll",
			@".\DualityEditor.*",
			@".\Duality.xml",
			@".\DualityPrimitives.xml",
			@".\DualityPhysics.xml",
			@".\DDoc.chm",
			@".\" + DualityEditorApp.UserData.Path,
			@".\AdamsLair.WinForms.*",
			@".\Aga.Controls.*",
			@".\VistaBridgeLibrary.*",
			@".\WeifenLuo.WinFormsUI.Docking.*",
			@".\Windows7.DesktopIntegration.*",
			@".\PopupControl.*",
			@".\DualityUpdater.*",
			@".\NuGet.Core.*",
			@".\Ionic.Zip.*",
			@".\Plugins\OpenTK.GLControl.*"};
		private static readonly Regex[] RegExTemporaryProjectFiles = TemporaryProjectFiles.Select(w => PathWildcardToRegex(w)).ToArray();
		private static readonly Regex[] RegExSourcePathBlacklist = SourcePathBlacklist.Select(w => PathWildcardToRegex(w)).ToArray();
		private static readonly Regex[] RegExEditorPathBlacklist = EditorPathBlacklist.Select(w => PathWildcardToRegex(w)).ToArray();

		private	Color	defaultTextBoxBackColor;

		public PublishGameDialog()
		{
			this.InitializeComponent();
			this.defaultTextBoxBackColor = this.textboxFolderPath.BackColor;
		}
		
		protected bool CheckInputValid()
		{
			Color errorColor = this.defaultTextBoxBackColor.MixWith(Color.Red, 0.25f, true);

			this.textboxFolderPath.BackColor = this.defaultTextBoxBackColor;
			
			if (string.IsNullOrWhiteSpace(this.textboxFolderPath.Text) || !PathHelper.IsPathValid(this.textboxFolderPath.Text))
			{
				this.textboxFolderPath.BackColor = errorColor;
				return false;
			}

			if (PathOp.ArePathsEqual(this.textboxFolderPath.Text, Environment.CurrentDirectory) ||
				PathOp.IsPathLocatedIn(this.textboxFolderPath.Text, Environment.CurrentDirectory))
			{
				this.textboxFolderPath.BackColor = errorColor;
				MessageBox.Show(
					GeneralRes.Msg_PublishProjectErrorNestedFolder_Desc,
					GeneralRes.Msg_PublishProjectErrorNestedFolder_Caption,
					MessageBoxButtons.OK,
					MessageBoxIcon.Warning);
				return false;
			}

			return true;
		}

		private void buttonBrowse_Click(object sender, EventArgs e)
		{
			publishFolder.SelectedPath = Environment.CurrentDirectory;
			if (publishFolder.ShowDialog() == DialogResult.OK)
			{
				textboxFolderPath.Text = publishFolder.SelectedPath;
			}
		}
		private void buttonCancel_Click(object sender, EventArgs e)
		{
			this.Close();
		}
		private void buttonPublish_Click(object sender, EventArgs e)
		{
			// Make sure the input is valid and display an error if not
			if (!this.CheckInputValid()) return;

			if (!Directory.Exists(textboxFolderPath.Text))
				Directory.CreateDirectory(textboxFolderPath.Text);

			PublishProject(
				textboxFolderPath.Text, 
				checkboxSource.Checked, 
				checkboxEditor.Checked, 
				checkboxCompress.Checked, 
				checkBoxShortcut.Checked,
				targetDir => MessageBox.Show(
					string.Format(GeneralRes.Msg_PublishConfirmDeleteTargetDir_Desc, Path.GetFileName(targetDir)), 
					GeneralRes.Msg_PublishConfirmDeleteTargetDir_Caption, 
					MessageBoxButtons.OKCancel, 
					MessageBoxIcon.Warning) == DialogResult.OK);
			this.Close();
		}

		public static void PublishProject(string targetDir, bool includeSource, bool includeEditor, bool compress, bool createShortcuts, Func<string,bool> targetExistsCallback = null)
		{
			// Determine a valid directory name for the game
			string gameDirName = PathOp.GetValidFileName(DualityApp.AppData.Instance.AppName);
			string targetGameDir = Path.Combine(targetDir, gameDirName);
			string archiveBaseDir = targetGameDir;

			// If we're creating shortcuts, move everything into a distinct subfolder to hide it from the user
			if (createShortcuts)
			{
				targetGameDir = Path.Combine(targetGameDir, "GameData");
			}

			// Make sure everything is saved before copying stuff
			DualityEditorApp.SaveAllProjectData();

			// If the dynamically created target directory already exists, delete it.
			if (Directory.Exists(archiveBaseDir))
			{
				bool empty = !Directory.EnumerateFiles(archiveBaseDir).Any();
				if (!empty && targetExistsCallback == null)
				{
					throw new ArgumentException("The target directory already contains a non-empty folder named '" + gameDirName + "'.", "targetDir");
				}
				else if (empty || targetExistsCallback(archiveBaseDir))
				{
					Directory.Delete(archiveBaseDir, true);
				}
				else
				{
					return;
				}
			}

			// Create the target directories
			Directory.CreateDirectory(archiveBaseDir);
			Directory.CreateDirectory(targetGameDir);

			// Copy files to the target directory
			PathHelper.CopyDirectory(Environment.CurrentDirectory, targetGameDir, true, delegate (string path)
			{
				string matchPath = Path.Combine(".", PathHelper.MakeFilePathRelative(path));

				// Exclude hidden files and folders
				if (!PathHelper.IsPathVisible(path))
				{
					string fileName = Path.GetFileName(path);
					if (!string.Equals(fileName, "desktop.ini", StringComparison.InvariantCultureIgnoreCase) &&
						!string.Equals(fileName, "WorkingFolderIcon.ico", StringComparison.InvariantCultureIgnoreCase))
					{
						return false;
					}
				}

				// Exclude temporary files
				if (RegExTemporaryProjectFiles.Any(entry => entry.IsMatch(matchPath)))
				{
					return false;
				}
				else if (!includeSource && RegExSourcePathBlacklist.Any(entry => entry.IsMatch(matchPath)))
				{
					return false;
				}
				else if (!includeEditor && RegExEditorPathBlacklist.Any(entry => entry.IsMatch(matchPath)))
				{
					return false;
				}

				return true;
			});

			// Create shortcuts when requested
			if (createShortcuts)
			{
				// Create the shortcut to the game
				string shortcutFilePath = Path.Combine(archiveBaseDir, gameDirName + ".bat");
				File.WriteAllText(
					shortcutFilePath,
					"cd GameData && start " + PathHelper.MakeFilePathRelative(DualityEditorApp.AppData.Instance.LauncherPath));

				// Create a shortcut to the editor
				if (includeEditor)
				{
					File.WriteAllText(
						Path.Combine(archiveBaseDir, gameDirName + " Editor.bat"),
						"cd GameData && start DualityEditor.exe");
				}
			}

			// Compress the directory
			if (compress)
			{
				string archivePath = Path.Combine(targetDir, gameDirName + ".zip");
				using (FileStream archiveStream = File.Open(archivePath, FileMode.Create))
				using (ZipArchive archive = new ZipArchive(archiveStream, ZipArchiveMode.Create))
				{
					archive.AddDirectory(archiveBaseDir);
				}
				Directory.Delete(archiveBaseDir, true);

				// Display compressed file to the user
				EditorHelper.ShowInExplorer(archivePath);
			}
			else
			{
				// Display directory to user
				EditorHelper.ShowInExplorer(targetGameDir);
			}

			return;
		}
		private static Regex PathWildcardToRegex(string pattern)
		{
			string regExString = "^" + Regex.Escape(pattern) + "$";
			regExString = Regex.Replace(regExString, @"(\\\\|/)", @"[\\/]");
			regExString = regExString.Replace(@"\*", @".*");
			regExString = regExString.Replace(@"\?", @".");
			Regex regEx = new Regex(regExString, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
			return regEx;
		}
	}
}


<|start_filename|>Source/Editor/DualityEditor/DesignTimeObjectDataManager.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;

using Duality.Serialization;
using Duality.Resources;

namespace Duality.Editor
{
	internal class DesignTimeObjectDataManager : ISerializeExplicit
	{
		private static readonly int GuidByteLength = Guid.Empty.ToByteArray().Length;
		private const int Version_First = 1;

		private	Dictionary<Guid,DesignTimeObjectData>	dataStore	= new Dictionary<Guid,DesignTimeObjectData>();

		public DesignTimeObjectData Request(Guid objId)
		{
			DesignTimeObjectData data;
			if (!this.dataStore.TryGetValue(objId, out data))
			{
				data = new DesignTimeObjectData(objId, DesignTimeObjectData.Default);
				this.dataStore[objId] = data;
			}
			return data;
		}
		public void Cleanup()
		{
			// Remove trivial / default data
			var removeQuery =
				from p in this.dataStore
				where p.Value.IsDefault
				select p.Value.ParentObjectId;
			foreach (Guid objId in removeQuery.ToArray())
				this.dataStore.Remove(objId);
		}
		public void Optimize()
		{
			// Optimize data by sharing
			var shareValues = this.dataStore.Values.ToList();
			while (shareValues.Count > 0)
			{
				DesignTimeObjectData data = shareValues[shareValues.Count - 1];
				foreach (DesignTimeObjectData other in shareValues)
				{
					data.TryShareData(other);
				}
				shareValues.RemoveAt(shareValues.Count - 1);
			}
		}

		void ISerializeExplicit.WriteData(IDataWriter writer)
		{
			this.Cleanup();
			this.Optimize();

			Guid[] guidArray = this.dataStore.Keys.ToArray();
			byte[] data = new byte[guidArray.Length * GuidByteLength];
			for (int i = 0; i < guidArray.Length; i++)
			{
				Array.Copy(
					guidArray[i].ToByteArray(), 0,
					data, i * GuidByteLength, GuidByteLength);
			}
			DesignTimeObjectData.DataContainer[] objData = this.dataStore.Values.Select(d => d.Data).ToArray();
			bool[] objDataDirty = this.dataStore.Values.Select(d => d.IsAttached).ToArray();

			writer.WriteValue("version", Version_First);
			writer.WriteValue("dataStoreKeys", data);
			writer.WriteValue("dataStoreValues", objData);
			writer.WriteValue("dataStoreDirtyFlag", objDataDirty);
		}
		void ISerializeExplicit.ReadData(IDataReader reader)
		{
			int version;
			reader.ReadValue("version", out version);

			if (this.dataStore == null)
				this.dataStore = new Dictionary<Guid, DesignTimeObjectData>();
			else
				this.dataStore.Clear();

			if (version == Version_First)
			{
				byte[] data;
				DesignTimeObjectData.DataContainer[] objData;
				bool[] objDataDirty;
				reader.ReadValue("dataStoreKeys", out data);
				reader.ReadValue("dataStoreValues", out objData);
				reader.ReadValue("dataStoreDirtyFlag", out objDataDirty);

				Guid[] guidArray = new Guid[data.Length / GuidByteLength];
				byte[] guidData = new byte[GuidByteLength];
				for (int i = 0; i < guidArray.Length; i++)
				{
					Array.Copy(
						data, i * GuidByteLength,
						guidData, 0, GuidByteLength);
					guidArray[i] = new Guid(guidData);
				}

				for (int i = 0; i < objData.Length; i++)
				{
					this.dataStore.Add(guidArray[i], new DesignTimeObjectData(guidArray[i], objData[i], objDataDirty[i]));
				}
			}
			else
			{
				// Unknown format
			}
		}
	}
}


<|start_filename|>Source/Editor/DualityEditor/Utility/DualityEditorAppData.cs<|end_filename|>
﻿namespace Duality.Editor
{
	public class DualityEditorAppData
	{
		private string launcherPath = "DualityGame.exe";
		public string LauncherPath
		{
			get { return this.launcherPath; }
			set { this.launcherPath = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/ObjectInspector/ObjectInspectorPluginSettings.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Xml.Linq;

namespace Duality.Editor.Plugins.ObjectInspector
{
	/// <summary>
	/// Contains all user settings for the object inspector plugin.
	/// </summary>
	public class ObjectInspectorPluginSettings
	{
		private Dictionary<int, ObjectInspectorSettings> inspectorSettingsById = new Dictionary<int, ObjectInspectorSettings>();

		/// <summary>
		/// Contains user settings that are defined per inspector window.
		/// </summary>
		public Dictionary<int, ObjectInspectorSettings> InspectorSettingsById
		{
			get { return this.inspectorSettingsById; }
			set { this.inspectorSettingsById = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorBase/Forms/PixmapSlicer/PixmapSlicerSettings.cs<|end_filename|>
﻿using Duality.Editor.Plugins.Base.Forms;
using Duality.Editor.Plugins.Base.Forms.PixmapSlicer.States;

namespace Duality.Editor.Plugins.Base
{
	/// <summary>
	/// Contains all user settings for the <see cref="PixmapSlicerForm"/>.
	/// </summary>
	public class PixmapSlicerSettings
	{
		private bool darkBackground = false;
		private PixmapNumberingStyle displayIndices = PixmapNumberingStyle.Hovered;

		/// <summary>
		/// Whether the slicer window should use a dark background instead of a bright one.
		/// </summary>
		public bool DarkBackground
		{
			get { return this.darkBackground; }
			set { this.darkBackground = value; }
		}
		/// <summary>
		/// Specifies if and how atlas slice indices are displayed to the user.
		/// </summary>
		public PixmapNumberingStyle DisplayIndices
		{
			get { return this.displayIndices; }
			set { this.displayIndices = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/LogView/LogViewSettings.cs<|end_filename|>
﻿namespace Duality.Editor.Plugins.LogView
{
	public class LogViewSettings
	{
		private bool showMessages = true;
		private bool showWarnings = true;
		private bool showErrors = true;
		private bool showCore = true;
		private bool showEditor = true;
		private bool showGame = true;
		private bool autoClear = true;
		private bool pauseOnError = true;

		public bool ShowMessages
		{
			get { return this.showMessages; }
			set { this.showMessages = value; }
		}
		public bool ShowWarnings
		{
			get { return this.showWarnings; }
			set { this.showWarnings = value; }
		}
		public bool ShowErrors
		{
			get { return this.showErrors; }
			set { this.showErrors = value; }
		}
		public bool ShowCore
		{
			get { return this.showCore; }
			set { this.showCore = value; }
		}
		public bool ShowEditor
		{
			get { return this.showEditor; }
			set { this.showEditor = value; }
		}
		public bool ShowGame
		{
			get { return this.showGame; }
			set { this.showGame = value; }
		}
		public bool AutoClear
		{
			get { return this.autoClear; }
			set { this.autoClear = value; }
		}
		public bool PauseOnError
		{
			get { return this.pauseOnError; }
			set { this.pauseOnError = value; }
		}
	}
}


<|start_filename|>Source/Plugins/EditorModules/SceneView/SceneViewSettings.cs<|end_filename|>
﻿namespace Duality.Editor.Plugins.SceneView
{
	public class SceneViewSettings
	{
		private bool showComponents = true;

		/// <summary>
		/// Controls whether components are shown as individual nodes in the scene tree
		/// </summary>
		public bool ShowComponents
		{
			get { return this.showComponents; }
			set { this.showComponents = value; }
		}
	}
}

<|start_filename|>Source/Editor/DualityEditor/DesignTimeObjectData.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;

using Duality.Serialization;
using Duality.Resources;

namespace Duality.Editor
{
	public class DesignTimeObjectData
	{
		internal class DataContainer : IEquatable<DataContainer>
		{
			public	bool	hidden	= false;
			public	bool	locked	= false;
			public	Dictionary<Type,object>	custom	= null;

			public DataContainer() {}
			public DataContainer(DataContainer baseData)
			{
				this.hidden = baseData.hidden;
				this.locked = baseData.locked;
				this.custom = baseData.custom != null ? new Dictionary<Type,object>(baseData.custom) : null;
			}

			public static bool operator ==(DataContainer a, DataContainer b)
			{
				if (object.ReferenceEquals(a, b)) return true;
				if (object.ReferenceEquals(a, null) || object.ReferenceEquals(b, null)) return false;

				if (a.hidden != b.hidden) return false;
				if (a.locked != b.locked) return false;

				if (a.custom != b.custom)
				{
					if (a.custom == null || b.custom == null) return false;
					if (a.custom.Count != b.custom.Count) return false;
					foreach (var pair in a.custom)
					{
						object valB;
						if (!b.custom.TryGetValue(pair.Key, out valB)) return false;
						if (!object.Equals(pair.Value, valB)) return false;
					}
				}

				return true;
			}
			public static bool operator !=(DataContainer a, DataContainer b)
			{
				return !(a == b);
			}
			public override bool Equals(object obj)
			{
				if (obj is DataContainer)
					return this.Equals(obj as DataContainer);
				else
					return base.Equals(obj);
			}
			public override int GetHashCode()
			{
				int hash = 17;
				MathF.CombineHashCode(hash, this.hidden.GetHashCode());
				MathF.CombineHashCode(hash, this.locked.GetHashCode());
				if (this.custom != null)
				{
					foreach (var pair in this.custom)
						MathF.CombineHashCode(hash, pair.Value.GetHashCode());
				}
				return hash;
			}
			public bool Equals(DataContainer other)
			{
				return this == other;
			}
		}

		internal static void Init()
		{
			Scene.Leaving += Scene_Leaving;
		}
		internal static void Terminate()
		{
			Scene.Leaving -= Scene_Leaving;
		}

		private static void Scene_Leaving(object sender, EventArgs e)
		{
			DualityEditorApp.UserData.Instance?.PerObjectData.Cleanup();
		}

		public static readonly DesignTimeObjectData Default = new DesignTimeObjectData();

		private	Guid			objId		= Guid.Empty;
		private DataContainer	data		= null;
		private	bool			attached	= false;

		internal DataContainer Data
		{
			get { return this.data; }
		}
		internal bool IsAttached
		{
			get { return this.attached; }
		}
		public Guid ParentObjectId
		{
			get { return this.objId; }
		}
		public bool IsHidden
		{
			get { return this.data.hidden; }
			set
			{
				if (this.data.hidden != value)
				{
					this.Detach();
					this.data.hidden = value;
				}
			}
		}
		public bool IsLocked
		{
			get { return this.data.locked; }
			set
			{
				if (this.data.locked != value)
				{
					this.Detach();
					this.data.locked = value;
				}
			}
		}
		public bool IsDefault
		{
			get
			{
				if (object.ReferenceEquals(this, Default)) return true;
				return this.data == Default.data;
			}
		}


		private DesignTimeObjectData() : this(Guid.Empty) {}
		internal DesignTimeObjectData(Guid parentId, DataContainer data, bool dirty)
		{
			this.objId = parentId;
			this.data = data;
			this.attached = dirty;
		}
		public DesignTimeObjectData(Guid parentId)
		{
			this.objId = parentId;
			this.data = new DataContainer();
		}
		public DesignTimeObjectData(Guid parentId, DesignTimeObjectData baseData)
		{
			this.objId = parentId;
			this.data = baseData.data;
			this.attached = true;
		}

		public static DesignTimeObjectData Get(GameObject obj)
		{
			return DualityEditorApp.UserData.Instance.PerObjectData.Request(obj.Id);
		}

		public T RequestCustomData<T>() where T : new()
		{
			this.Detach();

			if (this.data.custom == null) this.data.custom = new Dictionary<Type,object>();

			object val;
			if (!this.data.custom.TryGetValue(typeof(T), out val))
			{
				T newVal = new T();
				this.data.custom[typeof(T)] = newVal;
				return newVal;
			}
			else
			{
				return (T)val;
			}
		}
		public void RemoveCustomData<T>()
		{
			this.Detach();

			if (this.data.custom == null) return;
			this.data.custom.Remove(typeof(T));
		}

		internal bool TryShareData(DesignTimeObjectData other)
		{
			if (object.ReferenceEquals(this.data, other.data)) return true;
			if (this.data == other.data)
			{
				other.data = this.data;
				this.attached = true;
				other.attached = true;
				return true;
			}
			return false;
		}
		private void Detach()
		{
			if (!this.attached) return;
			if (this.data == null)	this.data = new DataContainer();
			else					this.data = new DataContainer(this.data);
			this.attached = false;
		}
	}
}


<|start_filename|>Source/Editor/DualityEditor/Utility/DualityEditorUserData.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Duality.Resources;

namespace Duality.Editor
{
	/// <summary>
	/// Contains user settings for the Duality editor application, such UI layout or preferences.
	/// </summary>
	public class DualityEditorUserData
	{
		private DesignTimeObjectDataManager perObjectData = new DesignTimeObjectDataManager();
		private XElement dockPanelState = XElement.Parse(Properties.GeneralRes.DefaultDockPanelData);
		private bool backups = true;
		private AutosaveFrequency autoSaves = AutosaveFrequency.ThirtyMinutes;
		private bool firstSession = true;
		private int activeDocumentIndex = 0;
		private bool startWithLastScene = true;
		private ContentRef<Scene> lastOpenScene = null;
		private PluginSettings pluginSettings = new PluginSettings();


		/// <summary>
		/// Per-object user settings, such as global object visibility or lock states.
		/// </summary>
		internal DesignTimeObjectDataManager PerObjectData
		{
			get { return this.perObjectData; }
			set { this.perObjectData = value; }
		}
		/// <summary>
		/// A serialized XML blob representing the UI dock panel layout to use when starting the editor.
		/// </summary>
		public XElement DockPanelState
		{
			get { return this.dockPanelState; }
			set { this.dockPanelState = value; }
		}
		/// <summary>
		/// Specifies whether the editor will copy previous versions of saved resources to a local Backups folder. 
		/// </summary>
		public bool Backups
		{
			get { return this.backups; }
			set { this.backups = value; }
		}
		/// <summary>
		/// Specifies auto-save behavior in the editor, e.g. the frequency at which unsaved Resources are saved.
		/// </summary>
		public AutosaveFrequency AutoSaves
		{
			get { return this.autoSaves; }
			set { this.autoSaves = value; }
		}
		/// <summary>
		/// Whether this is the first time the editor has been started in this project.
		/// </summary>
		public bool FirstSession
		{
			get { return this.firstSession; }
			set { this.firstSession = value; }
		}
		/// <summary>
		/// The index of the dock panel main area's active document, to be applied when starting the editor.
		/// </summary>
		public int ActiveDocumentIndex
		{
			get { return this.activeDocumentIndex; }
			set { this.activeDocumentIndex = value; }
		}
		/// <summary>
		/// Whether the editor should open the <see cref="LastOpenScene"/> on startup.
		/// </summary>
		public bool StartWithLastScene
		{
			get { return this.startWithLastScene; }
			set { this.startWithLastScene = value; }
		}
		/// <summary>
		/// A reference to the most recently opened scene. Used in combination with <see cref="StartWithLastScene"/>.
		/// </summary>
		public ContentRef<Scene> LastOpenScene
		{
			get { return this.lastOpenScene; }
			set { this.lastOpenScene = value; }
		}
		/// <summary>
		/// Container object for each editor plugin's specific user settings.
		/// </summary>
		public PluginSettings PluginSettings
		{
			get { return this.pluginSettings; }
			set { this.pluginSettings = value; }
		}
	}
}
