feat: minimize to tray and autostart

This commit is contained in:
Tien Do Nam
2023-01-21 01:07:47 +01:00
parent 03f5e53d83
commit f3b947761d
26 changed files with 417 additions and 64 deletions
+4 -1
View File
@@ -75,7 +75,10 @@
"language": "Language",
"languageOptions": {
"system": "System"
}
},
"minimizeToTray": "Minimize to tray",
"launchAtStartup": "Autostart after login",
"launchMinimized": "Start hidden"
},
"receive": {
"title": "Receive",
+4 -1
View File
@@ -75,7 +75,10 @@
"language": "Sprache",
"languageOptions": {
"system": "System"
}
},
"minimizeToTray": "In Symbolleiste minimieren",
"launchAtStartup": "Autostart nach Login",
"launchMinimized": "Versteckt starten"
},
"receive": {
"title": "Empfangen",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

+16 -3
View File
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -12,16 +11,19 @@ import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/platform_check.dart';
import 'package:localsend_app/util/snackbar.dart';
import 'package:localsend_app/util/tray_helper.dart';
import 'package:routerino/routerino.dart';
import 'package:share_handler/share_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:window_manager/window_manager.dart';
const launchAtStartupArg = 'autostart';
/// Will be called before the MaterialApp started
Future<PersistenceService> preInit() async {
Future<PersistenceService> preInit(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && checkPlatformIsDesktop()) {
if (checkPlatformIsDesktop()) {
await windowManager.ensureInitialized();
WindowManager.instance.setMinimumSize(const Size(400, 500));
@@ -46,6 +48,17 @@ Future<PersistenceService> preInit() async {
LocaleSettings.setLocale(locale);
}
if (checkPlatformIsDesktop()) {
// initialize tray AFTER i18n has been initialized
await initTray();
if (!args.contains(launchAtStartupArg) || !persistenceService.isLaunchMinimized()) {
// We show this app, when (1) app started manually, (2) app should not start minimized
// In other words: only start minimized when launched on startup and "launchMinimized" is configured
await windowManager.show();
}
}
return persistenceService;
}
+44 -20
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -10,11 +12,15 @@ import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/device_info_helper.dart';
import 'package:localsend_app/widget/life_cycle_watcher.dart';
import 'package:localsend_app/util/platform_check.dart';
import 'package:localsend_app/widget/watcher/life_cycle_watcher.dart';
import 'package:localsend_app/widget/watcher/tray_watcher.dart';
import 'package:localsend_app/widget/watcher/window_watcher.dart';
import 'package:routerino/routerino.dart';
import 'package:window_manager/window_manager.dart';
Future<void> main() async {
final persistenceService = await preInit();
Future<void> main(List<String> args) async {
final persistenceService = await preInit(args);
runApp(TranslationProvider(
child: ProviderScope(
overrides: [
@@ -32,23 +38,41 @@ class LocalSendApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(settingsProvider.select((settings) => settings.theme));
return LifeCycleWatcher(
onChangedState: (AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.read(networkInfoProvider.notifier).init();
}
},
child: MaterialApp(
title: t.appName,
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
theme: getTheme(Brightness.light),
darkTheme: getTheme(Brightness.dark),
themeMode: themeMode,
navigatorKey: Routerino.navigatorKey,
home: const HomePage(),
return TrayWatcher(
child: WindowWatcher(
onClose: () async {
if (!checkPlatformIsDesktop()) {
return;
}
try {
if (ref.read(settingsProvider).minimizeToTray) {
await windowManager.hide();
} else {
exit(0);
}
} catch (e) {
print(e);
}
},
child: LifeCycleWatcher(
onChangedState: (AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.read(networkInfoProvider.notifier).init();
}
},
child: MaterialApp(
title: t.appName,
locale: TranslationProvider.of(context).flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false,
theme: getTheme(Brightness.light),
darkTheme: getTheme(Brightness.dark),
themeMode: themeMode,
navigatorKey: Routerino.navigatorKey,
home: const HomePage(),
),
),
),
);
}
+3
View File
@@ -14,5 +14,8 @@ class Settings with _$Settings {
required String? destination, // null = default
required bool saveToGallery, // only Android, iOS
required bool quickSave, // automatically accept file requests
required bool minimizeToTray, // minimize to tray instead of exiting the app
required bool launchAtStartup, // launch on start / login
required bool launchMinimized, // start hidden in tray (only available when launchAtStartup is true)
}) = _Settings;
}
+93 -36
View File
@@ -1,7 +1,11 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/init.dart';
import 'package:localsend_app/pages/about_page.dart';
import 'package:localsend_app/pages/changelog_page.dart';
import 'package:localsend_app/provider/network/server_provider.dart';
@@ -15,6 +19,7 @@ import 'package:localsend_app/widget/custom_dropdown_button.dart';
import 'package:localsend_app/widget/dialogs/quick_save_notice.dart';
import 'package:localsend_app/widget/local_send_logo.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:routerino/routerino.dart';
class SettingsTab extends ConsumerStatefulWidget {
@@ -101,32 +106,61 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
},
),
),
if (checkPlatformIsDesktop()) ...[
_BooleanEntry(
label: t.settingsTab.general.minimizeToTray,
value: settings.minimizeToTray,
onChanged: (b) async {
await ref.read(settingsProvider.notifier).setMinimizeToTray(b);
},
),
_BooleanEntry(
label: t.settingsTab.general.launchAtStartup,
value: settings.launchAtStartup,
onChanged: (b) async {
try {
final packageInfo = await PackageInfo.fromPlatform();
launchAtStartup.setup(
appName: packageInfo.appName,
appPath: Platform.resolvedExecutable,
args: [launchAtStartupArg],
);
if (b) {
await launchAtStartup.enable();
} else {
await launchAtStartup.disable();
}
await ref.read(settingsProvider.notifier).setLaunchAtStartup(b);
} catch (e) {
print(e);
}
},
),
if (settings.launchAtStartup)
_BooleanEntry(
label: t.settingsTab.general.launchMinimized,
value: settings.launchMinimized,
onChanged: (b) async {
await ref.read(settingsProvider.notifier).setLaunchMinimized(b);
},
),
],
],
),
_SettingsSection(
title: t.settingsTab.receive.title,
children: [
_SettingsEntry(
_BooleanEntry(
label: t.settingsTab.receive.quickSave,
child: CustomDropdownButton<bool>(
value: settings.quickSave,
items: [false, true].map((b) {
return DropdownMenuItem(
value: b,
alignment: Alignment.center,
child: Text(b ? t.general.on : t.general.off),
);
}).toList(),
onChanged: (b) async {
if (b != null) {
final old = settings.quickSave;
await ref.read(settingsProvider.notifier).setQuickSave(b);
if (!old && b && mounted) {
QuickSaveNotice.open(context);
}
}
},
),
value: settings.quickSave,
onChanged: (b) async {
final old = settings.quickSave;
await ref.read(settingsProvider.notifier).setQuickSave(b);
if (!old && b && mounted) {
QuickSaveNotice.open(context);
}
},
),
if (checkPlatform([TargetPlatform.windows, TargetPlatform.macOS, TargetPlatform.linux]))
_SettingsEntry(
@@ -155,23 +189,12 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
),
),
if (checkPlatformWithGallery())
_SettingsEntry(
_BooleanEntry(
label: t.settingsTab.receive.saveToGallery,
child: CustomDropdownButton<bool>(
value: settings.saveToGallery,
items: [false, true].map((b) {
return DropdownMenuItem(
value: b,
alignment: Alignment.center,
child: Text(b ? t.general.on : t.general.off),
);
}).toList(),
onChanged: (b) async {
if (b != null) {
await ref.read(settingsProvider.notifier).setSaveToGallery(b);
}
},
),
value: settings.saveToGallery,
onChanged: (b) async {
await ref.read(settingsProvider.notifier).setSaveToGallery(b);
},
),
],
),
@@ -359,6 +382,40 @@ class _SettingsEntry extends StatelessWidget {
}
}
/// A specialized version of [_SettingsEntry].
class _BooleanEntry extends StatelessWidget {
final String label;
final bool value;
final ValueChanged<bool> onChanged;
const _BooleanEntry({
required this.label,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return _SettingsEntry(
label: label,
child: CustomDropdownButton<bool>(
value: value,
items: [false, true].map((b) {
return DropdownMenuItem(
value: b,
alignment: Alignment.center,
child: Text(b ? t.general.on : t.general.off),
);
}).toList(),
onChanged: (b) {
if (b != null) {
onChanged(b);
}
},
),
);
}
}
class _SettingsSection extends StatelessWidget {
final String title;
final List<Widget> children;
@@ -31,6 +31,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
/// This provider manages receiving file requests.
final serverProvider = StateNotifierProvider<ServerNotifier, ServerState?>((ref) {
@@ -158,6 +159,10 @@ class ServerNotifier extends StateNotifier<ServerState?> {
for (final f in dto.files.values) f.id: f.fileName,
};
} else {
if (checkPlatformIsDesktop() && (await windowManager.isMinimized() || !(await windowManager.isVisible()))) {
await windowManager.show();
}
// ignore: use_build_context_synchronously
Routerino.context.push(() => const ReceivePage());
+27
View File
@@ -16,6 +16,9 @@ const _portKey = 'ls_port';
const _destinationKey = 'ls_destination';
const _saveToGallery = 'ls_save_to_gallery';
const _quickSave = 'ls_quick_save';
const _minimizeToTray = 'ls_minimize_to_tray';
const _launchAtStartup = 'ls_launch_at_startup';
const _launchMinimized = 'ls_start_minimized';
const defaultPort = 53317;
@@ -110,4 +113,28 @@ class PersistenceService {
Future<void> setQuickSave(bool quickSave) async {
await _prefs.setBool(_quickSave, quickSave);
}
bool isMinimizeToTray() {
return _prefs.getBool(_minimizeToTray) ?? false;
}
Future<void> setMinimizeToTray(bool minimizeToTray) async {
await _prefs.setBool(_minimizeToTray, minimizeToTray);
}
bool isLaunchAtStartup() {
return _prefs.getBool(_launchAtStartup) ?? false;
}
Future<void> setLaunchAtStartup(bool launchAtStartup) async {
await _prefs.setBool(_launchAtStartup, launchAtStartup);
}
bool isLaunchMinimized() {
return _prefs.getBool(_launchMinimized) ?? true;
}
Future<void> setLaunchMinimized(bool launchMinimized) async {
await _prefs.setBool(_launchMinimized, launchMinimized);
}
}
+24
View File
@@ -23,6 +23,9 @@ class SettingsNotifier extends StateNotifier<Settings> {
destination: service.getDestination(),
saveToGallery: service.isSaveToGallery(),
quickSave: service.isQuickSave(),
minimizeToTray: service.isMinimizeToTray(),
launchAtStartup: service.isLaunchAtStartup(),
launchMinimized: service.isLaunchMinimized(),
);
}
@@ -74,4 +77,25 @@ class SettingsNotifier extends StateNotifier<Settings> {
quickSave: quickSave,
);
}
Future<void> setMinimizeToTray(bool minimizeToTray) async {
await _service.setMinimizeToTray(minimizeToTray);
state = state.copyWith(
minimizeToTray: minimizeToTray,
);
}
Future<void> setLaunchAtStartup(bool launchAtStartup) async {
await _service.setLaunchAtStartup(launchAtStartup);
state = state.copyWith(
launchAtStartup: launchAtStartup,
);
}
Future<void> setLaunchMinimized(bool launchMinimized) async {
await _service.setLaunchMinimized(launchMinimized);
state = state.copyWith(
launchMinimized: launchMinimized,
);
}
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
import 'package:localsend_app/gen/assets.gen.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/util/platform_check.dart';
import 'package:tray_manager/tray_manager.dart';
Future<void> initTray() async {
if (!checkPlatformIsDesktop()) {
return;
}
try {
await trayManager.setIcon(
checkPlatform([TargetPlatform.windows]) ? Assets.img.logo32Ico : Assets.img.logo32Png.path,
);
final items = [
MenuItem(
key: 'exit_app',
label: t.general.close,
),
];
await trayManager.setContextMenu(Menu(items: items));
await trayManager.setToolTip(t.appName);
} catch (e) {
print(e);
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter/material.dart' hide MenuItem;
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
class TrayWatcher extends StatefulWidget {
final Widget child;
const TrayWatcher({required this.child, super.key});
@override
State<TrayWatcher> createState() => _TrayWatcherState();
}
class _TrayWatcherState extends State<TrayWatcher> with TrayListener {
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void initState() {
super.initState();
trayManager.addListener(this);
}
@override
void dispose() {
trayManager.removeListener(this);
super.dispose();
}
@override
void onTrayIconMouseDown() async {
try {
if (await windowManager.isVisible() && !(await windowManager.isMinimized())) {
await trayManager.popUpContextMenu();
} else {
await windowManager.show();
}
} catch (e) {
print(e);
}
}
@override
void onTrayIconRightMouseDown() {
trayManager.popUpContextMenu();
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
// There is only an exit button
exit(0);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart' hide MenuItem;
import 'package:window_manager/window_manager.dart';
class WindowWatcher extends StatefulWidget {
final Widget child;
final VoidCallback onClose;
const WindowWatcher({
required this.child,
required this.onClose,
super.key,
});
@override
State<WindowWatcher> createState() => _WindowWatcherState();
}
class _WindowWatcherState extends State<WindowWatcher> with WindowListener {
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void initState() {
super.initState();
windowManager.addListener(this);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
// always handle close actions manually
await windowManager.setPreventClose(true);
} catch (e) {
print(e);
}
});
}
@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}
@override
void onWindowClose() {
widget.onClose();
}
@override
void onWindowFocus() {
// call set state according to window_manager README
setState(() {});
}
}
@@ -8,6 +8,7 @@
#include <desktop_drop/desktop_drop_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
@@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+1
View File
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
screen_retriever
tray_manager
url_launcher_linux
window_manager
)
+1 -1
View File
@@ -48,7 +48,7 @@ static void my_application_activate(GApplication* application) {
}
gtk_window_set_default_size(window, 400, 500);
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_realize(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
@@ -14,6 +14,7 @@ import path_provider_macos
import photo_manager
import screen_retriever
import shared_preferences_macos
import tray_manager
import url_launcher_macos
import wakelock_macos
import window_manager
@@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
+2 -1
View File
@@ -4,6 +4,7 @@ import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
// LocalSend handles the close event manually
return false
}
}
+7
View File
@@ -1,5 +1,6 @@
import Cocoa
import FlutterMacOS
import window_manager
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
@@ -12,4 +13,10 @@ class MainFlutterWindow: NSWindow {
super.awakeFromNib()
}
// window_manager: start hidden
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
super.order(place, relativeTo: otherWin)
hiddenWindowAtLaunch()
}
}
+35
View File
@@ -541,6 +541,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.5.4"
launch_at_startup:
dependency: "direct main"
description:
name: launch_at_startup
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
lints:
dependency: transitive
description:
@@ -576,6 +583,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
menu_base:
dependency: transitive
description:
name: menu_base
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
@@ -968,6 +982,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
shortid:
dependency: transitive
description:
name: shortid
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
@@ -1106,6 +1127,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
typed_data:
dependency: transitive
description:
@@ -1295,6 +1323,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
win32_registry:
dependency: transitive
description:
name: win32_registry
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
window_manager:
dependency: "direct main"
description:
+3
View File
@@ -26,6 +26,7 @@ dependencies:
image_gallery_saver: 1.7.1
image_picker: 0.8.6
json_annotation: 4.7.0
launch_at_startup: 0.2.1
network_info_plus: 3.0.1
open_filex: 4.3.2
package_info_plus: 3.0.2
@@ -39,6 +40,7 @@ dependencies:
shelf_router: 1.1.3
slang: 3.9.0
slang_flutter: 3.9.0
tray_manager: 0.2.0
url_launcher: 6.1.7
uuid: 3.0.7
wakelock: 0.6.2
@@ -69,3 +71,4 @@ msix_config:
identity_name: 11157TienDoNam.LocalSend
logo_path: assets\img\logo-512.png
languages: en-us, de-de
enable_at_startup: true
@@ -11,6 +11,7 @@
#include <network_info_plus/network_info_plus_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
@@ -25,6 +26,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
+1
View File
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
network_info_plus
permission_handler_windows
screen_retriever
tray_manager
url_launcher_windows
window_manager
)
+1 -1
View File
@@ -117,7 +117,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
window_class, title.c_str(), WS_OVERLAPPEDWINDOW, // do not add WS_VISIBLE since the window will be shown later
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);