Skip to content

Commit

Permalink
feat: add LINQPad 'new ui'-based editors
Browse files Browse the repository at this point in the history
  • Loading branch information
rdavisau committed May 4, 2019
1 parent ab7d3ac commit 537f975
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 74 deletions.
90 changes: 19 additions & 71 deletions src/DumpEditable/EditorRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,29 +63,18 @@ public static EditorRule ForNestedAnonymousType()
public static EditorRule ForEnums() =>
EditorRule.For(
(_, p) => p.PropertyType.IsEnum,
(o, p, get, set) =>
Util.HorizontalRun(true,
Enumerable.Concat(
new object[] { get(), "[" },
p.PropertyType
.GetEnumValues()
.OfType<object>()
.Select(v => new Hyperlinq(() => set(v), $"{v}")))
.Concat(new [] { "]" }))
);
(o, p, get, set) => EditableDumpContainer.DefaultOptions.OptionsEditor(
p.PropertyType.GetEnumValues().OfType<object>(),
false,
null)(o,p,get,set));

public static EditorRule ForBool() =>
EditorRule.For(
(_, p) => p.PropertyType == typeof(bool) || p.PropertyType == typeof(bool?),
(o, p, get, set) =>
Util.HorizontalRun(true,
Enumerable.Concat(
new object[] { get() ?? NullString, "[" },
new bool?[] { true, false, null }
.Where(b => p.PropertyType == typeof(bool?) || b != null)
.Select(v => new Hyperlinq(() => set(v), $"{(object)v ?? NullString }")))
.Concat(new[] { "]" }))
);
(o, p, get, set) => EditableDumpContainer.DefaultOptions.OptionsEditor(
new [] { true, false }.OfType<object>(),
p.PropertyType == typeof(bool?),
null)(o, p, get, set));

public static EditorRule ForTypeWithStringBasedEditor<T>(ParseFunc<string, T, bool> parseFunc, bool supportNullable = true, bool supportEnumerable = true)
=> new EditorRule
Expand All @@ -94,63 +83,22 @@ public static EditorRule ForTypeWithStringBasedEditor<T>(ParseFunc<string, T, bo
info.PropertyType == typeof(T)
|| (supportNullable && Nullable.GetUnderlyingType(info.PropertyType) == typeof(T))
|| (supportEnumerable && info.PropertyType.GetArrayLikeElementType() == typeof(T)),
Editor = (o, info, get, set) => GetStringInputBasedEditor(o, info, get, set, parseFunc, supportNullable, supportEnumerable)
Editor = (o, info, get, set) => EditableDumpContainer.DefaultOptions.StringBasedEditor(WrapParseFunc(parseFunc), supportNullable, supportEnumerable)(o, info, get, set),
DisableAutomaticRefresh = true,
};

protected static object GetStringInputBasedEditor<TOut>(object o, PropertyInfo p, Func<object> getCurrValue, Action<object> setNewValue, EditorRule.ParseFunc<string, TOut, bool> parseFunc,
bool supportNullable = true, bool supportEnumerable = true)
{
var type = p.PropertyType;
var currVal = getCurrValue();
var isEnumerable = supportEnumerable && type.GetArrayLikeElementType() != null;

// handle string which is IEnumerable<char>
if (typeof(TOut) == typeof(string) && type.GetArrayLikeElementType() == typeof(char))
isEnumerable = false;

var desc = currVal == null
? NullString
: (isEnumerable ? JsonConvert.SerializeObject(currVal) : $"{currVal}");

// hyperlinq doesn't like empty strings
if (desc == String.Empty)
desc = EmptyString;

var change = new Hyperlinq(() =>
private static ParseFunc<string, object, bool> WrapParseFunc<T>(ParseFunc<string, T, bool> parseFunc)
=> (string input, out object output) =>
{
var newVal = Interaction.InputBox("Set value for " + p.Name, p.Name, desc != EmptyString ? desc : String.Empty);
var ret = parseFunc(input, out var tOut);
output = tOut;
var canConvert = parseFunc(newVal, out var output);
if (isEnumerable)
{
try
{
var val = JsonConvert.DeserializeObject(newVal, type);
setNewValue(val);
}
catch
{
return; // can't deserialise
}
}
else if (canConvert)
{
setNewValue(output);
}
else if (supportNullable && (newVal == String.Empty))
{
setNewValue(null);
}
else
return; // can't convert
}, desc);
return ret;
};

return Util.HorizontalRun(true, change);
}

public delegate V ParseFunc<T, U, V>(T input, out U output);

private const string NullString = "(null)";
private const string EmptyString = "(empty string)";
public const string NullString = "(null)";
public const string EmptyString = "(empty string)";
}
}
241 changes: 241 additions & 0 deletions src/DumpEditable/Editors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LINQPad.Controls;
using LINQPad.DumpEditable.Helpers;
using Microsoft.VisualBasic;
using Newtonsoft.Json;

namespace LINQPad.DumpEditable
{
public static class Editors
{
public static Func<object, PropertyInfo, Func<object>, Action<object>, object> ChoicesWithRadioButtons<T>(
IEnumerable<T> choices, bool allowNull, Func<T, string> toString = null) =>
ChoicesWithRadioButtons(choices.OfType<object>(), allowNull, o => toString((T) o));

public static Func<object, PropertyInfo, Func<object>, Action<object>, object> ChoicesWithRadioButtons(
IEnumerable<object> choices, bool allowNull, Func<object, string> toString = null) =>
(o, p, gv, sv) =>
{
var group = Guid.NewGuid().ToString();
var v = gv();
var radioButtons =
choices
.Select(x => new RadioButton(@group, toString?.Invoke(x) ?? $"{x}", x.Equals(v), b => sv(x)))
.ToList();
if (allowNull)
radioButtons.Add(new RadioButton(@group, NullString, v == null, _ => sv(null)));
return Util.HorizontalRun((bool)true, (IEnumerable)radioButtons);
};

public static Func<object, PropertyInfo, Func<object>, Action<object>, object> ChoicesWithHyperlinqs<T>(
IEnumerable<T> choices, bool allowNull, Func<T, string> toString = null) =>
(o, p, gv, sv) =>
{
var preceding = new object[] {gv(), "["};
var trailing = new object[] {"]"};
var values = choices.Select(x => new Hyperlinq(() => sv(x), toString?.Invoke(x) ?? $"{x}")).ToList();
if (allowNull)
values.Add(new Hyperlinq(() => sv(null), NullString ));
return Util.HorizontalRun(
true,
Enumerable.Concat(
new object[] {gv(), "["},
choices.Select(x => new Hyperlinq(() => sv(x), toString?.Invoke(x) ?? $"{x}")))
.Concat(new[] {"]"}));
};

public static Func<EditorRule.ParseFunc<string, object, bool>, bool, bool, Func<object, PropertyInfo, Func<object>, Action<object>, object>> TextBoxBasedStringEditor
(bool liveUpdates) => (parse, nullable, enumerable) => (o, p, gv, sv) =>
Editors.StringWithTextBox(o, p, gv, sv, parse, nullable, enumerable, liveUpdates);

public static Func<EditorRule.ParseFunc<string, object, bool>, bool, bool, Func<object, PropertyInfo, Func<object>, Action<object>, object>> InputBoxBasedStringEditor
=> (parse, nullable, enumerable) => (o, p, gv, sv) =>
Editors.StringWithInputBox(o, p, gv, sv, parse, nullable, enumerable);

internal static object StringWithTextBox<TOut>(object o, PropertyInfo p, Func<object> gv, Action<object> sv,
EditorRule.ParseFunc<string, TOut, bool> parseFunc,
bool supportNullable = true, bool supportEnumerable = true, bool liveUpdate = true)
=> StringWithTextBox(o, p, gv, sv, (string input, out object output) =>
{
var ret = parseFunc(input, out var outT);
output = outT;
return ret;
}, supportNullable, supportEnumerable, liveUpdate);

public static object StringWithTextBox(object o, PropertyInfo p, Func<object> gv, Action<object> sv, EditorRule.ParseFunc<string, object, bool> parseFunc,
bool supportNullable = true, bool supportEnumerable = true, bool liveUpdate = true)
{
var type = p.PropertyType;
var isEnumerable = supportEnumerable && type.GetArrayLikeElementType() != null;

// handle string which is IEnumerable<char>
if (type == typeof(string) && type.GetArrayLikeElementType() == typeof(char))
isEnumerable = false;

string GetStringRepresentationForValue()
{
var v = gv();

var desc = v == null
? NullString
: (isEnumerable ? JsonConvert.SerializeObject(v) : $"{v}");

// hyperlinq doesn't like empty strings
if (desc == String.Empty)
desc = EmptyString;

return desc;
}

bool TryGetParsedValue(string str, out object @out)
{
var canConvert = parseFunc(str, out var output);
if (isEnumerable)
{
try
{
var val = JsonConvert.DeserializeObject(str, type);
@out = val;
return true;
}
catch
{
@out = null;
return false; // can't deserialise
}
}

if (canConvert)
{
@out = output;
return true;
}

if (supportNullable && (str == String.Empty))
{
@out = null;
return true;
}

@out = null;
return false; // can't convert
}

var updateButton = new Button("update") { Visible = false };

Action<ITextControl> onText = t =>
{
var canParse = TryGetParsedValue(t.Text, out var newValue);
if (liveUpdate && canParse)
sv(newValue);
else
updateButton.Visible = canParse;
};

var initialText = GetStringRepresentationForValue() ?? "";
var s = !isEnumerable
? (ITextControl) new TextBox(initialText, "18em", onText) { IsMultithreaded = true }
: (ITextControl) new TextArea(initialText, 40, onText) { IsMultithreaded = true };

updateButton.Click += (sender, e) =>
{
if (!TryGetParsedValue(s.Text, out var newValue)) return;
sv(newValue);
updateButton.Visible = false;
};

var dc = new DumpContainer
{
Style = "text-align: center; vertical-align: middle;",
Content = Util.HorizontalRun(true, s, updateButton)
};

return dc;
}

internal static object StringWithInputBox<TOut>(object o, PropertyInfo p, Func<object> getCurrValue, Action<object> setNewValue, EditorRule.ParseFunc<string, TOut, bool> parseFunc,
bool supportNullable = true, bool supportEnumerable = true)
{
var type = p.PropertyType;
var isEnumerable = supportEnumerable && type.GetArrayLikeElementType() != null;

// handle string which is IEnumerable<char>
if (type == typeof(string) && type.GetArrayLikeElementType() == typeof(char))
isEnumerable = false;

string GetStringDescription()
{
var currVal = getCurrValue();
var val = currVal == null
? NullString
: (isEnumerable ? JsonConvert.SerializeObject(currVal) : $"{currVal}");

// hyperlinq doesn't like empty strings
if (val == String.Empty)
val = EmptyString;

return val;
}

var dc = new DumpContainer();

Hyperlinq Update()
{
var desc = GetStringDescription();

return new Hyperlinq(() =>
{
var newVal = Interaction.InputBox("Set value for " + p.Name, p.Name,
desc != EmptyString ? desc : String.Empty);
var canConvert = parseFunc(newVal, out var output);
if (isEnumerable)
{
try
{
var val = JsonConvert.DeserializeObject(newVal, type);
setNewValue(val);
dc.Content = Update();
}
catch
{
return; // can't deserialise
}
}
else if (canConvert)
{
setNewValue(output);
dc.Content = Update();
}
else if (supportNullable && (newVal == String.Empty))
{
setNewValue(null);
dc.Content = Update();
}
else
return; // can't convert
}, desc);
}

dc.Content = Update();

return Util.HorizontalRun(true, dc);
}

public const string NullString = "(null)";
public const string EmptyString = "(empty string)";
}
}
17 changes: 14 additions & 3 deletions src/DumpEditable/Models/DumpEditableOptions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace LINQPad.DumpEditable.Models
{
public class DumpEditableOptions
{
public static DumpEditableOptions Defaults => new DumpEditableOptions();
public bool AutomaticallyKeepQueryRunning { get; set; } = true;
public bool FailSilently { get; set; } = false;
public static DumpEditableOptions Defaults => new DumpEditableOptions
{
AutomaticallyKeepQueryRunning = true,
FailSilently = false,
OptionsEditor = Editors.ChoicesWithRadioButtons,
StringBasedEditor = Editors.TextBoxBasedStringEditor(false),
};

public bool AutomaticallyKeepQueryRunning { get; set; }
public bool FailSilently { get; set; }

public Func<IEnumerable<object>, bool, Func<object,string>, Func<object, PropertyInfo, Func<object>, Action<object>, object>> OptionsEditor;
public Func<EditorRule.ParseFunc<string, object, bool>, bool, bool, Func<object, PropertyInfo, Func<object>, Action<object>, object>> StringBasedEditor;
}
}

0 comments on commit 537f975

Please sign in to comment.