Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial presentation UI implementation. #19

Merged
merged 5 commits into from
May 15, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Docs/Example-Metadata-Class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Here is an example template for decorating a mocked ingame block.

```cs
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using IngameScript.Mockups.Base;
using Sandbox.ModAPI.Interfaces;
using SpaceEngineers.Game.ModAPI.Ingame;

namespace IngameScript.Mockups.Blocks
{
#if !MOCKUP_DEBUG
[System.Diagnostics.DebuggerNonUserCode]
#endif
// Decorate the class with a DisplayName attribute to have it visible in the block picker.
[DisplayName("Air Vent")]
public partial class MockAirVent : MockFunctionalBlock, IMyAirVent
{
// Decorate a property with a DisplayName attribute to have it visible in the block details screen.
// Add Range and ReadOnly attributes when appropriate to control how the property is rendered.
[DisplayName("Oxygen Level"), Range(0, 1)]
public virtual float OxygenLevel { get; set; } = 0;

[DisplayName("Can Pressurize")]
public virtual bool CanPressurize { get; set; } = true;

[DisplayName("Is Depressurizing"), ReadOnly(true)]
public virtual bool IsDepressurizing => Enabled && (Status == VentStatus.Depressurizing || Status == VentStatus.Depressurized);

[DisplayName("De-pressurize")]
public virtual bool Depressurize { get; set; } = false;

[DisplayName("Status")]
public virtual VentStatus Status { get; set; }

public virtual bool PressurizationEnabled { get; } = true;

protected override IEnumerable<ITerminalProperty> CreateTerminalProperties()
{
return base.CreateTerminalProperties().Concat(new[]
{
new MockTerminalProperty<IMyAirVent, bool>("Depressurize", b => b.Depressurize, (b, v) => b.Depressurize = v)
});
}

// Decorate methods with a DisplayName attribute to add them to the list of actions in the block details screen.
[DisplayName("Get Oxygen Level")]
public virtual float GetOxygenLevel() => OxygenLevel;

public virtual bool IsPressurized() => PressurizationEnabled && (Status == VentStatus.Pressurized || Status == VentStatus.Pressurizing);
}
}
```

Additional things to consider:
* Space Engineers only supports C# 6.0, do not use any language features from higher versions (the TestScript will help confirm this).
* Do not create `private` properties unless they are only required for your specific implementation, these classes should be easily extendable.
11 changes: 9 additions & 2 deletions Docs/Getting-Started-Contribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,20 @@ Once you've made a change you wish to share, you will need to [create a pull req

## Rules and Etiquette: MDK-UI

* To implement editable property support to a Mocked block, the block type must be decorated with the `DisplayNameAttribute`.
* Editable properties must also be decorated with the `DisplayNameAttribute`.
* Properties which are locked between two values must be decorated with the `RangeAttribute`.
* Properties which are not editable must be decorated with the `ReadOnlyAttribute`.
* To implement methods which are executable via the interface should be decorate with a `DisplayNameAttribute`.
* Methods which accept arguments are not yet supported.
* Methods which return a value are output to a MessageBox.
* MDK-UI specific code must be part of the MDK-UI project, and not implemented within MDK-Mockups
* The MDK-Mockups Shared Project must remain usable by MDK-SE projects without the use of the MDK-UI project.
* Mockups should be implemented by creating a new _partial_ component and implementing the new functionality.
* Mockup classes which support custom UI interaction must be decorated with the `IMockupDataTemplateProvider` interface.
* Mockup classes which support realtime runtime updates (such as doors and lights) must implement the `IMockupRuntimeProvider` interface.
* If a mockup's existing implementation must be changed to facilty UI interaction (for example by replacing an already mocked method), the mockup class should be marked with the `[MockOverridden]` attribute and a sub-class created.


* Example metadata implementation. [MockAirVentMetadata.cs](Example-Metadata-Class.md)


### None Version-Controlled Configuration
Expand Down
5 changes: 0 additions & 5 deletions MDK-UI/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using Malware.MDKUtilities;
using Microsoft.Win32;
using Sandbox.ModAPI;

namespace MDK_UI
{
Expand Down
6 changes: 5 additions & 1 deletion MDK-UI/Dialogs/AddBlockDialogBox.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MDK_UI"
xmlns:tc="clr-namespace:MDK_UI.TemplateConverters"
mc:Ignorable="d"
Title="Add New Block" Width="300" WindowStyle="ToolWindow"
ResizeMode="NoResize" WindowStartupLocation="CenterOwner"
SizeToContent="Height"
DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}"
FocusManager.FocusedElement="{Binding ElementName=BlockName}">
<Window.Resources>
<tc:DisplayNameConverter x:Key="displayNameConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
Expand All @@ -30,7 +34,7 @@
<ComboBox x:Name="BlockType" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" ItemsSource="{Binding AvailableTypes}" Margin="2">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=TemplateDisplayName}" />
<TextBlock Text="{Binding Converter={StaticResource displayNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Expand Down
21 changes: 7 additions & 14 deletions MDK-UI/Dialogs/AddBlockDialogBox.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using IngameScript.Mockups.Base;
using MDK_UI.MockupExtensions;
using Sandbox.ModAPI.Ingame;

namespace MDK_UI
{
Expand All @@ -22,21 +13,23 @@ namespace MDK_UI
/// </summary>
public partial class AddBlockDialogBox : Window
{
private static Type BaseType { get; } = typeof(IMockupDataTemplateProvider);
private static Type BaseType { get; } = typeof(IMyTerminalBlock);
private static Type SelectorType { get; } = typeof(DisplayNameAttribute);
private static Type OverriddenType { get; } = typeof(MockOverriddenAttribute);

public delegate void BlockSubmittedEventHandler(object sender, string title, Type type);
public event BlockSubmittedEventHandler OnSubmit;

public IEnumerable<IMockupDataTemplateProvider> AvailableTypes { get; }
public IEnumerable<IMyTerminalBlock> AvailableTypes { get; }

public AddBlockDialogBox()
{
AvailableTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract && BaseType.IsAssignableFrom(t)))
.Where(t => t.CustomAttributes.Any(a => a.AttributeType == SelectorType))
.Where(t => !t.CustomAttributes.Any(a => a.AttributeType == OverriddenType))
.Select(t => Activator.CreateInstance(t))
.OfType<IMockupDataTemplateProvider>()
.OfType<IMyTerminalBlock>()
.ToList();

InitializeComponent();
Expand Down
14 changes: 1 addition & 13 deletions MDK-UI/Dialogs/AddGroupDialogBox.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows;

namespace MDK_UI
{
Expand Down
197 changes: 197 additions & 0 deletions MDK-UI/Extensions/ReflectionBindingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using MDK_UI.TemplateConverters;
using Xceed.Wpf.Toolkit;
using MessageBox = System.Windows.MessageBox;

namespace MDK_UI.Extensions
{
static class ReflectionBindingExtensions
{
public static UIElement ToUiElement(this PropertyInfo prop, object target, PropertyInfo parent = null)
{
switch (prop.GetDataType())
{
case DataType.MultilineText:
var textArea = new TextBox()
{
Height = 200,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
AcceptsReturn = true,
IsReadOnly = prop.IsReadOnly()
};

textArea.SetBinding(TextBox.TextProperty, prop.GetBinding(target));
return textArea;
default:
var type = parent?.PropertyType ?? prop.PropertyType;

if (type == typeof(bool))
{
var checkBox = new CheckBox
{
IsEnabled = !prop.IsReadOnly(),
VerticalAlignment = VerticalAlignment.Center
};

checkBox.SetBinding(ToggleButton.IsCheckedProperty, prop.GetBinding(target));
return checkBox;
}
else if (type == typeof(int) || type == typeof(float))
{
if (prop.HasAttribute<RangeAttribute>())
{
var values = prop.GetCustomAttribute<RangeAttribute>();
var range = new Slider
{
Minimum = (double)values.Minimum,
Maximum = (double)values.Maximum,
IsEnabled = !prop.IsReadOnly(),
VerticalAlignment = VerticalAlignment.Center
};

if (type == typeof(int))
{
range.TickFrequency = 1;
}
else
{
range.TickFrequency = 0.01;
}

range.SetBinding(RangeBase.ValueProperty, prop.GetBinding(target));
return range;
}
else
{
var range = new SingleUpDown
{
IsReadOnly = !prop.IsReadOnly(),
VerticalAlignment = VerticalAlignment.Center
};

if (type == typeof(int))
{
range.Increment = 1;
}
else
{
range.Increment = 0.01f;
}

range.SetBinding(SingleUpDown.ValueProperty, prop.GetBinding(target));
return range;
}
}
else if (type == typeof(VRageMath.Color))
{
var colorPicker = new ColorPicker
{
VerticalAlignment = VerticalAlignment.Center
};

var binder = prop.GetBinding(target);
binder.Converter = new ColorConverter();

colorPicker.SetBinding(ColorPicker.SelectedColorProperty, binder);
return colorPicker;
}
else if (type == typeof(string))
{
var textBox = new TextBox
{
AcceptsReturn = false,
IsReadOnly = prop.IsReadOnly()
};

textBox.SetBinding(TextBox.TextProperty, prop.GetBinding(target));

return textBox;
}
else if (type.IsEnum)
{
var comboBox = new ComboBox
{
IsReadOnly = prop.IsReadOnly(),
IsEnabled = !prop.IsReadOnly()
};

foreach (var value in Enum.GetValues(type))
{
comboBox.Items.Add(value);
}

var binding = prop.GetBinding(target);
//binding.Converter = new DisplayNameConverter();

comboBox.SetBinding(Selector.SelectedItemProperty, binding);
return comboBox;
}
else
{
return new TextBlock()
{
Text = $"Unsupported property type {type.Name}."
};
}
}
}

public static UIElement ToUIElement(this MethodInfo method, object target, MethodInfo parent = null)
{
var name = method.GetCustomAttribute<DisplayNameAttribute>().DisplayName;
var element = new Button
{
Content = name
};

method = parent ?? method;
if (method.GetParameters().Any())
{
element.Click += UnsupportedMethod;
}
else
{
element.Click += (sender, args) =>
{
var result = method.Invoke(target, new object[] { });

if (method.ReturnType != typeof(void))
element.InvokeMessageBox($"{name} returned:\n{result}", "Action Result", MessageBoxButton.OK, MessageBoxImage.Information);
};
}

return element;
}

private static void InvokeMessageBox(this UIElement element, string message, string caption, MessageBoxButton button, MessageBoxImage icon)
{
element.Dispatcher.Invoke(() => MessageBox.Show(message, caption, button, icon));
}

private static RoutedEventHandler UnsupportedMethod { get; } = (sender, args) =>
{
(sender as UIElement).InvokeMessageBox("Executing actions with parameters is not yet supported.", "Not Supported", MessageBoxButton.OK, MessageBoxImage.Exclamation);
};

private static bool IsReadOnly(this PropertyInfo prop)
=> !prop.CanWrite || (prop.GetCustomAttribute<ReadOnlyAttribute>()?.IsReadOnly ?? false);

private static DataType GetDataType(this PropertyInfo prop)
=> prop.GetCustomAttribute<DataTypeAttribute>()?.DataType ?? DataType.Text;

private static Binding GetBinding(this PropertyInfo prop, object target)
=> new Binding(prop.Name)
{
Mode = prop.IsReadOnly() ? BindingMode.OneWay : BindingMode.Default,
Source = target
};
}
}
Loading