From 5fe3b856dbec41aba23068adc49c9627e95b5abc Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 09:52:51 +0200 Subject: [PATCH 01/10] add windows field for AppUserModelID --- menuinst/_schema.py | 10 ++++++++++ menuinst/data/menuinst.default.json | 3 ++- menuinst/data/menuinst.schema.json | 6 ++++++ menuinst/platforms/win.py | 9 ++++++++- src/winshortcut.cpp | 29 +++++++++++++++++++++++++++-- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/menuinst/_schema.py b/menuinst/_schema.py index 4239df97..522c6ef6 100644 --- a/menuinst/_schema.py +++ b/menuinst/_schema.py @@ -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.``. + + See `AppUserModelID 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): diff --git a/menuinst/data/menuinst.default.json b/menuinst/data/menuinst.default.json index 586546a1..b220ad39 100644 --- a/menuinst/data/menuinst.default.json +++ b/menuinst/data/menuinst.default.json @@ -57,7 +57,8 @@ "desktop": true, "quicklaunch": true, "url_protocols": null, - "file_extensions": null + "file_extensions": null, + "app_user_model_id": null } } } diff --git a/menuinst/data/menuinst.schema.json b/menuinst/data/menuinst.schema.json index 85476aed..b76b8ba7 100644 --- a/menuinst/data/menuinst.schema.json +++ b/menuinst/data/menuinst.schema.json @@ -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 diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index cb5612e0..53bdc2c6 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -152,7 +152,7 @@ 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=""), @@ -160,6 +160,7 @@ def create(self) -> Tuple[Path, ...]: " ".join(arguments), working_dir, icon, + self._app_user_model_id(), ) self._register_file_extensions() @@ -389,3 +390,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 diff --git a/src/winshortcut.cpp b/src/winshortcut.cpp index 72f00fdf..e375c615 100644 --- a/src/winshortcut.cpp +++ b/src/winshortcut.cpp @@ -38,10 +38,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); @@ -51,9 +54,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; } @@ -116,6 +119,25 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) } } + if (app_id) { + hres = shellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore)); + if (FAILED(hres)) { + PyErr_Format(PyExc_OSError, + "QueryInterface(IPropertyStore) error 0x%x", hres); + goto error; + } + hr = InitPropVariantFromString(U8toWs(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)); @@ -145,6 +167,9 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) if (pShellLink) { pShellLink->Release(); } + if (pPropertyStore) { + pPropertyStore->Release(); + } CoUninitialize(); return NULL; From 9dd99c76406b173b2a307b2cf0fd298dbb1398d7 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 09:54:38 +0200 Subject: [PATCH 02/10] pre-commit --- src/winshortcut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/winshortcut.cpp b/src/winshortcut.cpp index e375c615..57b031fc 100644 --- a/src/winshortcut.cpp +++ b/src/winshortcut.cpp @@ -120,7 +120,7 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) } if (app_id) { - hres = shellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore)); + hres = shellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore)); if (FAILED(hres)) { PyErr_Format(PyExc_OSError, "QueryInterface(IPropertyStore) error 0x%x", hres); From 6f274d96428e76d9c7cbce3abb7fa2398538713c Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:00:45 +0200 Subject: [PATCH 03/10] add missing headers? --- src/winshortcut.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/winshortcut.cpp b/src/winshortcut.cpp index 57b031fc..a4721ddd 100644 --- a/src/winshortcut.cpp +++ b/src/winshortcut.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include "resource.h" #include @@ -120,7 +122,7 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) } if (app_id) { - hres = shellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore)); + hres = pShellLink->QueryInterface(IID_PPV_ARGS(&pPropertyStore)); if (FAILED(hres)) { PyErr_Format(PyExc_OSError, "QueryInterface(IPropertyStore) error 0x%x", hres); From 1b2d18b6ab6f29bafc7a42a8aae9615201c374ec Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:01:25 +0200 Subject: [PATCH 04/10] hr -> hres --- src/winshortcut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/winshortcut.cpp b/src/winshortcut.cpp index a4721ddd..b4ef24bb 100644 --- a/src/winshortcut.cpp +++ b/src/winshortcut.cpp @@ -128,7 +128,7 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) "QueryInterface(IPropertyStore) error 0x%x", hres); goto error; } - hr = InitPropVariantFromString(U8toWs(app_id), &pv); + hres = InitPropVariantFromString(U8toWs(app_id), &pv); if (FAILED(hres)) { PyErr_Format(PyExc_OSError, "InitPropVariantFromString() error 0x%x", hres); From dcf8ed5583fc887f48c37218378b3ffc9425b9f0 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:05:13 +0200 Subject: [PATCH 05/10] do not use U8toWs --- src/winshortcut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/winshortcut.cpp b/src/winshortcut.cpp index b4ef24bb..0be6e136 100644 --- a/src/winshortcut.cpp +++ b/src/winshortcut.cpp @@ -128,7 +128,7 @@ static PyObject *CreateShortcut(PyObject *self, PyObject *args) "QueryInterface(IPropertyStore) error 0x%x", hres); goto error; } - hres = InitPropVariantFromString(U8toWs(app_id), &pv); + hres = InitPropVariantFromString(app_id, &pv); if (FAILED(hres)) { PyErr_Format(PyExc_OSError, "InitPropVariantFromString() error 0x%x", hres); From 4f74b26290ed775a727a6a7eaa3d60eaca9b5107 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:13:04 +0200 Subject: [PATCH 06/10] missing arg --- menuinst/platforms/win.py | 1 + 1 file changed, 1 insertion(+) diff --git a/menuinst/platforms/win.py b/menuinst/platforms/win.py index 53bdc2c6..d3f88ff4 100644 --- a/menuinst/platforms/win.py +++ b/menuinst/platforms/win.py @@ -160,6 +160,7 @@ def create(self) -> Tuple[Path, ...]: " ".join(arguments), working_dir, icon, + 0, self._app_user_model_id(), ) From 5a1439f4fb97a25a49a6ba2e184240144d8c20f4 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:13:12 +0200 Subject: [PATCH 07/10] add tmate on failure --- .github/workflows/tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 05ea0d46..fdf70156 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,7 +80,12 @@ 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 + + - name: Setup tmate session + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 + timeout-minutes: 60 - uses: codecov/codecov-action@v1 with: From d3695f4f34206f58543f68d3f38f86200c26ac5c Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:13:59 +0200 Subject: [PATCH 08/10] pre-commit --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fdf70156..1c97596e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,7 +81,7 @@ jobs: name: Run test suite run: | python -I -m pytest tests/ --cov-append --cov-report=xml --cov=menuinst -vvv - + - name: Setup tmate session if: ${{ failure() }} uses: mxschmitt/action-tmate@v3 From 64f499a750d3d12271942a3e2229016da425858c Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:24:17 +0200 Subject: [PATCH 09/10] remove tmate --- .github/workflows/tests.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1c97596e..9ded1036 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,11 +82,6 @@ jobs: run: | python -I -m pytest tests/ --cov-append --cov-report=xml --cov=menuinst -vvv - - name: Setup tmate session - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3 - timeout-minutes: 60 - - uses: codecov/codecov-action@v1 with: name: ${{ matrix.os }}-py${{ matrix.python-version }} From 31bc4f7e0222947b6e133da724121825fd0446f5 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Mon, 19 Jun 2023 10:25:20 +0200 Subject: [PATCH 10/10] add news --- news/133-app-user-model-id | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 news/133-app-user-model-id diff --git a/news/133-app-user-model-id b/news/133-app-user-model-id new file mode 100644 index 00000000..5e3a7de4 --- /dev/null +++ b/news/133-app-user-model-id @@ -0,0 +1,19 @@ +### Enhancements + +* Add `app_user_model_id` field on Windows shortcuts to group taskbar icons together. (#127 via #133) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +*