Skip to content

Commit

Permalink
Set AppUserModelId on Windows shortcuts (#133)
Browse files Browse the repository at this point in the history
* add windows field for AppUserModelID

* pre-commit

* add missing headers?

* hr -> hres

* do not use U8toWs

* missing arg

* add tmate on failure

* pre-commit

* remove tmate

* add news
  • Loading branch information
jaimergp authored Jun 29, 2023
1 parent d374862 commit 93958fa
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
- shell: bash -el {0}
name: Run test suite
run: |
python -I -m pytest tests/ --cov-append --cov-report=xml --cov=menuinst
python -I -m pytest tests/ --cov-append --cov-report=xml --cov=menuinst -vvv
- uses: codecov/codecov-action@v1
with:
Expand Down
10 changes: 10 additions & 0 deletions menuinst/_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ class Windows(BasePlatformSpecific):
"URL protocols that will be associated with this program."
file_extensions: Optional[List[constr(regex=r"\.\S*")]] = None
"File extensions that will be associated with this program."
app_user_model_id: Optional[constr(regex=r"\S+\.\S+", max_length=128)] = None
"""
Identifier used in Windows 7 and above to associate processes, files and windows with a
particular application. If your shortcut produces duplicated icons, you need to define this
field. If not set, it will default to ``Menuinst.<name>``.
See `AppUserModelID docs <aumi-docs>`__ for more information on the required string format.
.. aumi-docs: https://learn.microsoft.com/en-us/windows/win32/shell/appids#how-to-form-an-application-defined-appusermodelid
"""


class Linux(BasePlatformSpecific):
Expand Down
3 changes: 2 additions & 1 deletion menuinst/data/menuinst.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"desktop": true,
"quicklaunch": true,
"url_protocols": null,
"file_extensions": null
"file_extensions": null,
"app_user_model_id": null
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions menuinst/data/menuinst.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@
"type": "string",
"pattern": "\\.\\S*"
}
},
"app_user_model_id": {
"title": "App User Model Id",
"maxLength": 128,
"pattern": "\\S+\\.\\S+",
"type": "string"
}
},
"additionalProperties": false
Expand Down
10 changes: 9 additions & 1 deletion menuinst/platforms/win.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,16 @@ def create(self) -> Tuple[Path, ...]:
# winshortcut is a windows-only C extension! create_shortcut has this API
# Notice args must be passed as positional, no keywords allowed!
# winshortcut.create_shortcut(path, description, filename, arguments="",
# workdir=None, iconpath=None, iconindex=0)
# workdir=None, iconpath=None, iconindex=0, app_id="")
create_shortcut(
target_path,
self._shortcut_filename(ext=""),
str(path),
" ".join(arguments),
working_dir,
icon,
0,
self._app_user_model_id(),
)

self._register_file_extensions()
Expand Down Expand Up @@ -389,3 +391,9 @@ def _unregister_url_protocols(self):
for protocol in protocols:
identifier = self._ftype_identifier(protocol)
unregister_url_protocol(protocol, identifier, mode=self.menu.mode)

def _app_user_model_id(self):
aumi = self.render_key("app_user_model_id")
if not aumi:
return f"Menuinst.{self.render_key('name', slug=True).replace('.', '')}"[:128]
return aumi
19 changes: 19 additions & 0 deletions news/133-app-user-model-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add `app_user_model_id` field on Windows shortcuts to group taskbar icons together. (#127 via #133)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
31 changes: 29 additions & 2 deletions src/winshortcut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include <objbase.h>
#include <shlobj.h>
#include <objidl.h>
#include <propvarutil.h>
#include <propkey.h>
#include "resource.h"

#include <stdio.h>
Expand All @@ -38,10 +40,13 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args)
Py_UNICODE *iconpath = NULL;
int iconindex = 0;
Py_UNICODE *workdir = NULL;
Py_UNICODE *app_id = NULL;

IShellLink *pShellLink = NULL;
IPersistFile *pPersistFile = NULL;
IPropertyStore *pPropertyStore = NULL;

PROPVARIANT pv;
HRESULT hres;

hres = CoInitialize(NULL);
Expand All @@ -51,9 +56,9 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args)
goto error;
}

if (!PyArg_ParseTuple(args, "uuu|uuui",
if (!PyArg_ParseTuple(args, "uuu|uuuiu",
&path, &description, &filename,
&arguments, &workdir, &iconpath, &iconindex)) {
&arguments, &workdir, &iconpath, &iconindex, &app_id)) {
return NULL;
}

Expand Down Expand Up @@ -116,6 +121,25 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args)
}
}

if (app_id) {
hres = pShellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore));
if (FAILED(hres)) {
PyErr_Format(PyExc_OSError,
"QueryInterface(IPropertyStore) error 0x%x", hres);
goto error;
}
hres = InitPropVariantFromString(app_id, &pv);
if (FAILED(hres)) {
PyErr_Format(PyExc_OSError,
"InitPropVariantFromString() error 0x%x", hres);
goto error;
}
pPropertyStore->SetValue(PKEY_AppUserModel_ID, pv);
pPropertyStore->Commit();
PropVariantClear(&pv);
pPropertyStore->Release();
}

hres = pPersistFile->Save(filename, TRUE);
if (FAILED(hres)) {
PyObject *fn = PyUnicode_FromWideChar(filename, wcslen(filename));
Expand Down Expand Up @@ -145,6 +169,9 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args)
if (pShellLink) {
pShellLink->Release();
}
if (pPropertyStore) {
pPropertyStore->Release();
}

CoUninitialize();
return NULL;
Expand Down

0 comments on commit 93958fa

Please sign in to comment.