Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 1d35ccd

Browse files
committed
Bug 1687562 - Part 1: Implement Taskbar pinning. r=mhowell
Differential Revision: https://phabricator.services.mozilla.com/D104779
1 parent 826df95 commit 1d35ccd

4 files changed

Lines changed: 309 additions & 1 deletion

File tree

browser/components/shell/moz.build

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ elif CONFIG["OS_ARCH"] == "WINNT":
5656
LOCAL_INCLUDES += [
5757
"../../../other-licenses/nsis/Contrib/CityHash/cityhash",
5858
]
59+
OS_LIBS += [
60+
"propsys",
61+
]
5962

6063
XPIDL_MODULE = "shellservice"
6164

@@ -68,7 +71,7 @@ EXTRA_JS_MODULES += [
6871
"ShellService.jsm",
6972
]
7073

71-
for var in ("MOZ_APP_NAME", "MOZ_APP_VERSION"):
74+
for var in ("MOZ_APP_DISPLAYNAME", "MOZ_APP_NAME", "MOZ_APP_VERSION"):
7275
DEFINES[var] = '"%s"' % CONFIG[var]
7376

7477
CXXFLAGS += CONFIG["TK_CFLAGS"]

browser/components/shell/nsIWindowsShellService.idl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,47 @@ interface nsIWindowsShellService : nsISupports
1212
void createShortcut(in nsIFile aBinary, in Array<AString> aArguments,
1313
in AString aDescription, in nsIFile aIconFile, in AString aAppUserModelId,
1414
in nsIFile aTarget);
15+
16+
/*
17+
* Pin the current app to the taskbar
18+
*
19+
* This MUST only be used in response to an active request from the user.
20+
*
21+
* Uses an existing shortcut on the Desktop or Start Menu, which would have
22+
* been created by the installer (for All Users or Current User), in order
23+
* to ensure that the pin is associated with this executable and AUMID for
24+
* proper launching and grouping.
25+
*
26+
* NOTE: This method probably shouldn't be used on the main thread, it
27+
* performs blocking disk I/O.
28+
*
29+
* NOTE: It is possible for the shortcut match to fail even when a
30+
* shortcut refers to the current executable, if the paths differ due
31+
* to e.g. symlinks. This should be rare.
32+
*
33+
* This will definitely fail on an OS before Windows 10 build 1809
34+
* (October 2018 Update).
35+
*
36+
* @throws NS_ERROR_NOT_AVAILABLE
37+
* if OS is not at least Windows 10 build 1809, or if creating the
38+
* Taskband Pin object fails
39+
* @throws NS_ERROR_FILE_NOT_FOUND
40+
* if a shortcut matching this app's AUMID and exe path wasn't found
41+
* @throws NS_ERROR_FAILURE
42+
* for unexpected errors
43+
*/
44+
void pinCurrentAppToTaskbar();
45+
46+
/*
47+
* Do a dry run of pinCurrentAppToTaskbar()
48+
*
49+
* This does all the same checks and setup, throws the same errors, but doesn't
50+
* do the final step of creating the pin.
51+
*
52+
* NOTE: This method probably shouldn't be used on the main thread, it
53+
* performs blocking disk I/O.
54+
*
55+
* @throws same as pinCurrentAppToTaskbar()
56+
*/
57+
void checkPinCurrentAppToTaskbar();
1558
};

browser/components/shell/nsWindowsShellService.cpp

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,16 @@
3535
#include <propvarutil.h>
3636
#include <propkey.h>
3737

38+
#ifdef __MINGW32__
39+
// MinGW-w64 headers are missing PropVariantToString.
40+
# include <propsys.h>
41+
PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
42+
#endif
43+
44+
#include <objbase.h>
3845
#include <shlobj.h>
3946
#include "WinUtils.h"
47+
#include "mozilla/widget/WinTaskbar.h"
4048

4149
#include <mbstring.h>
4250

@@ -621,6 +629,259 @@ nsWindowsShellService::CreateShortcut(nsIFile* aBinary,
621629
return NS_OK;
622630
}
623631

632+
// Constructs a path to an installer-created shortcut, under a directory
633+
// specified by a CSIDL.
634+
static nsresult GetShortcutPath(int aCSIDL, /* out */ nsAutoString& aPath) {
635+
wchar_t folderPath[MAX_PATH] = {};
636+
HRESULT hr = SHGetFolderPathW(nullptr, aCSIDL, nullptr, SHGFP_TYPE_CURRENT,
637+
folderPath);
638+
if (NS_WARN_IF(FAILED(hr))) {
639+
return NS_ERROR_FAILURE;
640+
}
641+
642+
aPath.Assign(folderPath);
643+
if (NS_WARN_IF(aPath.IsEmpty())) {
644+
return NS_ERROR_FAILURE;
645+
}
646+
if (aPath[aPath.Length() - 1] != '\\') {
647+
aPath.AppendLiteral("\\");
648+
}
649+
// NOTE: In the installer, shortcuts are named "${BrandShortName}.lnk".
650+
// This is set from MOZ_APP_DISPLAYNAME in defines.nsi.in. (Except in dev
651+
// edition where it's explicitly set to "Firefox Developer Edition" in
652+
// branding.nsi, which matches MOZ_APP_DISPLAYNAME in aurora/configure.sh.)
653+
//
654+
// If this changes, we could expand this to check shortcuts_log.ini,
655+
// which records the name of the shortcuts as created by the installer.
656+
aPath.AppendLiteral(MOZ_APP_DISPLAYNAME ".lnk");
657+
658+
return NS_OK;
659+
}
660+
661+
// Check if the instaler-created shortcut in the given location matches,
662+
// if so output its path
663+
//
664+
// NOTE: DO NOT USE if a false negative (mismatch) is unacceptable.
665+
// aExePath is compared directly to the path retrieved from the shortcut.
666+
// Due to the presence of symlinks or other filesystem issues, it's possible
667+
// for different paths to refer to the same file, which would cause the check
668+
// to fail.
669+
// This should rarely be an issue as we are most likely to be run from a path
670+
// written by the installer (shortcut, association, launch from installer),
671+
// which also wrote the shortcuts. But it is possible.
672+
//
673+
// aCSIDL the CSIDL of the directory containing the shortcut to check.
674+
// aAUMID the AUMID to check for
675+
// aExePath the target exe path to check for, should be a long path where
676+
// possible
677+
// aShortcutPath outparam, set to matching shortcut path if NS_OK is returned.
678+
//
679+
// Returns
680+
// NS_ERROR_FAILURE on errors before the shortcut was loaded
681+
// NS_ERROR_FILE_NOT_FOUND if the shortcut doesn't exist
682+
// NS_ERROR_FILE_ALREADY_EXISTS if the shortcut exists but doesn't match the
683+
// current app
684+
// NS_OK if the shortcut matches
685+
static nsresult GetMatchingShortcut(int aCSIDL, const nsAutoString& aAUMID,
686+
const wchar_t aExePath[MAXPATHLEN],
687+
/* out */ nsAutoString& aShortcutPath) {
688+
nsresult result = NS_ERROR_FAILURE;
689+
690+
nsAutoString path;
691+
nsresult rv = GetShortcutPath(aCSIDL, path);
692+
if (NS_WARN_IF(NS_FAILED(rv))) {
693+
return result;
694+
}
695+
696+
// Create a shell link object for loading the shortcut
697+
RefPtr<IShellLinkW> link;
698+
HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
699+
IID_IShellLinkW, getter_AddRefs(link));
700+
if (NS_WARN_IF(FAILED(hr))) {
701+
return result;
702+
}
703+
704+
// Load
705+
RefPtr<IPersistFile> persist;
706+
hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(persist));
707+
if (NS_WARN_IF(FAILED(hr))) {
708+
return result;
709+
}
710+
711+
hr = persist->Load(path.get(), STGM_READ);
712+
if (FAILED(hr)) {
713+
if (NS_WARN_IF(hr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))) {
714+
// empty branch, result unchanged but warning issued
715+
} else {
716+
result = NS_ERROR_FILE_NOT_FOUND;
717+
}
718+
return result;
719+
}
720+
result = NS_ERROR_FILE_ALREADY_EXISTS;
721+
722+
// Check the AUMID
723+
RefPtr<IPropertyStore> propStore;
724+
hr = link->QueryInterface(IID_IPropertyStore, getter_AddRefs(propStore));
725+
if (NS_WARN_IF(FAILED(hr))) {
726+
return result;
727+
}
728+
729+
PROPVARIANT pv;
730+
hr = propStore->GetValue(PKEY_AppUserModel_ID, &pv);
731+
if (NS_WARN_IF(FAILED(hr))) {
732+
return result;
733+
}
734+
735+
wchar_t storedAUMID[MAX_PATH];
736+
hr = PropVariantToString(pv, storedAUMID, MAX_PATH);
737+
PropVariantClear(&pv);
738+
if (NS_WARN_IF(FAILED(hr))) {
739+
return result;
740+
}
741+
742+
if (!aAUMID.Equals(storedAUMID)) {
743+
return result;
744+
}
745+
746+
// Check the exe path
747+
static_assert(MAXPATHLEN == MAX_PATH);
748+
wchar_t storedExePath[MAX_PATH] = {};
749+
// With no flags GetPath gets a long path
750+
hr = link->GetPath(storedExePath, ArrayLength(storedExePath), nullptr, 0);
751+
if (FAILED(hr) || hr == S_FALSE) {
752+
return result;
753+
}
754+
// Case insensitive path comparison
755+
if (wcsnicmp(storedExePath, aExePath, MAXPATHLEN) != 0) {
756+
return result;
757+
}
758+
759+
// Success, report the shortcut path
760+
aShortcutPath.Assign(path);
761+
result = NS_OK;
762+
763+
return result;
764+
}
765+
766+
static nsresult PinCurrentAppToTaskbarImpl(bool aCheckOnly) {
767+
// This enum is likely only used for Windows telemetry, INT_MAX is chosen to
768+
// avoid confusion with existing uses.
769+
enum PINNEDLISTMODIFYCALLER { PLMC_INT_MAX = INT_MAX };
770+
771+
// The types below, and the idea of using IPinnedList3::Modify,
772+
// are thanks to Gee Law <https://geelaw.blog/entries/msedge-pins/>
773+
static constexpr GUID CLSID_TaskbandPin = {
774+
0x90aa3a4e,
775+
0x1cba,
776+
0x4233,
777+
{0xb8, 0xbb, 0x53, 0x57, 0x73, 0xd4, 0x84, 0x49}};
778+
779+
static constexpr GUID IID_IPinnedList3 = {
780+
0x0dd79ae2,
781+
0xd156,
782+
0x45d4,
783+
{0x9e, 0xeb, 0x3b, 0x54, 0x97, 0x69, 0xe9, 0x40}};
784+
785+
struct IPinnedList3Vtbl;
786+
struct IPinnedList3 {
787+
IPinnedList3Vtbl* vtbl;
788+
};
789+
790+
typedef ULONG STDMETHODCALLTYPE ReleaseFunc(IPinnedList3 * that);
791+
typedef HRESULT STDMETHODCALLTYPE ModifyFunc(
792+
IPinnedList3 * that, PCIDLIST_ABSOLUTE unpin, PCIDLIST_ABSOLUTE pin,
793+
PINNEDLISTMODIFYCALLER caller);
794+
795+
struct IPinnedList3Vtbl {
796+
void* QueryInterface; // 0
797+
void* AddRef; // 1
798+
ReleaseFunc* Release; // 2
799+
void* Other[13]; // 3-15
800+
ModifyFunc* Modify; // 16
801+
};
802+
803+
struct ILFreeDeleter {
804+
void operator()(LPITEMIDLIST aPtr) {
805+
if (aPtr) {
806+
ILFree(aPtr);
807+
}
808+
}
809+
};
810+
811+
// First available on 1809
812+
if (!IsWin10Sep2018UpdateOrLater()) {
813+
return NS_ERROR_NOT_AVAILABLE;
814+
}
815+
816+
nsAutoString aumid;
817+
if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GetAppUserModelID(aumid))) {
818+
return NS_ERROR_FAILURE;
819+
}
820+
821+
wchar_t exePath[MAXPATHLEN] = {};
822+
if (NS_WARN_IF(NS_FAILED(BinaryPath::GetLong(exePath)))) {
823+
return NS_ERROR_FAILURE;
824+
}
825+
826+
// Try to find a shortcut matching the running app
827+
nsAutoString shortcutPath;
828+
int shortcutCSIDLs[] = {CSIDL_COMMON_PROGRAMS, CSIDL_PROGRAMS,
829+
CSIDL_COMMON_DESKTOPDIRECTORY,
830+
CSIDL_DESKTOPDIRECTORY};
831+
for (int i = 0; i < ArrayLength(shortcutCSIDLs); ++i) {
832+
// GetMatchingShortcut may fail when the exe path doesn't match, even
833+
// if it refers to the same file. This should be rare, and the worst
834+
// outcome would be failure to pin, so the risk is acceptable.
835+
nsresult rv =
836+
GetMatchingShortcut(shortcutCSIDLs[i], aumid, exePath, shortcutPath);
837+
if (NS_SUCCEEDED(rv)) {
838+
break;
839+
} else {
840+
shortcutPath.Truncate();
841+
}
842+
}
843+
if (shortcutPath.IsEmpty()) {
844+
return NS_ERROR_FILE_NOT_FOUND;
845+
}
846+
847+
mozilla::UniquePtr<__unaligned ITEMIDLIST, ILFreeDeleter> path(
848+
ILCreateFromPathW(shortcutPath.get()));
849+
if (NS_WARN_IF(!path)) {
850+
return NS_ERROR_FAILURE;
851+
}
852+
853+
IPinnedList3* pinnedList = nullptr;
854+
HRESULT hr =
855+
CoCreateInstance(CLSID_TaskbandPin, nullptr, CLSCTX_INPROC_SERVER,
856+
IID_IPinnedList3, (void**)&pinnedList);
857+
if (FAILED(hr) || !pinnedList) {
858+
return NS_ERROR_NOT_AVAILABLE;
859+
}
860+
861+
if (!aCheckOnly) {
862+
hr =
863+
pinnedList->vtbl->Modify(pinnedList, nullptr, path.get(), PLMC_INT_MAX);
864+
}
865+
866+
pinnedList->vtbl->Release(pinnedList);
867+
868+
if (FAILED(hr)) {
869+
return NS_ERROR_FAILURE;
870+
} else {
871+
return NS_OK;
872+
}
873+
}
874+
875+
NS_IMETHODIMP
876+
nsWindowsShellService::PinCurrentAppToTaskbar() {
877+
return PinCurrentAppToTaskbarImpl(/* aCheckOnly */ false);
878+
}
879+
880+
NS_IMETHODIMP
881+
nsWindowsShellService::CheckPinCurrentAppToTaskbar() {
882+
return PinCurrentAppToTaskbarImpl(/* aCheckOnly */ true);
883+
}
884+
624885
nsWindowsShellService::nsWindowsShellService() {}
625886

626887
nsWindowsShellService::~nsWindowsShellService() {}

widget/windows/moz.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ EXPORTS.mozilla.widget += [
5353
"WindowsSMTCProvider.h",
5454
"WinMessages.h",
5555
"WinModifierKeyState.h",
56+
"WinTaskbar.h",
5657
]
5758

5859
UNIFIED_SOURCES += [

0 commit comments

Comments
 (0)