Skip to content

Commit

Permalink
Merge pull request #19 from malware-dev/mockup-ui-presentation
Browse files Browse the repository at this point in the history
Initial presentation UI implementation.
  • Loading branch information
kwilliams1987 authored May 15, 2019
2 parents 5eef267 + 061f3a5 commit bb0c593
Show file tree
Hide file tree
Showing 39 changed files with 576 additions and 537 deletions.
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

0 comments on commit bb0c593

Please sign in to comment.