using System; using System.Windows; using System.Runtime.InteropServices; using System.IO; using System.Threading; namespace smx_config { public partial class App: Application { [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 an instance is already running, foreground it and exit. if(ForegroundExistingInstance()) { Shutdown(); return; } // This is used by the installer to close a running instance automatically when updating. ListenForShutdownRequest(); // If we're being launched on startup, but the LaunchOnStartup setting is false, // then the user turned off auto-launching but we're still being launched for some // reason (eg. a renamed launch shortcut that we couldn't find to remove). As // a safety so we don't launch when the user doesn't want us to, just exit in this // case. if(Helpers.LaunchedOnStartup() && !LaunchOnStartup.Enable) { Shutdown(); return; } LaunchOnStartup.Enable = true; if(!SMX.SMX.DLLExists()) { MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll couldn't be found:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig"); Current.Shutdown(); return; } if(!SMX.SMX.DLLAvailable()) { MessageBox.Show("SMXConfig encountered an unexpected error.\n\nSMX.dll failed to load:\n\n" + Helpers.GetLastWin32ErrorString(), "SMXConfig"); Current.Shutdown(); return; } if(Helpers.GetDebug()) SMX_Internal_OpenConsole(); CurrentSMXDevice.singleton = new CurrentSMXDevice(); // Load animations. Helpers.LoadSavedPanelAnimations(); CreateTrayIcon(); // Create the main window. if(!Helpers.LaunchedOnStartup()) 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(IsMinimizedToTray()) { window.Visibility = Visibility.Visible; window.Activate(); } else { MinimizeToTray(); } } public bool IsMinimizedToTray() { return window.Visibility == Visibility.Collapsed; } public void MinimizeToTray() { // Just hide the window. Don't actually set the window to minimized, since it // won't do anything and it causes problems when restoring the window. window.Visibility = Visibility.Collapsed; } public void BringToForeground() { // Restore or create the window. Don't minimize if we're already restored. if(window == null || IsMinimizedToTray()) ToggleMainWindow(); // Focus the window. window.WindowState = WindowState.Normal; window.Activate(); } private void MainWindowClosed(object sender, EventArgs e) { window = null; } private void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) { string message = e.ExceptionObject.ToString(); MessageBox.Show("SMXConfig encountered an unexpected error:\n\n" + message, "SMXConfig"); } protected override void OnExit(ExitEventArgs e) { base.OnExit(e); Console.WriteLine("Application exiting"); // 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; } } // If another instance other than this one is running, send it WM_USER to tell it to // foreground itself. Return true if another instance was found. private bool ForegroundExistingInstance() { bool createdNew = false; EventWaitHandle SMXConfigEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigEvent", out createdNew); if(!createdNew) { // Signal the event to foreground the existing instance. SMXConfigEvent.Set(); return true; } ThreadPool.RegisterWaitForSingleObject(SMXConfigEvent, ForegroundApplicationCallback, this, Timeout.Infinite, false); return false; } private static void ForegroundApplicationCallback(Object self, Boolean timedOut) { // This is called when another instance sends us a message over SMXConfigEvent. Application.Current.Dispatcher.Invoke(new Action(() => { App application = (App) Application.Current; application.BringToForeground(); })); } private void ListenForShutdownRequest() { // We've already checked that we're the only instance when we get here, so this event shouldn't // exist. If it already exists for some reason, we'll listen to it anyway. EventWaitHandle SMXConfigShutdown = new EventWaitHandle(false, EventResetMode.AutoReset, "SMXConfigShutdown"); ThreadPool.RegisterWaitForSingleObject(SMXConfigShutdown, ShutdownApplicationCallback, this, Timeout.Infinite, false); } private static void ShutdownApplicationCallback(Object self, Boolean timedOut) { // This is called when another instance sends us a message over SMXConfigShutdown. Application.Current.Dispatcher.Invoke(new Action(() => { App application = (App) Application.Current; application.Shutdown(); })); } // 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); } } }