Fix taskbar pinning on Windows

Closes #4107
This commit is contained in:
Damien Elmes 2025-06-29 20:53:43 +07:00
parent 6eb1db0f5d
commit 469fd763c7
7 changed files with 61 additions and 17 deletions

7
Cargo.lock generated
View file

@ -3548,6 +3548,7 @@ dependencies = [
"embed-resource", "embed-resource",
"libc", "libc",
"libc-stdhandle", "libc-stdhandle",
"widestring",
"windows 0.61.3", "windows 0.61.3",
] ]
@ -7375,6 +7376,12 @@ dependencies = [
"winsafe", "winsafe",
] ]
[[package]]
name = "widestring"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -138,8 +138,9 @@ unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.24" unicode-normalization = "0.1.24"
walkdir = "2.5.0" walkdir = "2.5.0"
which = "8.0.0" which = "8.0.0"
widestring = "1.1.0"
winapi = { version = "0.3", features = ["wincon", "winreg"] } 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" wiremock = "0.6.3"
xz2 = "0.1.7" xz2 = "0.1.7"
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }

View file

@ -42,6 +42,11 @@ if "--syncserver" in sys.argv:
# does not return # does not return
run_sync_server() run_sync_server()
if sys.platform == "win32":
from win32com.shell import shell
shell.SetCurrentProcessExplicitAppUserModelID("Ankitects.Anki")
import argparse import argparse
import builtins import builtins
import cProfile import cProfile

View file

@ -1719,11 +1719,37 @@ title="{}" {}>{}</button>""".format(
self.maybeHideAccelerators() self.maybeHideAccelerators()
self.hideStatusTips() self.hideStatusTips()
elif is_win: elif is_win:
# make sure ctypes is bundled self._setupWin32()
from ctypes import windll, wintypes # type: ignore
_dummy1 = windll def _setupWin32(self):
_dummy2 = wintypes """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: def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
if not self.hideMenuAccels: if not self.hideMenuAccels:

View file

@ -16,6 +16,7 @@ dirs.workspace = true
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows.workspace = true windows.workspace = true
widestring.workspace = true
libc.workspace = true libc.workspace = true
libc-stdhandle.workspace = true libc-stdhandle.workspace = true

View file

@ -101,7 +101,7 @@ pub fn respawn_launcher() -> Result<()> {
pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> {
#[cfg(windows)] #[cfg(windows)]
{ {
crate::platform::windows::attach_to_parent_console(); crate::platform::windows::prepare_to_launch_normally();
cmd.ensure_success()?; cmd.ensure_success()?;
} }
#[cfg(unix)] #[cfg(unix)]

View file

@ -1,15 +1,13 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::ffi::OsStr;
use std::io::stdin; use std::io::stdin;
use std::os::windows::ffi::OsStrExt;
use std::process::Command; use std::process::Command;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use widestring::u16cstr;
use windows::core::PCWSTR; use windows::core::PCWSTR;
use windows::Win32::System::Registry::HKEY_CURRENT_USER;
use windows::Win32::System::Console::AttachConsole; use windows::Win32::System::Console::AttachConsole;
use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Console::GetConsoleWindow;
use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; 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::RegOpenKeyExW;
use windows::Win32::System::Registry::RegQueryValueExW; use windows::Win32::System::Registry::RegQueryValueExW;
use windows::Win32::System::Registry::HKEY; 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::KEY_READ;
use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::Registry::REG_SZ;
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
pub fn ensure_terminal_shown() -> Result<()> { pub fn ensure_terminal_shown() -> Result<()> {
unsafe { unsafe {
@ -146,10 +146,7 @@ fn read_registry_install_dir() -> Option<std::path::PathBuf> {
let mut hkey = HKEY::default(); let mut hkey = HKEY::default();
// Convert the registry path to wide string // Convert the registry path to wide string
let subkey: Vec<u16> = OsStr::new("SOFTWARE\\Anki") let subkey = u16cstr!("SOFTWARE\\Anki");
.encode_wide()
.chain(std::iter::once(0))
.collect();
// Open the registry key // Open the registry key
let result = RegOpenKeyExW( let result = RegOpenKeyExW(
@ -165,10 +162,7 @@ fn read_registry_install_dir() -> Option<std::path::PathBuf> {
} }
// Query the Install_Dir64 value // Query the Install_Dir64 value
let value_name: Vec<u16> = OsStr::new("Install_Dir64") let value_name = u16cstr!("Install_Dir64");
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut value_type = REG_SZ; let mut value_type = REG_SZ;
let mut data_size = 0u32; let mut data_size = 0u32;
@ -211,3 +205,13 @@ fn read_registry_install_dir() -> Option<std::path::PathBuf> {
} }
} }
} }
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();
}