From 4bb4b77b501bba378201202dcc64c22d2db27e91 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 3 Aug 2026 12:21:06 +0200 Subject: [PATCH] Add defaultFileName support for SaveFile dialogs Add optional defaultFileName parameter to save-file APIs and plumbing so callers can pre-populate the save dialog filename. Changes: TypeScript contract and feature, JS tests, C# interface and extension methods, feature implementation, web message dispatcher, native dialog operation, docs example, and a new unit test (FilePickerDialogsDefaultFileNameTests). Behavior is additive (nullable defaultFileName) and preserves existing defaults. --- docs/docs/guides/core-window.md | 3 +- docs/docs/migration/photino-backlog.md | 2 +- ...lePickerDialogsInfiniFrameWindowFeature.ts | 2 +- ...lePickerDialogsInfiniFrameWindowFeature.ts | 4 +- .../Window/InfiniFrameWindowFeatures.test.ts | 3 +- ...lePickerDialogsInfiniFrameWindowFeature.cs | 6 ++- ...alogsInfiniFrameWindowFeatureExtensions.cs | 10 ++-- ...lePickerDialogsInfiniFrameWindowFeature.cs | 18 +++---- .../FilePickerDialogsWebMessageDispatcher.cs | 3 +- .../InfiniFileDialogOperation.cs | 5 +- .../FilePickerDialogsDefaultFileNameTests.cs | 48 +++++++++++++++++++ .../WindowFeatureDispatcherCommandTests.cs | 3 +- 12 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 tests/InfiniTests.InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsDefaultFileNameTests.cs diff --git a/docs/docs/guides/core-window.md b/docs/docs/guides/core-window.md index 679871911..196603829 100644 --- a/docs/docs/guides/core-window.md +++ b/docs/docs/guides/core-window.md @@ -561,7 +561,8 @@ string?[] folders = window.ShowOpenFolder("Select Folder", multiSelect: false); string? path = window.ShowSaveFile( title: "Save As", defaultPath: null, - filters: [("Text Files", ["txt"])] + filters: [("Text Files", ["txt"])], + defaultFileName: "document.txt" ); ``` diff --git a/docs/docs/migration/photino-backlog.md b/docs/docs/migration/photino-backlog.md index 3cc8f0a92..679dde3ae 100644 --- a/docs/docs/migration/photino-backlog.md +++ b/docs/docs/migration/photino-backlog.md @@ -81,7 +81,7 @@ This backlog will be used to track any remaining issues or features that need to | 📝 | Interoperability between C # and JavaScript | [Photino.NET#120](https://github.com/tryphotino/photino.NET/issues/120) | [InfiniFrame#294](https://github.com/InfiniLore/InfiniFrame/issues/219) | | ✅ | Window.external is deprecated by browsers | [Photino.NET#124](https://github.com/tryphotino/photino.NET/issues/124) | | | 📝 | Intercept navigation events | [Photino.NET#139](https://github.com/tryphotino/photino.NET/issues/139) | [InfiniFrame#294](https://github.com/InfiniLore/InfiniFrame/issues/294) | -| 📝 | ShowSaveFile How to set the default file name | [Photino.NET#140](https://github.com/tryphotino/photino.NET/issues/140) | [InfiniFrame#295](https://github.com/InfiniLore/InfiniFrame/issues/295) | +| ✅ | ShowSaveFile How to set the default file name | [Photino.NET#140](https://github.com/tryphotino/photino.NET/issues/140) | [InfiniFrame#295](https://github.com/InfiniLore/InfiniFrame/issues/295) | | 📝 | Running as administrator disables other instances | [Photino.NET#162](https://github.com/tryphotino/photino.NET/issues/162) | [InfiniFrame#296](https://github.com/InfiniLore/InfiniFrame/issues/296) | | ❌ | Mica/Fluent design window for windows | [Photino.NET#167](https://github.com/tryphotino/photino.NET/issues/167) | | | 📝 | Retrieve current Url of PhotinoWindow | [Photino.NET#197](https://github.com/tryphotino/photino.NET/issues/197) | [InfiniFrame#297](https://github.com/InfiniLore/InfiniFrame/issues/297) | diff --git a/src/InfiniFrame.Js/TypeScript/Contracts/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts b/src/InfiniFrame.Js/TypeScript/Contracts/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts index 219e1e721..4d6d79e25 100644 --- a/src/InfiniFrame.Js/TypeScript/Contracts/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts +++ b/src/InfiniFrame.Js/TypeScript/Contracts/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts @@ -8,5 +8,5 @@ import type {FilePickerFilter} from "./WindowFeatureTypes"; export interface FilePickerDialogsInfiniFrameWindowFeature { showOpenFileAsync(title?: string, defaultPath?: string | null, multiSelect?: boolean, filters?: FilePickerFilter[] | null): Promise<(string | null)[]>; showOpenFolderAsync(title?: string, defaultPath?: string | null, multiSelect?: boolean): Promise<(string | null)[]>; - showSaveFileAsync(title?: string, defaultPath?: string | null, filters?: FilePickerFilter[] | null): Promise; + showSaveFileAsync(title?: string, defaultPath?: string | null, filters?: FilePickerFilter[] | null, defaultFileName?: string | null): Promise; } diff --git a/src/InfiniFrame.Js/TypeScript/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts b/src/InfiniFrame.Js/TypeScript/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts index 854ccfdf8..b4ec2cf65 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/Features/FilePickerDialogsInfiniFrameWindowFeature.ts @@ -17,7 +17,7 @@ export class FilePickerDialogsInfiniFrameWindowFeature extends InfiniFrameWindow return this.get<(string | null)[]>("showOpenFolder", {title, defaultPath, multiSelect}); } - showSaveFileAsync(title = "Save file", defaultPath: string | null = null, filters: FilePickerFilter[] | null = null) { - return this.get("showSaveFile", {title, defaultPath, filters}); + showSaveFileAsync(title = "Save file", defaultPath: string | null = null, filters: FilePickerFilter[] | null = null, defaultFileName: string | null = null) { + return this.get("showSaveFile", {title, defaultPath, filters, defaultFileName}); } } diff --git a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts index 0794937df..bf7b935a4 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts @@ -75,7 +75,8 @@ const contracts: FeatureContract[] = [ gets: [ {method: "showOpenFileAsync", command: "showOpenFile", parameters: ["Open", "/tmp", true, [{name: "Text", extensions: ["txt"]}]], args: {title: "Open", defaultPath: "/tmp", multiSelect: true, filters: [{name: "Text", extensions: ["txt"]}]}, result: ["/tmp/a.txt"]}, {method: "showOpenFolderAsync", command: "showOpenFolder", parameters: ["Folder", "/tmp", true], args: {title: "Folder", defaultPath: "/tmp", multiSelect: true}, result: ["/tmp"]}, - {method: "showSaveFileAsync", command: "showSaveFile", parameters: ["Save", "/tmp/a.txt", null], args: {title: "Save", defaultPath: "/tmp/a.txt", filters: null}, result: "/tmp/a.txt"} + {method: "showSaveFileAsync", command: "showSaveFile", parameters: ["Save", "/tmp/a.txt", null, null], args: {title: "Save", defaultPath: "/tmp/a.txt", filters: null, defaultFileName: null}, result: "/tmp/a.txt"}, + {method: "showSaveFileAsync", command: "showSaveFile", parameters: ["Save", "/tmp/a.txt", null, "document.txt"], args: {title: "Save", defaultPath: "/tmp/a.txt", filters: null, defaultFileName: "document.txt"}, result: "/tmp/a.txt"} ] }, { diff --git a/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeature.cs b/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeature.cs index 834e223f5..59596ab8c 100644 --- a/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeature.cs @@ -52,8 +52,9 @@ public interface IFilePickerDialogsInfiniFrameWindowFeature { /// The dialog title. /// The default directory path. /// File extension filters. + /// The default file name to pre-populate in the save dialog. /// The selected file path, or null if cancelled. - string? ShowSaveFile(string title = "Save file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null); + string? ShowSaveFile(string title = "Save file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null); /// /// Shows a save file dialog asynchronously and returns the selected file path. @@ -61,7 +62,8 @@ public interface IFilePickerDialogsInfiniFrameWindowFeature { /// The dialog title. /// The default directory path. /// File extension filters. + /// The default file name to pre-populate in the save dialog. /// Cancellation token. /// A task that resolves to the selected file path, or null if cancelled. - Task ShowSaveFileAsync(string title = "Choose file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, CancellationToken ct = default); + Task ShowSaveFileAsync(string title = "Choose file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeatureExtensions.cs b/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeatureExtensions.cs index b9b2c6c0f..939b4c1fe 100644 --- a/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeatureExtensions.cs +++ b/src/InfiniFrame.Shared/Window/Features/FilePickerDialogs/IFilePickerDialogsInfiniFrameWindowFeatureExtensions.cs @@ -61,9 +61,10 @@ public static class IFilePickerDialogsInfiniFrameWindowFeatureExtensions { /// The dialog title. /// The default directory path. /// File extension filters. + /// The default file name to pre-populate in the save dialog. /// The selected file path, or null if cancelled. - public static string? ShowSaveFile(this IInfiniFrameWindow window, string title = "Save file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null) - => window.Features.FilePickerDialogs.ShowSaveFile(title, defaultPath, filters); + public static string? ShowSaveFile(this IInfiniFrameWindow window, string title = "Save file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null) + => window.Features.FilePickerDialogs.ShowSaveFile(title, defaultPath, filters, defaultFileName); /// /// Shows a save file dialog asynchronously and returns the selected file path. @@ -72,8 +73,9 @@ public static class IFilePickerDialogsInfiniFrameWindowFeatureExtensions { /// The dialog title. /// The default directory path. /// File extension filters. + /// The default file name to pre-populate in the save dialog. /// Cancellation token. /// A task that resolves to the selected file path, or null if cancelled. - public static Task ShowSaveFileAsync(this IInfiniFrameWindow window, string title = "Choose file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, CancellationToken ct = default) - => window.Features.FilePickerDialogs.ShowSaveFileAsync(title, defaultPath, filters, ct); + public static Task ShowSaveFileAsync(this IInfiniFrameWindow window, string title = "Choose file", string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null, CancellationToken ct = default) + => window.Features.FilePickerDialogs.ShowSaveFileAsync(title, defaultPath, filters, defaultFileName, ct); } \ No newline at end of file diff --git a/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsInfiniFrameWindowFeature.cs index 7a3c553ab..74753e5f9 100644 --- a/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsInfiniFrameWindowFeature.cs @@ -26,7 +26,7 @@ ILogger logger /// public Task ShowOpenFileAsync(string title = DefaultFilePickerTitle, string? defaultPath = null, bool multiSelect = false, (string Name, string[] Extensions)[]? filters = null, CancellationToken ct = default) - => ShowDialogAsync(InfiniFileDialogKind.OpenFile, title, defaultPath, multiSelect, filters, ct); + => ShowDialogAsync(InfiniFileDialogKind.OpenFile, title, defaultPath, multiSelect, filters, null, ct); /// public string?[] ShowOpenFolder(string title = DefaultFolderPickerTitle, string? defaultPath = null, bool multiSelect = false) @@ -34,10 +34,10 @@ ILogger logger /// public Task ShowOpenFolderAsync(string title = DefaultFolderPickerTitle, string? defaultPath = null, bool multiSelect = false, CancellationToken ct = default) - => ShowDialogAsync(InfiniFileDialogKind.OpenFolder, title, defaultPath, multiSelect, null, ct); + => ShowDialogAsync(InfiniFileDialogKind.OpenFolder, title, defaultPath, multiSelect, null, null, ct); /// - public string? ShowSaveFile(string title = DefaultSaveFilePickerTitle, string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null) { + public string? ShowSaveFile(string title = DefaultSaveFilePickerTitle, string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null) { if (window.IsClosedOrClosing()) return null; defaultPath ??= Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); @@ -54,15 +54,15 @@ ILogger logger defaultPath, nativeFilters, filters.Length, - null// TODO actually assign this + defaultFileName ); return result; } /// - public Task ShowSaveFileAsync(string title = DefaultSaveFilePickerTitle, string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, CancellationToken ct = default) - => ShowSaveDialogAsync(title, defaultPath, filters, ct); + public Task ShowSaveFileAsync(string title = DefaultSaveFilePickerTitle, string? defaultPath = null, (string Name, string[] Extensions)[]? filters = null, string? defaultFileName = null, CancellationToken ct = default) + => ShowSaveDialogAsync(title, defaultPath, filters, defaultFileName, ct); private string?[] ShowOpenDialog(bool foldersOnly, string title, string? defaultPath, bool multiSelect, (string Name, string[] Extensions)[]? filters) { if (window.IsClosedOrClosing()) return []; @@ -107,6 +107,7 @@ ILogger logger string? defaultPath, bool multiSelect, (string Name, string[] Extensions)[]? filters, + string? defaultFileName, CancellationToken ct ) { ct.ThrowIfCancellationRequested(); @@ -115,7 +116,7 @@ CancellationToken ct filters ??= []; var operation = new InfiniFileDialogOperation( window, logger, kind, title, defaultPath, multiSelect, - GetNativeFilters(filters, kind == InfiniFileDialogKind.OpenFolder), ct + GetNativeFilters(filters, kind == InfiniFileDialogKind.OpenFolder), defaultFileName, ct ); _ = operation.StartAsync(); return await operation.Task.WaitAsync(ct).ConfigureAwait(false); @@ -125,10 +126,11 @@ CancellationToken ct string title, string? defaultPath, (string Name, string[] Extensions)[]? filters, + string? defaultFileName, CancellationToken ct ) { string?[] values = await ShowDialogAsync( - InfiniFileDialogKind.SaveFile, title, defaultPath, false, filters, ct + InfiniFileDialogKind.SaveFile, title, defaultPath, false, filters, defaultFileName, ct ).ConfigureAwait(false); return values.FirstOrDefault(); } diff --git a/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsWebMessageDispatcher.cs b/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsWebMessageDispatcher.cs index e50c5b1f2..520c26bb6 100644 --- a/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsWebMessageDispatcher.cs +++ b/src/InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsWebMessageDispatcher.cs @@ -20,11 +20,12 @@ protected override IFilePickerDialogsInfiniFrameWindowFeature SelectFeature(IInf string? defaultPath = Arg(args, "defaultPath", null); WindowFeatureFilePickerFilter[]? filterDtos = Arg(args, "filters", null); (string Name, string[] Extensions)[]? filters = filterDtos?.Select(filter => (filter.Name, filter.Extensions)).ToArray(); + string? defaultFileName = Arg(args, "defaultFileName", null); return command switch { "showOpenFile" => feature.ShowOpenFile(Arg(args, "title", "Choose file"), defaultPath, Arg(args, "multiSelect", false), filters), "showOpenFolder" => feature.ShowOpenFolder(Arg(args, "title", "Select folder"), defaultPath, Arg(args, "multiSelect", false)), - "showSaveFile" => feature.ShowSaveFile(Arg(args, "title", "Save file"), defaultPath, filters), + "showSaveFile" => feature.ShowSaveFile(Arg(args, "title", "Save file"), defaultPath, filters, defaultFileName), _ => throw Unsupported(command) }; } diff --git a/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs b/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs index e71b7e0e2..d1f0faa63 100644 --- a/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs +++ b/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs @@ -25,6 +25,7 @@ internal sealed class InfiniFileDialogOperation { private readonly string _defaultPath; private readonly bool _multiSelect; private readonly string[] _filters; + private readonly string? _defaultFileName; private readonly CancellationToken _cancellationToken; private readonly string? _diagnosticKey; private readonly TaskCompletionSource _completion = @@ -48,6 +49,7 @@ public InfiniFileDialogOperation( string defaultPath, bool multiSelect, string[] filters, + string? defaultFileName, CancellationToken cancellationToken ) { _window = window; @@ -57,6 +59,7 @@ CancellationToken cancellationToken _defaultPath = defaultPath; _multiSelect = multiSelect; _filters = filters; + _defaultFileName = defaultFileName; _cancellationToken = cancellationToken; _diagnosticKey = (window as InfiniFrameWindow)?.BeginDiagnosticOperation(kind.ToString(), Id); } @@ -79,7 +82,7 @@ public async Task StartAsync() { _lease.Handle, Id, _title, _defaultPath, _multiSelect, CompletionCallback, context), InfiniFileDialogKind.SaveFile => InfiniFrameNative.BeginShowSaveFile( _lease.Handle, Id, _title, _defaultPath, - _filters, _filters.Length, string.Empty, CompletionCallback, context), + _filters, _filters.Length, _defaultFileName ?? string.Empty, CompletionCallback, context), _ => InfiniFrameNativeInteropStatus.InvalidArgument }; if (status != InfiniFrameNativeInteropStatus.Success) diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsDefaultFileNameTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsDefaultFileNameTests.cs new file mode 100644 index 000000000..534fcfabd --- /dev/null +++ b/tests/InfiniTests.InfiniFrame/Window/Features/FilePickerDialogs/FilePickerDialogsDefaultFileNameTests.cs @@ -0,0 +1,48 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame; + +namespace InfiniTests.InfiniFrame.Window.Features.FilePickerDialogs; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class FilePickerDialogsDefaultFileNameTests { + [Test] + [NotInParallelInfiniTests] + public async Task ShowSaveFile_WithDefaultFileName_ShouldAcceptParameter(CancellationToken ct) { + // Arrange + using var windowUtility = InfiniFrameTestWindow.Create(ct); + IInfiniFrameWindow window = windowUtility.Window; + await EnsureWindowClosed(window, ct); + + // Act + string? result = window.Features.FilePickerDialogs.ShowSaveFile(defaultFileName: "document.txt"); + + // Assert + await Assert.That(result).IsNull(); + } + + [Test] + [NotInParallelInfiniTests] + public async Task ShowSaveFile_Extension_WithDefaultFileName_ShouldAcceptParameter(CancellationToken ct) { + // Arrange + using var windowUtility = InfiniFrameTestWindow.Create(ct); + IInfiniFrameWindow window = windowUtility.Window; + await EnsureWindowClosed(window, ct); + + // Act + string? result = window.ShowSaveFile(defaultFileName: "document.txt"); + + // Assert + await Assert.That(result).IsNull(); + } + + private static async Task EnsureWindowClosed(IInfiniFrameWindow window, CancellationToken ct) { + window.Close(); + DateTime timeoutAt = DateTime.UtcNow.AddSeconds(5); + while (!window.IsClosedOrClosing() && DateTime.UtcNow < timeoutAt) { + await Task.Delay(50, ct); + } + } +} \ No newline at end of file diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs index d34fc9367..2f39c2501 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureDispatcherCommandTests.cs @@ -27,7 +27,8 @@ public class WindowFeatureDispatcherCommandTests { Get("decorations", "title", "get_Title"), Get("decorations", "iconFilePath", "get_IconFilePath"), Get("decorations", "limitLinuxWindowTitleLength", "get_LimitLinuxWindowTitleLength"), Get("filePickerDialogs", "showOpenFile", "ShowOpenFile", "{}"), Get("filePickerDialogs", "showOpenFolder", "ShowOpenFolder", "{}"), - Get("filePickerDialogs", "showSaveFile", "ShowSaveFile", "{}"), + Get("filePickerDialogs", "showSaveFile", "ShowSaveFile", "{\"defaultFileName\":null}"), + Get("filePickerDialogs", "showSaveFile", "ShowSaveFile", "{\"defaultFileName\":\"document.txt\"}"), Get("lifecycle", "state", "get_State"), Get("lifecycle", "isClosedOrClosing", "IsClosedOrClosing"), Get("monitors", "monitors", "GetMonitors"), Get("monitors", "mainMonitor", "GetMainMonitor"), Get("monitors", "mainMonitorScreenDpi", "GetMainMonitorScreenDpi"),