diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index da62df1..05d7f95 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -57,16 +57,74 @@ jobs: date_stamp="$(date -u +%Y%m%d)" default_tag_name="pre-${date_stamp}" - latest_stable_tag="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1)" - if [[ -z "${latest_stable_tag}" ]]; then - base_version="0.1.0" - else - base_version="${latest_stable_tag#v}" + # Every tag that carries a semver version: stable vX.Y.Z and plain + # semver pre-releases such as 3.0.0-rc2. Tags like pre-YYYYMMDD do not + # encode a version and are intentionally ignored (their versions never + # exceed the stable-based auto line). + all_versions="$( + git tag -l \ + | sed -E 's/^v//' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' \ + | sort -u || true + )" + + latest_stable_base="$( + printf '%s\n' "${all_versions}" \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V | tail -n 1 || true + )" + + latest_prerelease_base="$( + printf '%s\n' "${all_versions}" \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-' \ + | sed -E 's/-.*$//' \ + | sort -V | tail -n 1 || true + )" + + # An unreleased line is "in progress" when its base is newer than the + # latest stable, e.g. 3.0.0-rc2 while the latest stable is v2.7.2. + active_line="" + if [[ -n "${latest_prerelease_base}" ]]; then + newest_base="$(printf '%s\n%s\n' "${latest_stable_base:-0.0.0}" "${latest_prerelease_base}" | sort -V | tail -n 1)" + if [[ "${newest_base}" == "${latest_prerelease_base}" && "${latest_prerelease_base}" != "${latest_stable_base}" ]]; then + active_line="${latest_prerelease_base}" + fi fi - IFS='.' read -r major minor patch <<< "${base_version}" - next_patch=$((patch + 1)) - default_next_version="${major}.${minor}.${next_patch}-pre.${date_stamp}" + if [[ -n "${active_line}" ]]; then + # Continue the in-progress line with a date-stamped release candidate. + # The date keeps it ordering above the previous candidate + # (3.0.0-rc3.20260917 > 3.0.0-rc2) while still allowing only one + # pre-release per day; the tag always equals the version so the next + # run can detect it again. + escaped_line="${active_line//./\\.}" + highest_pre="$(printf '%s\n' "${all_versions}" | grep -E "^${escaped_line}-" | sort -V | tail -n 1)" + suffix="${highest_pre#${active_line}-}" + rc_number="" + + if [[ "${suffix}" =~ ^rc([0-9]+)$ ]]; then + # Manually cut candidate (e.g. rc2) -> start the next one (rc3). + rc_number=$(( BASH_REMATCH[1] + 1 )) + elif [[ "${suffix}" =~ ^rc([0-9]+)\. ]]; then + # Date-stamped candidate (e.g. rc3.20260917) -> keep the same + # candidate number and just refresh the date. + rc_number="${BASH_REMATCH[1]}" + else + highest_rc="$(printf '%s\n' "${all_versions}" | grep -Eo "^${escaped_line}-rc[0-9]+" | grep -Eo '[0-9]+$' | sort -n | tail -n 1 || true)" + rc_number=$(( ${highest_rc:-0} + 1 )) + fi + + default_next_version="${active_line}-rc${rc_number}.${date_stamp}" + default_tag_name="${default_next_version}" + else + if [[ -z "${latest_stable_base}" ]]; then + latest_stable_base="0.1.0" + fi + + IFS='.' read -r major minor patch <<< "${latest_stable_base}" + next_patch=$((patch + 1)) + default_next_version="${major}.${minor}.${next_patch}-pre.${date_stamp}" + fi tag_name="${default_tag_name}" next_version="${default_next_version}" diff --git a/MapWizard.Desktop/App.axaml b/MapWizard.Desktop/App.axaml index e267277..0c87dcf 100644 --- a/MapWizard.Desktop/App.axaml +++ b/MapWizard.Desktop/App.axaml @@ -4,7 +4,6 @@ xmlns:local="clr-namespace:MapWizard.Desktop" xmlns:mapWizardTheme="clr-namespace:MapWizard.Theme;assembly=MapWizard.Theme" xmlns:converters="clr-namespace:MapWizard.Desktop.Converters" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="MapWizard.Desktop.App"> @@ -15,8 +14,6 @@ - - + + + + + + + + + + + + + + + + - - - @@ -229,13 +272,12 @@ - - @@ -314,7 +356,7 @@ - @@ -400,7 +442,7 @@ IsEnabled="{Binding CanClearSelection, ElementName=Root}" ToolTip.Tip="Clear the current source and targets"> - + @@ -410,7 +452,7 @@ IsEnabled="{Binding HasOriginSelection, ElementName=Root}" ToolTip.Tip="Open the folder of the selected source beatmap"> - + @@ -418,8 +460,9 @@ - + + - - - @@ -493,7 +548,7 @@ IsVisible="{Binding !ShowDestinationSection, ElementName=Root}" ToolTip.Tip="Clear the selected beatmap"> - + @@ -503,7 +558,7 @@ IsVisible="{Binding !ShowDestinationSection, ElementName=Root}" ToolTip.Tip="Open the selected beatmap's folder"> - + @@ -610,9 +665,8 @@ - - - + + + + + + + - - - @@ -716,7 +794,7 @@ Click="DestinationMapsetExpandButton_OnClick" ToolTip.Tip="Show or hide this mapset's difficulties" /> - - + @@ -803,10 +881,9 @@ - @@ -835,9 +912,8 @@ - - + @@ -892,6 +984,7 @@ + diff --git a/MapWizard.Desktop/Views/Controls/BeatmapSelectionPanel.axaml.cs b/MapWizard.Desktop/Views/Controls/BeatmapSelectionPanel.axaml.cs index 5bcc0f4..e325444 100644 --- a/MapWizard.Desktop/Views/Controls/BeatmapSelectionPanel.axaml.cs +++ b/MapWizard.Desktop/Views/Controls/BeatmapSelectionPanel.axaml.cs @@ -59,22 +59,9 @@ public partial class BeatmapSelectionPanel : UserControl public static readonly StyledProperty OriginEmptyPromptProperty = AvaloniaProperty.Register(nameof(OriginEmptyPrompt), "Select an origin beatmap"); - public static readonly StyledProperty SourceSubtitleProperty = - AvaloniaProperty.Register( - nameof(SourceSubtitle), - "Select the difficulty you want to use."); - - public static readonly StyledProperty TargetSubtitleProperty = - AvaloniaProperty.Register( - nameof(TargetSubtitle), - "Select the difficulties you want to copy to."); - public static readonly StyledProperty CanClearSelectionProperty = AvaloniaProperty.Register(nameof(CanClearSelection)); - public static readonly StyledProperty ShowSourceSubtitleProperty = - AvaloniaProperty.Register(nameof(ShowSourceSubtitle)); - public static readonly StyledProperty ShowSourceOnlyBackdropProperty = AvaloniaProperty.Register(nameof(ShowSourceOnlyBackdrop)); @@ -259,30 +246,12 @@ public string OriginEmptyPrompt private set => SetValue(OriginEmptyPromptProperty, value); } - public string SourceSubtitle - { - get => GetValue(SourceSubtitleProperty); - set => SetValue(SourceSubtitleProperty, value); - } - - public string TargetSubtitle - { - get => GetValue(TargetSubtitleProperty); - set => SetValue(TargetSubtitleProperty, value); - } - public bool CanClearSelection { get => GetValue(CanClearSelectionProperty); private set => SetValue(CanClearSelectionProperty, value); } - public bool ShowSourceSubtitle - { - get => GetValue(ShowSourceSubtitleProperty); - private set => SetValue(ShowSourceSubtitleProperty, value); - } - public bool ShowSourceOnlyBackdrop { get => GetValue(ShowSourceOnlyBackdropProperty); @@ -667,7 +636,6 @@ private void UpdateShowDestinationSection() { ShowDestinationSection = ShowDestinationSelection; SourceColumnSpan = ShowDestinationSection ? 1 : 3; - ShowSourceSubtitle = ShowDestinationSection || !HasOriginSelection; ShowSourceOnlyBackdrop = HasOriginSelection && !ShowDestinationSection; ShowSourceMapCardBackdrop = HasOriginSelection && ShowDestinationSection; } diff --git a/MapWizard.Desktop/Views/Controls/ColourPointModeDropdown.axaml b/MapWizard.Desktop/Views/Controls/ColourPointModeDropdown.axaml index 9b02aef..cadae84 100644 --- a/MapWizard.Desktop/Views/Controls/ColourPointModeDropdown.axaml +++ b/MapWizard.Desktop/Views/Controls/ColourPointModeDropdown.axaml @@ -1,7 +1,6 @@ @@ -42,7 +41,7 @@ Foreground="{DynamicResource MapWizardTextBrush}" VerticalAlignment="Center" Text="{Binding SelectedModeLabel, ElementName=Root}" /> - @@ -63,7 +62,7 @@ Classes="HeaderClose" Click="CloseFlyout_OnClick" ToolTip.Tip="Close"> - + diff --git a/MapWizard.Desktop/Views/Controls/ComboColourDropdown.axaml b/MapWizard.Desktop/Views/Controls/ComboColourDropdown.axaml index 72ecc9c..91d03fc 100644 --- a/MapWizard.Desktop/Views/Controls/ComboColourDropdown.axaml +++ b/MapWizard.Desktop/Views/Controls/ComboColourDropdown.axaml @@ -1,7 +1,6 @@ - + @@ -161,7 +160,7 @@ Command="{Binding RemoveTokenCommand, ElementName=Root}" CommandParameter="{Binding RemoveTokenCommandParameter, ElementName=Root}" ToolTip.Tip="Remove this combo from sequence"> - diff --git a/MapWizard.Desktop/Views/Controls/ImportantNotice.axaml b/MapWizard.Desktop/Views/Controls/ImportantNotice.axaml index 60aec1b..90f7e2b 100644 --- a/MapWizard.Desktop/Views/Controls/ImportantNotice.axaml +++ b/MapWizard.Desktop/Views/Controls/ImportantNotice.axaml @@ -4,7 +4,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:MapWizard.Desktop.Views.Controls" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="MapWizard.Desktop.Views.Controls.ImportantNotice" x:DataType="controls:ImportantNotice" x:Name="Root" @@ -18,9 +17,8 @@ CornerRadius="10" Padding="12,10"> - diff --git a/MapWizard.Desktop/Views/Controls/MillisecondFlyoutInput.axaml b/MapWizard.Desktop/Views/Controls/MillisecondFlyoutInput.axaml index c620487..11734cd 100644 --- a/MapWizard.Desktop/Views/Controls/MillisecondFlyoutInput.axaml +++ b/MapWizard.Desktop/Views/Controls/MillisecondFlyoutInput.axaml @@ -1,7 +1,6 @@ @@ -33,14 +32,14 @@ HorizontalAlignment="Stretch" Click="TriggerButton_OnClick"> - - @@ -62,7 +61,7 @@ Classes="HeaderClose" Click="CloseFlyout_OnClick" ToolTip.Tip="Close"> - + @@ -80,7 +79,7 @@ Classes="Basic Compact" Click="ApplyButton_OnClick" ToolTip.Tip="Apply time"> - + diff --git a/MapWizard.Desktop/Views/Controls/ModalHost.axaml b/MapWizard.Desktop/Views/Controls/ModalHost.axaml index 578edc3..8bdf00b 100644 --- a/MapWizard.Desktop/Views/Controls/ModalHost.axaml +++ b/MapWizard.Desktop/Views/Controls/ModalHost.axaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="640" @@ -84,7 +83,7 @@ Click="CloseButton_OnClick" IsVisible="{Binding ShowCloseButton, ElementName=Root}" ToolTip.Tip="Close"> - + diff --git a/MapWizard.Desktop/Views/Controls/ModalHost.axaml.cs b/MapWizard.Desktop/Views/Controls/ModalHost.axaml.cs index 8571b6c..01ed680 100644 --- a/MapWizard.Desktop/Views/Controls/ModalHost.axaml.cs +++ b/MapWizard.Desktop/Views/Controls/ModalHost.axaml.cs @@ -19,7 +19,7 @@ public partial class ModalHost : UserControl private const double OverlayRightInset = 14d; private const double OverlayMinimumWidth = 460d; private const double OverlayMaximumWidth = 620d; - private const double OverlayBackgroundBlurRadius = 10d; + private const double BackgroundBlurRadius = 10d; private int _transitionVersion; private Visual? _blurredTarget; private IEffect? _previousBackgroundEffect; @@ -126,19 +126,12 @@ public async Task OpenAsync(CancellationToken cancellationToken = default) DialogCard.RenderTransform = GetClosedTransform(); IsVisible = true; IsOpen = true; - var animateBackgroundBlur = Presentation == ModalPresentation.MapPickerOverlay; - if (animateBackgroundBlur) - { - PrepareBackgroundBlur(); - } - - var blurAnimation = animateBackgroundBlur - ? AnimateBackgroundBlurAsync( - OverlayBackgroundBlurRadius, - TimeSpan.FromMilliseconds(260), - version, - cancellationToken) - : Task.CompletedTask; + PrepareBackgroundBlur(); + var blurAnimation = AnimateBackgroundBlurAsync( + BackgroundBlurRadius, + TimeSpan.FromMilliseconds(260), + version, + cancellationToken); try { diff --git a/MapWizard.Desktop/Views/Controls/ModernColorPicker.axaml b/MapWizard.Desktop/Views/Controls/ModernColorPicker.axaml index cf3679f..4f6c4fe 100644 --- a/MapWizard.Desktop/Views/Controls/ModernColorPicker.axaml +++ b/MapWizard.Desktop/Views/Controls/ModernColorPicker.axaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls.ColorPicker" xmlns:local="clr-namespace:MapWizard.Desktop.Views.Controls" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="MapWizard.Desktop.Views.Controls.ModernColorPicker" x:Name="Root" MinWidth="150"> @@ -89,10 +88,9 @@ Foreground="{DynamicResource MapWizardTextBrush}" VerticalAlignment="Center" Text="#FFFFFF" /> - @@ -113,9 +111,8 @@ Classes="HeaderClose" Click="CloseFlyout_OnClick" ToolTip.Tip="Close"> - + - - @@ -88,7 +87,7 @@ - - @@ -242,9 +241,8 @@ - - - - + Size="15" /> @@ -312,7 +307,7 @@ CommandParameter="{Binding}" ToolTip.Tip="Select or clear all difficulties in this mapset"> - + @@ -331,9 +326,8 @@ Command="{ReflectionBinding DataContext.SelectDifficultyCommand, RelativeSource={RelativeSource AncestorType=dialogs:SongSelectDialog}}" CommandParameter="{Binding}"> - - + diff --git a/MapWizard.Desktop/Views/Dialogs/SongSelectDialog.axaml.cs b/MapWizard.Desktop/Views/Dialogs/SongSelectDialog.axaml.cs index cb88a08..517f214 100644 --- a/MapWizard.Desktop/Views/Dialogs/SongSelectDialog.axaml.cs +++ b/MapWizard.Desktop/Views/Dialogs/SongSelectDialog.axaml.cs @@ -1,10 +1,10 @@ using System; -using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Threading; using Avalonia.VisualTree; using MapWizard.Desktop.ViewModels; @@ -13,6 +13,10 @@ namespace MapWizard.Desktop.Views.Dialogs; public partial class SongSelectDialog : UserControl { private const double CenterScrollEdgeThreshold = 56d; + private static readonly TimeSpan ScrollWorkInterval = TimeSpan.FromMilliseconds(50); + private readonly DispatcherTimer _scrollWorkTimer; + private ScrollViewer? _pendingScrollViewer; + private bool _hasPendingScrollWork; /// Raised when the header close (X) button is clicked. public event EventHandler? PickerCloseRequested; @@ -24,6 +28,8 @@ private void ClosePickerButton_OnClick(object? sender, RoutedEventArgs e) public SongSelectDialog() { + _scrollWorkTimer = new DispatcherTimer { Interval = ScrollWorkInterval }; + _scrollWorkTimer.Tick += OnScrollWorkTimerTick; InitializeComponent(); AddHandler( InputElement.PointerPressedEvent, @@ -34,7 +40,47 @@ public SongSelectDialog() private void MapsetScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) { - if (sender is not ScrollViewer scrollViewer || DataContext is not SongSelectDialogViewModel viewModel) + if (sender is not ScrollViewer scrollViewer) + { + return; + } + + _pendingScrollViewer = scrollViewer; + _hasPendingScrollWork = true; + + // Run the first update immediately, then cap the geometry scan at 20 Hz + // while precision scrolling is producing a high-frequency event stream. + if (!_scrollWorkTimer.IsEnabled) + { + ProcessPendingScrollWork(); + _scrollWorkTimer.Start(); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _scrollWorkTimer.Stop(); + _pendingScrollViewer = null; + _hasPendingScrollWork = false; + base.OnDetachedFromVisualTree(e); + } + + private void OnScrollWorkTimerTick(object? sender, EventArgs e) + { + if (!_hasPendingScrollWork) + { + _scrollWorkTimer.Stop(); + return; + } + + ProcessPendingScrollWork(); + } + + private void ProcessPendingScrollWork() + { + _hasPendingScrollWork = false; + if (_pendingScrollViewer is not { } scrollViewer || + DataContext is not SongSelectDialogViewModel viewModel) { return; } @@ -71,7 +117,7 @@ private void MapsetScrollViewer_OnScrollChanged(object? sender, ScrollChangedEve int? firstVisibleIndex = null; int? lastVisibleIndex = null; - foreach (var container in MapsetItemsControl.GetRealizedContainers().ToArray()) + foreach (var container in MapsetItemsControl.GetRealizedContainers()) { if (!container.IsVisible || container.Bounds.Height <= 0d) { diff --git a/MapWizard.Desktop/Views/HitSoundCopier/HitSoundCopierView.axaml b/MapWizard.Desktop/Views/HitSoundCopier/HitSoundCopierView.axaml index 7cb072e..7b908d9 100644 --- a/MapWizard.Desktop/Views/HitSoundCopier/HitSoundCopierView.axaml +++ b/MapWizard.Desktop/Views/HitSoundCopier/HitSoundCopierView.axaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:theme="clr-namespace:MapWizard.Theme.Controls;assembly=MapWizard.Theme" xmlns:controls="clr-namespace:MapWizard.Desktop.Views.Controls" xmlns:appControls="clr-namespace:MapWizard.Desktop.Controls" @@ -29,7 +28,7 @@ Command="{Binding CopyHitSoundsCommand}" ToolTip.Tip="Copy the hitsounds"> - + diff --git a/MapWizard.Desktop/Views/HitSoundVisualizer/HitSoundVisualizerView.axaml b/MapWizard.Desktop/Views/HitSoundVisualizer/HitSoundVisualizerView.axaml index 06377ca..80c5ecf 100644 --- a/MapWizard.Desktop/Views/HitSoundVisualizer/HitSoundVisualizerView.axaml +++ b/MapWizard.Desktop/Views/HitSoundVisualizer/HitSoundVisualizerView.axaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:theme="clr-namespace:MapWizard.Theme.Controls;assembly=MapWizard.Theme" xmlns:controls="clr-namespace:MapWizard.Desktop.Views.Controls" xmlns:appControls="clr-namespace:MapWizard.Desktop.Controls" @@ -197,7 +196,7 @@ Margin="0,0,8,0" ToolTip.Tip="Export hitsound timeline"> - + - + @@ -240,7 +239,6 @@ OriginMap="{Binding OriginBeatmap}" DestinationMaps="{Binding DestinationBeatmaps}" ShowDestinationSelection="False" - SourceSubtitle="Select the beatmap you want to visualize hitsounds from." OriginMemoryToolTip="Get the current beatmap selected in the osu! client." /> @@ -322,7 +320,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -331,7 +329,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -340,7 +338,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -358,26 +356,26 @@ Classes="timeline-toolbar-toggle" IsChecked="{Binding FollowPlaybackCursor}" ToolTip.Tip="Follow playback cursor"> - + @@ -478,7 +476,7 @@ IsEnabled="{Binding CanEditHeaderBanks}"> - + @@ -503,7 +501,7 @@ IsEnabled="{Binding CanEditHeaderBanks}"> - + @@ -521,7 +519,7 @@ @@ -682,7 +679,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -691,7 +688,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -700,7 +697,7 @@ IsEnabled="{Binding HasSelectedPoint}" Margin="0,0,8,8"> - + @@ -718,7 +715,7 @@ @@ -803,19 +800,19 @@ @@ -848,7 +845,7 @@ Command="{Binding ToggleHitSoundMuteCommand}" PointerWheelChanged="EffectsVolumeButton_OnPointerWheelChanged" ToolTip.Tip="{Binding HitSoundVolumeText, StringFormat=Effects volume: {0} | Scroll: +/-1% | Click: mute/unmute}"> - + @@ -857,7 +854,7 @@ Command="{Binding ToggleHitSoundMuteCommand}" PointerWheelChanged="EffectsVolumeButton_OnPointerWheelChanged"> - + diff --git a/MapWizard.Desktop/Views/MainWindow.axaml b/MapWizard.Desktop/Views/MainWindow.axaml index fadb677..99c3399 100644 --- a/MapWizard.Desktop/Views/MainWindow.axaml +++ b/MapWizard.Desktop/Views/MainWindow.axaml @@ -4,7 +4,6 @@ xmlns:vm="using:MapWizard.Desktop.ViewModels" xmlns:views="clr-namespace:MapWizard.Desktop.Views" xmlns:theme="clr-namespace:MapWizard.Theme.Controls;assembly=MapWizard.Theme" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:welcomePage="clr-namespace:MapWizard.Desktop.Views.WelcomePage" xmlns:hitSoundVisualizer="clr-namespace:MapWizard.Desktop.Views.HitSoundVisualizer" xmlns:metadataManager="clr-namespace:MapWizard.Desktop.Views.MetadataManager" @@ -94,16 +93,43 @@ - + + @@ -151,8 +177,8 @@ - + - @@ -216,7 +242,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsHitSoundCopierSelected}" /> - @@ -245,7 +271,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsMetadataManagerSelected}" /> - @@ -274,7 +300,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsHitSoundVisualizerSelected}" /> - @@ -303,7 +329,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsComboColourStudioSelected}" /> - @@ -332,7 +358,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsMapCleanerSelected}" /> - @@ -363,7 +389,7 @@ Background="{DynamicResource MapWizardAccentBrush}" Opacity="0.20" IsVisible="{Binding IsSettingsSelected}" /> - @@ -390,10 +416,11 @@ - - + + @@ -429,7 +456,7 @@ Spacing="9" VerticalAlignment="Center" IsHitTestVisible="False"> - - + + + + ViewModel.NavigateToHitSoundCopier(); + public void NavigateToHitSoundVisualizer() => ViewModel.NavigateToHitSoundVisualizer(); + public void NavigateToMetadataManager() => ViewModel.NavigateToMetadataManager(); public void NavigateToComboColourStudio() => ViewModel.NavigateToComboColourStudio(); @@ -73,8 +75,16 @@ private void ApplyPlatformWindowChrome() private void UpdateShellCorners() { + if (OperatingSystem.IsWindows()) + { + WindowShell.CornerRadius = default; + WindowShell.ClipToBounds = false; + return; + } + var square = WindowState is WindowState.Maximized or WindowState.FullScreen; - WindowShell.CornerRadius = square ? default : new CornerRadius(ShellCornerRadius); + WindowShell.CornerRadius = square ? default : new CornerRadius(12d); + WindowShell.ClipToBounds = true; } private void UpdateWindowControlGlyphs() diff --git a/MapWizard.Desktop/Views/MapCleaner/MapCleanerView.axaml b/MapWizard.Desktop/Views/MapCleaner/MapCleanerView.axaml index 437b631..150ea8a 100644 --- a/MapWizard.Desktop/Views/MapCleaner/MapCleanerView.axaml +++ b/MapWizard.Desktop/Views/MapCleaner/MapCleanerView.axaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:theme="clr-namespace:MapWizard.Theme.Controls;assembly=MapWizard.Theme" xmlns:controls="clr-namespace:MapWizard.Desktop.Views.Controls" xmlns:appControls="clr-namespace:MapWizard.Desktop.Controls" @@ -24,7 +23,7 @@ - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - + - - - - - - - - + + + + + + - - - - + + + diff --git a/MapWizard.Desktop/Views/WelcomePage/WelcomePageView.axaml.cs b/MapWizard.Desktop/Views/WelcomePage/WelcomePageView.axaml.cs index 2f9a1ce..4bda96f 100644 --- a/MapWizard.Desktop/Views/WelcomePage/WelcomePageView.axaml.cs +++ b/MapWizard.Desktop/Views/WelcomePage/WelcomePageView.axaml.cs @@ -1,6 +1,8 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.VisualTree; +using MapWizard.Desktop.Models; +using MapWizard.Desktop.ViewModels; namespace MapWizard.Desktop.Views.WelcomePage; @@ -9,20 +11,45 @@ public partial class WelcomePageView : UserControl public WelcomePageView() { InitializeComponent(); + AttachedToVisualTree += (_, _) => + { + if (DataContext is WelcomePageViewModel vm) + { + vm.RefreshSongsFolderStatus(); + } + }; } - private void OpenHitSoundCopier_OnClick(object? sender, RoutedEventArgs e) + private void OpenTool_OnClick(object? sender, RoutedEventArgs e) { - this.FindAncestorOfType()?.NavigateToHitSoundCopier(); - } + if (sender is not Button { DataContext: QuickStartTool tool }) + { + return; + } - private void OpenMetadataManager_OnClick(object? sender, RoutedEventArgs e) - { - this.FindAncestorOfType()?.NavigateToMetadataManager(); + var mainWindow = this.FindAncestorOfType(); + switch (tool.Key) + { + case "copier": + mainWindow?.NavigateToHitSoundCopier(); + break; + case "metadata": + mainWindow?.NavigateToMetadataManager(); + break; + case "hitsound-editor": + mainWindow?.NavigateToHitSoundVisualizer(); + break; + case "combo-colour": + mainWindow?.NavigateToComboColourStudio(); + break; + case "map-cleaner": + mainWindow?.NavigateToMapCleaner(); + break; + } } - private void OpenComboColourStudio_OnClick(object? sender, RoutedEventArgs e) + private void OpenSettings_OnClick(object? sender, RoutedEventArgs e) { - this.FindAncestorOfType()?.NavigateToComboColourStudio(); + this.FindAncestorOfType()?.NavigateToSettings(); } } diff --git a/MapWizard.Theme/Controls/AnimatedHeightBorder.cs b/MapWizard.Theme/Controls/AnimatedHeightBorder.cs index bb89abc..4f652ea 100644 --- a/MapWizard.Theme/Controls/AnimatedHeightBorder.cs +++ b/MapWizard.Theme/Controls/AnimatedHeightBorder.cs @@ -22,6 +22,7 @@ public sealed class AnimatedHeightBorder : Border private Size _lastMeasuredContentSize; private DateTime _animationEndsAt; private bool _isAttached; + private bool _isTrackingLayout; public AnimatedHeightBorder() { @@ -31,7 +32,6 @@ public AnimatedHeightBorder() }; _settledMeasureTimer.Tick += OnSettledMeasureTimerTick; SizeChanged += OnSizeChanged; - LayoutUpdated += OnLayoutUpdated; } protected override Type StyleKeyOverride => typeof(AnimatedHeightBorder); @@ -46,6 +46,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _isAttached = true; + UpdateLayoutTracking(); ScheduleHeightUpdate(); RestartSettledMeasureTimer(); } @@ -53,6 +54,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _isAttached = false; + UpdateLayoutTracking(); _settledMeasureTimer.Stop(); base.OnDetachedFromVisualTree(e); } @@ -64,6 +66,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == IsExpandedProperty) { PseudoClasses.Set(":expanded", IsExpanded); + UpdateLayoutTracking(); ScheduleHeightUpdate(); RestartSettledMeasureTimer(); } @@ -78,7 +81,7 @@ private void OnSizeChanged(object? sender, SizeChangedEventArgs e) { // Height changes continuously during the transition. Only a width change // can alter wrapping and therefore the measured expansion target. - if (Math.Abs(e.NewSize.Width - e.PreviousSize.Width) <= 0.5d) + if (!IsExpanded || Math.Abs(e.NewSize.Width - e.PreviousSize.Width) <= 0.5d) { return; } @@ -102,6 +105,26 @@ private void OnLayoutUpdated(object? sender, EventArgs e) } } + private void UpdateLayoutTracking() + { + var shouldTrack = _isAttached && IsExpanded; + if (shouldTrack == _isTrackingLayout) + { + return; + } + + if (shouldTrack) + { + LayoutUpdated += OnLayoutUpdated; + } + else + { + LayoutUpdated -= OnLayoutUpdated; + } + + _isTrackingLayout = shouldTrack; + } + private void OnSettledMeasureTimerTick(object? sender, EventArgs e) { _settledMeasureTimer.Stop(); diff --git a/MapWizard.Theme/Controls/BusyIndicator.cs b/MapWizard.Theme/Controls/BusyIndicator.cs index 9680c43..15c68fe 100644 --- a/MapWizard.Theme/Controls/BusyIndicator.cs +++ b/MapWizard.Theme/Controls/BusyIndicator.cs @@ -24,6 +24,7 @@ public sealed class BusyIndicator : TemplatedControl private Control? _track; private Control? _indicator; private CompositionVisual? _indicatorVisual; + private List? _geometrySubscriptions; public static readonly StyledProperty IsRunningProperty = AvaloniaProperty.Register(nameof(IsRunning)); @@ -56,6 +57,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _track = e.NameScope.Find("PART_Track"); _indicator = e.NameScope.Find("PART_Indicator"); + TrackGeometry(); UpdateAnimationState(); } @@ -69,6 +71,8 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { StopAnimation(); + _geometrySubscriptions?.ForEach(subscription => subscription.Dispose()); + _geometrySubscriptions = null; _track = null; _indicator = null; TrackAncestors(null); @@ -77,6 +81,26 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e private List? _ancestorSubscriptions; + private void TrackGeometry() + { + _geometrySubscriptions?.ForEach(subscription => subscription.Dispose()); + _geometrySubscriptions = []; + + if (_track is not null) + { + _geometrySubscriptions.Add(_track + .GetPropertyChangedObservable(Visual.BoundsProperty) + .Subscribe(new AnonymousObserver(_ => UpdateAnimationState()))); + } + + if (_indicator is not null) + { + _geometrySubscriptions.Add(_indicator + .GetPropertyChangedObservable(Visual.BoundsProperty) + .Subscribe(new AnonymousObserver(_ => UpdateAnimationState()))); + } + } + private void TrackAncestors(Visual? root) { _ancestorSubscriptions?.ForEach(d => d.Dispose()); diff --git a/MapWizard.Theme/Controls/MapWizardWindow.cs b/MapWizard.Theme/Controls/MapWizardWindow.cs index 2eb9502..bb5757f 100644 --- a/MapWizard.Theme/Controls/MapWizardWindow.cs +++ b/MapWizard.Theme/Controls/MapWizardWindow.cs @@ -1,5 +1,5 @@ using System; -using System.Linq; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -10,17 +10,13 @@ namespace MapWizard.Theme.Controls; /// /// MapWizard's client-side-decorated application window. -/// Decoration painting is done by the app: a custom titlebar with caption -/// buttons plus Avalonia's drawn frame (border, shadow, resize grips), so the -/// look is identical on Linux (Wayland/X11), Windows and macOS. +/// The app provides the titlebar controls. Windows owns its outer shape and +/// corners; Linux keeps the app-drawn shell because compositor behavior varies. /// On macOS the native traffic lights are kept (full decorations) so the /// window controls sit at the top-left, as per platform convention. /// public class MapWizardWindow : Window { - /// The corner radius applied to the window shell in its normal state. - public const double ShellCornerRadius = 12; - public MapWizardWindow() { WindowDecorations = OperatingSystem.IsMacOS() @@ -28,8 +24,16 @@ public MapWizardWindow() : WindowDecorations.BorderOnly; ExtendClientAreaToDecorationsHint = true; ExtendClientAreaTitleBarHeightHint = 60; - TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; - Background = Brushes.Transparent; + if (OperatingSystem.IsWindows()) + { + Classes.Add("Windows"); + TransparencyLevelHint = [WindowTransparencyLevel.None]; + } + else + { + TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; + Background = Brushes.Transparent; + } AddHandler(PointerPressedEvent, OnWindowPointerPressed, RoutingStrategies.Tunnel); } @@ -64,11 +68,24 @@ private void OnWindowPointerPressed(object? sender, PointerPressedEventArgs e) return; } - var ancestors = source.GetVisualAncestors().OfType().ToArray(); - var isTitleBar = source.Classes.Contains("WindowTitleBar") || - ancestors.Any(x => x.Classes.Contains("WindowTitleBar")); - var isButton = source is Button || ancestors.Any(x => x is Button); - if (!isTitleBar || isButton) + var isTitleBar = false; + Visual? current = source; + while (current is not null) + { + if (current is Button) + { + return; + } + + if (current is Control control && control.Classes.Contains("WindowTitleBar")) + { + isTitleBar = true; + } + + current = current.GetVisualParent(); + } + + if (!isTitleBar) { return; } diff --git a/MapWizard.Theme/MapWizard.Theme.csproj b/MapWizard.Theme/MapWizard.Theme.csproj index 57e8444..61f46e9 100644 --- a/MapWizard.Theme/MapWizard.Theme.csproj +++ b/MapWizard.Theme/MapWizard.Theme.csproj @@ -8,5 +8,6 @@ + diff --git a/MapWizard.Theme/MapWizardTheme.axaml b/MapWizard.Theme/MapWizardTheme.axaml index c274cbb..d514a18 100644 --- a/MapWizard.Theme/MapWizardTheme.axaml +++ b/MapWizard.Theme/MapWizardTheme.axaml @@ -113,7 +113,7 @@ 15 0:0:0.12 0:0:0.2 - Quicksand, Noto Sans, Noto Sans CJK SC, Segoe UI, Arial + $Default, fonts:Inter#Inter, Noto Sans, Noto Sans CJK SC, Segoe UI, Arial @@ -124,6 +124,16 @@ + + + + - - - -