using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using TradeIdeas.TIProData;
using TradeIdeas.XML;
using System.Drawing;
using System.Collections;
using System.Drawing.Drawing2D;
using System.Xml;
using System.Reflection;
namespace TradeIdeas.TIProGUI
{
///
/// The assumption is that a lot of the GUIs will be mostly fixed when we display them for
/// the user. But the user will be able to change the symbol at run time.
///
/// This works closely with DataNode.IReplaceWith. A call to
/// IReplaceWithRuntime.ReplaceWith() will almost certainly result in several calls to
/// DataNode.IReplaceWith.ReplaceWith().
///
/// DataNode.IReplaceWith is mostly aimed at DataNode.Factory objects. DataNode.Factory
/// objects are all read only. Calling ReplaceWith on a DataNode.Factory object will
/// generate a new object with the requested changes, and will leave the original in tact.
///
/// IReplaceWithRuntime is mostly aimed at Control objects. You don't want to rebuild
/// your GUI each time someone changes the symbol. Only the data. So calling
/// IReplaceWithRuntime.ReplaceWith() will change the current object, telling it to
/// request and display a different type of data.
///
public interface IReplaceWithRuntime
{
///
/// This says what data to look for. This is often implemented by one or more calls
/// to ReplaceWith() on DataNode.Factory objects. Then we throw out the old
/// DataNode objects and use the new Factory objects to Find() new DataNode objects.
///
/// The value should be a mapping from the name of a PlaceHolder to the value that
/// should replace that PlaceHolder.
///
/// This can be null. Null is the typical initial state, and null is a way to tell
/// this object to stop what it's doing. Null might be treated the same as an
/// empty dictionary. That seems reasonable when the object only cares about one
/// PlaceHolder, typically the symbol.
///
/// The object that you set this to should never change. This value might be sent
/// to a lot of objects, and some might hold on to it.
///
Dictionary Replacements { set; }
}
///
/// A place for extension methods.
///
public static class IReplaceWithRuntimeHelper
{
///
/// This is a simple way to set the symbol for a control. Normally we give you the
/// option to set any number of values at once. The symbol is a special case, so we
/// make it easy.
///
/// The recipient of this request.
///
/// The new value for the symbol.
/// Null is a legal value. That's a common way to turn off all data in a control.
///
public static void SetSymbol(this IReplaceWithRuntime obj, string symbol)
{
if (null == symbol)
obj.Replacements = null;
else
obj.Replacements = new Dictionary { { DataNode.PlaceHolder.SYMBOL.Key, symbol } };
}
///
/// This is used in a lot of the container classes to pass the replacement message from the
/// owner to each of its children. If the control argument implements IReplaceWithRuntime
/// then we do the cast and set the replacements. If not, we do nothing.
///
/// The recipient of the requests.
///
/// The new values.
/// Null is legal here. That's a common way to turn off all data in a control.
///
public static void TryReplaceWith(this Control control, Dictionary replacements)
{
IReplaceWithRuntime irwr = control as IReplaceWithRuntime;
if (null != irwr)
irwr.Replacements = replacements;
}
///
/// A combination of SetSymbol() and TryReplaceWith(). This tries to cast the control
/// to a IReplaceWithRuntime. If that fails we do nothing. If that succeeds, we call
/// SetSymbol() on it.
///
///
///
public static void TrySetSymbol(this Control control, string symbol)
{
IReplaceWithRuntime irwr = control as IReplaceWithRuntime;
if (null != irwr)
irwr.SetSymbol(symbol);
}
}
///
/// This helps you build hash codes for a composite object from the hash codes of its
/// components.
///
public struct Hasher
{
public Hasher(params object[] fields)
{
_accumulator = 0;
Add(fields);
}
private int _accumulator;
private static int Rotate(int value)
{
uint v = (uint)value;
return (int)((v << 7) | (v >> (32 - 7)));
}
public void AddOne(object o)
{
_accumulator = Rotate(_accumulator);
if (null != o)
_accumulator ^= o.GetHashCode();
}
public void Add(params object[] fields)
{
foreach (object o in fields)
AddOne(o);
}
public override int GetHashCode()
{
return _accumulator;
}
}
///
/// Similar to DataNode.Factory, but simpler. We never reuse controls, so we don't have
/// to worry about Equals() or GetHashCode.
///
/// Note that factories are optional for controls. In some places an input will clearly
/// be a factory, not a control. But, unlike DataNode objects, most controls have public
/// constructors.
///
/// It would be very tempting to say that DataNodeControlFactory inherits from
/// DataNode.IReplaceWith.
///
/// IReplaceWith is a minor pain, but it might still be worth it. Presumably the
/// DataNode.Factory objects used to create a DataNodeControl will still have a
/// symbol PlaceHolder when we create the DataNodeControlFactory, and when that
/// creates the Control. ISetSymbol will take care of that.
///
/// It seems like IReplaceWith could be valuable while building controls. For example,
/// think about the GWT SmartLayoutPanel objects. They are built in advance with
/// names for each cell. Later you fill in the panels by name. That's not a perfect
/// example because that doesn't work with factories. In particular, sometime you change
/// templates, but you keep the controls inside. You translpant the controls to the
/// new parent without rebuilding them.
///
public abstract class DataNodeControlFactory
{
public abstract Control Create();
protected DataNodeControlFactory() { }
protected DataNodeControlFactory(XmlElement top)
{
}
public override bool Equals(object obj)
{
if (null == obj)
// Required by https://msdn.microsoft.com/en-us/library/bsc2ak47(v=vs.110).aspx
// "Implementations of Equals must not throw exceptions; they should always return a value.
// For example, if obj is null, the Equals method should return false instead of throwing
// an ArgumentNullException."
return false;
if (ReferenceEquals(this, obj))
return true;
if (!GetType().Equals(obj.GetType()))
// Note that we don't compare objects to type DataNodeControlFactory. That works for
// some types, but this is an abstract type. The assumption is that superclasses will
// start their Equals() methods by calling this. If this returns true, then they will
// cast their argumnt and check additional fields.
return false;
// Add more fields here. TODO
return true;
}
public override int GetHashCode()
{
return GetHashCodeBase().GetHashCode();
}
///
/// Get the part of the hash code which is based only on the fields available to this class.
/// Superclasses will probably override GetHashCode() and that will call MakeHashCode()
/// which in turn will call this.
///
///
private Hasher GetHashCodeBase()
{ // TODO add more fields.
return new Hasher(GetType());
}
protected int MakeHashCode(params object[] fields)
{
Hasher hasher = GetHashCodeBase();
hasher.Add(fields);
return hasher.GetHashCode();
}
}
///
/// A very simple way to display strings. DataNodes provide the value to
/// display, along with the foreground and background colors.
///
/// Currently this is based on a Label. That's an implementation detail that
/// is likely to change. In particular, we like the ability to display
/// different things depending on how large the field is. We might display
/// "100,000,002" if we have a lot of space, but "100.0M" if we have less space.
///
public class DataNodeLabel : Label, IReplaceWithRuntime
{
private readonly List _links = new List();
private DataNode _valueDataNode;
private DataNode _foreColorDataNode;
private DataNode _backColorDataNode;
private DataNode.Factory _valueFactory;
private DataNode.Factory _foreColorFactory;
private DataNode.Factory _backColorFactory;
///
/// Create the new object.
///
/// Initially the symbol will be null, so we won't try to instantiate anything.
/// We will set the symbol on each formula before trying to create any DataNode
/// objects.
///
///
/// This provides the value to display.
/// If the value is null we display "".
/// Otherwise we call ToString() on the value.
///
/// We will automatically wrap this factory in a GuiDataNode to ensure there are
/// no threading issues.
///
///
/// The foreground color of the label.
///
/// If the foreground factory and/or the background factory are null, we will
/// create reasonable factories. The colors are chosen so they will work together
/// and make the value easy to read.
///
/// If the DataNode returns null, we'll try to pick a color. This algorithm
/// is much simpler than leaving the factory null. In particular, this algorithm
/// doesn't know or care about the other color. If both the foreground and
/// background return null at the same time, then our choices will work.
///
/// We will automatically wrap this factory in a GuiDataNode to ensure there are
/// no threading issues.
///
///
/// The background color of the label.
///
/// If the foreground factory and/or the background factory are null, we will
/// create reasonable factories. The colors are chosen so they will work together
/// and make the value easy to read.
///
/// If the DataNode returns null, we'll try to pick a color. This algorithm
/// is much simpler than leaving the factory null. In particular, this algorithm
/// doesn't know or care about the other color. If both the foreground and
/// background return null at the same time, then our choices will look good.
///
/// We will automatically wrap this factory in a GuiDataNode to ensure there are
/// no threading issues.
///
public DataNodeLabel(DataNode.Factory value,
DataNode.Factory foreColor = null,
DataNode.Factory backColor = null)
{
_valueFactory = GuiDataNode.GetFactory(value);
if (null == foreColor)
if (null == backColor)
{ // Nothing is specified. Always use the default colors.
_foreColorFactory = ConstantDataNode.GetFactory(DEFAULT_FOREGROUND);
_backColorFactory = ConstantDataNode.GetFactory(DEFAULT_BACKGROUND);
}
else
{ // The user didn't specify a foreground color, so pick one that should look good
// with the background color. Notice that the two colors sharing the GuiDataNode and the
// data its holding.
_backColorFactory = GuiDataNode.GetFactory(backColor);
_foreColorFactory =
TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, _backColorFactory);
}
else
if (null == backColor)
{ // The user didn't specifiy a background color, so pick one based on the background.
_foreColorFactory = GuiDataNode.GetFactory(foreColor);
_backColorFactory = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, _foreColorFactory);
}
else
{ // The user specified everything.
_foreColorFactory = GuiDataNode.GetFactory(foreColor);
_backColorFactory = GuiDataNode.GetFactory(backColor);
}
AutoSize = true; // To be more like the form builder.
this.Disposed += delegate { Replacements = null; };
}
public class Factory : DataNodeControlFactory
{
// For DataNodeControlFactory
public override Control Create()
{
Control result = new DataNodeLabel(Value, ForeColor, BackColor);
if (null != FinalSetup)
FinalSetup(result);
return result;
}
///
/// See DataNodeLabel.DataNodeLabel() for more details.
///
public DataNode.Factory Value;
///
/// May be null. See DataNodeLabel.DataNodeLabel() for more details.
///
public DataNode.Factory ForeColor;
///
/// May be null. See DataNodeLabel.DataNodeLabel() for more details.
///
public DataNode.Factory BackColor;
///
/// May be null. This is a good place to do things that aren't specific to this type
/// of Control. Like setting the width.
///
public Action FinalSetup;
public override int GetHashCode()
{
return MakeHashCode(Value, ForeColor, BackColor, FinalSetup);
}
public override bool Equals(object obj)
{
if (!base.Equals(obj))
return false;
// We should have already bailed out by now if obj is not the right type.
Factory other = (Factory)obj;
return Equals(Value, other.Value) && Equals(ForeColor, other.ForeColor)
&& Equals(BackColor, other.BackColor) && Equals(FinalSetup, other.FinalSetup);
}
}
public Dictionary Replacements
{
set
{
foreach (DataNode.Link link in _links)
link.Release();
_links.Clear();
if (null == value)
{
_valueDataNode = null;
_foreColorDataNode = null;
_backColorDataNode = null;
}
else
{
_links.Add(_valueFactory.ReplaceWithF(value).Find(out _valueDataNode, ValueCallback));
_links.Add(_foreColorFactory.ReplaceWithF(value).Find(out _foreColorDataNode, ForeColorCallback));
_links.Add(_backColorFactory.ReplaceWithF(value).Find(out _backColorDataNode, BackColorCallback));
ValueCallback();
ForeColorCallback();
BackColorCallback();
}
}
}
private void ValueCallback()
{
object value = _valueDataNode.Value;
if (null == value)
Text = "";
else
Text = value.ToString();
}
private static readonly Color DEFAULT_FOREGROUND = SystemColors.ControlText;
private static readonly Color DEFAULT_BACKGROUND = SystemColors.Control;
private void ForeColorCallback()
{
object value = _foreColorDataNode.Value;
if (value is Color)
ForeColor = (Color)value;
else
ForeColor = DEFAULT_FOREGROUND;
}
private void BackColorCallback()
{
object value = _backColorDataNode.Value;
if (value is Color)
BackColor = (Color)value;
else
BackColor = DEFAULT_BACKGROUND;
}
}
public class DataNodeTableLayoutPanel : TableLayoutPanel, IReplaceWithRuntime
{
private Dictionary _replacements;
public Dictionary Replacements
{
set
{
_replacements = value;
foreach (Control control in Controls)
control.TryReplaceWith(value);
}
}
public DataNodeTableLayoutPanel()
{
ControlAdded += DoControlAdded;
}
private void DoControlAdded(object sender, System.Windows.Forms.ControlEventArgs e)
{
e.Control.TryReplaceWith(_replacements);
}
}
public class DataNodeList
{
private List _links = new List();
private List _dataNodes = new List();
public void Release()
{
_dataNodes = null;
foreach (DataNode.Link link in _links)
link.Release();
_links.Clear();
}
public object this[int index]
{
get
{
if ((index >= _dataNodes.Count) || (index < 0))
return null;
DataNode dataNode = _dataNodes[index];
if (null == dataNode)
return null;
return dataNode.Value;
}
}
public string GetString(int index, string defaultValue = "")
{
object value = this[index];
if (null == value)
return defaultValue;
else
return value.ToString();
}
public double? GetDouble(int index)
{
object asObject = this[index];
if (asObject is double)
return (double)asObject;
else if (asObject is Int64)
return (Int64)asObject;
else
return null;
}
public event Action OnChange;
private void SomethingHappened()
{
OnChange?.Invoke();
}
///
/// Null for no message. Mostly aimed at a developer.
///
public string ErrorMessage { get; private set; }
public DataNodeList(params DataNode.Factory[] factories)
{
try
{
_links.Capacity = factories.Length;
_dataNodes.Capacity = factories.Length;
foreach (DataNode.Factory factory in factories)
if (null == factory)
_dataNodes.Add(null);
else
{
DataNode dataNode;
_links.Add(factory.Find(out dataNode, SomethingHappened));
_dataNodes.Add(dataNode);
}
}
catch (Exception ex)
{
foreach (DataNode.Link link in _links)
link.Release();
_links.Clear();
throw ex;
}
}
static private DataNode.Factory[] MakeArray(DataNode.StaticList input)
{
DataNode.Factory[] result = new DataNode.Factory[input.Length];
for (int i = 0; i < result.Length; i++)
result[i] = (DataNode.Factory)input[i];
return result;
}
public static DataNodeList Create(DataNode.StaticList factories)
{
if (!factories.ReplacementsComplete())
{
DataNodeList result = new DataNodeList();
result.ErrorMessage = "Missing replacements.";
return result;
}
else
try
{
return new DataNodeList(MakeArray(factories));
}
catch (Exception ex)
{
DataNodeList result = new DataNodeList();
result.ErrorMessage = ex.Message;
return result;
}
}
}
///
/// This describes a "view". If you have a table, you probably want one of these per
/// column. If you're only using data nodes to populate your table, you probably want
/// one for every column.
///
/// This doesn't require a table. If you use another widget you can still use this
/// to paint. So we can reuse our various paint routines.
///
/// One class that inherits from this will have one style of drawing. E.g.
/// DataNodeTextViewer will display text with various colors for the foreground and
/// background. A different class will draw logarithmic triangles. Different
/// classes can request different amounts of data and different types of data.
///
/// Each object of this type can have different data associated with it. For example
/// we might use the triangles to display the % volume in one column, and a user's
/// custom formula in a different column.
///
/// These objects are typically read only. These do not need factories. You can
/// uses these along side DataNode.Factory and DataNodeControlFactory objects as
/// as you conver to and from XML. But these also sit side by side with DataNode
/// and DataNodeControl objects at run time. In some sense DataNodeViewer objects
/// are their own factories.
///
/// Note: These objects have some state that can change. But that's completely
/// internal. For example it was inconvenient to copy the client width from each
/// function to the next. So we create a temporary variable in the DataNodeViewer
/// object to handle that. But that's an implemention detail.
///
public abstract class DataNodeViewer
{
///
/// Request the data for one instance of this view.
/// For a table you'll probably call this once on each cell. The viewer will
/// come from the column. The replacements are specific to the row.
///
///
/// The same input you'd use in DataNode.IReplaceWith.ReplaceWith().
/// Very often this will be the stock symbol and nothing else.
///
///
/// A DataNodeList suitable for use with Paint(). Don't mix and match.
/// When you call Paint() on an object, the DataNodeList must have come from
/// the same object.
///
public abstract DataNodeList GetData(Dictionary replacements);
protected abstract void Paint(DataNodeList data, Graphics graphics, Rectangle bounds,
Color ForeColor, Color BackColor, bool alwaysUseTheseColors);
public void Paint(DataNodeList data, Graphics graphics, Rectangle bounds,
Color ForeColor, Color BackColor, bool alwaysUseTheseColors, Font font)
{
Font = font;
try
{
ClientWidth = bounds.Width;
Paint(data, graphics, bounds, ForeColor, BackColor, alwaysUseTheseColors);
}
finally
{
Font = null;
}
}
///
/// This is specifically aimed at DataGridView. By default the user can copy
/// the highlighted section of the table to the clipboard. The DataGridView
/// goes to each cell to ask for a string version of itself. The cell should
/// come here.
///
///
/// The output from a previous call to GetData() on this SAME object.
///
/// A user friendly description of the data.
public abstract String GetForCopy(DataNodeList data);
// What about GetForSort()? We like sortable tables. Most of that's built
// into DataGridView. What we need is to give an unformatted object
// (presumably an integer or a double most of the time) to the DataGridView.
protected int ClientWidth { get; set; }
protected Font Font { get; private set; }
protected bool TooWide(string text)
{
Size textSize = TextRenderer.MeasureText(text, Font);
return textSize.Width > ClientWidth;
}
private string MakeTruncatedDoubleFit(double adjusted, char symbol)
{
string result = String.Format("{0:f2}", adjusted) + symbol;
if (TooWide(result))
{
result = String.Format("{0:f1}", adjusted) + symbol;
}
if (TooWide(result))
{
result = String.Format("{0:f0}", adjusted) + symbol;
}
if (TooWide(result))
{
result = "...";
}
return result;
}
private string MakeDoubleFitByTruncating(double value)
{
if (value < 1000)
return "...";
else if (value < 1000000)
return MakeTruncatedDoubleFit(value / 1.0e+3, 'K');
else if (value < 1000000000)
return MakeTruncatedDoubleFit(value / 1.0e+6, 'M');
else
return MakeTruncatedDoubleFit(value / 1.0e+9, 'B');
}
private static readonly string[] DOUBLE_FORMAT_STRINGS =
new string[] { "#,##0", "#,#0.0", "#,#0.#0", "#,#0.##0", "#,#0.###0", "#,#0.####0", "#,#0.#####0" };
protected string MakeDoubleFit(double value, int digits)
{
if (digits >= DOUBLE_FORMAT_STRINGS.Length)
digits = DOUBLE_FORMAT_STRINGS.Length - 1;
else if (digits < 0)
digits = 0;
while (true)
{
if (digits < 0)
return MakeDoubleFitByTruncating(value);
string result = value.ToString(DOUBLE_FORMAT_STRINGS[digits]);
if (!TooWide(result))
return result;
digits--;
}
}
protected static void FixColorFactories(ref DataNode.Factory foreColor,
ref DataNode.Factory backColor)
{
if (null == foreColor)
if (null == backColor)
{ // Nothing is specified. Always use the default colors.
// Note: If we leave a factory as null, DataNodeList will act as if this was a constant factory always returning null.
// Note: DataNodeLabel hard coded a specific color value here.
// We can't do that. The color will get picked at run time. That way we don't need
// a special event if someone changes the color of a label. Presumably a table row
// might change colors if someone clicks on that row. Instead Paint() will ask for
// default colors and use them if either color data node returns null.
}
else
{ // The user didn't specify a foreground color, so pick one that should look good
// with the background color. Notice that the two colors sharing the GuiDataNode and the
// data its holding.
backColor = GuiDataNode.GetFactory(backColor);
foreColor =
TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, backColor);
}
else
if (null == backColor)
{ // The user didn't specifiy a background color, so pick one based on the foreground.
foreColor = GuiDataNode.GetFactory(foreColor);
backColor = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, foreColor);
}
else
{ // The user specified everything.
foreColor = GuiDataNode.GetFactory(foreColor);
backColor = GuiDataNode.GetFactory(backColor);
}
}
protected static void MergeColors(ref Color foreColor, ref Color backColor,
object dataForeColor, object dataBackColor)
{
Color? foreground = dataForeColor as Color?;
Color? background = dataBackColor as Color?;
if (foreground.HasValue && background.HasValue)
{ // Only change the color if both the forground and background are valid.
// If you use the specified foreground color and the default background
// color (or vice versa) you might make an unreadable combination.
foreColor = foreground.Value;
backColor = background.Value;
}
}
///
///
///
///
///
/// We clip to this area. fatEnd and thinEnd are relative to this.
///
///
/// 0.0 is the left edge of the destination. 1.0 is the right edge of the destination.
/// -1.0 and 2.0 are outside of the destination, to the left and right respectively.
///
///
/// 0.0 is the left edge of the destination. 1.0 is the right edge of the destination.
/// -1.0 and 2.0 are outside of the destination, to the left and right respectively.
///
///
protected void DrawTriangle(Graphics graphics, Rectangle destination, double fatEnd, double thinEnd, Brush brush)
{
Region previousClip = graphics.Clip;
graphics.SetClip(destination, CombineMode.Intersect);
SmoothingMode previousSmoothingMode = graphics.SmoothingMode;
graphics.SmoothingMode = SmoothingMode.HighQuality;
if (destination.Width < 1) return;
int fatX = destination.X + (int)(destination.Width * fatEnd + 0.5);
int thinX = destination.X + (int)(destination.Width * thinEnd + 0.5);
Point[] points = { new Point(fatX, destination.Y), new Point(thinX, destination.Y + (destination.Height / 2)), new Point(fatX, destination.Y + destination.Height) };
graphics.FillPolygon(brush, points);
graphics.Clip = previousClip;
graphics.SmoothingMode = previousSmoothingMode; //must reset back to "none" to prevent gridline anomalies...
}
protected void DrawDouble(Graphics graphics, Rectangle destination, double value, int digits, Color color)
{
if (double.IsNaN(value) || double.IsNaN(value))
return;
string text = MakeDoubleFit(value, digits);
TextRenderer.DrawText(graphics, text, Font, destination, color, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Right);
}
public static void MakeCylinder(Graphics g, Rectangle rect)
{
using (LinearGradientBrush brush =
new LinearGradientBrush(rect, Color.Transparent, Color.Transparent, LinearGradientMode.Vertical))
{
ColorBlend colorBlend = new ColorBlend(4);
int alpha = 145;
colorBlend.Colors[0] = Color.FromArgb(0, 255, 255, 255);
colorBlend.Positions[0] = 0.0f;
colorBlend.Colors[1] = Color.FromArgb(alpha, 255, 255, 255);
colorBlend.Positions[1] = 0.25f;
colorBlend.Colors[2] = Color.FromArgb(0, 255, 255, 255);
colorBlend.Positions[2] = 0.5f;
colorBlend.Colors[3] = Color.FromArgb(alpha, 0, 0, 0);
colorBlend.Positions[3] = 1.0f;
brush.InterpolationColors = colorBlend;
g.FillRectangle(brush, rect);
}
}
private const string ARGS = "ARGS";
static protected void Encode(XmlNode body, string type, params object[] args)
{
body.SetProperty(XmlSerializer.TYPE, type);
XmlSerializer.Encode(new DataNode.StaticList(args), body.NewNode(ARGS));
}
static protected void RegisterDecoder(string xmlType, Type internalType)
{
XmlSerializer.RegisterDecoder(xmlType, source => StandardDecoder(internalType, source));
}
private static XmlElement GetArgs(XmlElement factory)
{
XmlElement result = factory.Node(ARGS);
if (null == result)
throw new ArgumentException("Could not find ARGS in DataNodeViewer.");
return result;
}
private static DataNodeViewer StandardDecoder(Type type, XmlElement source)
{
XmlElement args = GetArgs(source);
DataNode.StaticList argsAsList = new DataNode.StaticList(args);
object[] argsAsArray = argsAsList.GetItems();
object result = type.InvokeMember(null, BindingFlags.Instance |
BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.OptionalParamBinding,
null, null, argsAsArray);
return (DataNodeViewer)result;
}
private Hasher GetHashCodeBase()
{ // This is probably it. I don't think we'll add any fields to this class.
return new Hasher(GetType());
}
protected int MakeHashCode(params object[] fields)
{
Hasher hasher = GetHashCodeBase();
hasher.Add(fields);
return hasher.GetHashCode();
}
}
///
/// Display DataNode results as text. You have to provide a DataNode.Factory for the
/// string to display, the foreground color, and the background color.
///
[XmlSerializerInit("*")]
public class DataNodeTextViewer : DataNodeViewer, XmlSerializer.Self
{
private const int VALUE_INDEX = 0;
private const int FOREGROUND_INDEX = 1;
private const int BACKGROUND_INDEX = 2;
private readonly DataNode.StaticList _effectiveFactories;
public override DataNodeList GetData(Dictionary replacements)
{
return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements));
}
protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds,
Color ForeColor, Color BackColor, bool alwaysUseTheseColors)
{
if (!alwaysUseTheseColors)
MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]);
using (Brush bgBrush = new SolidBrush(BackColor))
{
graphics.FillRectangle(bgBrush, bounds);
object text = data[VALUE_INDEX];
if (null != text)
TextRenderer.DrawText(graphics, text.ToString(), Font, bounds, ForeColor, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter);
}
}
public override String GetForCopy(DataNodeList data)
{ // I think this is required for copy and paste in the table.
// Can a table cell return HTML for formatting?
// Should we also return a value for sorting?
return data.GetString(VALUE_INDEX);
}
public DataNodeTextViewer(DataNode.Factory value,
DataNode.Factory foreColor = null,
DataNode.Factory backColor = null)
{
ValueFactory = value;
ForegroundFactory = foreColor;
BackgroundFactory = backColor;
value = GuiDataNode.GetFactory(value);
FixColorFactories(ref foreColor, ref backColor);
_effectiveFactories = DataNode.StaticList.Create(value, foreColor, backColor);
}
public DataNode.Factory ValueFactory { get; private set; }
public DataNode.Factory ForegroundFactory { get; private set; }
public DataNode.Factory BackgroundFactory { get; private set; }
private const string TYPE = "DATA_NODE_TEXT_VIEWER";
public void Encode(XmlElement body)
{
Encode(body, TYPE, ValueFactory, ForegroundFactory, BackgroundFactory);
}
private static void XmlSerializerInit()
{
RegisterDecoder(TYPE, typeof(DataNodeTextViewer));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is DataNodeTextViewer))
return false;
DataNodeTextViewer other = (DataNodeTextViewer)obj;
return Equals(ForegroundFactory, other.ForegroundFactory)
&& Equals(BackgroundFactory, other.BackgroundFactory)
&& Equals(ValueFactory, other.ValueFactory);
}
public override int GetHashCode()
{
return MakeHashCode(ForegroundFactory, BackgroundFactory, ValueFactory);
}
}
///
/// Display DataNode results as a number. You have to provide a DataNode.Factory for the
/// string to display, the foreground color, and the background color. Also provide a
/// constant for the preferred number of digits after the decimal.
///
[XmlSerializerInit("*")]
public class DataNodeNumberViewer : DataNodeViewer, XmlSerializer.Self
{
private const int VALUE_INDEX = 0;
private const int FOREGROUND_INDEX = 1;
private const int BACKGROUND_INDEX = 2;
///
/// We do various things to the request. For example, if you only specify one color,
/// we pick the other for you. But that doesn't change the properties. If backColor
/// was null in the constructor the BackColor property will return null, even if we
/// choose to display a different color.
///
/// _effectiveFactories could be recomputed at any time from the properties.
///
private readonly DataNode.StaticList _effectiveFactories;
public readonly int Digits;
public override DataNodeList GetData(Dictionary replacements)
{
return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements));
}
protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds,
Color ForeColor, Color BackColor, bool alwaysUseTheseColors)
{
if (!alwaysUseTheseColors)
MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]);
using (Brush bgBrush = new SolidBrush(BackColor))
{
graphics.FillRectangle(bgBrush, bounds);
double? value = data.GetDouble(VALUE_INDEX);
if (value.HasValue)
DrawDouble(graphics, bounds, value.Value, Digits, ForeColor);
}
}
private static String GetValue(DataNodeList data)
{
object value = data[VALUE_INDEX];
if (null == value)
return "";
else
return value.ToString();
}
public override String GetForCopy(DataNodeList data)
{ // I think this is required for copy and paste in the table.
// Can a table cell return HTML for formatting?
// Should we also return a value for sorting?
return GetValue(data);
}
public DataNodeNumberViewer(DataNode.Factory value,
int digits,
DataNode.Factory foreColor = null,
DataNode.Factory backColor = null)
{
ValueFactory = value;
ForegroundFactory = foreColor;
BackgroundFactory = backColor;
value = GuiDataNode.GetFactory(value);
FixColorFactories(ref foreColor, ref backColor);
_effectiveFactories = DataNode.StaticList.Create(value, foreColor, backColor);
Digits = digits;
}
public DataNode.Factory ValueFactory { get; private set; }
public DataNode.Factory ForegroundFactory { get; private set; }
public DataNode.Factory BackgroundFactory { get; private set; }
private const string TYPE = "DATA_NODE_NUMBER_VIEWER";
public void Encode(XmlElement body)
{
Encode(body, TYPE, ValueFactory, Digits, ForegroundFactory, BackgroundFactory);
}
private static void XmlSerializerInit()
{
RegisterDecoder(TYPE, typeof(DataNodeNumberViewer));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is DataNodeNumberViewer))
return false;
DataNodeNumberViewer other = (DataNodeNumberViewer)obj;
return (Digits == other.Digits) && Equals(ForegroundFactory, other.ForegroundFactory)
&& Equals(BackgroundFactory, other.BackgroundFactory)
&& Equals(ValueFactory, other.ValueFactory);
}
public override int GetHashCode()
{
return MakeHashCode(Digits, ForegroundFactory, BackgroundFactory, ValueFactory);
}
}
///
/// Display DataNode.Value as a series of triangles. Also display it as a number.
/// The colors are fixed.
///
[XmlSerializerInit("*")]
public class DataNodeLogTrianglesViewer : DataNodeViewer, XmlSerializer.Self
{
private const int VALUE_INDEX = 0;
private readonly DataNode.StaticList _effectiveFactories;
public readonly int Digits;
public override DataNodeList GetData(Dictionary replacements)
{
return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements));
}
private static readonly Color TEXT_COLOR = Color.White;
private static readonly Brush TRIANGLE1_BRUSH = new SolidBrush(Color.FromArgb(15, 28, 239));
private static readonly Brush TRIANGLE10_BRUSH = new SolidBrush(Color.DarkMagenta);
private static readonly Brush TRIANGLE100_BRUSH = new SolidBrush(Color.FromArgb(12, 70, 21));
private static readonly Brush BACKGROUND_BRUSH = new SolidBrush(Color.FromArgb(0, 0, 9));
protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds,
Color ForeColor, Color BackColor, bool alwaysUseTheseColors)
{
graphics.FillRectangle(BACKGROUND_BRUSH, bounds);
double? value = data.GetDouble(VALUE_INDEX);
if (value.HasValue)
{
DrawTriangle(graphics, bounds, 0, value.Value / 100.0, TRIANGLE1_BRUSH);
DrawTriangle(graphics, bounds, 0, value.Value / 1000.0, TRIANGLE10_BRUSH);
DrawTriangle(graphics, bounds, 0, value.Value / 10000.0, TRIANGLE100_BRUSH);
DrawDouble(graphics, bounds, value.Value, Digits, TEXT_COLOR);
}
}
private static String GetValue(DataNodeList data)
{
object value = data[VALUE_INDEX];
if (null == value)
return "";
else
return value.ToString();
}
public override String GetForCopy(DataNodeList data)
{ // I think this is required for copy and paste in the table.
// Can a table cell return HTML for formatting?
// Should we also return a value for sorting?
return GetValue(data);
}
public DataNodeLogTrianglesViewer(DataNode.Factory value, int digits = 1)
{
ValueFactory = value;
value = GuiDataNode.GetFactory(value);
_effectiveFactories = DataNode.StaticList.Create(value);
Digits = Math.Max(0, Math.Min(6, digits));
}
public DataNode.Factory ValueFactory { get; private set; }
private const string TYPE = "DATA_NODE_LOG_TRIANGLES_VIEWER";
public void Encode(XmlElement body)
{
Encode(body, TYPE, ValueFactory, Digits);
}
private static void XmlSerializerInit()
{
RegisterDecoder(TYPE, typeof(DataNodeLogTrianglesViewer));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is DataNodeLogTrianglesViewer))
return false;
DataNodeLogTrianglesViewer other = (DataNodeLogTrianglesViewer)obj;
return (Digits == other.Digits) && Equals(ValueFactory, other.ValueFactory);
}
public override int GetHashCode()
{
return MakeHashCode(Digits, ValueFactory);
}
}
[XmlSerializerInit("*")]
public class DataNodeRectangleViewer : DataNodeViewer, XmlSerializer.Self
{
public DataNodeRectangleViewer(DataNode.Factory left, DataNode.Factory foreground,
DataNode.Factory background, DataNode.Factory secondValue, ThreeD threeD,
SecondValueMeaning secondValueMeaning)
{
LeftValueFactory = left;
ForegroundFactory = foreground;
BackgroundFactory = background;
OtherValueFactory = secondValue;
left = GuiDataNode.GetFactory(left);
secondValue = GuiDataNode.GetFactory(secondValue);
FixColorFactories(ref foreground, ref background);
_effectiveFactories = DataNode.StaticList.Create(left, foreground, background, secondValue);
_threeD = threeD;
_secondValueMeaning = secondValueMeaning;
}
private const int LEFT_VALUE_INDEX = 0;
private const int FOREGROUND_INDEX = 1;
private const int BACKGROUND_INDEX = 2;
private const int OTHER_VALUE_INDEX = 3;
private readonly DataNode.StaticList _effectiveFactories;
public enum ThreeD { None, Rectangle, All }
private readonly ThreeD _threeD;
public enum SecondValueMeaning { Width, Right }
private readonly SecondValueMeaning _secondValueMeaning;
public override DataNodeList GetData(Dictionary replacements)
{
return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements));
}
public override string GetForCopy(DataNodeList data)
{
if (_secondValueMeaning == SecondValueMeaning.Width)
return data.GetString(LEFT_VALUE_INDEX);
else
return "";
}
protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors)
{
if (!alwaysUseTheseColors)
MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]);
using (Brush bgBrush = new SolidBrush(BackColor))
{
graphics.FillRectangle(bgBrush, bounds);
}
double? leftValue = data.GetDouble(LEFT_VALUE_INDEX);
double? otherValue = data.GetDouble(OTHER_VALUE_INDEX);
if (leftValue.HasValue && otherValue.HasValue)
{
double leftRatio;
double rightRatio;
if (_secondValueMeaning == SecondValueMeaning.Right)
{
leftRatio = leftValue.Value / 100;
rightRatio = otherValue.Value / 100;
}
else
{
double width = otherValue.Value;
if ((width < 0) || (width >= 100))
// Invalid
leftRatio = rightRatio = -1;
else
{
double remainingSpace = (100 - width) / 100;
leftRatio = leftValue.Value / 100 * remainingSpace;
rightRatio = leftRatio + width / 100;
}
}
if (leftRatio > rightRatio)
{ // By default FillRectangle will ignore something with a negative width.
double temp = leftRatio;
leftRatio = rightRatio;
rightRatio = temp;
}
int left = (int)(leftRatio * bounds.Width + 0.5);
int right = (int)(rightRatio * bounds.Width + 0.5);
using (Brush brush = new SolidBrush(ForeColor))
{
Rectangle selection = new Rectangle(bounds.X + left, bounds.Y, right - left, bounds.Height);
graphics.FillRectangle(brush, selection);
if (_threeD == ThreeD.Rectangle)
MakeCylinder(graphics, selection);
}
if (_threeD == ThreeD.All)
// We make the entire thing a cylinder. But only if we have values for the rectangle.
// If there's a null, then we're always flat.
MakeCylinder(graphics, bounds);
}
}
public DataNode.Factory LeftValueFactory { get; private set; }
public DataNode.Factory ForegroundFactory { get; private set; }
public DataNode.Factory BackgroundFactory { get; private set; }
public DataNode.Factory OtherValueFactory { get; private set; }
public ThreeD GetThreeD() { return _threeD; }
public SecondValueMeaning GetSecondValueMeaning() { return _secondValueMeaning; }
private const string TYPE = "DATA_NODE_RECTANGLE_VIEWER";
public void Encode(XmlElement body)
{
Encode(body, TYPE, LeftValueFactory, ForegroundFactory, BackgroundFactory, OtherValueFactory, _threeD, _secondValueMeaning);
}
private static void XmlSerializerInit()
{
RegisterDecoder(TYPE, typeof(DataNodeRectangleViewer));
XmlSerializer.AddEnum(typeof(ThreeD));
XmlSerializer.AddEnum(typeof(SecondValueMeaning));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is DataNodeRectangleViewer))
return false;
DataNodeRectangleViewer other = (DataNodeRectangleViewer)obj;
return (_threeD == other._threeD) && (_secondValueMeaning == other._secondValueMeaning)
&& Equals(LeftValueFactory, other.LeftValueFactory)
&& Equals(ForegroundFactory, other.ForegroundFactory)
&& Equals(BackgroundFactory, other.BackgroundFactory)
&& Equals(OtherValueFactory, other.OtherValueFactory);
}
public override int GetHashCode()
{ // TODO this could be cached, like in a DataNode.StaticList
return MakeHashCode(LeftValueFactory, ForegroundFactory, BackgroundFactory, OtherValueFactory, _threeD, _secondValueMeaning);
}
};
[XmlSerializerInit("*")]
public class DataNodeTriangleViewer : DataNodeViewer, XmlSerializer.Self
{
public DataNodeTriangleViewer(DataNode.Factory fat, DataNode.Factory foreground,
DataNode.Factory background, DataNode.Factory thin)
{
FatValueFactory = fat;
ForegroundFactory = foreground;
BackgroundFactory = background;
ThinValueFactory = thin;
fat = GuiDataNode.GetFactory(fat);
thin = GuiDataNode.GetFactory(thin);
FixColorFactories(ref foreground, ref background);
_effectiveFactories = DataNode.StaticList.Create(fat, foreground, background, thin);
}
private const int FAT_VALUE_INDEX = 0;
private const int FOREGROUND_INDEX = 1;
private const int BACKGROUND_INDEX = 2;
private const int THIN_VALUE_INDEX = 3;
private readonly DataNode.StaticList _effectiveFactories;
public override DataNodeList GetData(Dictionary replacements)
{
return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements));
}
public override string GetForCopy(DataNodeList data)
{
return "";
}
protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors)
{
if (!alwaysUseTheseColors)
MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]);
using (Brush bgBrush = new SolidBrush(BackColor))
{
graphics.FillRectangle(bgBrush, bounds);
}
double? fatValue = data.GetDouble(FAT_VALUE_INDEX);
double? thinValue = data.GetDouble(THIN_VALUE_INDEX);
if (fatValue.HasValue && thinValue.HasValue)
using (Brush brush = new SolidBrush(ForeColor))
DrawTriangle(graphics, bounds, fatValue.Value / 100, thinValue.Value / 100, brush);
}
public DataNode.Factory FatValueFactory { get; private set; }
public DataNode.Factory ForegroundFactory { get; private set; }
public DataNode.Factory BackgroundFactory { get; private set; }
public DataNode.Factory ThinValueFactory { get; private set; }
private const string TYPE = "DATA_NODE_TRIANGLE_VIEWER";
public void Encode(XmlElement body)
{
Encode(body, TYPE, FatValueFactory, ForegroundFactory, BackgroundFactory, ThinValueFactory);
}
private static void XmlSerializerInit()
{
RegisterDecoder(TYPE, typeof(DataNodeTriangleViewer));
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
if (!(obj is DataNodeTriangleViewer))
return false;
DataNodeTriangleViewer other = (DataNodeTriangleViewer)obj;
return Equals(FatValueFactory, other.FatValueFactory)
&& Equals(ForegroundFactory, other.ForegroundFactory)
&& Equals(BackgroundFactory, other.BackgroundFactory)
&& Equals(ThinValueFactory, other.ThinValueFactory);
}
public override int GetHashCode()
{
return MakeHashCode(FatValueFactory, ForegroundFactory, BackgroundFactory, ThinValueFactory);
}
};
public class SimpleDataNodeControl : Control, IReplaceWithRuntime
{
private DataNodeList _data;
///
/// This is required for the form designer.
///
/// A SimpleDataNodeControl isn't useful until you set the viewer, but we
/// allow you to set (and change and clear) the viewer later because sometimes
/// that is conveneint.
///
public SimpleDataNodeControl()
{ // ResizeRedraw is essential. By default Windows assumes that whatever we are drawing is anchored
// to the top left corner. If this control shrinks but the top left corner doesn't move, we
// never receive a call to OnPaint(). If the control grows, Windows will set a recommended clipping
// region before calling OnPaint(). It will tell us only to draw the new part. We want a complete
// redraw each time. Most of our drawings are centered or stretched, so the default assumption is
// not true.
//
// ControlStyles.AllPaintingInWmPaint & ControlStyles.UserPaint should prevent windows from calling
// OnPaintBackground(). I didn't have any luck with that. I left these in place anyway because they
// didn't seem to hurt and everything I've read said they should be true.
SetStyle(ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
}
public SimpleDataNodeControl(DataNodeViewer viewer) : this()
{
Viewer = viewer;
}
protected override void Dispose(bool disposing)
{
Viewer = null;
base.Dispose(disposing);
}
private static readonly Brush ERROR_BACKGROUND = new SolidBrush(Color.Black);
private static readonly Color ERROR_FOREGROUND = Color.Red;
private static readonly Color DESIGN_MODE_FOREGROUND = Color.White;
protected override void OnPaint(PaintEventArgs e)
{
Rectangle bounds = new Rectangle(0, 0, Width, Height);
//System.Diagnostics.Debug.WriteLine(DateTime.Now.ToLongTimeString() + ": OnPaint(), Name=" + Name
// + ", Bounds=" + Bounds + ", bounds =" + bounds + ", ClipRectangle=" + e.ClipRectangle);
if (null == _data)
{
e.Graphics.FillRectangle(ERROR_BACKGROUND, bounds);
if (DesignMode)
TextRenderer.DrawText(e.Graphics, Name, Font, bounds, DESIGN_MODE_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Left);
else
TextRenderer.DrawText(e.Graphics, "null", Font, bounds, ERROR_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);
}
else if (null != _data.ErrorMessage)
{
e.Graphics.FillRectangle(ERROR_BACKGROUND, bounds);
TextRenderer.DrawText(e.Graphics, _data.ErrorMessage, Font, bounds, ERROR_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Left);
}
else
_viewer.Paint(_data, CreateGraphics(), bounds, ForeColor, BackColor, _forceColors, Font);
// Call any Paint delegates.
base.OnPaint(e);
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{ // It seems like ControlStyles.AllPaintingInWmPaint and/or ControlStyles.UserPaint
// should have prevented windows from calling this method. However, we are still
// gettings called. The default (a.k.a. "base") method will paint in the control's
// background color. That caused a lot of flickering.
//base.OnPaintBackground(pevent);
// Commenting out the line above didn't stop ALL the flickering!
// On closer examination, it seems that one of the parent controls was drawing its
// own background each time we resized. See ViewerEditor.cs. I changed the BackColor
// of these objects to something different form the container's BackColor. Then I
// could see that commenting out the base.OnPaintBackground() did help some. It
// prevented us from drawing our own background color. But it didn't help with
// everything.
// I tried double buffering in different places. That didn't help, either. I
// suspect our current setup will work great as long as you are just changing values.
// It seems that some flickering is inevitable if you have a complicated layout and
// you resize the window.
}
private bool _forceColors;
///
/// If this is true we always try to use the specified ForeColor and BackColor.
/// If this is false, ForeColor and BackColor are defaults.
/// The viewer can do what it wants with this, including ignoring it completely.
/// This is mostly for testing. Similar functionality is REQUIRED in the table.
///
public bool ForceColors
{
get { return _forceColors; }
set
{
if (_forceColors != value)
{
_forceColors = value;
Invalidate();
}
}
}
private void GetData()
{
if (null != _data)
_data.Release();
_data = null;
if ((null != _viewer) && (null != _replacements))
{
_data = Viewer.GetData(Replacements);
_data.OnChange += Invalidate;
}
Invalidate();
}
private Dictionary _replacements = new Dictionary();
[System.Xml.Serialization.XmlIgnore]
public Dictionary Replacements
{
get { return _replacements; }
set
{
_replacements = value;
GetData();
}
}
private DataNodeViewer _viewer;
///
/// Null is allowed, but it is intended as a temporary state. It might display an
/// error message aimed at a developer, not an end user. Setting this to null will
/// cancel any data requests.
///
public DataNodeViewer Viewer
{
get { return _viewer; }
set
{
_viewer = value;
GetData();
}
}
[XmlSerializerInit("*")]
public class Factory : DataNodeControlFactory, XmlSerializer.Self
{
public DataNodeViewer Viewer { get; set; }
public Factory(DataNodeViewer viewer = null)
{
Viewer = viewer;
}
public override Control Create()
{
return new SimpleDataNodeControl(Viewer);
}
public override bool Equals(object obj)
{
if (!base.Equals(obj))
return false;
Factory other = (Factory)obj;
return Equals(Viewer, other.Viewer);
}
public override int GetHashCode()
{
return MakeHashCode(Viewer);
}
private const string TYPE = "SIMPLE_DATA_NODE_CONTROL";
private const string VIEWER = "VIEWER";
public void Encode(XmlElement body)
{
body.SetProperty(XmlSerializer.TYPE, TYPE);
XmlSerializer.Encode(Viewer, body.NewNode(VIEWER));
}
private static void XmlSerializerInit()
{
XmlSerializer.RegisterDecoder(TYPE, Decoder);
}
private static object Decoder(XmlElement body)
{
XmlElement viewerXml = body.Node(VIEWER);
if (null == viewerXml)
throw new ArgumentException("Could not find " + VIEWER);
Factory result = new Factory();
result.Viewer = (DataNodeViewer)XmlSerializer.Decode(viewerXml);
return result;
}
}
}
public class SimpleDataNodePanel : Panel, IReplaceWithRuntime
{
private Dictionary _replacements;
[System.Xml.Serialization.XmlIgnore]
public Dictionary Replacements
{
get { return _replacements; }
set
{
_replacements = value;
foreach (Control control in Controls)
control.TryReplaceWith(Replacements);
}
}
public SimpleDataNodePanel()
{
ControlAdded += DoControlAdded;
}
private void DoControlAdded(object sender, System.Windows.Forms.ControlEventArgs e)
{
e.Control.TryReplaceWith(_replacements);
}
///
/// My version of System.Windows.Forms.AnchorStyles.
///
public enum StickTo
{
///
/// The distance from the bottom or right will change as you resize the window.
///
First,
///
/// The distance from the top or left will change as you resize the window.
///
Second,
///
/// The width or height will change as you resize the window.
///
Both
}
[XmlSerializerInit("*")]
public class Child : XmlSerializer.Self
{
///
/// All fields are null. This is not a valid state. It's just a convenient
/// place to start.
///
public Child() { }
///
/// Set the size/position/anchors to a reasonable default, something legal.
/// Copy the factory as is.
///
/// May be null.
public Child(DataNodeControlFactory factory)
{
Factory = factory;
Left = 5;
Width = 100;
Top = 5;
Height = 25;
}
///
/// Load from XML.
///
///
public Child(XmlElement top)
{
XmlElement factoryXml = top.Node(FACTORY);
if (null != factoryXml)
Factory = (DataNodeControlFactory)XmlSerializer.Decode(factoryXml);
// else throw new ArgumentException("Cannot find FACTORY for child.");
// We've gone both ways on that else. Typically when I serialize something
// I'm somewhat strict, and I don't like to see bad objects. Typically I'd
// do a lot of checking, and the deserialization function (like this one)
// would throw an exception rather than returning a bad value. However, it
// seems convenient to leave this factory null while you're in the process
// of building a panel.
Top = top.PropertyInt32(TOP);
Height = top.PropertyInt32(HEIGHT);
Bottom = top.PropertyInt32(BOTTOM);
Left = top.PropertyInt32(LEFT);
Width = top.PropertyInt32(WIDTH);
Right = top.PropertyInt32(RIGHT);
if (!Valid)
throw new ArgumentException("Invalid position information.");
}
private const string FACTORY = "FACTORY";
private const string PANEL_CHILD = "PANEL_CHILD";
private const string TOP = "TOP";
private const string HEIGHT = "HEIGHT";
private const string BOTTOM = "BOTTOM";
private const string LEFT = "LEFT";
private const string WIDTH = "WIDTH";
private const string RIGHT = "RIGHT";
private static void AddValue(XmlElement body, string Name, int? i)
{
if (i.HasValue)
body.SetProperty(Name, i.Value);
}
public void Encode(XmlElement body)
{
body.SetProperty(XmlSerializer.TYPE, PANEL_CHILD);
if (null != Factory)
XmlSerializer.Encode(Factory, body.NewNode(FACTORY));
AddValue(body, TOP, Top);
AddValue(body, HEIGHT, Height);
AddValue(body, BOTTOM, Bottom);
AddValue(body, LEFT, Left);
AddValue(body, WIDTH, Width);
AddValue(body, RIGHT, Right);
}
private static void XmlSerializerInit()
{
XmlSerializer.RegisterDecoder(PANEL_CHILD, top => new Child(top));
}
public DataNodeControlFactory Factory;
///
/// The top of the control. 0 means the top of the control touches the top
/// of the parent. Positive numbers mean to go down. Exactly 1 of Top, Height,
/// and Bottom should be null.
///
public int? Top;
///
/// The height of the control, in pixels. Exactly 1 of Top, Height, and
/// Bottom should be null.
///
public int? Height;
///
/// The bottom of the control. 0 means the bottom of the control touches the
/// bottom of the parent. Positive numbers mean to go up. (So this does NOT
/// match the Bottom property of a Control.) Exactly 1 of Top, Height, and
/// Bottom should be null.
///
public int? Bottom;
///
/// The left of the control. 0 means the left of the control touches the left
/// of the parent. Positive numbers mean to go right. Exactly 1 of Left,
/// Width, and Right should be null.
///
public int? Left;
///
/// The width of the control, in pixels. Exactly 1 of Left, Width, and Right
/// should be null.
///
public int? Width;
///
/// The right of the control. 0 means the right of the control touches the
/// right of the parent. Positive numbers mean to go left. (So this does NOT
/// match the Right property of a Control.) Exactly 1 of Left, Width, and
/// Right should be null.
///
public int? Right;
private static bool OneDimensionValid(int? a, int? b, int? c)
{
int count = 0;
if (a.HasValue) count++;
if (b.HasValue) count++;
if (c.HasValue) count++;
return count == 2;
}
public bool Valid
{
get { return OneDimensionValid(Top, Height, Bottom) && OneDimensionValid(Left, Width, Right); }
}
public AnchorStyles AnchorStyles
{
get
{
AnchorStyles result = AnchorStyles.None;
if (Top.HasValue) result |= AnchorStyles.Top;
if (Left.HasValue) result |= AnchorStyles.Left;
if (Bottom.HasValue) result |= AnchorStyles.Bottom;
if (Right.HasValue) result |= AnchorStyles.Right;
return result;
}
}
///
/// Find the value for the Top property of this control. Someone might have
/// directly specified that, in which case we return that as is. Alternatively
/// someone might have specified the bottom (relative to the container's bottom)
/// and the height. In that case we compute the requested value.
///
/// If Valid is false, the results of this function are undefined. This function
/// might fail an assertion.
///
/// Typically this the ClientSize of the Parent.
/// The requested Top property for this control.
public int GetTop(Size relativeTo)
{
if (Top.HasValue)
return Top.Value;
else if (Height.HasValue && Bottom.HasValue)
return (relativeTo.Height - Bottom.Value) - Height.Value;
else
// Valid is false. We could throw and exception or fail and assertion.
// Instead I'm returning a reasonable default.
return 0;
}
///
/// Find the value for the Left property of this control. Someone might have
/// directly specified that, in which case we return that as is. Alternatively
/// someone might have specified the right (relative to the container's right)
/// and the width. In that case we compute the requested value.
///
/// If Valid is false, the results of this function are undefined. This function
/// might fail an assertion.
///
/// Typically this the ClientSize of the Parent.
/// The requested Left property for this control.
public int GetLeft(Size relativeTo)
{
if (Left.HasValue)
return Left.Value;
else if (Width.HasValue && Right.HasValue)
return (relativeTo.Width - Right.Value) - Width.Value;
else
// Valid is false. We could throw and exception or fail and assertion.
// Instead I'm returning a reasonable default.
return 0;
}
public int GetHeight(Size relativeTo)
{
if (Height.HasValue)
return Height.Value;
else if (Top.HasValue && Bottom.HasValue)
return relativeTo.Height - Top.Value - Bottom.Value;
else
throw new ArgumentException("Exactly one of Height, Top, and Bottom should be null.");
}
public int GetWidth(Size relativeTo)
{
if (Width.HasValue)
return Width.Value;
else if (Left.HasValue && Right.HasValue)
return relativeTo.Width - Left.Value - Right.Value;
else
throw new ArgumentException("Exactly one of Width, Left and Right should be null.");
}
public void AddToParent(Control child, Panel parent)
{
child.Parent = null;
child.Anchor = AnchorStyles;
Size size = parent.ClientSize;
child.Top = GetTop(size);
child.Left = GetLeft(size);
// TODO what if a height or width is negative?
child.Height = GetHeight(size);
child.Width = GetWidth(size);
parent.Controls.Add(child);
}
///
/// This coresponds to the Right property of this object.
///
///
///
public int GetRightFromRight(Size relativeTo)
{
if (Right.HasValue)
return Right.Value;
return (relativeTo.Width - Left.Value) - Width.Value;
}
///
/// The coresponds to the Bottom property of this object.
///
///
///
public int GetBottomFromBottom(Size relativeTo)
{
if (Bottom.HasValue)
return Bottom.Value;
return (relativeTo.Height - Top.Value) - Height.Value;
}
///
/// This corresponds to the Right property of a normal Control.
///
///
///
public int GetRightFromLeft(Size relativeTo)
{
return relativeTo.Width - GetRightFromRight(relativeTo);
}
///
/// This corresponds to the Bottom property of a normal Control.
///
///
///
public int GetBottomFromTop(Size relativeTo)
{
return relativeTo.Height - GetBottomFromBottom(relativeTo);
}
public Control Create(Panel parent = null)
{
Control result = Factory.Create();
if (null != parent)
try
{
AddToParent(result, parent);
}
catch
{
result.Dispose();
throw;
}
return result;
}
public void MoveLeftEdgeToTheRight(int pixelsRight)
{
if (Left.HasValue)
Left += pixelsRight;
if (Width.HasValue)
Width -= pixelsRight;
}
public void MoveToTheRight(int pixelsRight)
{
if (Left.HasValue)
Left += pixelsRight;
if (Right.HasValue)
Right += pixelsRight;
}
public void MoveRightEdgeToTheRight(int pixelsRight)
{
if (Width.HasValue)
Width += pixelsRight;
if (Right.HasValue)
Right += pixelsRight;
}
public void MoveTopEdgeDown(int pixelsDown)
{
if (Top.HasValue)
Top += pixelsDown;
if (Height.HasValue)
Height -= pixelsDown;
}
public void MoveDown(int pixelsDown)
{
if (Top.HasValue)
Top += pixelsDown;
if (Bottom.HasValue)
Bottom += pixelsDown;
}
public void MoveBottomEdgeDown(int pixelsDown)
{
if (Height.HasValue)
Height += pixelsDown;
if (Bottom.HasValue)
Bottom += pixelsDown;
}
public void AutoAdjustLeft(Size relativeTo)
{
if (!Left.HasValue) return; // We're already there.
if (!Width.HasValue)
Width = GetWidth(relativeTo);
if (!Right.HasValue)
Right = GetRightFromRight(relativeTo);
Left = null;
}
public void AutoAdjustWidth(Size relativeTo)
{
if (!Width.HasValue) return; // We're already there.
if (!Left.HasValue)
Left = GetLeft(relativeTo);
if (!Right.HasValue)
Right = GetRightFromRight(relativeTo);
Width = null;
}
public void AutoAdjustRight(Size relativeTo)
{
if (!Right.HasValue) return; // We're already there.
if (!Left.HasValue)
Left = GetLeft(relativeTo);
if (!Width.HasValue)
Width = GetWidth(relativeTo);
Right = null;
}
public void AutoAdjustTop(Size relativeTo)
{
if (!Top.HasValue) return; // We're already there.
if (!Height.HasValue)
Height = GetHeight(relativeTo);
if (!Bottom.HasValue)
Bottom = GetBottomFromBottom(relativeTo);
Top = null;
}
public void AutoAdjustHeight(Size relativeTo)
{
if (!Height.HasValue) return; // We're already there.
if (!Top.HasValue)
Top = GetTop(relativeTo);
if (!Bottom.HasValue)
Bottom = GetBottomFromBottom(relativeTo);
Height = null;
}
public void AutoAdjustBottom(Size relativeTo)
{
if (!Bottom.HasValue) return; // We're already there.
if (!Top.HasValue)
Top = GetTop(relativeTo);
if (!Height.HasValue)
Height = GetHeight(relativeTo);
Bottom = null;
}
public override int GetHashCode()
{
Hasher hasher = new Hasher(typeof(Child));
hasher.Add(Factory, Top, Height, Bottom, Left, Width, Right);
return hasher.GetHashCode();
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
Child other = obj as Child;
if (null == other) return false;
return Top == other.Top && Height == other.Height && Bottom == other.Bottom
&& Left == other.Left && Width == other.Width && Right == other.Right
&& Equals(Factory, other.Factory);
}
}
public class Factory : DataNodeControlFactory
{
public override Control Create()
{
SimpleDataNodePanel panel = new SimpleDataNodePanel();
foreach (Child child in Children)
try
{
child.Create(panel);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unable to create child in SimpleDataNodePanel: " + ex);
}
return panel;
}
public readonly List Children = new List();
public override bool Equals(object obj)
{
if (!base.Equals(obj))
return false;
return Children.SequenceEqual(((Factory)obj).Children);
}
public override int GetHashCode()
{
return MakeHashCode(Children.ToArray