Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 83 additions & 8 deletions Src/StartMenu/StartMenuDLL/ItemManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<IShellItem> &target, CComPtr<IPropertyStore> &targetStore )
{
CComPtr<IShellItem> 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<IShellLink> pLink;
CAbsolutePidl targetPidl;
CComPtr<IShellItem> pTarget;
CComPtr<IPropertyStore> 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 )
{
Expand Down Expand Up @@ -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<IShellItem> target;
CComPtr<IPropertyStore> store;
if (pStore && GetMetroShortcutTarget(pStore,target,store))
{
pItem=std::move(target);
pStore=store;
}
}
if (pStore)
{
Expand All @@ -1957,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};
Expand Down Expand Up @@ -2473,6 +2535,18 @@ 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<IPropertyStore> 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 appid=GetPropertyStoreString(pStore,PKEY_AppUserModel_ID);
CString savedPackageName=GetPropertyStoreString(pStore,PKEY_MetroPackageName);
CString packageName=GetPackageFullName(appid);
if (packageName.IsEmpty())
packageName=savedPackageName;
if (packageName.IsEmpty()) return;

unsigned int hash;
{
CComString pName;
Expand All @@ -2481,20 +2555,21 @@ 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;

// extract icon
HBITMAP hSmallBitmap=NULL, hLargeBitmap=NULL, hExtraLargeBitmap=NULL;

CComPtr<IPropertyStore> 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);
// 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<IResourceManager> pResManager;
Expand Down
123 changes: 123 additions & 0 deletions Src/StartMenu/StartMenuDLL/MetroLinkManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
#include "ResourceHelper.h"
#include "Translations.h"
#include <propkey.h>
#include <appmodel.h>
#include <appxpackaging.h>
#include <shlwapi.h>
#include <map>
#include <vector>

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
Expand Down Expand Up @@ -373,8 +377,127 @@ bool IsEdgeDefaultBrowser( void )
return false;
}

// 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<decltype(&GetPackagesByPackageFamily)>((void*)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetPackagesByPackageFamily"));
CString family, appName;
if (!pGetPackagesByPackageFamily || !SplitAppId(appId, family, appName))
return {};

UINT32 count = 0, bufferLength = 0;
if (pGetPackagesByPackageFamily(family, &count, nullptr, &bufferLength, nullptr) != ERROR_INSUFFICIENT_BUFFER || count == 0)
return {};
std::vector<PWSTR> names(count);
std::vector<wchar_t> buffer(bufferLength);
if (pGetPackagesByPackageFamily(family, &count, names.data(), &bufferLength, buffer.data()) != ERROR_SUCCESS)
return {};

for (PWSTR name : names)
{
if (IsMainPackage(name))
return name;
}
return {};
}

// Returns the folder where the package is installed, or an empty string
CString GetPackageInstallPath(const wchar_t* packageFullName)
{
static auto pGetPackagePathByFullName = static_cast<decltype(&GetPackagePathByFullName)>((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;
}

// Opens the list of applications in the manifest of an installed package. Returns null if the manifest can't be read
static CComPtr<IAppxManifestApplicationsEnumerator> ReadManifestApplications(const wchar_t* packagePath)
{
CString path = packagePath;
path += L"\\AppxManifest.xml";
CComPtr<IAppxFactory> factory;
CComPtr<IStream> stream;
CComPtr<IAppxManifestReader> reader;
CComPtr<IAppxManifestApplicationsEnumerator> 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<IAppxManifestApplicationsEnumerator> apps = packagePath.IsEmpty() ? nullptr : ReadManifestApplications(packagePath);
if (!apps)
return {};

BOOL bHasCurrent = FALSE;
for (; SUCCEEDED(apps->GetHasCurrent(&bHasCurrent)) && bHasCurrent; apps->MoveNext(&bHasCurrent))
{
CComPtr<IAppxManifestApplication> app;
CComString id;
if (SUCCEEDED(apps->GetCurrent(&app)) && SUCCEEDED(app->GetStringValue(L"Id", &id)) && _wcsicmp(id, appName) == 0)
return GetManifestLogo(app);
}
return {};
}

CString GetPackageFullName(const wchar_t* appId)
{
CString packageName = GetInstalledPackageFullName(appId);
if (!packageName.IsEmpty())
return packageName;

CComPtr<IShellItem> item;
if (SUCCEEDED(SHCreateItemInKnownFolder(FOLDERID_AppsFolder, 0, appId, IID_PPV_ARGS(&item))))
{
Expand Down
8 changes: 7 additions & 1 deletion Src/StartMenu/StartMenuDLL/MetroLinkManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,11 @@ CComPtr<IContextMenu> 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);

// Returns the small logo of the app, from the manifest of the package installed now
CString GetAppLogoName(const wchar_t* appId);
5 changes: 4 additions & 1 deletion Src/StartMenu/StartMenuDLL/SearchManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<m_LastProgramsRequestId)
continue;
Expand Down