Add a UI to set panel animations.

This is driven by the config tool, which needs to stay running.
master
Glenn Maynard 6 years ago
parent e5e75e0106
commit 135e3c5401
  1. 82
      sdk/Windows/SMXPanelAnimation.cpp
  2. 5
      smx-config/App.xaml.cs
  3. 103
      smx-config/Helpers.cs
  4. 39
      smx-config/MainWindow.xaml
  5. 84
      smx-config/MainWindow.xaml.cs
  6. BIN
      smx-config/Resources/pressed.gif
  7. BIN
      smx-config/Resources/released.gif
  8. 4
      smx-config/SMXConfig.csproj
  9. 12
      smx-config/Widgets.cs

@ -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 "SMXManager.h"
#include "SMXDevice.h"
@ -185,37 +227,33 @@ 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.
vector<pair<int,int>> graphic_positions = {
{ 0,0 },
{ 1,0 },
{ 2,0 },
{ 0,1 },
{ 1,1 },
{ 2,1 },
{ 0,2 },
{ 1,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)
{
vector<pair<int,int>> graphic_positions = {
{ 0,0 },
{ 1,0 },
{ 2,0 },
{ 0,1 },
{ 1,1 },
{ 2,1 },
{ 0,2 },
{ 1,2 },
{ 2,2 },
};
dst.clear();
// The top-left corner for this panel:
int x = graphic_positions[panel].first * 8;
int y = graphic_positions[panel].second * 8;
int x = graphic_positions[panel].first * 5;
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 dx = 0; dx < 4; ++dx)
dst.push_back(src.get(x+dx*2, y+dy*2));
// 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));
dst.push_back(src.get(x+dx, y+dy));
}
}

@ -33,6 +33,11 @@ namespace smx_config
SMX_Internal_OpenConsole();
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)

@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Resources;
using System.Windows.Threading;
using SMXJSON;
@ -227,6 +229,34 @@ namespace smx_config
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 "".
public static string ReadFile(string path)
{
@ -238,6 +268,70 @@ namespace smx_config
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.
@ -304,6 +398,11 @@ namespace smx_config
if(LightsTimer.IsEnabled)
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.
//AutoLightsColorRefreshColor();
@ -314,8 +413,8 @@ namespace smx_config
{
LightsTimer.Stop();
// Reenable auto-lights immediately, without waiting for lights to time out.
SMX.SMX.ReenableAutoLights();
// Turn lighting control back on.
SMX.SMX.LightsAnimation_SetAuto(true);
}
private void AutoLightsColorRefreshColor()

@ -162,6 +162,24 @@ Use if the platform is too sensitive.</clr:String>
</Setter>
</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}">
<Setter Property="Template">
<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">
<TabItem Header="Settings">
<Grid Background="#FFE5E5E5" RenderTransformOrigin="0.5,0.5">
<StackPanel Margin="0,0,0,0" VerticalAlignment="Top">
<TextBlock HorizontalAlignment="Center" Margin="0,15,0,10" VerticalAlignment="Top"
TextAlignment="Center"
@ -664,6 +681,26 @@ Use if the platform is too sensitive.</clr:String>
<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
Visibility="Visible"
Name="ColorPickerGroup" HorizontalAlignment="Center" Orientation="Vertical">

@ -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)
{
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;
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);
// 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);
}
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

@ -191,6 +191,10 @@
<Name>SMX</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\pressed.gif" />
<Resource Include="Resources\released.gif" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 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.

@ -7,6 +7,7 @@ using System.Windows.Input;
using System.Windows.Data;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;
using System.ComponentModel;
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.
public class PresetButton: Control
{

Loading…
Cancel
Save