diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 41058ad..6f8d1b8 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -45,18 +45,19 @@ jobs: uses: microsoft/setup-msbuild@main with: msbuild-architecture: x64 - - name: Setup .NET 8 + - name: Setup .NET 9 uses: actions/setup-dotnet@main with: - dotnet-version: 8.x + dotnet-version: 9.x + dotnet-quality: 'preview' - name: Setup Windows 11 SDK (10.0.22621.0) uses: GuillaumeFalourd/setup-windows10-sdk-action@main with: sdk-version: 22621 - name: Restore - run: msbuild SUBSTitute.sln /t:restore + run: dotnet restore SUBSTitute.sln - name: Build - run: msbuild SUBSTitute.sln /p:Configuration=${{ github.event_name != 'workflow_dispatch' && 'Debug' || inputs.build_configuration }} /p:VersionSuffix=ci /p:RestorePackages=false + run: dotnet build SUBSTitute.sln --configuration ${{ github.event_name != 'workflow_dispatch' && 'Debug' || inputs.build_configuration }} --no-restore -p:VersionSuffix=ci sonarcloud: name: SonarCloud runs-on: windows-latest @@ -66,10 +67,11 @@ jobs: uses: actions/checkout@main with: fetch-depth: 0 - - name: Setup .NET 8 + - name: Setup .NET 9 uses: actions/setup-dotnet@main with: - dotnet-version: 8.x + dotnet-version: 9.x + dotnet-quality: 'preview' - name: Setup Windows 11 SDK (10.0.22621.0) uses: GuillaumeFalourd/setup-windows10-sdk-action@main with: @@ -109,5 +111,5 @@ jobs: run: | .\.sonar\scanner\dotnet-sonarscanner begin /k:"sungaila_SUBSTitute" /o:"sungaila" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml dotnet restore SUBSTitute.sln - dotnet build SUBSTitute.sln --configuration ${{ github.event_name != 'workflow_dispatch' && 'Debug' || inputs.build_configuration }} --no-restore + dotnet build SUBSTitute.sln --configuration ${{ github.event_name != 'workflow_dispatch' && 'Debug' || inputs.build_configuration }} --no-restore .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/README.md b/README.md index 323b2e7..1a13002 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,14 @@ # SUBSTitute A GUI for mapping directories to virtual drives (see *SUBST* command). It is built on top of **.NET 8.0** and **Windows UI Library 3 (WinUI 3)**. -Screenshot from version 2.0.0 +Screenshot from version 2.1.1 + +Screenshot from version 2.1.1 +Screenshot from version 2.1.1 +Screenshot from version 2.1.1 +Screenshot from version 2.1.1 +
- Get it from Microsoft Store + Get it from Microsoft Store diff --git a/etc/screenshot1_de-de.png b/etc/screenshot1_de-de.png index 30ba67f..4020fb8 100644 Binary files a/etc/screenshot1_de-de.png and b/etc/screenshot1_de-de.png differ diff --git a/etc/screenshot1_en-us.png b/etc/screenshot1_en-us.png index 772e86e..8008568 100644 Binary files a/etc/screenshot1_en-us.png and b/etc/screenshot1_en-us.png differ diff --git a/etc/screenshot2_de-de.png b/etc/screenshot2_de-de.png index 71a279b..c6c9137 100644 Binary files a/etc/screenshot2_de-de.png and b/etc/screenshot2_de-de.png differ diff --git a/etc/screenshot2_en-us.png b/etc/screenshot2_en-us.png index 16570fe..e58c726 100644 Binary files a/etc/screenshot2_en-us.png and b/etc/screenshot2_en-us.png differ diff --git a/etc/screenshot3_de-de.png b/etc/screenshot3_de-de.png index 4b19b08..451120f 100644 Binary files a/etc/screenshot3_de-de.png and b/etc/screenshot3_de-de.png differ diff --git a/etc/screenshot3_en-us.png b/etc/screenshot3_en-us.png index b86645f..3fbbb74 100644 Binary files a/etc/screenshot3_en-us.png and b/etc/screenshot3_en-us.png differ diff --git a/etc/screenshot4_de-de.png b/etc/screenshot4_de-de.png index 860f3c0..7d8be9a 100644 Binary files a/etc/screenshot4_de-de.png and b/etc/screenshot4_de-de.png differ diff --git a/etc/screenshot4_en-us.png b/etc/screenshot4_en-us.png index 649fe44..aa7608c 100644 Binary files a/etc/screenshot4_en-us.png and b/etc/screenshot4_en-us.png differ diff --git a/etc/screenshot5_de-de.png b/etc/screenshot5_de-de.png index 711122f..5e421c1 100644 Binary files a/etc/screenshot5_de-de.png and b/etc/screenshot5_de-de.png differ diff --git a/etc/screenshot5_en-us.png b/etc/screenshot5_en-us.png index 610627d..de0da0d 100644 Binary files a/etc/screenshot5_en-us.png and b/etc/screenshot5_en-us.png differ diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..7e59a38 --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/App.manifest b/src/App.manifest index 3e66d15..414b90a 100644 --- a/src/App.manifest +++ b/src/App.manifest @@ -2,7 +2,7 @@ + version="2.1.1.0" /> diff --git a/src/App.xaml b/src/App.xaml index 72ff7c0..2cb626b 100644 --- a/src/App.xaml +++ b/src/App.xaml @@ -6,6 +6,7 @@ + diff --git a/src/Commands/AddDriveCommands.cs b/src/Commands/AddDriveCommands.cs index fdf38e7..68f2168 100644 --- a/src/Commands/AddDriveCommands.cs +++ b/src/Commands/AddDriveCommands.cs @@ -46,7 +46,7 @@ public static class AddDriveCommands parameter.SelectedLetter = parameter.AvailableLetters.FirstOrDefault(); }); - public static readonly IRelayCommand AddVirtualDrive = new AsyncRelayCommand(async parameter => + public static readonly IRelayCommand AddVirtualDrive = new RelayCommand(parameter => { parameter!.CancelClose = false; @@ -55,7 +55,7 @@ public static class AddDriveCommands var selectedPath = Path.GetFullPath(parameter.SelectedPath.Trim('\"')); - _ = await StorageFolder.GetFolderFromPathAsync(selectedPath); + _ = StorageFolder.GetFolderFromPathAsync(selectedPath).GetAwaiter().GetResult(); if (parameter.IsPermanent) { diff --git a/src/Commands/MappingCommands.cs b/src/Commands/MappingCommands.cs index 6bba23f..98da81e 100644 --- a/src/Commands/MappingCommands.cs +++ b/src/Commands/MappingCommands.cs @@ -61,21 +61,29 @@ internal static DriveViewModel GetDriveViewModel(MappingViewModel mappingViewMod Task.Run(async () => { - // get the disk label and file system + string? driveName = null; + string? driveFormat = null; + try { - var folder = await StorageFolder.GetFolderFromPathAsync(driveInfo.Name); - var properties = await folder.GetBasicPropertiesAsync(); - var prop = await properties.RetrievePropertiesAsync(["System.Volume.FileSystem"]); - var filesystem = prop.First().Value as string; - - App.MainWindow?.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => - { - result.Label = folder.DisplayName; - result.DriveFormat = filesystem ?? string.Empty; - }); + driveName = (await StorageFolder.GetFolderFromPathAsync(driveInfo.Name)).DisplayName; } catch { } + + try + { + driveFormat = driveInfo.DriveFormat; + } + catch (IOException ex) when (ex.HResult == -2147024875) + { + // ERROR_NOT_READY: Device not ready + } + + App.MainWindow?.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => + { + result.Label = driveName ?? string.Empty; + result.DriveFormat = driveFormat ?? string.Empty; + }); }); return result; diff --git a/src/Converters/ElementThemeToIntConverter.cs b/src/Converters/ElementThemeToIntConverter.cs new file mode 100644 index 0000000..094c071 --- /dev/null +++ b/src/Converters/ElementThemeToIntConverter.cs @@ -0,0 +1,35 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using System; + +namespace Sungaila.SUBSTitute.Converters +{ + public partial class ElementThemeToIntConverter : IValueConverter + { + public object? Convert(object value, Type targetType, object parameter, string language) + { + if (value is not ElementTheme theme) + return DependencyProperty.UnsetValue; + + return theme switch + { + ElementTheme.Light => 0, + ElementTheme.Dark => 1, + _ => 2 + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is not int selectedIndex) + return DependencyProperty.UnsetValue; + + return selectedIndex switch + { + 0 => ElementTheme.Light, + 1 => ElementTheme.Dark, + _ => ElementTheme.Default + }; + } + } +} \ No newline at end of file diff --git a/src/Extensions/HackedCollectionView.Defer.cs b/src/Extensions/HackedCollectionView.Defer.cs deleted file mode 100644 index e95605e..0000000 --- a/src/Extensions/HackedCollectionView.Defer.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; - -namespace Sungaila.SUBSTitute.Extensions; - -/// -/// A collection view implementation that supports filtering, grouping, sorting and incremental loading -/// -public partial class HackedCollectionView -{ - /// - /// Stops refreshing until it is disposed - /// - /// An disposable object - public IDisposable DeferRefresh() - { - return new NotificationDeferrer(this); - } - - /// - /// Notification deferrer helper class - /// -#pragma warning disable CA1063 // Implement IDisposable Correctly - public partial class NotificationDeferrer : IDisposable -#pragma warning restore CA1063 // Implement IDisposable Correctly - { - private readonly HackedCollectionView _acvs; - private readonly object _currentItem; - - /// - /// Initializes a new instance of the class. - /// - /// Source ACVS - public NotificationDeferrer(HackedCollectionView acvs) - { - _acvs = acvs; - _currentItem = _acvs.CurrentItem; - _acvs._deferCounter++; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// 2 -#pragma warning disable CA1063 // Implement IDisposable Correctly - public void Dispose() -#pragma warning restore CA1063 // Implement IDisposable Correctly - { - _acvs.MoveCurrentTo(_currentItem); - _acvs._deferCounter--; - _acvs.Refresh(); - } - } -} diff --git a/src/Extensions/HackedCollectionView.Events.cs b/src/Extensions/HackedCollectionView.Events.cs deleted file mode 100644 index b3331b5..0000000 --- a/src/Extensions/HackedCollectionView.Events.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.UI.Xaml.Data; -using Windows.Foundation.Collections; - -namespace Sungaila.SUBSTitute.Extensions; - -/// -/// A collection view implementation that supports filtering, grouping, sorting and incremental loading -/// -public partial class HackedCollectionView -{ - /// - /// Currently selected item changing event - /// - /// event args - private void OnCurrentChanging(CurrentChangingEventArgs e) - { - if (_deferCounter > 0) - { - return; - } - - CurrentChanging?.Invoke(this, e); - } - - /// - /// Currently selected item changed event - /// - /// event args - private void OnCurrentChanged(object e) - { - if (_deferCounter > 0) - { - return; - } - - CurrentChanged?.Invoke(this, e); - - // ReSharper disable once ExplicitCallerInfoArgument - OnPropertyChanged(nameof(CurrentItem)); - } - - /// - /// Vector changed event - /// - /// event args - private void OnVectorChanged(IVectorChangedEventArgs e) - { - if (_deferCounter > 0) - { - return; - } - - VectorChanged?.Invoke(this, e); - - // ReSharper disable once ExplicitCallerInfoArgument - OnPropertyChanged(nameof(Count)); - } -} diff --git a/src/Extensions/HackedCollectionView.cs b/src/Extensions/HackedCollectionView.cs deleted file mode 100644 index 0657e72..0000000 --- a/src/Extensions/HackedCollectionView.cs +++ /dev/null @@ -1,812 +0,0 @@ -using CommunityToolkit.WinUI.Collections; -using CommunityToolkit.WinUI.Helpers; -using Microsoft.UI.Xaml.Data; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using Windows.Foundation; -using Windows.Foundation.Collections; - -namespace Sungaila.SUBSTitute.Extensions; - -public partial class HackedCollectionView : IAdvancedCollectionView, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer -{ - private readonly List _view; - - private readonly ObservableCollection _sortDescriptions; - - private readonly Dictionary _sortProperties; - - private readonly bool _liveShapingEnabled; - - private readonly HashSet _observedFilterProperties = new HashSet(); - - private IList _source; - - private Predicate _filter; - private int _deferCounter; - - private WeakEventListener _sourceWeakEventListener; - - /// - /// Initializes a new instance of the class. - /// - public HackedCollectionView() - : this(new List(0)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// source IEnumerable - /// Denotes whether or not this ACV should re-filter/re-sort if a PropertyChanged is raised for an observed property. -#pragma warning disable CS8767 -#pragma warning disable CS8769 -#pragma warning disable CS8622 -#pragma warning disable CS8600 -#pragma warning disable CS8601 -#pragma warning disable CS8604 -#pragma warning disable CS8603 // Possible null reference return. -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public HackedCollectionView(IList source, bool isLiveShaping = false) - { - _liveShapingEnabled = isLiveShaping; - _view = new List(); - _sortDescriptions = new ObservableCollection(); - _sortDescriptions.CollectionChanged += SortDescriptions_CollectionChanged; - _sortProperties = new Dictionary(); - Source = source; - } - - /// - /// Gets or sets the source - /// - public IList Source - { - get - { - return _source; - } - - set - { - // ReSharper disable once PossibleUnintendedReferenceComparison - if (_source == value) - { - return; - } - - if (_source != null) - { - DetachPropertyChangedHandler(_source); - } - - _source = value; - AttachPropertyChangedHandler(_source); - - _sourceWeakEventListener?.Detach(); - - if (_source is INotifyCollectionChanged sourceNcc) - { - _sourceWeakEventListener = - new WeakEventListener(this) - { - // Call the actual collection changed event - OnEventAction = (source, changed, arg3) => SourceNcc_CollectionChanged(source, arg3), - - // The source doesn't exist anymore - OnDetachAction = (listener) => sourceNcc.CollectionChanged -= _sourceWeakEventListener!.OnEvent - }; - sourceNcc.CollectionChanged += _sourceWeakEventListener.OnEvent; - } - - HandleSourceChanged(); - OnPropertyChanged(); - } - } - - /// - /// Manually refresh the view - /// - public void Refresh() - { - HandleSourceChanged(); - } - - /// - public void RefreshFilter() - { - HandleFilterChanged(); - } - - /// - public void RefreshSorting() - { - HandleSortChanged(); - } - - /// - public IEnumerator GetEnumerator() => _view.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => _view.GetEnumerator(); - - /// - public void Add(object item) - { - if (IsReadOnly) - { - throw new NotSupportedException("Collection is read-only."); - } - - _source.Add(item); - } - - /// - public void Clear() - { - if (IsReadOnly) - { - throw new NotSupportedException("Collection is read-only."); - } - - _source.Clear(); - } - - /// - public bool Contains(object item) => _view.Contains(item); - - /// - public void CopyTo(object[] array, int arrayIndex) => _view.CopyTo(array, arrayIndex); - - /// - public bool Remove(object item) - { - if (IsReadOnly) - { - throw new NotSupportedException("Collection is read-only."); - } - - _source.Remove(item); - return true; - } - - /// - public int Count => _view.Count; - - /// - public bool IsReadOnly => _source == null || _source.IsReadOnly; - - /// - public int IndexOf(object item) => _view.IndexOf(item); - - /// - public void Insert(int index, object item) - { - if (IsReadOnly) - { - throw new NotSupportedException("Collection is read-only."); - } - - _source.Insert(index, item); - } - - /// - /// Removes the item at the specified index. - /// - /// The zero-based index of the item to remove. is not a valid index in the .The is read-only. - public void RemoveAt(int index) => Remove(_view[index]); - - /// - /// Gets or sets the element at the specified index. - /// - /// - /// The element at the specified index. - /// - /// The zero-based index of the element to get or set. is not a valid index in the .The property is set and the is read-only. - public object this[int index] - { - get { return _view[index]; } - set { _view[index] = value; } - } - - /// - /// Occurs when the vector changes. - /// - public event Windows.Foundation.Collections.VectorChangedEventHandler VectorChanged; - - /// - /// Move current index to item - /// - /// item - /// success of operation - public bool MoveCurrentTo(object item) => item == CurrentItem || MoveCurrentToIndex(IndexOf(item)); - - /// - /// Moves selected item to position - /// - /// index - /// success of operation - public bool MoveCurrentToPosition(int index) => MoveCurrentToIndex(index); - - /// - /// Move current item to first item - /// - /// success of operation - public bool MoveCurrentToFirst() => MoveCurrentToIndex(0); - - /// - /// Move current item to last item - /// - /// success of operation - public bool MoveCurrentToLast() => MoveCurrentToIndex(_view.Count - 1); - - /// - /// Move current item to next item - /// - /// success of operation - public bool MoveCurrentToNext() => MoveCurrentToIndex(CurrentPosition + 1); - - /// - /// Move current item to previous item - /// - /// success of operation - public bool MoveCurrentToPrevious() => MoveCurrentToIndex(CurrentPosition - 1); - - /// - /// Load more items from the source - /// - /// number of items to load - /// Async operation of LoadMoreItemsResult - /// Not implemented yet... - public IAsyncOperation LoadMoreItemsAsync(uint count) - { - var sil = _source as ISupportIncrementalLoading; - return sil?.LoadMoreItemsAsync(count); - } - - /// - /// Gets the groups in collection - /// - public IObservableVector CollectionGroups => null; - - /// - /// Gets or sets the current item - /// - public object CurrentItem - { - - get { return CurrentPosition > -1 && CurrentPosition < _view.Count ? _view[CurrentPosition] : null; } -#pragma warning restore CS8603 // Possible null reference return. - set { MoveCurrentTo(value); } - } - - /// - /// Gets the position of current item - /// - public int CurrentPosition { get; private set; } - - /// - /// Gets a value indicating whether the source has more items - /// - public bool HasMoreItems => (_source as ISupportIncrementalLoading)?.HasMoreItems ?? false; - - /// - /// Gets a value indicating whether the current item is after the last visible item - /// - public bool IsCurrentAfterLast => CurrentPosition >= _view.Count; - - /// - /// Gets a value indicating whether the current item is before the first visible item - /// - public bool IsCurrentBeforeFirst => CurrentPosition < 0; - - /// - /// Current item changed event handler - /// - public event EventHandler CurrentChanged; - - /// - /// Current item changing event handler - /// - public event CurrentChangingEventHandler CurrentChanging; - - /// - /// Gets a value indicating whether this CollectionView can filter its items - /// - public bool CanFilter => true; - - /// - /// Gets or sets the predicate used to filter the visible items - /// - public Predicate Filter - { - get - { - return _filter; - } - - set - { - if (_filter == value) - { - return; - } - - _filter = value; - HandleFilterChanged(); - } - } - - /// - /// Gets a value indicating whether this CollectionView can sort its items - /// - public bool CanSort => true; - - /// - /// Gets SortDescriptions to sort the visible items - /// - public IList SortDescriptions => _sortDescriptions; - - /* - /// - /// Gets a value indicating whether this CollectionView can group its items - /// - public bool CanGroup => false; - - /// - /// Gets GroupDescriptions to group the visible items - /// - public IList GroupDescriptions => null; - */ - - /// - /// Gets the source collection - /// - public IEnumerable SourceCollection => _source; - - /// - /// IComparer implementation - /// - /// Object A - /// Object B - /// Comparison value -#pragma warning disable CA1033 // Interface methods should be callable by child types - int IComparer.Compare(object x, object y) -#pragma warning restore CA1033 // Interface methods should be callable by child types - { - if (!_sortProperties.Any()) - { - var listType = _source?.GetType(); - Type type; - - if (listType != null && listType.IsGenericType) - { - type = listType.GetGenericArguments()[0]; - } - else - { - type = x.GetType(); - } - - foreach (var sd in _sortDescriptions) - { - if (!string.IsNullOrEmpty(sd.PropertyName)) - { - _sortProperties[sd.PropertyName] = type.GetProperty(sd.PropertyName); - } - } - } - - foreach (var sd in _sortDescriptions) - { - object cx, cy; - - if (string.IsNullOrEmpty(sd.PropertyName)) - { - cx = x; - cy = y; - } - else - { - var pi = _sortProperties[sd.PropertyName]; - - cx = pi.GetValue(x!); - cy = pi.GetValue(y!); - } - - var cmp = sd.Comparer.Compare(cx, cy); - - if (cmp != 0) - { - return sd.Direction == SortDirection.Ascending ? +cmp : -cmp; - } - } - - return 0; - } - - /// - /// Occurs when a property value changes. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Property changed event invoker - /// - /// name of the property that changed - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - /// - public void ObserveFilterProperty(string propertyName) - { - _observedFilterProperties.Add(propertyName); - } - - /// - public void ClearObservedFilterProperties() - { - _observedFilterProperties.Clear(); - } - - private void ItemOnPropertyChanged(object item, PropertyChangedEventArgs e) - { - if (!_liveShapingEnabled) - { - return; - } - - var filterResult = _filter?.Invoke(item); - - if (filterResult.HasValue && _observedFilterProperties.Contains(e.PropertyName)) - { - var viewIndex = _view.IndexOf(item); - if (viewIndex != -1 && !filterResult.Value) - { - RemoveFromView(viewIndex, item); - } - else if (viewIndex == -1 && filterResult.Value) - { - var index = _source.IndexOf(item); - HandleItemAdded(index, item); - } - } - - if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName)) - { - var oldIndex = _view.IndexOf(item); - - // Check if item is in view: - if (oldIndex < 0) - { - return; - } - - _view.RemoveAt(oldIndex); - var targetIndex = _view.BinarySearch(item, this); - if (targetIndex < 0) - { - targetIndex = ~targetIndex; - } - - // Only trigger expensive UI updates if the index really changed: - if (targetIndex != oldIndex) - { - OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, item)); - - _view.Insert(targetIndex, item); - - OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, item)); - } - else - { - _view.Insert(targetIndex, item); - } - } - else if (string.IsNullOrEmpty(e.PropertyName)) - { - HandleSourceChanged(); - } - } - - private void AttachPropertyChangedHandler(IEnumerable items) - { - if (!_liveShapingEnabled || items == null) - { - return; - } - - foreach (var item in items.OfType()) - { - item.PropertyChanged += ItemOnPropertyChanged; - } - } - - private void DetachPropertyChangedHandler(IEnumerable items) - { - if (!_liveShapingEnabled || items == null) - { - return; - } - - foreach (var item in items.OfType()) - { - item.PropertyChanged -= ItemOnPropertyChanged; - } - } - - private void HandleSortChanged() - { - _sortProperties.Clear(); - _view.Sort(this); - _sortProperties.Clear(); - OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); - } - - private void HandleFilterChanged() - { - if (_filter != null) - { - for (var index = 0; index < _view.Count; index++) - { - var item = _view.ElementAt(index); - if (_filter(item)) - { - continue; - } - - RemoveFromView(index, item); - index--; - } - } - - var viewHash = new HashSet(_view); - var viewIndex = 0; - for (var index = 0; index < _source.Count; index++) - { - var item = _source[index]; - if (viewHash.Contains(item)) - { - viewIndex++; - continue; - } - - if (HandleItemAdded(index, item, viewIndex)) - { - viewIndex++; - } - } - } - - private void HandleSourceChanged() - { - _sortProperties.Clear(); - var currentItem = CurrentItem; - _view.Clear(); - foreach (var item in Source) - { - if (_filter != null && !_filter(item)) - { - continue; - } - - if (_sortDescriptions.Any()) - { - var targetIndex = _view.BinarySearch(item, this); - if (targetIndex < 0) - { - targetIndex = ~targetIndex; - } - - _view.Insert(targetIndex, item); - } - else - { - _view.Add(item); - } - } - - _sortProperties.Clear(); - OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); - MoveCurrentTo(currentItem); - } - - private void SourceNcc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - // ReSharper disable once SwitchStatementMissingSomeCases - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - AttachPropertyChangedHandler(e.NewItems); - if (_deferCounter <= 0) - { - if (e.NewItems?.Count == 1) - { - HandleItemAdded(e.NewStartingIndex, e.NewItems[0]); - } - else - { - HandleSourceChanged(); - } - } - - break; - case NotifyCollectionChangedAction.Remove: - DetachPropertyChangedHandler(e.OldItems); - if (_deferCounter <= 0) - { - if (e.OldItems?.Count == 1) - { - HandleItemRemoved(e.OldStartingIndex, e.OldItems[0]); - } - else - { - HandleSourceChanged(); - } - } - - break; - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - case NotifyCollectionChangedAction.Reset: - if (_deferCounter <= 0) - { - HandleSourceChanged(); - } - - break; - } - } - - private bool HandleItemAdded(int newStartingIndex, object newItem, int? viewIndex = null) - { - if (_filter != null && !_filter(newItem)) - { - return false; - } - - var newViewIndex = _view.Count; - - if (_sortDescriptions.Any()) - { - _sortProperties.Clear(); - newViewIndex = _view.BinarySearch(newItem, this); - if (newViewIndex < 0) - { - newViewIndex = ~newViewIndex; - } - } - else if (_filter != null) - { - if (_source == null) - { - HandleSourceChanged(); - return false; - } - - if (newStartingIndex == 0 || _view.Count == 0) - { - newViewIndex = 0; - } - else if (newStartingIndex == _source.Count - 1) - { - newViewIndex = _view.Count; - } - else if (viewIndex.HasValue) - { - newViewIndex = viewIndex.Value; - } - else - { - for (int i = 0, j = 0; i < _source.Count; i++) - { - if (i == newStartingIndex) - { - newViewIndex = j; - break; - } - - if (_view[j] == _source[i]) - { - j++; - } - } - } - } - - _view.Insert(newViewIndex, newItem); - if (newViewIndex <= CurrentPosition) - { - CurrentPosition++; - } - - var e = new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem); - OnVectorChanged(e); - return true; - } - - private void HandleItemRemoved(int oldStartingIndex, object oldItem) - { - if (_filter != null && !_filter(oldItem)) - { - return; - } - - if (oldStartingIndex < 0 || oldStartingIndex >= _view.Count || !Equals(_view[oldStartingIndex], oldItem)) - { - oldStartingIndex = _view.IndexOf(oldItem); - } - - if (oldStartingIndex < 0) - { - return; - } - - RemoveFromView(oldStartingIndex, oldItem); - } - - private void RemoveFromView(int itemIndex, object item) - { - if (itemIndex <= CurrentPosition) - { - CurrentPosition--; - } - - var e = new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item); - OnVectorChanged(e); - _view.RemoveAt(itemIndex); - } - - private void SortDescriptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_deferCounter > 0) - { - return; - } - - HandleSortChanged(); - } - - private bool MoveCurrentToIndex(int i) - { - if (i < -1 || i >= _view.Count) - { - return false; - } - - if (i == CurrentPosition) - { - return false; - } - - var e = new CurrentChangingEventArgs(); - OnCurrentChanging(e); - if (e.Cancel) - { - return false; - } - - CurrentPosition = i; - OnCurrentChanged(null!); - return true; - } -} - -#pragma warning restore CS8767 -#pragma warning restore CS8769 -#pragma warning restore CS8622 -#pragma warning restore CS8601 -#pragma warning restore CS8600 -#pragma warning restore CS8604 -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -#pragma warning restore CS8603 // Possible null reference return. diff --git a/src/Extensions/TypeExtension.cs b/src/Extensions/TypeExtension.cs index 09f5a26..1439655 100644 --- a/src/Extensions/TypeExtension.cs +++ b/src/Extensions/TypeExtension.cs @@ -4,13 +4,13 @@ namespace Sungaila.SUBSTitute.Extensions { - [MarkupExtensionReturnType(ReturnType = typeof(string))] + [MarkupExtensionReturnType(ReturnType = typeof(Type))] public partial class TypeExtension : MarkupExtension { public Type? Type { get; set; } - protected override object? ProvideValue() => Type?.FullName; + protected override object? ProvideValue() => Type; - protected override object? ProvideValue(IXamlServiceProvider serviceProvider) => Type?.FullName; + protected override object? ProvideValue(IXamlServiceProvider serviceProvider) => Type; } } \ No newline at end of file diff --git a/src/Extensions/VectorChangedEventArgs.cs b/src/Extensions/VectorChangedEventArgs.cs deleted file mode 100644 index 8e9392e..0000000 --- a/src/Extensions/VectorChangedEventArgs.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Windows.Foundation.Collections; - -namespace Sungaila.SUBSTitute.Extensions; - -/// -/// Vector changed EventArgs -/// -internal partial class VectorChangedEventArgs : IVectorChangedEventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// collection change type - /// index of item changed - /// item changed - public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!) - { - CollectionChange = cc; - Index = (uint)index; - } - - /// - /// Gets the type of change that occurred in the vector. - /// - /// - /// The type of change in the vector. - /// - public CollectionChange CollectionChange { get; } - - /// - /// Gets the position where the change occurred in the vector. - /// - /// - /// The zero-based position where the change occurred in the vector, if applicable. - /// - public uint Index { get; } -} diff --git a/src/Extensions/WindowExtensions.cs b/src/Extensions/WindowExtensions.cs deleted file mode 100644 index d040ddd..0000000 --- a/src/Extensions/WindowExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.UI.Xaml; -using Sungaila.ImmersiveDarkMode.WinUI; -using System; -using Windows.UI.ViewManagement; - -namespace Sungaila.SUBSTitute.Extensions -{ - public static class WindowExtensions - { - public static void InitTitlebarTheme2(this Window window) - { - ArgumentNullException.ThrowIfNull(window); - - void colorValuesChangedHandler(UISettings sender, object args) => window.SetTitlebarTheme(); - - var uiSettings = new UISettings(); - uiSettings.ColorValuesChanged += colorValuesChangedHandler; - - void closedHandler(object sender, WindowEventArgs args) - { - window.Closed -= closedHandler; - uiSettings!.ColorValuesChanged -= colorValuesChangedHandler; - uiSettings = null; - }; - - window.Closed -= closedHandler; - window.Closed += closedHandler; - - window.SetTitlebarTheme(); - } - } -} diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt index be080e5..f1dc897 100644 --- a/src/NativeMethods.txt +++ b/src/NativeMethods.txt @@ -1,6 +1,3 @@ DefineDosDevice QueryDosDevice -IFileOpenDialog -FileOpenDialog -CoCreateInstance GetWindowPlacement \ No newline at end of file diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index ee7c0f5..eaee4cf 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -10,7 +10,7 @@ + Version="2.1.1.0" /> diff --git a/src/Properties/PublishProfiles/win-arm64.pubxml b/src/Properties/PublishProfiles/win-arm64.pubxml index 3526538..d0e1491 100644 --- a/src/Properties/PublishProfiles/win-arm64.pubxml +++ b/src/Properties/PublishProfiles/win-arm64.pubxml @@ -12,8 +12,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121. False False True - False - True - False + True \ No newline at end of file diff --git a/src/Properties/PublishProfiles/win-x64.pubxml b/src/Properties/PublishProfiles/win-x64.pubxml index e6a0cd6..9f35710 100644 --- a/src/Properties/PublishProfiles/win-x64.pubxml +++ b/src/Properties/PublishProfiles/win-x64.pubxml @@ -12,8 +12,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121. False False True - False - True - False + True \ No newline at end of file diff --git a/src/SUBSTitute.csproj b/src/SUBSTitute.csproj index 0ce4d7c..a79c1b7 100644 --- a/src/SUBSTitute.csproj +++ b/src/SUBSTitute.csproj @@ -3,7 +3,7 @@ WinExe - net8.0-windows10.0.22621.0 + net9.0-windows10.0.22621.0 10.0.22621.0 10.0.22621.45 SUBSTitute @@ -19,14 +19,19 @@ 8CD1A8DE2BD78D45B0761089D621324F934DA0CA True false - partial - true - true + + + + + full + false + false + <_AggressiveAttributeTrimming>true - 2.1.0 + 2.1.1 David Sungaila false @@ -96,16 +101,16 @@ - - - - - + + + + + + - + - diff --git a/src/Strings/de-de/Resources.resw b/src/Strings/de-de/Resources.resw index 3e4cc61..37f9dde 100644 --- a/src/Strings/de-de/Resources.resw +++ b/src/Strings/de-de/Resources.resw @@ -141,10 +141,10 @@ Entfernen - + Laufwerk - + Dateisystem @@ -168,7 +168,7 @@ Unbekannt - + Typ @@ -201,7 +201,7 @@ Virtuelles Laufwerk - + Beschriftung diff --git a/src/Strings/en-us/Resources.resw b/src/Strings/en-us/Resources.resw index 66f30fc..c62182f 100644 --- a/src/Strings/en-us/Resources.resw +++ b/src/Strings/en-us/Resources.resw @@ -141,10 +141,10 @@ Remove - + Drive - + File system @@ -168,7 +168,7 @@ Unknown - + Type @@ -201,7 +201,7 @@ Virtual drive - + Label diff --git a/src/ThirdParty/DataTable/DataColumn.cs b/src/ThirdParty/DataTable/DataColumn.cs new file mode 100644 index 0000000..3cd1a24 --- /dev/null +++ b/src/ThirdParty/DataTable/DataColumn.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using System; + +namespace CommunityToolkit.WinUI.Controls; + +[TemplatePart(Name = nameof(PART_ColumnSizer), Type = typeof(ContentSizer))] +public partial class DataColumn : ContentControl +{ + private static GridLength StarLength = new GridLength(1, GridUnitType.Star); + + private ContentSizer? PART_ColumnSizer; + + private WeakReference? _parent; + + /// + /// Gets or sets the width of the largest child contained within the visible s of the . + /// + internal double MaxChildDesiredWidth { get; set; } + + /// + /// Gets or sets the internal copy of the property to be used in calculations, this gets manipulated in Auto-Size mode. + /// + internal GridLength CurrentWidth { get; private set; } + + /// + /// Gets or sets whether the column can be resized by the user. + /// + public bool CanResize + { + get { return (bool)GetValue(CanResizeProperty); } + set { SetValue(CanResizeProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty CanResizeProperty = + DependencyProperty.Register("CanResize", typeof(bool), typeof(DataColumn), new PropertyMetadata(false)); + + /// + /// Gets or sets the desired width of the column upon initialization. Defaults to a of 1 . + /// + public GridLength DesiredWidth + { + get { return (GridLength)GetValue(DesiredWidthProperty); } + set { SetValue(DesiredWidthProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty DesiredWidthProperty = + DependencyProperty.Register(nameof(DesiredWidth), typeof(GridLength), typeof(DataColumn), new PropertyMetadata(GridLength.Auto, DesiredWidth_PropertyChanged)); + + private static void DesiredWidth_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // If the developer updates the size of the column, update our internal copy + if (d is DataColumn col) + { + col.CurrentWidth = col.DesiredWidth; + } + } + + public DataColumn() + { + this.DefaultStyleKey = typeof(DataColumn); + } + + protected override void OnApplyTemplate() + { + if (PART_ColumnSizer != null) + { + PART_ColumnSizer.TargetControl = null; + PART_ColumnSizer.ManipulationDelta -= this.PART_ColumnSizer_ManipulationDelta; + PART_ColumnSizer.ManipulationCompleted -= this.PART_ColumnSizer_ManipulationCompleted; + } + + PART_ColumnSizer = GetTemplateChild(nameof(PART_ColumnSizer)) as ContentSizer; + + if (PART_ColumnSizer != null) + { + PART_ColumnSizer.TargetControl = this; + PART_ColumnSizer.ManipulationDelta += this.PART_ColumnSizer_ManipulationDelta; + PART_ColumnSizer.ManipulationCompleted += this.PART_ColumnSizer_ManipulationCompleted; + } + + // Get DataTable parent weak reference for when we manipulate columns. + var parent = this.FindAscendant(); + if (parent != null) + { + _parent = new(parent); + } + + base.OnApplyTemplate(); + } + + private void PART_ColumnSizer_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + ColumnResizedByUserSizer(); + } + + private void PART_ColumnSizer_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + ColumnResizedByUserSizer(); + } + + private void ColumnResizedByUserSizer() + { + // Update our internal representation to be our size now as a fixed value. + CurrentWidth = new(this.ActualWidth); + + // Notify the rest of the table to update + if (_parent?.TryGetTarget(out DataTable? parent) == true + && parent != null) + { + parent.ColumnResized(); + } + } +} diff --git a/src/ThirdParty/DataTable/DataColumn.xaml b/src/ThirdParty/DataTable/DataColumn.xaml new file mode 100644 index 0000000..3841bb3 --- /dev/null +++ b/src/ThirdParty/DataTable/DataColumn.xaml @@ -0,0 +1,55 @@ + + + + + + + + diff --git a/src/ThirdParty/DataTable/DataRow.cs b/src/ThirdParty/DataTable/DataRow.cs new file mode 100644 index 0000000..080c5da --- /dev/null +++ b/src/ThirdParty/DataTable/DataRow.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Linq; +using Windows.Foundation; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class DataRow : Panel +{ + // TODO: Create our own helper class here for the Header as well vs. straight-Grid. + // TODO: WeakReference? + private Panel? _parentPanel; + private DataTable? _parentTable; + + private bool _isTreeView; + private double _treePadding; + + public DataRow() + { + Unloaded += this.DataRow_Unloaded; + } + + private void DataRow_Unloaded(object sender, RoutedEventArgs e) + { + // Remove our references on unloaded + _parentTable?.Rows.Remove(this); + _parentTable = null; + _parentPanel = null; + } + + private Panel? InitializeParentHeaderConnection() + { + // TODO: Think about this expression instead... + // Drawback: Can't have Grid between table and header + // Positive: don't have to restart climbing the Visual Tree if we don't find ItemsPresenter... + ////var parent = this.FindAscendant(static (element) => element is ItemsPresenter or Grid); + + // TODO: Investigate what a scenario with an ItemsRepeater would look like (with a StackLayout, but using DataRow as the item's panel inside) + Panel? panel = null; + + // 1a. Get parent ItemsPresenter to find header + if (this.FindAscendant() is ItemsPresenter itemsPresenter) + { + // 2. Quickly check if the header is just what we're looking for. + if (itemsPresenter.Header is Grid or DataTable) + { + panel = itemsPresenter.Header as Panel; + } + else + { + // 3. Otherwise, try and find the inner thing we want. + panel = itemsPresenter.FindDescendant(static (element) => element is Grid or DataTable); + } + + // Check if we're in a TreeView + _isTreeView = itemsPresenter.FindAscendant() is TreeView; + } + + // 1b. If we can't find the ItemsPresenter, then we reach up outside to find the next thing we could use as a parent + panel ??= this.FindAscendant(static (element) => element is Grid or DataTable); + + // Cache actual datatable reference + if (panel is DataTable table) + { + _parentTable = table; + _parentTable.Rows.Add(this); // Add us to the row list. + } + + return panel; + } + + protected override Size MeasureOverride(Size availableSize) + { + // We should probably only have to do this once ever? + _parentPanel ??= InitializeParentHeaderConnection(); + + double maxHeight = 0; + + if (Children.Count > 0) + { + // If we don't have a grid, just measure first child to get row height and take available space + if (_parentPanel is null) + { + Children[0].Measure(availableSize); + return new Size(availableSize.Width, Children[0].DesiredSize.Height); + } + // Handle DataTable Parent + else if (_parentTable != null + && _parentTable.Children.Count == Children.Count) + { + // TODO: Need to check visibility + // Measure all children since we need to determine the row's height at minimum + for (int i = 0; i < Children.Count; i++) + { + if (_parentTable.Children[i] is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Auto } col) + { + Children[i].Measure(availableSize); + + // For TreeView in the first column, we want the header to expand to encompass + // the maximum indentation of the tree. + double padding = 0; + //// TODO: We only want/need to do this once? We may want to do if we're not an Auto column too...? + if (i == 0 && _isTreeView) + { + // Get our containing grid from TreeViewItem, start with our indented padding + var parentContainer = this.FindAscendant("MultiSelectGrid") as Grid; + if (parentContainer != null) + { + _treePadding = parentContainer.Padding.Left; + // We assume our 'DataRow' is in the last child slot of the Grid, need to know how large the other columns are. + for (int j = 0; j < parentContainer.Children.Count - 1; j++) + { + // TODO: We may need to get the actual size here later in Arrange? + _treePadding += parentContainer.Children[j].DesiredSize.Width; + } + } + padding = _treePadding; + } + + // TODO: Do we want this to ever shrink back? + var prev = col.MaxChildDesiredWidth; + col.MaxChildDesiredWidth = Math.Max(col.MaxChildDesiredWidth, Children[i].DesiredSize.Width + padding); + if (col.MaxChildDesiredWidth != prev) + { + // If our measure has changed, then we have to invalidate the arrange of the DataTable + _parentTable.ColumnResized(); + } + + } + else if (_parentTable.Children[i] is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Pixel } pixel) + { + Children[i].Measure(new(pixel.DesiredWidth.Value, availableSize.Height)); + } + else + { + Children[i].Measure(availableSize); + } + + maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); + } + } + // Fallback for Grid Hybrid scenario... + else if (_parentPanel is Grid grid + && _parentPanel.Children.Count == Children.Count + && grid.ColumnDefinitions.Count == Children.Count) + { + // TODO: Need to check visibility + // Measure all children since we need to determine the row's height at minimum + for (int i = 0; i < Children.Count; i++) + { + if (grid.ColumnDefinitions[i].Width.GridUnitType == GridUnitType.Pixel) + { + Children[i].Measure(new(grid.ColumnDefinitions[i].Width.Value, availableSize.Height)); + } + else + { + Children[i].Measure(availableSize); + } + + maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); + } + } + // TODO: What do we want to do if there's unequal children in the DataTable vs. DataRow? + } + + // Otherwise, return our parent's size as the desired size. + return new(_parentPanel?.DesiredSize.Width ?? availableSize.Width, maxHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + int column = 0; + double x = 0; + + // Try and grab Column Spacing from DataTable, if not a parent Grid, if not 0. + double spacing = _parentTable?.ColumnSpacing ?? (_parentPanel as Grid)?.ColumnSpacing ?? 0; + + double width = 0; + + if (_parentPanel != null) + { + int i = 0; + foreach (UIElement child in Children.Where(static e => e.Visibility == Visibility.Visible)) + { + if (_parentPanel is Grid grid && + column < grid.ColumnDefinitions.Count) + { + width = grid.ColumnDefinitions[column++].ActualWidth; + } + // TODO: Need to check Column visibility here as well... + else if (_parentPanel is DataTable table && + column < table.Children.Count) + { + // TODO: This is messy... + width = (table.Children[column++] as DataColumn)?.ActualWidth ?? 0; + } + + // Note: For Auto, since we measured our children and bubbled that up to the DataTable layout, then the DataColumn size we grab above should account for the largest of our children. + if (i == 0) + { + child.Arrange(new Rect(x, 0, width, finalSize.Height)); + } + else + { + // If we're in a tree, remove the indentation from the layout of columns beyond the first. + child.Arrange(new Rect(x - _treePadding, 0, width, finalSize.Height)); + } + + x += width + spacing; + i++; + } + + return new Size(x - spacing, finalSize.Height); + } + + return finalSize; + } +} diff --git a/src/ThirdParty/DataTable/DataTable.cs b/src/ThirdParty/DataTable/DataTable.cs new file mode 100644 index 0000000..c421b31 --- /dev/null +++ b/src/ThirdParty/DataTable/DataTable.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Collections.Generic; +using System.Linq; +using Windows.Foundation; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A is a which lays out s based on +/// their configured properties (akin to ); similar to a with a single row. +/// +public partial class DataTable : Panel +{ + // TODO: We should cache this result and update if column properties change + internal bool IsAnyColumnAuto => Children.Any(static e => e is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Auto }); + + // TODO: Check with Sergio if there's a better structure here, as I don't need a Dictionary like ConditionalWeakTable + internal HashSet Rows { get; private set; } = new(); + + internal void ColumnResized() + { + InvalidateArrange(); + + foreach (var row in Rows) + { + row.InvalidateArrange(); + } + } + + //// TODO: Would we want this named 'Spacing' instead if we support an Orientation in the future for columns being items instead of rows? + /// + /// Gets or sets the amount of space to place between columns within the table. + /// + public double ColumnSpacing + { + get { return (double)GetValue(ColumnSpacingProperty); } + set { SetValue(ColumnSpacingProperty, value); } + } + + /// + /// Gets the . + /// + public static readonly DependencyProperty ColumnSpacingProperty = + DependencyProperty.Register(nameof(ColumnSpacing), typeof(double), typeof(DataTable), new PropertyMetadata(0d)); + + protected override Size MeasureOverride(Size availableSize) + { + double fixedWidth = 0; + double proportionalUnits = 0; + double autoSized = 0; + + double maxHeight = 0; + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible && e is DataColumn); + + // We only need to measure elements that are visible + foreach (DataColumn column in elements) + { + if (column.CurrentWidth.IsStar) + { + proportionalUnits += column.DesiredWidth.Value; + } + else if (column.CurrentWidth.IsAbsolute) + { + fixedWidth += column.DesiredWidth.Value; + } + } + + // Add in spacing between columns to our fixed size allotment + fixedWidth += (elements.Count() - 1) * ColumnSpacing; + + // TODO: Handle infinite width? + var proportionalAmount = (availableSize.Width - fixedWidth) / proportionalUnits; + + foreach (DataColumn column in elements) + { + if (column.CurrentWidth.IsStar) + { + column.Measure(new Size(proportionalAmount * column.CurrentWidth.Value, availableSize.Height)); + } + else if (column.CurrentWidth.IsAbsolute) + { + column.Measure(new Size(column.CurrentWidth.Value, availableSize.Height)); + } + else + { + // TODO: Technically this is using 'Auto' on the Header content + // What the developer probably intends is it to be adjusted based on the contents of the rows... + // To enable this scenario, we'll need to actually measure the contents of the rows for that column + // in DataRow and figure out the maximum size to report back and adjust here in some sort of hand-shake + // for the layout process... (i.e. get the data in the measure step, use it in the arrange step here, + // then invalidate the child arranges [don't re-measure and cause loop]...) + + // For now, we'll just use the header content as a guideline to see if things work. + column.Measure(new Size(Math.Max(availableSize.Width - fixedWidth - autoSized, 0), availableSize.Height)); + + // Keep track of already 'allotted' space, use either the maximum child size (if we know it) or the header content + autoSized += Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth); + } + + maxHeight = Math.Max(maxHeight, column.DesiredSize.Height); + } + + return new Size(availableSize.Width, maxHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + double fixedWidth = 0; + double proportionalUnits = 0; + double autoSized = 0; + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible && e is DataColumn); + + // We only need to measure elements that are visible + foreach (DataColumn column in elements) + { + if (column.CurrentWidth.IsStar) + { + proportionalUnits += column.CurrentWidth.Value; + } + else if (column.CurrentWidth.IsAbsolute) + { + fixedWidth += column.CurrentWidth.Value; + } + else + { + autoSized += Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth); + } + } + + // TODO: Handle infinite width? + // TODO: This can go out of bounds or something around here when pushing a resized column to the right... + var proportionalAmount = (finalSize.Width - fixedWidth - autoSized) / proportionalUnits; + + double width = 0; + double x = 0; + + foreach (DataColumn column in elements) + { + if (column.CurrentWidth.IsStar) + { + width = Math.Max(proportionalAmount * column.CurrentWidth.Value, 0); + column.Arrange(new Rect(x, 0, width, finalSize.Height)); + } + else if (column.CurrentWidth.IsAbsolute) + { + width = Math.Max(column.CurrentWidth.Value, 0); + column.Arrange(new Rect(x, 0, width, finalSize.Height)); + } + else + { + // TODO: We use the comparison of sizes a lot, should we cache in the DataColumn itself? + width = Math.Max(Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth), 0); + column.Arrange(new Rect(x, 0, width, finalSize.Height)); + } + + x += width + ColumnSpacing; + } + + return finalSize; + } +} \ No newline at end of file diff --git a/src/TrimmerRoots.xml b/src/TrimmerRoots.xml index 9a22884..3023810 100644 --- a/src/TrimmerRoots.xml +++ b/src/TrimmerRoots.xml @@ -1,20 +1,10 @@  - - - - - - - - - + + + + + - - - - - - \ No newline at end of file diff --git a/src/ViewModels/AddDriveViewModel.cs b/src/ViewModels/AddDriveViewModel.cs index 8de7719..647ac26 100644 --- a/src/ViewModels/AddDriveViewModel.cs +++ b/src/ViewModels/AddDriveViewModel.cs @@ -2,9 +2,11 @@ using Sungaila.SUBSTitute.Commands; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; +using WinRT; namespace Sungaila.SUBSTitute.ViewModels { + [GeneratedBindableCustomProperty] public partial class AddDriveViewModel : ViewModel { public required MappingViewModel ParentViewModel { get; init; } diff --git a/src/ViewModels/LanguageViewModel.cs b/src/ViewModels/LanguageViewModel.cs new file mode 100644 index 0000000..a006fe0 --- /dev/null +++ b/src/ViewModels/LanguageViewModel.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using WinRT; + +namespace Sungaila.SUBSTitute.ViewModels +{ + [GeneratedBindableCustomProperty] + public partial class LanguageViewModel(string ietfLanguageTag, string nativeName) : ViewModel + { + public string IetfLanguageTag { get; } = ietfLanguageTag; + + public string NativeName { get; } = nativeName; + + public override bool Equals(object? obj) => obj is LanguageViewModel viewModel && viewModel.IetfLanguageTag == IetfLanguageTag; + + public override int GetHashCode() => IetfLanguageTag.GetHashCode(); + + public static implicit operator LanguageViewModel(CultureInfo c) => new(c.IetfLanguageTag, c.NativeName); + } +} \ No newline at end of file diff --git a/src/ViewModels/LetterViewModel.cs b/src/ViewModels/LetterViewModel.cs index f6468c1..4d9b027 100644 --- a/src/ViewModels/LetterViewModel.cs +++ b/src/ViewModels/LetterViewModel.cs @@ -1,5 +1,8 @@ -namespace Sungaila.SUBSTitute.ViewModels +using WinRT; + +namespace Sungaila.SUBSTitute.ViewModels { + [GeneratedBindableCustomProperty] public partial class LetterViewModel : ViewModel { private char _name; diff --git a/src/ViewModels/MainViewModel.cs b/src/ViewModels/MainViewModel.cs index 49e612a..852bef2 100644 --- a/src/ViewModels/MainViewModel.cs +++ b/src/ViewModels/MainViewModel.cs @@ -1,5 +1,8 @@ -namespace Sungaila.SUBSTitute.ViewModels +using WinRT; + +namespace Sungaila.SUBSTitute.ViewModels { + [GeneratedBindableCustomProperty] public partial class MainViewModel : ViewModel { public MappingViewModel Mapping { get; } = new(); diff --git a/src/ViewModels/MappingViewModel.cs b/src/ViewModels/MappingViewModel.cs index 08ae920..e81f1cf 100644 --- a/src/ViewModels/MappingViewModel.cs +++ b/src/ViewModels/MappingViewModel.cs @@ -1,11 +1,12 @@ using CommunityToolkit.Mvvm.Input; using CommunityToolkit.WinUI.Collections; using Sungaila.SUBSTitute.Commands; -using Sungaila.SUBSTitute.Extensions; using System.Collections.ObjectModel; +using WinRT; namespace Sungaila.SUBSTitute.ViewModels { + [GeneratedBindableCustomProperty] public partial class MappingViewModel : ViewModel { private bool _showAllDrives = App.LocalSettings.Values["MappingShowAllDrives"] as bool? ?? true; @@ -19,7 +20,6 @@ public bool ShowAllDrives { App.LocalSettings.Values["MappingShowAllDrives"] = value; DrivesFiltered.RefreshFilter(); - DrivesFilteredForDataGrid.RefreshFilter(); } } } @@ -42,8 +42,6 @@ public int SelectedIndex public AdvancedCollectionView DrivesFiltered { get; } = []; - public HackedCollectionView DrivesFilteredForDataGrid { get; } = []; - public IRelayCommand QueryDrives { get; } = MappingCommands.QueryDrives; public IRelayCommand AddVirtualDrive { get; } = MappingCommands.AddVirtualDrive; @@ -53,10 +51,6 @@ public MappingViewModel() DrivesFiltered.Filter = FilterDrives; DrivesFiltered.Source = Drives; DrivesFiltered.SortDescriptions.Add(new SortDescription(nameof(DriveViewModel.Letter), SortDirection.Ascending)); - - DrivesFilteredForDataGrid.Filter = FilterDrives; - DrivesFilteredForDataGrid.Source = Drives; - DrivesFilteredForDataGrid.SortDescriptions.Add(new SortDescription(nameof(DriveViewModel.Path), SortDirection.Ascending)); } private bool FilterDrives(object obj) diff --git a/src/ViewModels/RemoveDriveViewModel.cs b/src/ViewModels/RemoveDriveViewModel.cs index b9ca672..0b3a27f 100644 --- a/src/ViewModels/RemoveDriveViewModel.cs +++ b/src/ViewModels/RemoveDriveViewModel.cs @@ -1,7 +1,9 @@ using Microsoft.Win32; +using WinRT; namespace Sungaila.SUBSTitute.ViewModels { + [GeneratedBindableCustomProperty] public partial class RemoveDriveViewModel : ViewModel { public required MappingViewModel ParentViewModel { get; init; } diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs index 4e052f3..5a68e2a 100644 --- a/src/ViewModels/SettingsViewModel.cs +++ b/src/ViewModels/SettingsViewModel.cs @@ -8,9 +8,9 @@ namespace Sungaila.SUBSTitute.ViewModels { public partial class SettingsViewModel : ViewModel { - private CultureInfo _selectedLanguage = new(ApplicationLanguages.PrimaryLanguageOverride); + private LanguageViewModel _selectedLanguage = new CultureInfo(ApplicationLanguages.PrimaryLanguageOverride); - public CultureInfo SelectedLanguage + public LanguageViewModel SelectedLanguage { get => _selectedLanguage; set @@ -22,7 +22,7 @@ public CultureInfo SelectedLanguage } } - public ObservableCollection AvailableLanguages { get; } = new(ApplicationLanguages.ManifestLanguages.Select(l => new CultureInfo(l))); + public ObservableCollection AvailableLanguages { get; } = new(ApplicationLanguages.ManifestLanguages.Select(l => (LanguageViewModel)new CultureInfo(l))); private ElementTheme _selectedTheme; diff --git a/src/Views/AddDriveView.xaml b/src/Views/AddDriveView.xaml index ddfa930..4ce9ea6 100644 --- a/src/Views/AddDriveView.xaml +++ b/src/Views/AddDriveView.xaml @@ -27,15 +27,15 @@ Grid.Row="0" Grid.Column="0" DisplayMemberPath="Name" - ItemsSource="{Binding AvailableLetters}" - SelectedItem="{Binding SelectedLetter, Mode=TwoWay}" /> + ItemsSource="{x:Bind Data.AvailableLetters}" + SelectedItem="{x:Bind Data.SelectedLetter, Mode=TwoWay}" /> + Text="{x:Bind Data.SelectedPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + + + + - - - - - - - - - + + + + \ No newline at end of file diff --git a/src/Views/ListView.xaml.cs b/src/Views/ListView.xaml.cs index 506c36d..fd8e635 100644 --- a/src/Views/ListView.xaml.cs +++ b/src/Views/ListView.xaml.cs @@ -1,5 +1,3 @@ -using CommunityToolkit.WinUI.Collections; -using CommunityToolkit.WinUI.UI.Controls; using Microsoft.UI.Xaml.Controls; using Sungaila.SUBSTitute.ViewModels; @@ -7,49 +5,11 @@ namespace Sungaila.SUBSTitute.Views { public sealed partial class ListView : Page { + private MappingViewModel? Data => DataContext as MappingViewModel; + public ListView() { this.InitializeComponent(); } - - private void DataGrid_Sorting(object sender, DataGridColumnEventArgs e) - { - if (sender is not DataGrid dataGrid) - return; - - if (dataGrid.DataContext is not MappingViewModel mappingViewModel) - return; - - string? bindingPath; - - if (e.Column is DataGridBoundColumn boundColumn) - { - bindingPath = boundColumn.Binding.Path.Path; - } - else - { - bindingPath = e.Column.Tag as string; - } - - if (string.IsNullOrEmpty(bindingPath)) - return; - - mappingViewModel.DrivesFilteredForDataGrid.SortDescriptions.Clear(); - - foreach (var column in dataGrid.Columns) - { - if (column == e.Column) - continue; - - column.SortDirection = null; - } - - var newDirection = e.Column.SortDirection == DataGridSortDirection.Descending - ? DataGridSortDirection.Ascending - : DataGridSortDirection.Descending; - - mappingViewModel.DrivesFilteredForDataGrid.SortDescriptions.Add(new SortDescription(bindingPath, newDirection == DataGridSortDirection.Descending ? SortDirection.Descending : SortDirection.Ascending)); - e.Column.SortDirection = newDirection; - } } } \ No newline at end of file diff --git a/src/Views/MainWindow.xaml.cs b/src/Views/MainWindow.xaml.cs index 1a7a22a..0aa5d88 100644 --- a/src/Views/MainWindow.xaml.cs +++ b/src/Views/MainWindow.xaml.cs @@ -24,8 +24,7 @@ public MainWindow() var exePath = Environment.ProcessPath!; var icon = Icon.ExtractAssociatedIcon(exePath)!; - var iconId = Win32Interop.GetIconIdFromIcon(icon.Handle); - this.SetIcon(iconId); + this.AppWindow.SetIcon(Win32Interop.GetIconIdFromIcon(icon.Handle)); if (Content is FrameworkElement frameworkElement) { @@ -91,9 +90,9 @@ private void NavigationView_SelectionChanged(NavigationView sender, NavigationVi { pageType = typeof(SettingsView); } - else if (args.SelectedItemContainer?.Tag is string typeName) + else if (args.SelectedItemContainer?.Tag is Type type) { - pageType = Type.GetType(typeName); + pageType = type; } ContentFrame.Navigate( @@ -115,7 +114,9 @@ public void ShowInfoBar(string message, InfoBarSeverity severity) InfoBar.IsOpen = true; } +#pragma warning disable CA1822 private void WindowEx_WindowStateChanged(object sender, WindowState e) +#pragma warning restore CA1822 { if (e == WindowState.Minimized) return; diff --git a/src/Views/MappingView.xaml b/src/Views/MappingView.xaml index 47626ec..71a7365 100644 --- a/src/Views/MappingView.xaml +++ b/src/Views/MappingView.xaml @@ -16,7 +16,7 @@ - + @@ -33,12 +33,12 @@ @@ -46,12 +46,12 @@ + IsChecked="{x:Bind Data.Mapping.ShowAllDrives, Mode=TwoWay}" /> - + \ No newline at end of file diff --git a/src/Views/MappingView.xaml.cs b/src/Views/MappingView.xaml.cs index 14d0638..1203680 100644 --- a/src/Views/MappingView.xaml.cs +++ b/src/Views/MappingView.xaml.cs @@ -1,12 +1,15 @@ using CommunityToolkit.WinUI.Controls; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Sungaila.SUBSTitute.ViewModels; using System; namespace Sungaila.SUBSTitute.Views { public sealed partial class MappingView : Page { + private MainViewModel? Data => DataContext as MainViewModel; + public MappingView() { this.InitializeComponent(); @@ -14,10 +17,10 @@ public MappingView() private void Segmented_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (sender is not Segmented segmented || segmented.SelectedItem is not FrameworkElement element || element.Tag is not string typeName) + if (sender is not Segmented segmented || segmented.SelectedItem is not FrameworkElement element || element.Tag is not Type type) return; - ContentFrame?.Navigate(Type.GetType(typeName)); + ContentFrame?.Navigate(type); } } } \ No newline at end of file diff --git a/src/Views/RemoveDriveView.xaml b/src/Views/RemoveDriveView.xaml index 3fd57f2..bb6eb5a 100644 --- a/src/Views/RemoveDriveView.xaml +++ b/src/Views/RemoveDriveView.xaml @@ -24,8 +24,8 @@ Margin="0,5,0,0" Orientation="Horizontal" Spacing="5" - Visibility="{Binding IsPermanent, Mode=OneTime, Converter={StaticResource BoolToVisibilityConverter}}"> - + Visibility="{x:Bind Data.IsPermanent, Mode=OneTime, Converter={StaticResource BoolToVisibilityConverter}}"> + DataContext as RemoveDriveViewModel; + public RemoveDriveView() { this.InitializeComponent(); diff --git a/src/Views/SettingsView.xaml b/src/Views/SettingsView.xaml index 35282c1..5176985 100644 --- a/src/Views/SettingsView.xaml +++ b/src/Views/SettingsView.xaml @@ -14,12 +14,13 @@ mc:Ignorable="d"> + - + - @@ -51,9 +52,9 @@ HeaderIcon="{ui:FontIcon Glyph=}"> + SelectedItem="{x:Bind Data.Settings.SelectedLanguage, Mode=TwoWay}" /> + SelectedIndex="{x:Bind Data.Settings.SelectedTheme, Mode=TwoWay, Converter={StaticResource ElementThemeToIntConverter}}" /> diff --git a/src/Views/SettingsView.xaml.cs b/src/Views/SettingsView.xaml.cs index 3d749e9..ebc8169 100644 --- a/src/Views/SettingsView.xaml.cs +++ b/src/Views/SettingsView.xaml.cs @@ -11,6 +11,8 @@ namespace Sungaila.SUBSTitute.Views { public sealed partial class SettingsView : Page { + private MainViewModel? Data => DataContext as MainViewModel; + public SettingsView() { this.InitializeComponent(); diff --git a/src/Views/TileView.xaml b/src/Views/TileView.xaml index aeb8790..6466439 100644 --- a/src/Views/TileView.xaml +++ b/src/Views/TileView.xaml @@ -17,7 +17,9 @@ - + @@ -86,6 +88,6 @@ FlowDirection="LeftToRight" IsItemClickEnabled="False" ItemTemplate="{StaticResource LinedFlowLayoutItemTemplate}" - ItemsSource="{Binding DrivesFiltered}" + ItemsSource="{x:Bind Data.DrivesFiltered}" SelectionMode="None" /> \ No newline at end of file diff --git a/src/Views/TileView.xaml.cs b/src/Views/TileView.xaml.cs index 53d0f61..0695448 100644 --- a/src/Views/TileView.xaml.cs +++ b/src/Views/TileView.xaml.cs @@ -1,9 +1,12 @@ using Microsoft.UI.Xaml.Controls; +using Sungaila.SUBSTitute.ViewModels; namespace Sungaila.SUBSTitute.Views { public sealed partial class TileView : Page { + private MappingViewModel? Data => DataContext as MappingViewModel; + public TileView() { this.InitializeComponent();