Add a UI to set panel animations.

This is driven by the config tool, which needs to stay running.
This commit is contained in:
Glenn Maynard 2018-11-08 16:00:20 -06:00
parent e5e75e0106
commit 135e3c5401
9 changed files with 305 additions and 26 deletions

View File

@ -1,3 +1,45 @@
// Handle playing GIF animations from inside SMXConfig.
//
// This can load two GIF animations, one for when panels are released
// and one for when they're pressed, and play them automatically on the
// pad in the background. Applications that control lights can do more
// sophisticated things with the lights, but this gives an easy way for
// people to create simple animations.
//
// If you're implementing the SDK in a game, you don't need this and should
// use SMX.h instead.
//
// An animation is a single GIF with animations for all panels, in the
// following layout:
//
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// 0000|1111|2222
// --------------
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// 3333|4444|5555
// --------------
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// 6666|7777|8888
// x-------------
//
// The - | regions are ignored and are only there to space out the animation
// to make it easier to view.
//
// The extra bottom row is a flag row and should normally be black. The first
// pixel (bottom-left) optionally marks a loop frame. By default, the animation
// plays all the way through and then loops back to the beginning. If the loop
// frame pixel is white, it marks a frame to loop to instead of the beginning.
// This allows pressed animations to have a separate lead-in and loop.
//
// Each animation is for a single pad. You can load the same animation for both
// pads or use different ones.
#include "SMXPanelAnimation.h" #include "SMXPanelAnimation.h"
#include "SMXManager.h" #include "SMXManager.h"
#include "SMXDevice.h" #include "SMXDevice.h"
@ -185,10 +227,7 @@ namespace
} }
namespace { namespace {
// Given a 23x24 graphic frame and a panel number, return an array of 25 colors, containing // The X,Y positions of each possible panel.
// each light in the order it's sent to the master controller.
void ConvertToPanelGraphic(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
vector<pair<int,int>> graphic_positions = { vector<pair<int,int>> graphic_positions = {
{ 0,0 }, { 0,0 },
{ 1,0 }, { 1,0 },
@ -201,21 +240,20 @@ namespace {
{ 2,2 }, { 2,2 },
}; };
// Given a 14x15 graphic frame and a panel number, return an array of 16 colors, containing
// each light in the order it's sent to the master controller.
void ConvertToPanelGraphic(const SMXGif::GIFImage &src, vector<SMXGif::Color> &dst, int panel)
{
dst.clear(); dst.clear();
// The top-left corner for this panel: // The top-left corner for this panel:
int x = graphic_positions[panel].first * 8; int x = graphic_positions[panel].first * 5;
int y = graphic_positions[panel].second * 8; int y = graphic_positions[panel].second * 5;
// Add the 4x4 grid first. // Add the 4x4 grid.
for(int dy = 0; dy < 4; ++dy) for(int dy = 0; dy < 4; ++dy)
for(int dx = 0; dx < 4; ++dx) for(int dx = 0; dx < 4; ++dx)
dst.push_back(src.get(x+dx*2, y+dy*2)); dst.push_back(src.get(x+dx, y+dy));
// Add the 3x3 grid.
for(int dy = 0; dy < 3; ++dy)
for(int dx = 0; dx < 3; ++dx)
dst.push_back(src.get(x+dx*2+1, y+dy*2+1));
} }
} }

View File

@ -33,6 +33,11 @@ namespace smx_config
SMX_Internal_OpenConsole(); SMX_Internal_OpenConsole();
CurrentSMXDevice.singleton = new CurrentSMXDevice(); CurrentSMXDevice.singleton = new CurrentSMXDevice();
// Load animations, and tell the SDK to handle auto-lighting as long as
// we're running.
Helpers.LoadSavedPanelAnimations();
SMX.SMX.LightsAnimation_SetAuto(true);
} }
private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)

View File

@ -2,7 +2,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Resources;
using System.Windows.Threading; using System.Windows.Threading;
using SMXJSON; using SMXJSON;
@ -227,6 +229,34 @@ namespace smx_config
v = M; v = M;
} }
// Return our settings directory, creating it if it doesn't exist.
public static string GetSettingsDirectory()
{
string result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/StepManiaX/";
System.IO.Directory.CreateDirectory(result);
return result;
}
public static byte[] ReadFileFromSettings(string filename)
{
string outputFilename = GetSettingsDirectory() + filename;
try {
return System.IO.File.ReadAllBytes(outputFilename);
} catch {
// If the file doesn't exist or can't be read for some other reason, just
// return null.
return null;
}
}
public static void SaveFileToSettings(string filename, byte[] data)
{
string outputFilename = GetSettingsDirectory() + filename;
string directory = System.IO.Path.GetDirectoryName(outputFilename);
System.IO.Directory.CreateDirectory(directory);
System.IO.File.WriteAllBytes(outputFilename, data);
}
// Read path. If an error is encountered, return "". // Read path. If an error is encountered, return "".
public static string ReadFile(string path) public static string ReadFile(string path)
{ {
@ -238,6 +268,70 @@ namespace smx_config
return ""; return "";
} }
} }
// Read path. If an error is encountered, return null.
public static byte[] ReadBinaryFile(string path)
{
try {
return System.IO.File.ReadAllBytes(path);
}
catch(System.IO.IOException)
{
return null;
}
}
public static Dictionary<SMX.SMX.LightsType, string> LightsTypeNames = new Dictionary<SMX.SMX.LightsType, string>()
{
{ SMX.SMX.LightsType.LightsType_Pressed, "pressed" },
{ SMX.SMX.LightsType.LightsType_Released, "released" },
};
// Load any saved animations from disk. If useDefault is true, load the default
// animation even if there's a user animation saved.
public static void LoadSavedPanelAnimations(bool useDefault=false)
{
for(int pad = 0; pad < 2; ++pad)
{
foreach(var it in LightsTypeNames)
LoadSavedAnimationType(pad, it.Key, useDefault);
}
}
public static void SaveAnimationToDisk(int pad, SMX.SMX.LightsType type, byte[] data)
{
string filename = LightsTypeNames[type] + ".gif";
string path = "Animations/Pad" + (pad+1) + "/" + filename;
Helpers.SaveFileToSettings(path, data);
}
// Read a saved PanelAnimation.
//
// Data will always be returned. If the user hasn't saved anything, we'll return
// our default animation.
public static byte[] ReadSavedAnimationType(int pad, SMX.SMX.LightsType type, bool useDefault=false)
{
string filename = LightsTypeNames[type] + ".gif";
string path = "Animations/Pad" + (pad+1) + "/" + filename;
byte[] gif = Helpers.ReadFileFromSettings(path);
if(gif == null || useDefault)
{
// If the user has never loaded a file, load our default.
Uri url = new Uri("pack://application:,,,/Resources/" + filename);
StreamResourceInfo info = Application.GetResourceStream(url);
gif = new byte[info.Stream.Length];
info.Stream.Read(gif, 0, gif.Length);
}
return gif;
}
// Load a PanelAnimation from disk.
public static void LoadSavedAnimationType(int pad, SMX.SMX.LightsType type, bool useDefault=false)
{
byte[] gif = ReadSavedAnimationType(pad, type, useDefault);
string error;
SMX.SMX.LightsAnimation_Load(gif, pad, type, out error);
}
} }
// This class just makes it easier to assemble binary command packets. // This class just makes it easier to assemble binary command packets.
@ -304,6 +398,11 @@ namespace smx_config
if(LightsTimer.IsEnabled) if(LightsTimer.IsEnabled)
return; return;
// We normally leave lights animation control enabled while this application is
// running. Turn it off temporarily while we're showing the lights sample, or the
// two will fight.
SMX.SMX.LightsAnimation_SetAuto(false);
// Don't wait for an interval to send the first update. // Don't wait for an interval to send the first update.
//AutoLightsColorRefreshColor(); //AutoLightsColorRefreshColor();
@ -314,8 +413,8 @@ namespace smx_config
{ {
LightsTimer.Stop(); LightsTimer.Stop();
// Reenable auto-lights immediately, without waiting for lights to time out. // Turn lighting control back on.
SMX.SMX.ReenableAutoLights(); SMX.SMX.LightsAnimation_SetAuto(true);
} }
private void AutoLightsColorRefreshColor() private void AutoLightsColorRefreshColor()

View File

@ -162,6 +162,24 @@ Use if the platform is too sensitive.</clr:String>
</Setter> </Setter>
</Style> </Style>
<!-- SelectableButton changes color when selected. -->
<Style TargetType="{x:Type controls:SelectableButton}">
<Style.Triggers>
<Trigger Property="Selected" Value="True">
<Setter Property="BorderThickness" Value="2" />
<!--<Setter Property="Foreground" Value="#FFF" />
<Setter Property="Background" Value="#04F" />-->
</Trigger>
<!--
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Foreground" Value="#FFF" />
<Setter Property="Background" Value="#04F" />
</Trigger>
-->
</Style.Triggers>
</Style>
<Style TargetType="{x:Type controls:PresetButton}"> <Style TargetType="{x:Type controls:PresetButton}">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
@ -643,7 +661,6 @@ Use if the platform is too sensitive.</clr:String>
<TabControl x:Name="Main" Margin="0,0,0,0" Visibility='Visible' HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> <TabControl x:Name="Main" Margin="0,0,0,0" Visibility='Visible' HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<TabItem Header="Settings"> <TabItem Header="Settings">
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5"> <Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5">
<StackPanel Margin="0,0,0,0" VerticalAlignment="Top"> <StackPanel Margin="0,0,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top" <TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center" TextAlignment="Center"
@ -664,6 +681,26 @@ Use if the platform is too sensitive.</clr:String>
<Separator Margin="0,10,0,4" /> <Separator Margin="0,10,0,4" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
Margin="0,0,0,10">
<controls:SelectableButton x:Name="PanelColorsButton" Content="Panel colors" Padding="15,2" Margin="5,10" Click="PressedColorModeButton" />
<controls:SelectableButton x:Name="GIFAnimationsButton" Content="GIF animations" Padding="15,2" Margin="5,10" Click="PressedColorModeButton" />
</StackPanel>
<StackPanel Name="GIFGroup" HorizontalAlignment="Center" Orientation="Vertical">
<TextBlock TextAlignment="Center">
Load panel animations
</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button Content="Load idle GIF" Padding="5,2" Margin="5,10" Name="LoadIdle" Click="LoadGIF" />
<Button Content="Load pressed GIF" Padding="5,2" Margin="5,10" Name="LoadPressed" Click="LoadGIF" />
</StackPanel>
<TextBlock Name="LeaveRunning" TextAlignment="Center">
Leave this application running to play animations.
</TextBlock>
</StackPanel>
<StackPanel <StackPanel
Visibility="Visible" Visibility="Visible"
Name="ColorPickerGroup" HorizontalAlignment="Center" Orientation="Vertical"> Name="ColorPickerGroup" HorizontalAlignment="Center" Orientation="Vertical">

View File

@ -71,6 +71,27 @@ namespace smx_config
}; };
} }
private void PressedColorModeButton(object sender, RoutedEventArgs e)
{
// The user pressed either the "panel colors" or "GIF animations" button.
bool pressedPanelColors = sender == PanelColorsButton;
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
{
SMX.SMXConfig config = activePad.Item2;
// If we're in panel colors mode, clear the AutoLightingUsePressedAnimations flag.
// Otherwise, set it.
if(pressedPanelColors)
config.configFlags &= ~SMX.SMXConfigFlags.SMXConfigFlags_AutoLightingUsePressedAnimations;
else
config.configFlags |= SMX.SMXConfigFlags.SMXConfigFlags_AutoLightingUsePressedAnimations;
SMX.SMX.SetConfig(activePad.Item1, config);
}
CurrentSMXDevice.singleton.FireConfigurationChanged(null);
}
private void LoadUIFromConfig(LoadFromConfigDelegateArgs args) private void LoadUIFromConfig(LoadFromConfigDelegateArgs args)
{ {
bool EitherControllerConnected = args.controller[0].info.connected || args.controller[1].info.connected; bool EitherControllerConnected = args.controller[0].info.connected || args.controller[1].info.connected;
@ -80,6 +101,26 @@ namespace smx_config
PanelColorP1.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed; PanelColorP1.Visibility = args.controller[0].info.connected? Visibility.Visible:Visibility.Collapsed;
PanelColorP2.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed; PanelColorP2.Visibility = args.controller[1].info.connected? Visibility.Visible:Visibility.Collapsed;
// Show the color slider or GIF UI depending on which one is set in flags.
// If both pads are turned on, just use the first one.
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
{
SMX.SMXConfig config = activePad.Item2;
// If SMXConfigFlags_AutoLightingUsePressedAnimations is set, show the GIF UI.
// If it's not set, show the color slider UI.
SMX.SMXConfigFlags flags = config.configFlags;
bool usePressedAnimations = (flags & SMX.SMXConfigFlags.SMXConfigFlags_AutoLightingUsePressedAnimations) != 0;
ColorPickerGroup.Visibility = usePressedAnimations? Visibility.Collapsed:Visibility.Visible;
GIFGroup.Visibility = usePressedAnimations? Visibility.Visible:Visibility.Collapsed;
// Tell the color mode buttons which one is selected, to set the button highlight.
PanelColorsButton.Selected = !usePressedAnimations;
GIFAnimationsButton.Selected = usePressedAnimations;
break;
}
RefreshConnectedPadList(args); RefreshConnectedPadList(args);
// If a second controller has connected and we're on Both, see if we need to prompt // If a second controller has connected and we're on Both, see if we need to prompt
@ -259,5 +300,48 @@ namespace smx_config
CurrentSMXDevice.singleton.FireConfigurationChanged(null); CurrentSMXDevice.singleton.FireConfigurationChanged(null);
} }
private void LoadGIF(object sender, RoutedEventArgs e)
{
// If the "load idle GIF" button was pressed, load the released animation.
// Otherwise, load the pressed animation.
bool pressed = sender == this.LoadPressed;
// Prompt for a file to read.
Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog();
dialog.FileName = "Select an animated GIF";
dialog.DefaultExt = ".gif";
dialog.Filter = "Animated GIF (.gif)|*.gif";
bool? result = dialog.ShowDialog();
if(result == null || !(bool) result)
return;
byte[] buf = Helpers.ReadBinaryFile(dialog.FileName);
SMX.SMX.LightsType type = pressed? SMX.SMX.LightsType.LightsType_Pressed:SMX.SMX.LightsType.LightsType_Released;
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
{
int pad = activePad.Item1;
// Load the animation.
string error;
if(!SMX.SMX.LightsAnimation_Load(buf, pad, type, out error))
{
// Any errors here are problems with the GIF, so there's no point trying
// to load it for the second pad if the first returns an error. Just show the
// error and stop.
MessageBox.Show(error, "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
// Return without saving to settings on error.
return;
}
// Save the GIF to disk so we can load it quickly later.
Helpers.SaveAnimationToDisk(pad, type, buf);
// Refresh after loading a GIF to update the "Leave this application running" text.
CurrentSMXDevice.singleton.FireConfigurationChanged(null);
}
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

View File

@ -191,6 +191,10 @@
<Name>SMX</Name> <Name>SMX</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="Resources\pressed.gif" />
<Resource Include="Resources\released.gif" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -7,6 +7,7 @@ using System.Windows.Input;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Shapes; using System.Windows.Shapes;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.ComponentModel;
namespace smx_config namespace smx_config
{ {
@ -260,6 +261,17 @@ namespace smx_config
} }
} }
// A button with a selectable highlight.
public class SelectableButton: Button
{
public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected",
typeof(bool), typeof(SelectableButton), new FrameworkPropertyMetadata(false));
public bool Selected {
get { return (bool) GetValue(SelectedProperty); }
set { SetValue(SelectedProperty, value); }
}
}
// A button that selects a preset, and shows a checkmark if that preset is set. // A button that selects a preset, and shows a checkmark if that preset is set.
public class PresetButton: Control public class PresetButton: Control
{ {