diff --git a/Trdo/App.xaml.cs b/Trdo/App.xaml.cs index 65d57c5..a085ee6 100644 --- a/Trdo/App.xaml.cs +++ b/Trdo/App.xaml.cs @@ -11,7 +11,6 @@ using Trdo.Pages; using Trdo.Services; using Trdo.ViewModels; -using Windows.UI; using Windows.UI.ViewManagement; using WinUIEx; @@ -157,6 +156,7 @@ private void InitializeTrayIcon() private void TrayIcon_ContextMenu(TrayIcon sender, TrayIconEventArgs args) { + WindowPlacementService.CapturePointerAnchor(); args.Flyout = CreateFlyout(); } @@ -166,6 +166,7 @@ private void TrayIcon_Selected(TrayIcon sender, TrayIconEventArgs args) if (!_playerVm.CanPlay) { // No stations available, show the flyout to encourage user to add a station + WindowPlacementService.CapturePointerAnchor(); args.Flyout = CreateFlyout(); return; } @@ -190,6 +191,7 @@ private Flyout CreateFlyout() flyout.Opened += (s, e) => { + WindowPlacementService.CapturePointerAnchor(); // Clear the back stack when flyout opens to prevent accumulation Services.NavigationService.Instance.ClearBackStack(); }; @@ -272,7 +274,7 @@ private void UpdatePlayPauseCommandText() } else if (_playerVm.IsPlaying) { - + // Include now playing info if available if (_playerVm.HasNowPlaying) { diff --git a/Trdo/Controls/ManualStationWindow.xaml b/Trdo/Controls/ManualStationWindow.xaml new file mode 100644 index 0000000..b222264 --- /dev/null +++ b/Trdo/Controls/ManualStationWindow.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Trdo/Controls/ManualStationWindow.xaml.cs b/Trdo/Controls/ManualStationWindow.xaml.cs new file mode 100644 index 0000000..2c7cc49 --- /dev/null +++ b/Trdo/Controls/ManualStationWindow.xaml.cs @@ -0,0 +1,60 @@ +using Trdo.Models; +using Trdo.Services; +using Trdo.ViewModels; +using WinUIEx; + +namespace Trdo.Controls; + +/// +/// A small standalone window for manually adding or editing a radio station. +/// Opens as a pop-out window so that closing the tray flyout does not clear the form fields. +/// +public sealed partial class ManualStationWindow : WindowEx +{ + public AddStationViewModel ViewModel { get; } + + public ManualStationWindow() + { + InitializeComponent(); + + ViewModel = new AddStationViewModel(); + ViewModel.SetPlayerViewModel(PlayerViewModel.Shared); + + ExtendsContentIntoTitleBar = true; + SetTitleBar(ModernTitlebar); + + Activated += ManualStationWindow_Activated; + } + + /// + /// Opens the window pre-filled with the given station's data for editing. + /// + public void LoadStationForEdit(RadioStation station) + { + ViewModel.LoadStationForEdit(station); + } + + private void ManualStationWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) + { + // Focus the station name field once the window is ready + if (args.WindowActivationState != Microsoft.UI.Xaml.WindowActivationState.Deactivated) + { + WindowPlacementService.PositionWindowNearAnchor(this, 400, 500); + StationNameTextBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); + Activated -= ManualStationWindow_Activated; + } + } + + private void SaveButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + if (ViewModel.Save()) + { + Close(); + } + } + + private void CancelButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + Close(); + } +} diff --git a/Trdo/Controls/TutorialWindow.xaml.cs b/Trdo/Controls/TutorialWindow.xaml.cs index 0f8327f..758c27c 100644 --- a/Trdo/Controls/TutorialWindow.xaml.cs +++ b/Trdo/Controls/TutorialWindow.xaml.cs @@ -13,6 +13,16 @@ public TutorialWindow() ExtendsContentIntoTitleBar = true; SetTitleBar(ModernTitlebar); + Activated += TutorialWindow_Activated; + } + + private void TutorialWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) + { + if (args.WindowActivationState == Microsoft.UI.Xaml.WindowActivationState.Deactivated) + return; + + WindowPlacementService.PositionWindowNearAnchor(this, 400, 600); + Activated -= TutorialWindow_Activated; } private void Button_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/Trdo/Pages/PlayingPage.xaml.cs b/Trdo/Pages/PlayingPage.xaml.cs index 4da53e9..e7998b4 100644 --- a/Trdo/Pages/PlayingPage.xaml.cs +++ b/Trdo/Pages/PlayingPage.xaml.cs @@ -9,7 +9,6 @@ using Trdo.Models; using Trdo.Services; using Trdo.ViewModels; -using WinUIEx; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -23,9 +22,9 @@ public sealed partial class PlayingPage : Page private const int MinIndexForScrolling = 3; private const string FilledStar = "\uE735"; private const string OutlineStar = "\uE734"; - + private readonly FavoritesService _favoritesService = FavoritesService.Instance; - + public PlayerViewModel ViewModel { get; } private ShellViewModel? _shellViewModel; @@ -117,7 +116,7 @@ private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.Pro UpdatePlayButtonState(); UpdateStationSelection(); - if (e.PropertyName == nameof(PlayerViewModel.CurrentMetadata) || + if (e.PropertyName == nameof(PlayerViewModel.CurrentMetadata) || e.PropertyName == nameof(PlayerViewModel.HasNowPlaying)) { UpdateFavoriteButtonState(); @@ -321,7 +320,7 @@ private void NowPlayingInfo_Click(object sender, RoutedEventArgs e) private void FavoriteButton_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("[PlayingPage] Favorite button clicked"); - + if (ViewModel.CurrentMetadata?.HasMetadata != true) { Debug.WriteLine("[PlayingPage] No metadata to favorite"); @@ -331,7 +330,7 @@ private void FavoriteButton_Click(object sender, RoutedEventArgs e) string stationName = ViewModel.SelectedStation?.Name ?? "Unknown Station"; bool isFavorited = _favoritesService.ToggleFavorite(ViewModel.CurrentMetadata, stationName); Debug.WriteLine($"[PlayingPage] Track favorite toggled. IsFavorited: {isFavorited}"); - + // UpdateFavoriteButtonState will be called via the FavoritesChanged event } @@ -356,8 +355,12 @@ private void EditStation_Click(object sender, RoutedEventArgs e) if (sender is MenuFlyoutItem menuItem && menuItem.Tag is RadioStation station) { Debug.WriteLine($"[PlayingPage] Edit station clicked: {station.Name}"); - // Navigate to AddStation page in edit mode with the station data - _shellViewModel?.NavigateToAddStationPage(station); + // Open a pop-out window for editing so the flyout closing doesn't clear the fields + WindowPlacementService.CapturePointerAnchor(); + ManualStationWindow editWindow = new(); + WindowHelper.Track(editWindow); + editWindow.LoadStationForEdit(station); + editWindow.Activate(); } } diff --git a/Trdo/Pages/SearchStation.xaml.cs b/Trdo/Pages/SearchStation.xaml.cs index ecc3ad6..070ae68 100644 --- a/Trdo/Pages/SearchStation.xaml.cs +++ b/Trdo/Pages/SearchStation.xaml.cs @@ -1,7 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; -using System; +using Trdo.Controls; using Trdo.Models; using Trdo.Services; using Trdo.ViewModels; @@ -139,8 +139,11 @@ private void AddStationButton_Click(object sender, RoutedEventArgs e) private void ManualEntryButton_Click(object sender, RoutedEventArgs e) { StopPreview(); - // Navigate to manual entry page - _shellViewModel?.NavigateToAddStationPage(); + // Open a pop-out window for manual station entry so the flyout closing doesn't clear the fields + WindowPlacementService.CapturePointerAnchor(); + ManualStationWindow addWindow = new(); + WindowHelper.Track(addWindow); + addWindow.Activate(); } private void CancelButton_Click(object sender, RoutedEventArgs e) diff --git a/Trdo/Services/WindowPlacementService.cs b/Trdo/Services/WindowPlacementService.cs new file mode 100644 index 0000000..2c47938 --- /dev/null +++ b/Trdo/Services/WindowPlacementService.cs @@ -0,0 +1,114 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using System.Runtime.InteropServices; +using Windows.Graphics; +using WinUIEx; + +namespace Trdo.Services; + +internal static partial class WindowPlacementService +{ + private const int WindowMargin = 12; + private static PointInt32? _lastAnchorPoint; + + public static void CapturePointerAnchor() + { + if (GetCursorPos(out POINT point)) + _lastAnchorPoint = new PointInt32(point.X, point.Y); + } + + public static void PositionWindowNearAnchor(Window window, int width, int height) + { + PointInt32 anchor = GetAnchorPoint(); + DisplayArea? displayArea = DisplayArea.GetFromPoint(anchor, DisplayAreaFallback.Nearest); + RectInt32 workArea = displayArea?.WorkArea ?? DisplayArea.Primary.WorkArea; + + bool placeLeft = anchor.X >= workArea.X + (workArea.Width / 2); + bool placeAbove = anchor.Y >= workArea.Y + (workArea.Height / 2); + + int x = placeLeft ? anchor.X - width - WindowMargin : anchor.X + WindowMargin; + int y = placeAbove ? anchor.Y - height - WindowMargin : anchor.Y + WindowMargin; + + x = System.Math.Clamp(x, workArea.X, workArea.X + workArea.Width - width); + y = System.Math.Clamp(y, workArea.Y, workArea.Y + workArea.Height - height); + + window.MoveAndResize(x, y, width, height); + } + + private static PointInt32 GetAnchorPoint() + { + if (_lastAnchorPoint is PointInt32 anchor) + return anchor; + + if (TryGetTaskbarAnchorPoint(out anchor)) + return anchor; + + RectInt32 workArea = DisplayArea.Primary.WorkArea; + return new PointInt32(workArea.X + workArea.Width - WindowMargin, workArea.Y + workArea.Height - WindowMargin); + } + + private static bool TryGetTaskbarAnchorPoint(out PointInt32 anchor) + { + APPBARDATA appBarData = new() + { + cbSize = Marshal.SizeOf() + }; + + if (SHAppBarMessage(ABM_GETTASKBARPOS, ref appBarData) == 0) + { + anchor = default; + return false; + } + + anchor = appBarData.uEdge switch + { + ABE_BOTTOM => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Top + ((appBarData.rc.Bottom - appBarData.rc.Top) / 2)), + ABE_TOP => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin), + ABE_LEFT => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin), + ABE_RIGHT => new PointInt32(appBarData.rc.Left + WindowMargin, appBarData.rc.Bottom - WindowMargin), + _ => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin) + }; + + return true; + } + + private const uint ABM_GETTASKBARPOS = 0x00000005; + private const uint ABE_LEFT = 0; + private const uint ABE_TOP = 1; + private const uint ABE_RIGHT = 2; + private const uint ABE_BOTTOM = 3; + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct APPBARDATA + { + public int cbSize; + public nint hWnd; + public uint uCallbackMessage; + public uint uEdge; + public RECT rc; + public nint lParam; + } + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetCursorPos(out POINT point); + + [LibraryImport("shell32.dll")] + private static partial uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData); +} diff --git a/Trdo/Trdo.csproj b/Trdo/Trdo.csproj index a5f24ad..cb3749a 100644 --- a/Trdo/Trdo.csproj +++ b/Trdo/Trdo.csproj @@ -20,6 +20,7 @@ + @@ -119,6 +120,11 @@ MSBuild:Compile + + + MSBuild:Compile + + MSBuild:Compile