Skip to content

Commit 2ae0e30

Browse files
committed
feat: enforce default profile in QGIS Project file association in Windows registry
1 parent 87ee129 commit 2ae0e30

File tree

6 files changed

+293
-23
lines changed

6 files changed

+293
-23
lines changed

docs/jobs/default_profile_setter.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ Sample job configuration in your scenario file:
1616
force_profile_selection_policy: true
1717
```
1818
19+
On Windows, if you want to ensure that double-clicking on QGIS Project files will launch QGIS with the chosen default profile and disable version checking:
20+
21+
```yaml
22+
- name: Set default profile to conf_qgis_fr
23+
uses: default-profile-setter
24+
with:
25+
profile: conf_qgis_fr
26+
force_profile_selection_policy: true
27+
force_profile_file_association: true
28+
profile_file_association_arguments: "--noversioncheck"
29+
```
30+
1931
----
2032
2133
## Options
@@ -29,6 +41,22 @@ Name of the profile to set as default profile.
2941
Force the key `selectionPolicy` to 1, which will always open the profile defined in the `defaultProfile` key in `profiles.ini` file. In this context, this job will force QGIS to always start with the profile specified in this job.
3042
It's the same behavior as [the option _Always use profile_ in QGIS user profiles preferences](https://docs.qgis.org/latest/en/docs/user_manual/introduction/qgis_configuration.html#setting-user-profile).
3143

44+
### force_profile_file_association
45+
46+
:::{note}
47+
Windows-only feature
48+
:::
49+
50+
Modify the Windows registry to ensure that QGIS Project files always open with the default profile when launched.
51+
52+
### profile_file_association_arguments
53+
54+
:::{note}
55+
Windows-only feature
56+
:::
57+
58+
Arguments to pass to QGIS executable.
59+
3260
----
3361

3462
## Schema

docs/schemas/scenario/jobs/default-profile-setter.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
"description": "Force the profile selection policy to 1 (always open default profile).",
1414
"type": "boolean",
1515
"default": false
16+
},
17+
"force_profile_file_association": {
18+
"description": "Force the QGIS project file association to starts QGIS with the default profile.",
19+
"type": "boolean",
20+
"default": false
21+
},
22+
"profile_file_association_arguments": {
23+
"description": "Arguments to pass to QGIS executable when force_profile_file_association is true.",
24+
"type": "string",
25+
"example": "--noversioncheck"
26+
},
27+
"force_registry_key_creation": {
28+
"description": "Force the creation of the registry key for the default profile. This is useful when QGIS is not installed yet.",
29+
"type": "boolean",
30+
"default": false
1631
}
17-
}
32+
},
33+
"required": [
34+
"profile"
35+
]
1836
}

qgis_deployment_toolbelt/jobs/job_default_profile_setter.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
# Standard library
1515
import logging
16+
from sys import platform as opersys
1617

1718
# package
1819
from qgis_deployment_toolbelt.jobs.generic_job import GenericJob
1920
from qgis_deployment_toolbelt.utils.ini_parser_with_path import CustomConfigParser
21+
from qgis_deployment_toolbelt.utils.win32utils import set_qgis_command
2022

2123
# #############################################################################
2224
# ########## Globals ###############
@@ -52,6 +54,29 @@ class JobDefaultProfileSetter(GenericJob):
5254
"possible_values": None,
5355
"condition": None,
5456
},
57+
"force_profile_file_association": {
58+
"type": bool,
59+
"required": False,
60+
"default": False,
61+
"possible_values": None,
62+
"condition": None,
63+
},
64+
"profile_file_association_arguments": {
65+
"type": str,
66+
"required": False,
67+
"default": None,
68+
"possible_values": None,
69+
"condition": lambda options: options.get("force_profile_file_association")
70+
is True,
71+
},
72+
"force_registry_key_creation": {
73+
"type": bool,
74+
"required": False,
75+
"default": False,
76+
"possible_values": None,
77+
"condition": lambda options: options.get("force_profile_file_association")
78+
is True,
79+
},
5580
}
5681

5782
def __init__(self, options: dict) -> None:
@@ -81,32 +106,61 @@ def run(self) -> None:
81106
logger.error("No QGIS profile matching the provided profile name.")
82107
return
83108

84-
ini_profiles_path = self.qgis_profiles_path / "profiles.ini"
109+
self.ini_profiles_path = self.qgis_profiles_path / "profiles.ini"
85110

86111
# check if the profiles.ini file exists and create it with default profile set
87112
# if not
88-
if not ini_profiles_path.exists():
89-
logger.warning(
90-
"Configuration file profiles.ini doesn't exist. "
91-
"It will be created but maybe it was not the expected behavior."
113+
if not self.ini_profiles_path.exists():
114+
self._create_ini_profiles()
115+
else:
116+
self._alter_ini_profiles()
117+
118+
if self.options.get("force_profile_file_association"):
119+
qgis_cmd = (
120+
f'"{self.os_config.get_qgis_bin_path}" --profile "{qdt_profile.name}"'
92121
)
93-
ini_profiles_path.touch(exist_ok=True)
122+
if self.options.get("profile_file_association_arguments"):
123+
qgis_cmd += f' {self.options.get("profile_file_association_arguments")}'
124+
qgis_cmd += ' "%1"'
125+
logger.debug(f"Command to set file association: {qgis_cmd}")
126+
if opersys == "win32":
127+
command_setted = set_qgis_command(
128+
qgis_cmd=qgis_cmd,
129+
force_key_creation=self.options.get(
130+
"force_registry_key_creation", False
131+
),
132+
)
133+
if not command_setted:
134+
logger.error(
135+
"Failed to set file association for the default profile."
136+
)
137+
else:
138+
logger.warning(
139+
"File association is only supported on Windows. "
140+
"Skipping file association setting."
141+
)
142+
143+
logger.debug(f"Job {self.ID} ran successfully.")
144+
145+
def _create_ini_profiles(self) -> None:
146+
"""Create the profiles.ini file with the default profile."""
147+
if not self.ini_profiles_path.exists():
148+
self.ini_profiles_path.touch(exist_ok=True)
94149
data = f"[core]\ndefaultProfile={self.options.get('profile')}"
95150
if self.options.get("force_profile_selection_policy"):
96151
data += "\nselectionPolicy=1"
97-
ini_profiles_path.write_text(
152+
self.ini_profiles_path.write_text(
98153
data=data,
99154
encoding="UTF8",
100155
)
101156
logger.info(f"Default profile set to {self.options.get('profile')}")
102-
logger.debug(f"Job {self.ID} ran successfully.")
103-
return
104157

158+
def _alter_ini_profiles(self) -> None:
159+
"""Alter the ini profiles with the default profile."""
105160
ini_profiles = CustomConfigParser()
106161
ini_profiles.optionxform = str
107162
ini_profiles.read(self.qgis_profiles_path / "profiles.ini", encoding="UTF8")
108163

109-
# set the default profile
110164
if not ini_profiles.has_section("core"):
111165
ini_profiles.add_section("core")
112166

@@ -122,8 +176,6 @@ def run(self) -> None:
122176
)
123177
ini_profiles.set("core", "selectionPolicy", "1")
124178

125-
with ini_profiles_path.open("w", encoding="UTF8") as wf:
179+
with self.ini_profiles_path.open("w", encoding="UTF8") as wf:
126180
ini_profiles.write(wf, space_around_delimiters=False)
127181
logger.info(f"Default profile set to {self.options.get('profile')}")
128-
129-
logger.debug(f"Job {self.ID} ran successfully.")

qgis_deployment_toolbelt/utils/win32utils.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from os import sep # required since pathlib strips trailing whitespace
2121
from pathlib import Path
2222
from sys import platform as opersys
23+
from typing import Literal
2324

2425
# Imports depending on operating system
2526
if opersys == "win32":
@@ -39,11 +40,19 @@
3940

4041
if opersys == "win32":
4142
"""windows"""
42-
system_hkey = (
43+
env_system_hkey = (
4344
winreg.HKEY_LOCAL_MACHINE,
4445
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
4546
)
46-
user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
47+
env_user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
48+
qgis_command_system_hkey = (
49+
winreg.HKEY_LOCAL_MACHINE,
50+
r"QGIS Project\Shell\open\command",
51+
)
52+
qgis_command_user_hkey = (
53+
winreg.HKEY_CURRENT_USER,
54+
r"Software\Classes\QGIS Project\Shell\open\command",
55+
)
4756

4857

4958
# #############################################################################
@@ -115,9 +124,9 @@ def delete_environment_variable(envvar_name: str, scope: str = "user") -> bool:
115124
"""
116125
# user or system
117126
if scope == "user":
118-
hkey = user_hkey
127+
hkey = env_user_hkey
119128
else:
120-
hkey = system_hkey
129+
hkey = env_system_hkey
121130

122131
# get it to check if variable exits
123132
try:
@@ -150,9 +159,9 @@ def get_environment_variable(envvar_name: str, scope: str = "user") -> str | Non
150159
"""
151160
# user or system
152161
if scope == "user":
153-
hkey = user_hkey
162+
hkey = env_user_hkey
154163
else:
155-
hkey = system_hkey
164+
hkey = env_system_hkey
156165

157166
# try to get the value
158167
try:
@@ -281,9 +290,9 @@ def set_environment_variable(
281290
"""
282291
# user or system
283292
if scope == "user":
284-
hkey = user_hkey
293+
hkey = env_user_hkey
285294
else:
286-
hkey = system_hkey
295+
hkey = env_system_hkey
287296

288297
# try to set the value
289298
try:
@@ -298,6 +307,79 @@ def set_environment_variable(
298307
return False
299308

300309

310+
def read_registry_value(
311+
key: tuple, value_name: str, access_mode: Literal["read", "write"] = "read"
312+
) -> str | None:
313+
r"""Read a value from the Windows registry.
314+
Args:
315+
key (tuple): registry key to read from, e.g. (winreg.HKEY_CURRENT_USER, r"Software\Classes\QGIS Project\Shell\open\command")
316+
value_name (str): name of the value to read
317+
access (str, optional): access mode for the registry key, defaults to read
318+
Returns:
319+
str | None: the value as a string if found, None if not found or an error occurs
320+
"""
321+
if access_mode == "read":
322+
access = winreg.KEY_READ
323+
elif access_mode == "write":
324+
access = winreg.KEY_WRITE
325+
326+
try:
327+
with winreg.OpenKey(*key, access=access) as reg_key:
328+
value, _ = winreg.QueryValueEx(reg_key, value_name)
329+
return value
330+
except FileNotFoundError:
331+
logger.error(f"Registry key {key} or value {value_name} not found.")
332+
return None
333+
except OSError as err:
334+
logger.error(f"Error reading registry key {key}: {err}")
335+
return None
336+
337+
338+
def set_qgis_command(
339+
qgis_cmd: str, scope: str = "user", force_key_creation: bool = False
340+
) -> bool:
341+
"""Set QGIS command in Windows registry.
342+
343+
Args:
344+
qgis_path (str): path to QGIS installation folder
345+
scope (str, optional): environment variable scope. Must be "user" or "system",
346+
defaults to "user". Defaults to "user".
347+
348+
Returns:
349+
bool: True is the variable has been successfully set
350+
"""
351+
# user or system
352+
if scope == "user":
353+
hkey = qgis_command_user_hkey
354+
else:
355+
hkey = qgis_command_system_hkey
356+
357+
if force_key_creation:
358+
# ensure the key exists
359+
try:
360+
with winreg.CreateKeyEx(*hkey, access=winreg.KEY_WRITE) as key:
361+
pass # just create the key if it does not exist
362+
except OSError as err:
363+
logger.error(
364+
f"Create QGIS command registry key for scope '{scope}' failed. Trace: {err}"
365+
)
366+
return False
367+
368+
# try to set the value
369+
try:
370+
with winreg.OpenKey(*hkey, access=winreg.KEY_WRITE) as key:
371+
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, qgis_cmd)
372+
return True
373+
except FileNotFoundError:
374+
logger.error(f"Registry key {hkey} not found. Is QGIS installed?")
375+
return False
376+
except OSError as err:
377+
logger.error(
378+
f"Set QGIS command '{qgis_cmd}' to scope '{scope}' failed. Trace: {err}"
379+
)
380+
return False
381+
382+
301383
# #############################################################################
302384
# ##### Stand alone program ########
303385
# ##################################

0 commit comments

Comments
 (0)