From d0a7df4577ddd0090af7b96c0fe521adfc3f7639 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 25 Sep 2026 19:44:54 -0600 Subject: [PATCH 1/5] Resolve Apps folder items that stand for a shortcut to a Store app With a shortcut to a Store app in the Start Menu (for example a program pinned to the Classic menu, which lives in the Start Menu root), the Apps folder lists that shortcut in place of the app. Creating the app's item from its AppUserModelID then gives an item with the shortcut's properties: PKEY_MetroAppLauncher=0, no package, and the shortcut named in System.AppUserModel.BestShortcut. An app without a shortcut reports PKEY_MetroAppLauncher=2 and its package, as expected. RefreshItemInfo took such an item for a desktop program: not a link, not a Metro app, so it dropped the appid. In the Recent Programs list this showed as: - a blank page instead of the app's icon, because the icon came from the shortcut, which has no icon of its own; - the app listed in Recent although it is pinned, because for apps the duplicate check compares appids and this one was empty. Seen with Microsoft To Do and Microsoft Store pinned; Quick Assist, not pinned, was fine. Now such an item is resolved through its shortcut to the app, the same way RefreshItemInfo already resolves a link to a Store app: the item and property store become the shortcut's target, if that target has the same appid and is a Metro app. The appid, package and Metro icon then come out right, and the existing duplicate check works. --- Src/StartMenu/StartMenuDLL/ItemManager.cpp | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Src/StartMenu/StartMenuDLL/ItemManager.cpp b/Src/StartMenu/StartMenuDLL/ItemManager.cpp index 27b2d17ed..5e96b6c25 100644 --- a/Src/StartMenu/StartMenuDLL/ItemManager.cpp +++ b/Src/StartMenu/StartMenuDLL/ItemManager.cpp @@ -1816,6 +1816,56 @@ CString GetPropertyStoreString( IPropertyStore *pStore, REFPROPERTYKEY key ) return res; } +// Name: System.AppUserModel.BestShortcut +// Type: Buffer -- VT_VECTOR | VT_UI1 (an ID list) +// FormatID: {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}, 10 +static const PROPERTYKEY PKEY_AppUserModel_BestShortcut={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 10}; + +// Checks that the buffer holds a whole ID list, so it can be used as one +static bool IsValidIDList( const BYTE *data, ULONG size ) +{ + for (ULONG pos=0;pos+sizeof(USHORT)<=size;) + { + USHORT cb=*(const USHORT*)(data+pos); + if (cb==0) return true; + pos+=cb; + } + return false; +} + +// When the Start Menu has a shortcut to a Metro app, the Apps folder lists the shortcut in place of the app. Its item then has the +// shortcut's properties (no package, PKEY_MetroAppLauncher=0) and names the shortcut in PKEY_AppUserModel_BestShortcut. +// Returns the app the shortcut points to, with its property store, the same way a link to a Metro app is resolved +static bool GetMetroShortcutTarget( IPropertyStore *pStore, CComPtr &target, CComPtr &targetStore ) +{ + CComPtr pShortcut; + PROPVARIANT val; + PropVariantInit(&val); + if (SUCCEEDED(pStore->GetValue(PKEY_AppUserModel_BestShortcut,&val)) && val.vt==(VT_VECTOR|VT_UI1) && IsValidIDList(val.caub.pElems,val.caub.cElems)) + SHCreateItemFromIDList((PCIDLIST_ABSOLUTE)val.caub.pElems,IID_PPV_ARGS(&pShortcut)); + PropVariantClear(&val); + if (!pShortcut) return false; + + CComPtr pLink; + CAbsolutePidl targetPidl; + CComPtr pTarget; + CComPtr pTargetStore; + if (FAILED(pShortcut->BindToHandler(NULL,BHID_SFUIObject,IID_PPV_ARGS(&pLink))) || FAILED(pLink->GetIDList(&targetPidl)) || + FAILED(SHCreateItemFromIDList(targetPidl,IID_PPV_ARGS(&pTarget))) || FAILED(pTarget->BindToHandler(NULL,BHID_PropertyStore,IID_PPV_ARGS(&pTargetStore)))) + return false; + + // only a shortcut to the same app, and only if it is a Metro app + if (_wcsicmp(GetPropertyStoreString(pStore,PKEY_AppUserModel_ID),GetPropertyStoreString(pTargetStore,PKEY_AppUserModel_ID))!=0) + return false; + bool bMetro=SUCCEEDED(pTargetStore->GetValue(PKEY_MetroAppLauncher,&val)) && (val.vt==VT_I4 || val.vt==VT_UI4) && val.intVal; + PropVariantClear(&val); + if (!bMetro) return false; + + target=std::move(pTarget); + targetStore=std::move(pTargetStore); + return true; +} + // doesn't require the lock to be held void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellItem *pItem0, bool bHasWriteLock ) { @@ -1935,6 +1985,15 @@ void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellIte { newInfo.bLink=false; pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); + // an Apps folder item that stands for a shortcut to a Metro app: use the app, or it would be taken for a desktop program + // without an appid (wrong icon, and not recognized as a pinned program) + CComPtr target; + CComPtr store; + if (pStore && GetMetroShortcutTarget(pStore,target,store)) + { + pItem=std::move(target); + pStore=store; + } } if (pStore) { From a0d86be74c2727479380531e43aba5ed19bcf633 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Fri, 25 Sep 2026 19:52:05 -0600 Subject: [PATCH 2/5] Find a Start Menu shortcut to a Store app as a duplicate in search Searching for a Store app that has a shortcut in the Start Menu (for example one pinned to the Classic menu) listed it twice under Programs: once from the Start Menu folder and once from the Apps folder. GetSearchResults drops a program as a duplicate when its name and appid match one already found, comparing names with wcscmp. Names from the folders are stored uppercase (CollectSearchItems), and so are Metro link names (AddSearchItem), but on Windows 10 and later the apps from GetMetroLinks were added with their display name as it is. So "MICROSOFT TO DO" from the shortcut never matched "Microsoft To Do" from the Apps folder, although both have the same appid (checked with the search log). Store these names uppercase too. The name is only used for matching, which ignores case, and for finding duplicates; the results show the item's own display name, so nothing visible changes except that the duplicate is gone. --- Src/StartMenu/StartMenuDLL/SearchManager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Src/StartMenu/StartMenuDLL/SearchManager.cpp b/Src/StartMenu/StartMenuDLL/SearchManager.cpp index 037794941..c2ca36e34 100644 --- a/Src/StartMenu/StartMenuDLL/SearchManager.cpp +++ b/Src/StartMenu/StartMenuDLL/SearchManager.cpp @@ -692,7 +692,10 @@ void CSearchManager::SearchThread( void ) { CComString pName; if (SUCCEEDED(it->pItem->GetDisplayName(SIGDN_NORMALDISPLAY,&pName))) - AddSearchItem(it->pItem,pName,COLLECT_PROGRAMS|COLLECT_METRO,CATEGORY_PROGRAM,searchRequest); + { + pName.MakeUpper(); // like the names from the folders, so GetSearchResults can tell a Start Menu shortcut to the same app is a duplicate + AddSearchItem(it->pItem,pName,COLLECT_PROGRAMS|COLLECT_METRO,CATEGORY_PROGRAM,searchRequest); + } } if (searchRequest.requestId Date: Wed, 30 Sep 2026 10:14:32 -0600 Subject: [PATCH 3/5] Take a Store app's package from its appid, not from a shortcut's saved copy A shortcut to a Store app stores a snapshot of the app's item: the full package name, which includes the version, the install folder and the logo name. Open-Shell resolves such a shortcut to that item and read the package from it. After the app updates itself, that package no longer exists, so: - LoadMetroIcon could not open the package's resources, and the item showed a blank tile. Seen with Microsoft Store pinned to the Classic menu the morning after it updated from 22608.1401.4.0 to 1401.5.0; the shortcut still named 1401.4.0. - the item's package path pointed to a folder that is gone. The Recent Programs list drops an app whose package path does not exist. GetPackageFullName had the same weakness from the other side: it looked the appid up in the Apps folder, and with a shortcut to the app in the Start Menu that item is the shortcut, which has no package at all. The appid (package family name!app name) does not change with updates. Now GetPackageFullName asks Windows for the package of that family installed now (GetPackagesByPackageFamily), and takes the main package, since a family can also list a bundle and resource packages. The old lookup stays as the fallback. LoadMetroIcon and the package path use it, and the saved values only when that finds nothing. The package name is part of the icon cache key now, so a new version of an app shows its new icon. The package functions are loaded with GetProcAddress, as the DLL still loads on Windows 7. CanUninstallMetroApp reads the package path through the Apps folder in the same way; not changed here. --- Src/StartMenu/StartMenuDLL/ItemManager.cpp | 22 +++++--- .../StartMenuDLL/MetroLinkManager.cpp | 54 +++++++++++++++++++ Src/StartMenu/StartMenuDLL/MetroLinkManager.h | 5 +- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Src/StartMenu/StartMenuDLL/ItemManager.cpp b/Src/StartMenu/StartMenuDLL/ItemManager.cpp index 5e96b6c25..e9c94108f 100644 --- a/Src/StartMenu/StartMenuDLL/ItemManager.cpp +++ b/Src/StartMenu/StartMenuDLL/ItemManager.cpp @@ -2016,7 +2016,10 @@ void CItemManager::RefreshItemInfo( ItemInfo *pInfo, int refreshFlags, IShellIte if (newInfo.bMetroApp) { pAppItem=pItem; - newInfo.packagePath=GetPropertyStoreString(pStore,PKEY_MetroPackagePath); + // the package installed now, not the one named by the item of a shortcut, which is gone after an update + newInfo.packagePath=GetPackageInstallPath(GetPackageFullName(newInfo.appid)); + if (newInfo.packagePath.IsEmpty()) + newInfo.packagePath=GetPropertyStoreString(pStore,PKEY_MetroPackagePath); if (!newInfo.packagePath.IsEmpty()) { FILETIME writeTime={0}, createTime={0}; @@ -2532,6 +2535,16 @@ static bool SetResContextTargetSize( IResourceContext *pResContext, int size ) void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const IconInfo *&smallIcon, const IconInfo *&largeIcon, const IconInfo *&extraLargeIcon, const DWORD *pMetroColor ) { + CComPtr pStore; + pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); + if (!pStore) return; + + // the package installed now. The item of a shortcut names the package the shortcut was created with, which is gone after an update + CString packageName=GetPackageFullName(GetPropertyStoreString(pStore,PKEY_AppUserModel_ID)); + if (packageName.IsEmpty()) + packageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); + if (packageName.IsEmpty()) return; + unsigned int hash; { CComString pName; @@ -2540,6 +2553,7 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic } hash=CalcFNVHash(pMetroColor,4,hash); + hash=CalcFNVHash(packageName,hash); // a new version of the app can come with a new icon FindInCache(hash,refreshFlags,smallIcon,largeIcon,extraLargeIcon); if (!refreshFlags) return; @@ -2547,12 +2561,6 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic // extract icon HBITMAP hSmallBitmap=NULL, hLargeBitmap=NULL, hExtraLargeBitmap=NULL; - CComPtr pStore; - pItem->BindToHandler(NULL,BHID_PropertyStore,IID_IPropertyStore,(void**)&pStore); - if (!pStore) return; - - CString packageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); - if (packageName.IsEmpty()) return; CString iconName=GetPropertyStoreString(pStore,PKEY_MetroIcon); if (iconName.IsEmpty()) return; diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp index 67406b678..c0a969dc7 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp @@ -10,7 +10,9 @@ #include "ResourceHelper.h" #include "Translations.h" #include +#include #include +#include PROPERTYKEY PKEY_MetroAppLink={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 10}; // pidl PROPERTYKEY PKEY_MetroAppLauncher={{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, 14}; // =1 for metro apps @@ -373,8 +375,60 @@ bool IsEdgeDefaultBrowser( void ) return false; } +// The package of the app as installed now, found from the package family name in the appid. The full name contains the version, so it +// changes with every update, while the appid does not. A shortcut to a Metro app keeps the full name of the package it was created with, +// which is gone after an update. And with such a shortcut in the Start Menu, the Apps folder item for the appid is the shortcut, without a package +static CString GetInstalledPackageFullName(const wchar_t* appId) +{ + static auto pGetPackagesByPackageFamily = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagesByPackageFamily")); + const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; + if (!pGetPackagesByPackageFamily || !bang) + return {}; + CString family(appId, (int)(bang - appId)); + + UINT32 count = 0, bufferLength = 0; + if (pGetPackagesByPackageFamily(family, &count, nullptr, &bufferLength, nullptr) != ERROR_INSUFFICIENT_BUFFER || count == 0) + return {}; + std::vector names(count); + std::vector buffer(bufferLength); + if (pGetPackagesByPackageFamily(family, &count, names.data(), &bufferLength, buffer.data()) != ERROR_SUCCESS) + return {}; + + // the family can also list a bundle and resource packages. The app is in the main package, the one without a resource id. + // A full name is Name_Version_Architecture_ResourceId_PublisherId, and no part contains '_' + for (UINT32 i = 0; i < count; i++) + { + const wchar_t* resourceId = names[i]; + for (int part = 0; part < 3 && resourceId; part++) + { + resourceId = wcschr(resourceId, '_'); + if (resourceId) resourceId++; + } + if (resourceId && *resourceId == '_') + return names[i]; + } + return {}; +} + +// Returns the folder where the package is installed +CString GetPackageInstallPath(const wchar_t* packageFullName) +{ + static auto pGetPackagePathByFullName = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagePathByFullName")); + if (!pGetPackagePathByFullName || !packageFullName || !*packageFullName) + return {}; + wchar_t path[_MAX_PATH]; + UINT32 length = _countof(path); + if (pGetPackagePathByFullName(packageFullName, &length, path) != ERROR_SUCCESS) + return {}; + return path; +} + CString GetPackageFullName(const wchar_t* appId) { + CString packageName = GetInstalledPackageFullName(appId); + if (!packageName.IsEmpty()) + return packageName; + CComPtr item; if (SUCCEEDED(SHCreateItemInKnownFolder(FOLDERID_AppsFolder, 0, appId, IID_PPV_ARGS(&item)))) { diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h index 83b41886f..bb12c5464 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h @@ -54,5 +54,8 @@ CComPtr GetMetroPinMenu( const wchar_t *appid ); // Determines if Edge is the default browser bool IsEdgeDefaultBrowser( void ); -// Returns full package name for given App ID +// Returns full package name for given App ID, of the package installed now CString GetPackageFullName(const wchar_t* appId); + +// Returns the folder where the package is installed +CString GetPackageInstallPath(const wchar_t* packageFullName); From 92a7efd8fc8cc7f1f95991d2538fb741f486fb0b Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 30 Sep 2026 11:01:41 -0600 Subject: [PATCH 4/5] Take a Store app's logo name from its installed manifest when the item has none The previous commit made LoadMetroIcon open the package installed now, but the pinned Microsoft Store still showed a blank page. A diagnostic build logged why: the item came from the data cache already marked as a Metro app, so only its icon was refreshed, and for that RefreshItemInfo looks the appid up in the Apps folder. With a shortcut to the app in the Start Menu, that item is the Apps folder's stand-in for the shortcut, which has no package and no logo name (PKEY_MetroIcon). LoadMetroIcon gave up, and the fallback shell icon of the shortcut is the blank page the taskbar shows too. To Do, Terminal and Teams hid the same failure, because the shell icon of their shortcuts is still good. Now the logo name comes from the item only when the item names the package installed now. For a stand-in, or a snapshot of another version, it comes from AppxManifest.xml of the installed package (Square44x44Logo, or SmallLogo in Windows 8 manifests), read with the Appx manifest reader. That is also right when an update renames the logo. Checked on the real machine with the diagnostic build: all 5 pinned Store apps (Store, To Do, Terminal, Teams, Outlook) loaded their icon from the package, none left to the fallback. Reading the Store's manifest took about 30 ms, once per session. --- Src/StartMenu/StartMenuDLL/ItemManager.cpp | 14 +++++-- .../StartMenuDLL/MetroLinkManager.cpp | 39 +++++++++++++++++++ Src/StartMenu/StartMenuDLL/MetroLinkManager.h | 3 ++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Src/StartMenu/StartMenuDLL/ItemManager.cpp b/Src/StartMenu/StartMenuDLL/ItemManager.cpp index e9c94108f..c4b9734e1 100644 --- a/Src/StartMenu/StartMenuDLL/ItemManager.cpp +++ b/Src/StartMenu/StartMenuDLL/ItemManager.cpp @@ -2540,9 +2540,11 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic if (!pStore) return; // the package installed now. The item of a shortcut names the package the shortcut was created with, which is gone after an update - CString packageName=GetPackageFullName(GetPropertyStoreString(pStore,PKEY_AppUserModel_ID)); + CString appid=GetPropertyStoreString(pStore,PKEY_AppUserModel_ID); + CString savedPackageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); + CString packageName=GetPackageFullName(appid); if (packageName.IsEmpty()) - packageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName); + packageName=savedPackageName; if (packageName.IsEmpty()) return; unsigned int hash; @@ -2561,7 +2563,13 @@ void CItemManager::LoadMetroIcon( IShellItem *pItem, int &refreshFlags, const Ic // extract icon HBITMAP hSmallBitmap=NULL, hLargeBitmap=NULL, hExtraLargeBitmap=NULL; - CString iconName=GetPropertyStoreString(pStore,PKEY_MetroIcon); + // the logo named by the item belongs to the package the item names. When that is another version, or the item is the Apps folder's + // stand-in for a shortcut to the app (no package, no logo), take the logo from the manifest of the package installed now + CString iconName; + if (_wcsicmp(savedPackageName,packageName)==0) + iconName=GetPropertyStoreString(pStore,PKEY_MetroIcon); + if (iconName.IsEmpty()) + iconName=GetAppLogoName(appid); if (iconName.IsEmpty()) return; CComPtr pResManager; diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp index c0a969dc7..e6e2a1669 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp @@ -11,6 +11,8 @@ #include "Translations.h" #include #include +#include +#include #include #include @@ -423,6 +425,43 @@ CString GetPackageInstallPath(const wchar_t* packageFullName) return path; } +// Returns the small logo of the app (a resource name like Assets\AppList.png), from the manifest of the package installed now +CString GetAppLogoName(const wchar_t* appId) +{ + const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; + if (!bang) + return {}; + CString path = GetPackageInstallPath(GetInstalledPackageFullName(appId)); + if (path.IsEmpty()) + return {}; + path += L"\\AppxManifest.xml"; + + CComPtr factory; + CComPtr stream; + CComPtr reader; + CComPtr apps; + if (FAILED(factory.CoCreateInstance(__uuidof(AppxFactory))) || FAILED(SHCreateStreamOnFileEx(path, STGM_READ | STGM_SHARE_DENY_NONE, 0, FALSE, nullptr, &stream)) || + FAILED(factory->CreateManifestReader(stream, &reader)) || FAILED(reader->GetApplications(&apps))) + return {}; + + BOOL bHasCurrent = FALSE; + for (; SUCCEEDED(apps->GetHasCurrent(&bHasCurrent)) && bHasCurrent; apps->MoveNext(&bHasCurrent)) + { + CComPtr app; + CComString id; + if (FAILED(apps->GetCurrent(&app)) || FAILED(app->GetStringValue(L"Id", &id)) || _wcsicmp(id, bang + 1) != 0) + continue; + CComString logo; + if (SUCCEEDED(app->GetStringValue(L"Square44x44Logo", &logo)) && logo && *logo) + return (const wchar_t*)logo; + logo.Clear(); + if (SUCCEEDED(app->GetStringValue(L"SmallLogo", &logo)) && logo && *logo) // the name in Windows 8 manifests + return (const wchar_t*)logo; + break; + } + return {}; +} + CString GetPackageFullName(const wchar_t* appId) { CString packageName = GetInstalledPackageFullName(appId); diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h index bb12c5464..f0b8d962f 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.h +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.h @@ -59,3 +59,6 @@ CString GetPackageFullName(const wchar_t* appId); // Returns the folder where the package is installed CString GetPackageInstallPath(const wchar_t* packageFullName); + +// Returns the small logo of the app, from the manifest of the package installed now +CString GetAppLogoName(const wchar_t* appId); From 1a61bbf113533797323c342412844e1e4350f1e1 Mon Sep 17 00:00:00 2001 From: Emilio Heredia Date: Wed, 30 Sep 2026 12:55:51 -0600 Subject: [PATCH 5/5] Split the Store app package helpers into small functions GetInstalledPackageFullName and GetAppLogoName each did several things in 30-40 lines: splitting the appid, asking Windows for the family's packages, picking the main one; opening the manifest, finding the app, reading its logo. Each step is now its own function (SplitAppId, IsMainPackage, ReadManifestApplications, GetManifestLogo), and the appid is split in one place instead of two. The comments say what each returns when it finds nothing. No change in behaviour: the functions, compiled on their own from this file, return the same packages, folders and logo names as before for Microsoft Store, To Do, Terminal and Teams, and nothing for an unknown package, a desktop program, or an unknown app in a known package. --- .../StartMenuDLL/MetroLinkManager.cpp | 102 +++++++++++------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp index e6e2a1669..102616448 100644 --- a/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp +++ b/Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp @@ -377,16 +377,41 @@ bool IsEdgeDefaultBrowser( void ) return false; } -// The package of the app as installed now, found from the package family name in the appid. The full name contains the version, so it -// changes with every update, while the appid does not. A shortcut to a Metro app keeps the full name of the package it was created with, -// which is gone after an update. And with such a shortcut in the Start Menu, the Apps folder item for the appid is the shortcut, without a package +// Splits an appid (PackageFamilyName!AppName) into its two parts. Returns false if it is not one +static bool SplitAppId(const wchar_t* appId, CString& family, CString& appName) +{ + const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; + if (!bang) + return false; + family.SetString(appId, (int)(bang - appId)); + appName = bang + 1; + return true; +} + +// A package family can also list a bundle and resource packages; the app is in the main package, the one +// without a resource id. A full name is Name_Version_Architecture_ResourceId_PublisherId, and no part contains '_' +static bool IsMainPackage(const wchar_t* fullName) +{ + const wchar_t* resourceId = fullName; + for (int part = 0; part < 3 && resourceId; part++) + { + resourceId = wcschr(resourceId, '_'); + if (resourceId) resourceId++; + } + return resourceId && *resourceId == '_'; +} + +// The full name of the app's package as installed now, found from the package family name in the appid. +// The full name contains the version, so it changes with every update, while the appid does not. A shortcut to a +// Metro app keeps the full name of the package it was created with, which is gone after an update. And with such a +// shortcut in the Start Menu, the Apps folder item for the appid is the shortcut, without a package. +// Returns an empty string if the app is not installed, or before Windows 8 static CString GetInstalledPackageFullName(const wchar_t* appId) { static auto pGetPackagesByPackageFamily = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagesByPackageFamily")); - const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; - if (!pGetPackagesByPackageFamily || !bang) + CString family, appName; + if (!pGetPackagesByPackageFamily || !SplitAppId(appId, family, appName)) return {}; - CString family(appId, (int)(bang - appId)); UINT32 count = 0, bufferLength = 0; if (pGetPackagesByPackageFamily(family, &count, nullptr, &bufferLength, nullptr) != ERROR_INSUFFICIENT_BUFFER || count == 0) @@ -396,23 +421,15 @@ static CString GetInstalledPackageFullName(const wchar_t* appId) if (pGetPackagesByPackageFamily(family, &count, names.data(), &bufferLength, buffer.data()) != ERROR_SUCCESS) return {}; - // the family can also list a bundle and resource packages. The app is in the main package, the one without a resource id. - // A full name is Name_Version_Architecture_ResourceId_PublisherId, and no part contains '_' - for (UINT32 i = 0; i < count; i++) + for (PWSTR name : names) { - const wchar_t* resourceId = names[i]; - for (int part = 0; part < 3 && resourceId; part++) - { - resourceId = wcschr(resourceId, '_'); - if (resourceId) resourceId++; - } - if (resourceId && *resourceId == '_') - return names[i]; + if (IsMainPackage(name)) + return name; } return {}; } -// Returns the folder where the package is installed +// Returns the folder where the package is installed, or an empty string CString GetPackageInstallPath(const wchar_t* packageFullName) { static auto pGetPackagePathByFullName = static_cast((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagePathByFullName")); @@ -425,23 +442,43 @@ CString GetPackageInstallPath(const wchar_t* packageFullName) return path; } -// Returns the small logo of the app (a resource name like Assets\AppList.png), from the manifest of the package installed now -CString GetAppLogoName(const wchar_t* appId) +// Opens the list of applications in the manifest of an installed package. Returns null if the manifest can't be read +static CComPtr ReadManifestApplications(const wchar_t* packagePath) { - const wchar_t* bang = appId ? wcschr(appId, '!') : nullptr; - if (!bang) - return {}; - CString path = GetPackageInstallPath(GetInstalledPackageFullName(appId)); - if (path.IsEmpty()) - return {}; + CString path = packagePath; path += L"\\AppxManifest.xml"; - CComPtr factory; CComPtr stream; CComPtr reader; CComPtr apps; if (FAILED(factory.CoCreateInstance(__uuidof(AppxFactory))) || FAILED(SHCreateStreamOnFileEx(path, STGM_READ | STGM_SHARE_DENY_NONE, 0, FALSE, nullptr, &stream)) || FAILED(factory->CreateManifestReader(stream, &reader)) || FAILED(reader->GetApplications(&apps))) + return nullptr; + return apps; +} + +// The small logo of a manifest application: Square44x44Logo, or SmallLogo in Windows 8 manifests +static CString GetManifestLogo(IAppxManifestApplication* app) +{ + for (const wchar_t* attribute : {L"Square44x44Logo", L"SmallLogo"}) + { + CComString logo; + if (SUCCEEDED(app->GetStringValue(attribute, &logo)) && logo && *logo) + return (const wchar_t*)logo; + } + return {}; +} + +// Returns the small logo of the app (a resource name like Assets\AppList.png), from the manifest of the package +// installed now, or an empty string +CString GetAppLogoName(const wchar_t* appId) +{ + CString family, appName; + if (!SplitAppId(appId, family, appName)) + return {}; + CString packagePath = GetPackageInstallPath(GetInstalledPackageFullName(appId)); + CComPtr apps = packagePath.IsEmpty() ? nullptr : ReadManifestApplications(packagePath); + if (!apps) return {}; BOOL bHasCurrent = FALSE; @@ -449,15 +486,8 @@ CString GetAppLogoName(const wchar_t* appId) { CComPtr app; CComString id; - if (FAILED(apps->GetCurrent(&app)) || FAILED(app->GetStringValue(L"Id", &id)) || _wcsicmp(id, bang + 1) != 0) - continue; - CComString logo; - if (SUCCEEDED(app->GetStringValue(L"Square44x44Logo", &logo)) && logo && *logo) - return (const wchar_t*)logo; - logo.Clear(); - if (SUCCEEDED(app->GetStringValue(L"SmallLogo", &logo)) && logo && *logo) // the name in Windows 8 manifests - return (const wchar_t*)logo; - break; + if (SUCCEEDED(apps->GetCurrent(&app)) && SUCCEEDED(app->GetStringValue(L"Id", &id)) && _wcsicmp(id, appName) == 0) + return GetManifestLogo(app); } return {}; }