diff --git a/Cargo.lock b/Cargo.lock index 3e8f381d6..654437d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3548,6 +3548,7 @@ dependencies = [ "embed-resource", "libc", "libc-stdhandle", + "widestring", "windows 0.61.3", ] @@ -7375,6 +7376,12 @@ dependencies = [ "winsafe", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 0ff7289b5..a22badd97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,8 +138,9 @@ unic-ucd-category = "0.9.0" unicode-normalization = "0.1.24" walkdir = "2.5.0" which = "8.0.0" +widestring = "1.1.0" winapi = { version = "0.3", features = ["wincon", "winreg"] } -windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_Foundation"] } +windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_Foundation", "Win32_UI_Shell"] } wiremock = "0.6.3" xz2 = "0.1.7" zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index e0864b301..53bdc3c92 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -42,6 +42,11 @@ if "--syncserver" in sys.argv: # does not return run_sync_server() +if sys.platform == "win32": + from win32com.shell import shell + + shell.SetCurrentProcessExplicitAppUserModelID("Ankitects.Anki") + import argparse import builtins import cProfile diff --git a/qt/aqt/main.py b/qt/aqt/main.py index e2e6f8534..4e0b82dd5 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1719,11 +1719,37 @@ title="{}" {}>{}""".format( self.maybeHideAccelerators() self.hideStatusTips() elif is_win: - # make sure ctypes is bundled - from ctypes import windll, wintypes # type: ignore + self._setupWin32() - _dummy1 = windll - _dummy2 = wintypes + def _setupWin32(self): + """Fix taskbar display/pinning""" + if sys.platform != "win32": + return + + launcher_path = os.environ.get("ANKI_LAUNCHER") + if not launcher_path: + return + + from win32com.propsys import propsys, pscon + from win32com.propsys.propsys import PROPVARIANTType + + hwnd = int(self.winId()) + prop_store = propsys.SHGetPropertyStoreForWindow(hwnd) # type: ignore[call-arg] + prop_store.SetValue( + pscon.PKEY_AppUserModel_ID, PROPVARIANTType("Ankitects.Anki") + ) + prop_store.SetValue( + pscon.PKEY_AppUserModel_RelaunchCommand, + PROPVARIANTType(f'"{launcher_path}"'), + ) + prop_store.SetValue( + pscon.PKEY_AppUserModel_RelaunchDisplayNameResource, PROPVARIANTType("Anki") + ) + prop_store.SetValue( + pscon.PKEY_AppUserModel_RelaunchIconResource, + PROPVARIANTType(f"{launcher_path},0"), + ) + prop_store.Commit() def maybeHideAccelerators(self, tgt: Any | None = None) -> None: if not self.hideMenuAccels: diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml index 5584e5296..fd6f2230c 100644 --- a/qt/launcher/Cargo.toml +++ b/qt/launcher/Cargo.toml @@ -16,6 +16,7 @@ dirs.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true +widestring.workspace = true libc.workspace = true libc-stdhandle.workspace = true diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index 235058757..bbb42df10 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -101,7 +101,7 @@ pub fn respawn_launcher() -> Result<()> { pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { #[cfg(windows)] { - crate::platform::windows::attach_to_parent_console(); + crate::platform::windows::prepare_to_launch_normally(); cmd.ensure_success()?; } #[cfg(unix)] diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs index ded968938..3c060a9de 100644 --- a/qt/launcher/src/platform/windows.rs +++ b/qt/launcher/src/platform/windows.rs @@ -1,15 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::ffi::OsStr; use std::io::stdin; -use std::os::windows::ffi::OsStrExt; use std::process::Command; use anyhow::Context; use anyhow::Result; +use widestring::u16cstr; use windows::core::PCWSTR; -use windows::Win32::System::Registry::HKEY_CURRENT_USER; use windows::Win32::System::Console::AttachConsole; use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; @@ -17,8 +15,10 @@ use windows::Win32::System::Registry::RegCloseKey; use windows::Win32::System::Registry::RegOpenKeyExW; use windows::Win32::System::Registry::RegQueryValueExW; use windows::Win32::System::Registry::HKEY; +use windows::Win32::System::Registry::HKEY_CURRENT_USER; use windows::Win32::System::Registry::KEY_READ; use windows::Win32::System::Registry::REG_SZ; +use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; pub fn ensure_terminal_shown() -> Result<()> { unsafe { @@ -146,10 +146,7 @@ fn read_registry_install_dir() -> Option { let mut hkey = HKEY::default(); // Convert the registry path to wide string - let subkey: Vec = OsStr::new("SOFTWARE\\Anki") - .encode_wide() - .chain(std::iter::once(0)) - .collect(); + let subkey = u16cstr!("SOFTWARE\\Anki"); // Open the registry key let result = RegOpenKeyExW( @@ -165,10 +162,7 @@ fn read_registry_install_dir() -> Option { } // Query the Install_Dir64 value - let value_name: Vec = OsStr::new("Install_Dir64") - .encode_wide() - .chain(std::iter::once(0)) - .collect(); + let value_name = u16cstr!("Install_Dir64"); let mut value_type = REG_SZ; let mut data_size = 0u32; @@ -211,3 +205,13 @@ fn read_registry_install_dir() -> Option { } } } + +pub fn prepare_to_launch_normally() { + // Set the App User Model ID for Windows taskbar grouping + unsafe { + let _ = + SetCurrentProcessExplicitAppUserModelID(PCWSTR(u16cstr!("Ankitects.Anki").as_ptr())); + } + + attach_to_parent_console(); +}