diff --git a/smx-config/App.xaml b/smx-config/App.xaml index aaa2d48..02fc130 100644 --- a/smx-config/App.xaml +++ b/smx-config/App.xaml @@ -1,8 +1,7 @@ + xmlns:local="clr-namespace:smx_config"> diff --git a/smx-config/App.xaml.cs b/smx-config/App.xaml.cs index da3de5d..6c3ad6d 100644 --- a/smx-config/App.xaml.cs +++ b/smx-config/App.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Windows; using System.Runtime.InteropServices; +using System.IO; namespace smx_config { @@ -11,10 +12,18 @@ namespace smx_config [DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl)] private static extern void SMX_Internal_OpenConsole(); + private System.Windows.Forms.NotifyIcon trayIcon; + private MainWindow window; + App() { AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler; - + } + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + if(!SMX.SMX.DLLExists()) { MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll couldn't be found:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig"); @@ -38,6 +47,40 @@ namespace smx_config // we're running. Helpers.LoadSavedPanelAnimations(); SMX.SMX.LightsAnimation_SetAuto(true); + + CreateTrayIcon(); + + // Create the main window. + ToggleMainWindow(); + } + + // Open or close the main window. + // + // We don't create our UI until the first time it's opened, so we use + // less memory when we're launched on startup. However, when we're minimized + // back to the tray, we don't destroy the main window. WPF is just too + // leaky to recreate the main window each time it's called due to internal + // circular references. Instead, we just focus on minimizing CPU overhead. + void ToggleMainWindow() + { + if(window == null) + { + window = new MainWindow(); + window.Closed += MainWindowClosed; + window.Show(); + } + else if(window.WindowState == WindowState.Minimized) + { + window.WindowState = WindowState.Normal; + window.Activate(); + } + else + window.WindowState = WindowState.Minimized; + } + + private void MainWindowClosed(object sender, EventArgs e) + { + window = null; } private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) @@ -50,13 +93,69 @@ namespace smx_config { base.OnExit(e); - // Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown. Console.WriteLine("Application exiting"); - if(CurrentSMXDevice.singleton == null) - return; - CurrentSMXDevice.singleton.Shutdown(); - CurrentSMXDevice.singleton = null; + + // Remove the tray icon. + if(trayIcon != null) + { + trayIcon.Visible = false; + trayIcon = null; + } + + // Shut down cleanly, to make sure we don't run any threaded callbacks during shutdown. + if(CurrentSMXDevice.singleton != null) + { + CurrentSMXDevice.singleton.Shutdown(); + CurrentSMXDevice.singleton = null; + } } + // Create a tray icon. For some reason there's no WPF interface for this, + // so we have to use Forms. + void CreateTrayIcon() + { + Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/window%20icon%20grey.ico")).Stream; + System.Drawing.Icon icon = new System.Drawing.Icon(iconStream); + + trayIcon = new System.Windows.Forms.NotifyIcon(); + trayIcon.Text = "StepManiaX"; + trayIcon.Visible = true; + + // Show or hide the application window on click. + trayIcon.Click += delegate (object sender, EventArgs e) { ToggleMainWindow(); }; + trayIcon.DoubleClick += delegate (object sender, EventArgs e) { ToggleMainWindow(); }; + + CurrentSMXDevice.singleton.ConfigurationChanged += delegate(LoadFromConfigDelegateArgs args) { + RefreshTrayIcon(args); + }; + + // Do the initial refresh. + RefreshTrayIcon(CurrentSMXDevice.singleton.GetState(), true); + } + + // Refresh the tray icon when we're connected or disconnected. + bool wasConnected; + void RefreshTrayIcon(LoadFromConfigDelegateArgs args, bool force=false) + { + if(trayIcon == null) + return; + + bool EitherControllerConnected = false; + for(int pad = 0; pad < 2; ++pad) + if(args.controller[pad].info.connected) + EitherControllerConnected = true; + + // Skip the refresh if the connected state didn't change. + if(wasConnected == EitherControllerConnected && !force) + return; + wasConnected = EitherControllerConnected; + + trayIcon.Text = EitherControllerConnected? "StepManiaX (connected)":"StepManiaX (disconnected)"; + + // Set the tray icon. + string filename = EitherControllerConnected? "window%20icon.ico":"window%20icon%20grey.ico"; + Stream iconStream = GetResourceStream(new Uri( "pack://application:,,,/Resources/" + filename)).Stream; + trayIcon.Icon = new System.Drawing.Icon(iconStream); + } } } diff --git a/smx-config/MainWindow.xaml.cs b/smx-config/MainWindow.xaml.cs index 0deb9f7..8eeb02a 100644 --- a/smx-config/MainWindow.xaml.cs +++ b/smx-config/MainWindow.xaml.cs @@ -18,6 +18,51 @@ namespace smx_config onConfigChange = new OnConfigChange(this, delegate(LoadFromConfigDelegateArgs args) { LoadUIFromConfig(args); }); + + // If we're controlling GIF animations, confirm exiting, since you can minimize + // to tray to keep playing animations. If we're not controlling animations, + // don't bug the user with a prompt. + Closing += delegate(object sender, System.ComponentModel.CancelEventArgs e) + { + LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState(); + + // Don't use ActivePads here. Even if P1 is selected for configuration, + // we can still be controlling animations for P2, so check both connected + // pads. + bool shouldConfirmExit = false; + for(int pad = 0; pad < 2; ++pad) + { + SMX.SMXConfig config; + if(!SMX.SMX.GetConfig(pad, out config)) + continue; + + // If AutoLightingUsePressedAnimations isn't set, the panel is using step + // coloring instead of pressed animations. All firmwares support this. + // Don't confirm exiting for this mode. + if((config.configFlags & SMX.SMXConfigFlags.SMXConfigFlags_AutoLightingUsePressedAnimations) == 0) + continue; + + shouldConfirmExit = true; + } + + if(!shouldConfirmExit) + return; + + MessageBoxResult result = MessageBox.Show( + "Close StepManiaX configuration?\n\n" + + "GIF animations will keep playing if the application is minimized.", + "StepManiaX", System.Windows.MessageBoxButton.YesNo); + if(result == MessageBoxResult.No) + e.Cancel = true; + }; + + StateChanged += delegate(object sender, EventArgs e) + { + // Closing the main window entirely when minimized to the tray would be + // nice, but with WPF we don't really save memory by doing that, so + // just hide the window. + ShowInTaskbar = WindowState != WindowState.Minimized; + }; } public override void OnApplyTemplate() diff --git a/smx-config/Resources/window icon grey.ico b/smx-config/Resources/window icon grey.ico new file mode 100644 index 0000000..86c5cce Binary files /dev/null and b/smx-config/Resources/window icon grey.ico differ diff --git a/smx-config/Resources/window icon grey.png b/smx-config/Resources/window icon grey.png new file mode 100644 index 0000000..835e3f7 Binary files /dev/null and b/smx-config/Resources/window icon grey.png differ diff --git a/smx-config/SMXConfig.csproj b/smx-config/SMXConfig.csproj index d5c23eb..9e628f8 100644 --- a/smx-config/SMXConfig.csproj +++ b/smx-config/SMXConfig.csproj @@ -81,6 +81,7 @@ + @@ -195,6 +196,12 @@ + + + + + +