diff --git a/src/CodingWithCalvin.VSToolbox/App.xaml b/src/CodingWithCalvin.VSToolbox/App.xaml index 54cf87d..52d1542 100644 --- a/src/CodingWithCalvin.VSToolbox/App.xaml +++ b/src/CodingWithCalvin.VSToolbox/App.xaml @@ -40,6 +40,64 @@ + + + diff --git a/src/CodingWithCalvin.VSToolbox/App.xaml.cs b/src/CodingWithCalvin.VSToolbox/App.xaml.cs index 20a8183..50fab76 100644 --- a/src/CodingWithCalvin.VSToolbox/App.xaml.cs +++ b/src/CodingWithCalvin.VSToolbox/App.xaml.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using System.Threading; using CodingWithCalvin.VSToolbox.Services; using Microsoft.UI; using Microsoft.UI.Windowing; @@ -7,12 +8,66 @@ namespace CodingWithCalvin.VSToolbox; +// Windows API for setting window corner preference +internal static class NativeMethods +{ + [DllImport("dwmapi.dll")] + internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + [DllImport("user32.dll")] + internal static extern int GetWindowLong(IntPtr hwnd, int nIndex); + + [DllImport("user32.dll")] + internal static extern int SetWindowLong(IntPtr hwnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + internal static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + internal const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + internal const int DWMWCP_DONOTROUND = 1; + internal const int DWMWA_CAPTION_COLOR = 35; + internal const int DWMWA_BORDER_COLOR = 34; + + internal const int GWL_STYLE = -16; + internal const int WS_CAPTION = 0x00C00000; + internal const int WS_THICKFRAME = 0x00040000; + internal const uint SWP_FRAMECHANGED = 0x0020; + internal const uint SWP_NOMOVE = 0x0002; + internal const uint SWP_NOSIZE = 0x0001; + internal const uint SWP_NOZORDER = 0x0004; + + // For finding and showing existing window + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + internal static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + internal static extern bool SetForegroundWindow(IntPtr hWnd); + + internal const int SW_RESTORE = 9; + internal const int SW_SHOW = 5; +} + public partial class App : Application { + private const string MutexName = "CodingWithCalvin.VSToolbox.SingleInstance"; + private static Mutex? _mutex; private Window? _window; private AppWindow? _appWindow; private TrayIconService? _trayIconService; + public Window? MainWindow => _window; + public App() { InitializeComponent(); @@ -20,6 +75,16 @@ public App() protected override void OnLaunched(LaunchActivatedEventArgs e) { + // Check for single instance + _mutex = new Mutex(true, MutexName, out var createdNew); + if (!createdNew) + { + // Another instance is already running - try to bring it to front + BringExistingInstanceToFront(); + Environment.Exit(0); + return; + } + _window = new Window { Title = "Visual Studio Toolbox" @@ -37,6 +102,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) _appWindow.SetIcon(iconPath); } + // Configure custom title bar with square corners + ConfigureCustomTitleBar(); + // Set up the main content var rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; @@ -57,6 +125,41 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) _window.Activate(); } + private void ConfigureCustomTitleBar() + { + if (_appWindow is null || _window is null) return; + + // Get the window handle for native API calls + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(_window); + + // Set square corners using DWM API + var cornerPreference = NativeMethods.DWMWCP_DONOTROUND; + NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_WINDOW_CORNER_PREFERENCE, + ref cornerPreference, sizeof(int)); + + // Set caption and border color to purple (#68217A = 0x007A2168 in COLORREF BGR format) + var purpleColor = 0x007A2168; // BGR format for #68217A + NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_CAPTION_COLOR, + ref purpleColor, sizeof(int)); + NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_BORDER_COLOR, + ref purpleColor, sizeof(int)); + + // Remove the caption from window style to eliminate the title bar area + var style = NativeMethods.GetWindowLong(hwnd, NativeMethods.GWL_STYLE); + style &= ~NativeMethods.WS_CAPTION; // Remove caption + NativeMethods.SetWindowLong(hwnd, NativeMethods.GWL_STYLE, style); + NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, + NativeMethods.SWP_FRAMECHANGED | NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOZORDER); + + // Make window borderless (no system title bar at all) + if (_appWindow.Presenter is OverlappedPresenter presenter) + { + presenter.SetBorderAndTitleBar(false, false); + presenter.IsResizable = true; + presenter.IsMaximizable = false; + } + } + private void PositionWindowBottomRight(int width, int height) { if (_appWindow is null) return; @@ -86,4 +189,29 @@ private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) { throw new InvalidOperationException($"Failed to load Page {e.SourcePageType.FullName}"); } + + private static void BringExistingInstanceToFront() + { + const string windowTitle = "Visual Studio Toolbox"; + IntPtr foundWindow = IntPtr.Zero; + + NativeMethods.EnumWindows((hWnd, lParam) => + { + var sb = new System.Text.StringBuilder(256); + NativeMethods.GetWindowText(hWnd, sb, sb.Capacity); + if (sb.ToString() == windowTitle) + { + foundWindow = hWnd; + return false; // Stop enumeration + } + return true; // Continue enumeration + }, IntPtr.Zero); + + if (foundWindow != IntPtr.Zero) + { + // Show and bring the window to front + NativeMethods.ShowWindow(foundWindow, NativeMethods.SW_RESTORE); + NativeMethods.SetForegroundWindow(foundWindow); + } + } } diff --git a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml index 8f7071c..73ce610 100644 --- a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml +++ b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml @@ -8,149 +8,261 @@ xmlns:models="using:CodingWithCalvin.VSToolbox.Core.Models" Loaded="OnPageLoaded" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs index 21cac23..528ce20 100644 --- a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs +++ b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs @@ -2,6 +2,7 @@ using CodingWithCalvin.VSToolbox.Services; using CodingWithCalvin.VSToolbox.ViewModels; using Microsoft.UI; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; @@ -25,6 +26,10 @@ public MainPage() private async void OnPageLoaded(object sender, RoutedEventArgs e) { + // Set the title bar drag region + var window = ((App)Application.Current).MainWindow; + window?.SetTitleBar(AppTitleBar); + await ViewModel.LoadInstancesCommand.ExecuteAsync(null); } @@ -164,4 +169,43 @@ private void OnButtonPointerExited(object sender, PointerRoutedEventArgs e) button.Opacity = 0.6; } } + + private void OnTabChanged(object sender, RoutedEventArgs e) + { + // Skip if controls aren't loaded yet + if (InstalledContent is null || SettingsContent is null || RefreshButton is null) + return; + + if (sender is not RadioButton radioButton) + return; + + var isInstalledTab = radioButton == InstalledTab; + + InstalledContent.Visibility = isInstalledTab ? Visibility.Visible : Visibility.Collapsed; + SettingsContent.Visibility = isInstalledTab ? Visibility.Collapsed : Visibility.Visible; + RefreshButton.Visibility = isInstalledTab ? Visibility.Visible : Visibility.Collapsed; + } + + private void OnMinimizeClick(object sender, RoutedEventArgs e) + { + var window = App.Current as App; + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(((App)Application.Current).MainWindow); + var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); + var appWindow = AppWindow.GetFromWindowId(windowId); + + if (appWindow.Presenter is OverlappedPresenter presenter) + { + presenter.Minimize(); + } + } + + private void OnCloseClick(object sender, RoutedEventArgs e) + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(((App)Application.Current).MainWindow); + var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); + var appWindow = AppWindow.GetFromWindowId(windowId); + + // This will trigger the Closing event which hides instead of closes + appWindow.Hide(); + } }