fix: open folder pickers at current paths
This commit is contained in:
@@ -17,7 +17,7 @@ public sealed partial class MainWindow
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
completion.TrySetResult(
|
completion.TrySetResult(
|
||||||
LegacyBackgroundFileDialog.Show(
|
LegacyFileDialog.ShowBackgroundFile(
|
||||||
WindowNative.GetWindowHandle(this),
|
WindowNative.GetWindowHandle(this),
|
||||||
initialDirectory));
|
initialDirectory));
|
||||||
}
|
}
|
||||||
@@ -40,14 +40,54 @@ public sealed partial class MainWindow
|
|||||||
return completion.Task;
|
return completion.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LegacyBackgroundFileDialog
|
private Task<string?> PickOperatorFolderAsync(
|
||||||
|
string? initialDirectory,
|
||||||
|
string title)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(title);
|
||||||
|
var canonicalInitialDirectory = initialDirectory is null
|
||||||
|
? null
|
||||||
|
: Path.GetFullPath(initialDirectory);
|
||||||
|
var completion = new TaskCompletionSource<string?>(
|
||||||
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
void ShowPicker()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
completion.TrySetResult(
|
||||||
|
LegacyFileDialog.ShowFolder(
|
||||||
|
WindowNative.GetWindowHandle(this),
|
||||||
|
canonicalInitialDirectory,
|
||||||
|
title));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
completion.TrySetException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DispatcherQueue.HasThreadAccess)
|
||||||
|
{
|
||||||
|
ShowPicker();
|
||||||
|
}
|
||||||
|
else if (!DispatcherQueue.TryEnqueue(ShowPicker))
|
||||||
|
{
|
||||||
|
completion.TrySetException(
|
||||||
|
new InvalidOperationException("The folder picker could not reach the UI thread."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LegacyFileDialog
|
||||||
{
|
{
|
||||||
private const int CancelledHResult = unchecked((int)0x800704C7);
|
private const int CancelledHResult = unchecked((int)0x800704C7);
|
||||||
private const uint FileSystemPathDisplayName = 0x80058000;
|
private const uint FileSystemPathDisplayName = 0x80058000;
|
||||||
private static readonly Guid FileOpenDialogClassId =
|
private static readonly Guid FileOpenDialogClassId =
|
||||||
new("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7");
|
new("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7");
|
||||||
|
|
||||||
public static string? Show(nint owner, string initialDirectory)
|
public static string? ShowBackgroundFile(nint owner, string initialDirectory)
|
||||||
{
|
{
|
||||||
if (owner == nint.Zero || !Directory.Exists(initialDirectory))
|
if (owner == nint.Zero || !Directory.Exists(initialDirectory))
|
||||||
{
|
{
|
||||||
@@ -127,6 +167,89 @@ public sealed partial class MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string? ShowFolder(
|
||||||
|
nint owner,
|
||||||
|
string? initialDirectory,
|
||||||
|
string title)
|
||||||
|
{
|
||||||
|
if (owner == nint.Zero)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The folder picker requires an owner window.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialDirectory is not null &&
|
||||||
|
!Directory.Exists(initialDirectory))
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException(
|
||||||
|
"The initial folder-picker directory is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IFileDialog? dialog = null;
|
||||||
|
IShellItem? initialFolder = null;
|
||||||
|
IShellItem? result = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dialogType = Type.GetTypeFromCLSID(
|
||||||
|
FileOpenDialogClassId,
|
||||||
|
throwOnError: true) ??
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The Windows folder dialog is unavailable.");
|
||||||
|
dialog = (IFileDialog)(Activator.CreateInstance(dialogType) ??
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The Windows folder dialog could not be created."));
|
||||||
|
dialog.GetOptions(out var existingOptions);
|
||||||
|
dialog.SetOptions(
|
||||||
|
existingOptions |
|
||||||
|
FileOpenOptions.NoChangeDirectory |
|
||||||
|
FileOpenOptions.PickFolders |
|
||||||
|
FileOpenOptions.ForceFileSystem |
|
||||||
|
FileOpenOptions.PathMustExist |
|
||||||
|
FileOpenOptions.NoDereferenceLinks |
|
||||||
|
FileOpenOptions.DoNotAddToRecent);
|
||||||
|
dialog.SetTitle(title);
|
||||||
|
dialog.SetOkButtonLabel("폴더 선택");
|
||||||
|
|
||||||
|
if (initialDirectory is not null)
|
||||||
|
{
|
||||||
|
var shellItemId = typeof(IShellItem).GUID;
|
||||||
|
var createResult = SHCreateItemFromParsingName(
|
||||||
|
initialDirectory,
|
||||||
|
nint.Zero,
|
||||||
|
ref shellItemId,
|
||||||
|
out initialFolder);
|
||||||
|
Marshal.ThrowExceptionForHR(createResult);
|
||||||
|
dialog.SetFolder(initialFolder);
|
||||||
|
dialog.SetDefaultFolder(initialFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var showResult = dialog.Show(owner);
|
||||||
|
if (showResult == CancelledHResult)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Marshal.ThrowExceptionForHR(showResult);
|
||||||
|
dialog.GetResult(out result);
|
||||||
|
result.GetDisplayName(FileSystemPathDisplayName, out var pathPointer);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Marshal.PtrToStringUni(pathPointer) ??
|
||||||
|
throw new IOException("The selected folder path is unavailable.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeCoTaskMem(pathPointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(result);
|
||||||
|
ReleaseComObject(initialFolder);
|
||||||
|
ReleaseComObject(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ReleaseComObject(object? value)
|
private static void ReleaseComObject(object? value)
|
||||||
{
|
{
|
||||||
if (value is not null && Marshal.IsComObject(value))
|
if (value is not null && Marshal.IsComObject(value))
|
||||||
@@ -139,6 +262,7 @@ public sealed partial class MainWindow
|
|||||||
private enum FileOpenOptions : uint
|
private enum FileOpenOptions : uint
|
||||||
{
|
{
|
||||||
NoChangeDirectory = 0x00000008,
|
NoChangeDirectory = 0x00000008,
|
||||||
|
PickFolders = 0x00000020,
|
||||||
ForceFileSystem = 0x00000040,
|
ForceFileSystem = 0x00000040,
|
||||||
PathMustExist = 0x00000800,
|
PathMustExist = 0x00000800,
|
||||||
FileMustExist = 0x00001000,
|
FileMustExist = 0x00001000,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ using MBN_STOCK_WEBVIEW.Infrastructure;
|
|||||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||||
using MMoneyCoderSharp.Data;
|
using MMoneyCoderSharp.Data;
|
||||||
using Windows.Storage.Pickers;
|
|
||||||
using WinRT.Interop;
|
|
||||||
|
|
||||||
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
||||||
|
|
||||||
@@ -85,18 +83,11 @@ public sealed partial class MainWindow
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var picker = new FolderPicker
|
var initialDirectory = ResolveOperatorFolderPickerStartDirectory(kind);
|
||||||
{
|
var selectedPath = await PickOperatorFolderAsync(
|
||||||
SuggestedStartLocation = PickerLocationId.ComputerFolder,
|
initialDirectory,
|
||||||
ViewMode = PickerViewMode.List
|
ResolveOperatorFolderPickerTitle(kind));
|
||||||
};
|
if (selectedPath is null)
|
||||||
picker.FileTypeFilter.Add("*");
|
|
||||||
var owner = _windowHandle != 0
|
|
||||||
? _windowHandle
|
|
||||||
: WindowNative.GetWindowHandle(this);
|
|
||||||
InitializeWithWindow.Initialize(picker, owner);
|
|
||||||
var selected = await picker.PickSingleFolderAsync();
|
|
||||||
if (selected is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -105,7 +96,7 @@ public sealed partial class MainWindow
|
|||||||
var validationKind = ToSettingsFolderKind(kind);
|
var validationKind = ToSettingsFolderKind(kind);
|
||||||
if (!LegacyOperatorFolderValidator.TryValidate(
|
if (!LegacyOperatorFolderValidator.TryValidate(
|
||||||
validationKind,
|
validationKind,
|
||||||
selected.Path,
|
selectedPath,
|
||||||
out var validated,
|
out var validated,
|
||||||
out var warningMessage) ||
|
out var warningMessage) ||
|
||||||
validated is null)
|
validated is null)
|
||||||
@@ -156,6 +147,81 @@ public sealed partial class MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? ResolveOperatorFolderPickerStartDirectory(
|
||||||
|
LegacyOperatorFolderKind kind)
|
||||||
|
{
|
||||||
|
var preferredPath = kind switch
|
||||||
|
{
|
||||||
|
LegacyOperatorFolderKind.Design =>
|
||||||
|
_operatorSettings.SceneDirectory ??
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Cuts"),
|
||||||
|
LegacyOperatorFolderKind.Resource =>
|
||||||
|
_operatorSettings.ResourceDirectory ??
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Res"),
|
||||||
|
LegacyOperatorFolderKind.Background =>
|
||||||
|
_operatorSettings.BackgroundDirectory ??
|
||||||
|
ResolveAutomaticBackgroundDirectory(),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
|
};
|
||||||
|
|
||||||
|
return FindNearestExistingDirectory(preferredPath) ??
|
||||||
|
FindNearestExistingDirectory(AppContext.BaseDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveAutomaticBackgroundDirectory()
|
||||||
|
{
|
||||||
|
var sceneDirectory = _operatorSettings.SceneDirectory ??
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "Cuts");
|
||||||
|
var canonicalSceneDirectory = Path.TrimEndingDirectorySeparator(
|
||||||
|
Path.GetFullPath(sceneDirectory));
|
||||||
|
var sceneParent = Path.GetDirectoryName(canonicalSceneDirectory);
|
||||||
|
return Path.Combine(
|
||||||
|
sceneParent ?? AppContext.BaseDirectory,
|
||||||
|
"배경");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindNearestExistingDirectory(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) ||
|
||||||
|
!Path.IsPathFullyQualified(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var candidate = new DirectoryInfo(Path.GetFullPath(path));
|
||||||
|
candidate is not null;
|
||||||
|
candidate = candidate.Parent)
|
||||||
|
{
|
||||||
|
if (candidate.Exists)
|
||||||
|
{
|
||||||
|
return candidate.FullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is ArgumentException or
|
||||||
|
IOException or
|
||||||
|
NotSupportedException or
|
||||||
|
System.Security.SecurityException or
|
||||||
|
UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveOperatorFolderPickerTitle(
|
||||||
|
LegacyOperatorFolderKind kind) => kind switch
|
||||||
|
{
|
||||||
|
LegacyOperatorFolderKind.Design => "폴더 선택 - 디자인",
|
||||||
|
LegacyOperatorFolderKind.Resource => "폴더 선택 - 설정",
|
||||||
|
LegacyOperatorFolderKind.Background => "폴더 선택 - 배경",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
|
};
|
||||||
|
|
||||||
private void ResetOperatorFolder(LegacyOperatorFolderKind kind)
|
private void ResetOperatorFolder(LegacyOperatorFolderKind kind)
|
||||||
{
|
{
|
||||||
var next = kind switch
|
var next = kind switch
|
||||||
|
|||||||
@@ -11,26 +11,85 @@ public sealed class LegacyOperatorSettingsNativeContractTests
|
|||||||
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
||||||
private static readonly string Settings = File.ReadAllText(
|
private static readonly string Settings = File.ReadAllText(
|
||||||
Path.Combine(AppRoot, "MainWindow.Settings.cs"));
|
Path.Combine(AppRoot, "MainWindow.Settings.cs"));
|
||||||
|
private static readonly string FolderPicker = File.ReadAllText(
|
||||||
|
Path.Combine(AppRoot, "MainWindow.BackgroundPicker.cs"));
|
||||||
private static readonly string Playout = File.ReadAllText(
|
private static readonly string Playout = File.ReadAllText(
|
||||||
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
|
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FolderPathsComeOnlyFromTheWindowOwnedNativePicker()
|
public void FolderPathsComeOnlyFromTheWindowOwnedNativePicker()
|
||||||
{
|
{
|
||||||
Assert.Contains("var picker = new FolderPicker", Settings, StringComparison.Ordinal);
|
Assert.Contains(
|
||||||
Assert.Contains("picker.FileTypeFilter.Add(\"*\")", Settings,
|
"var initialDirectory = ResolveOperatorFolderPickerStartDirectory(kind)",
|
||||||
|
Settings,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("InitializeWithWindow.Initialize(picker, owner);", Settings,
|
Assert.Contains("await PickOperatorFolderAsync(", Settings,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("await picker.PickSingleFolderAsync()", Settings,
|
Assert.Contains("LegacyFileDialog.ShowFolder(", FolderPicker,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("selected.Path", Settings, StringComparison.Ordinal);
|
Assert.Contains("WindowNative.GetWindowHandle(this)", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FileOpenOptions.PickFolders", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FileOpenOptions.ForceFileSystem", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FileOpenOptions.PathMustExist", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FileOpenOptions.NoDereferenceLinks", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("dialog.SetFolder(initialFolder);", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("dialog.SetDefaultFolder(initialFolder);", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("selectedPath", Settings, StringComparison.Ordinal);
|
||||||
Assert.Contains("LegacyOperatorFolderValidator.TryValidate(", Settings,
|
Assert.Contains("LegacyOperatorFolderValidator.TryValidate(", Settings,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("PickerLocationId.ComputerFolder", Settings,
|
||||||
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("intent.Path", Settings, StringComparison.Ordinal);
|
Assert.DoesNotContain("intent.Path", Settings, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("choose.Path", Settings, StringComparison.Ordinal);
|
Assert.DoesNotContain("choose.Path", Settings, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FolderPickerStartsAtTheLatestSavedPathOrItsNearestExistingParent()
|
||||||
|
{
|
||||||
|
var resolver = Slice(
|
||||||
|
Settings,
|
||||||
|
"private string? ResolveOperatorFolderPickerStartDirectory(",
|
||||||
|
"private static string ResolveOperatorFolderPickerTitle(");
|
||||||
|
|
||||||
|
Assert.Contains("_operatorSettings.SceneDirectory", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_operatorSettings.ResourceDirectory", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_operatorSettings.BackgroundDirectory", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("_appliedOperatorSettings", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Path.Combine(AppContext.BaseDirectory, \"Cuts\")", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Path.Combine(AppContext.BaseDirectory, \"Res\")", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("ResolveAutomaticBackgroundDirectory()", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FindNearestExistingDirectory(preferredPath)", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("candidate = candidate.Parent", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("FindNearestExistingDirectory(AppContext.BaseDirectory)", resolver,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FolderPickerTitleAndButtonRemainCompatibleWithTheLocalStateSmoke()
|
||||||
|
{
|
||||||
|
Assert.Contains("\"폴더 선택 - 디자인\"", Settings, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("\"폴더 선택 - 설정\"", Settings, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("\"폴더 선택 - 배경\"", Settings, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("dialog.SetOkButtonLabel(\"폴더 선택\")", FolderPicker,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SelectedFoldersAreLoadedAtStartupAndAppliedOnlyAtCompositionBoundaries()
|
public void SelectedFoldersAreLoadedAtStartupAndAppliedOnlyAtCompositionBoundaries()
|
||||||
{
|
{
|
||||||
@@ -162,7 +221,7 @@ public sealed class LegacyOperatorSettingsNativeContractTests
|
|||||||
{
|
{
|
||||||
var cancelledPicker = Slice(
|
var cancelledPicker = Slice(
|
||||||
Settings,
|
Settings,
|
||||||
"if (selected is null)",
|
"if (selectedPath is null)",
|
||||||
"cancellationToken.ThrowIfCancellationRequested();");
|
"cancellationToken.ThrowIfCancellationRequested();");
|
||||||
|
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
|
|||||||
Reference in New Issue
Block a user