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
28 changes: 28 additions & 0 deletions docs/jobs/default_profile_setter.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ Sample job configuration in your scenario file:
force_profile_selection_policy: true
```

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:

```yaml
- name: Set default profile to conf_qgis_fr
uses: default-profile-setter
with:
profile: conf_qgis_fr
force_profile_selection_policy: true
force_profile_file_association: true
profile_file_association_arguments: "--noversioncheck"
```

----

## Options
Expand All @@ -29,6 +41,22 @@ Name of the profile to set as default profile.
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.
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).

### force_profile_file_association

:::{note}
Windows-only feature
:::

Modify the Windows registry to ensure that QGIS Project files always open with the default profile when launched.

### profile_file_association_arguments

:::{note}
Windows-only feature
:::

Arguments to pass to QGIS executable.

----

## Schema
Expand Down
20 changes: 19 additions & 1 deletion docs/schemas/scenario/jobs/default-profile-setter.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@
"description": "Force the profile selection policy to 1 (always open default profile).",
"type": "boolean",
"default": false
},
"force_profile_file_association": {
"description": "Force the QGIS project file association to starts QGIS with the default profile.",
"type": "boolean",
"default": false
},
"profile_file_association_arguments": {
"description": "Arguments to pass to QGIS executable when force_profile_file_association is true.",
"type": "string",
"example": "--noversioncheck"
},
"force_registry_key_creation": {
"description": "Force the creation of the registry key for the default profile. This is useful when QGIS is not installed yet.",
"type": "boolean",
"default": false
}
}
},
"required": [
"profile"
]
}
78 changes: 65 additions & 13 deletions qgis_deployment_toolbelt/jobs/job_default_profile_setter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

# Standard library
import logging
from sys import platform as opersys

# package
from qgis_deployment_toolbelt.jobs.generic_job import GenericJob
from qgis_deployment_toolbelt.utils.ini_parser_with_path import CustomConfigParser
from qgis_deployment_toolbelt.utils.win32utils import set_qgis_command

# #############################################################################
# ########## Globals ###############
Expand Down Expand Up @@ -52,6 +54,29 @@ class JobDefaultProfileSetter(GenericJob):
"possible_values": None,
"condition": None,
},
"force_profile_file_association": {
"type": bool,
"required": False,
"default": False,
"possible_values": None,
"condition": None,
},
"profile_file_association_arguments": {
"type": str,
"required": False,
"default": None,
"possible_values": None,
"condition": lambda options: options.get("force_profile_file_association")
is True,
},
"force_registry_key_creation": {
"type": bool,
"required": False,
"default": False,
"possible_values": None,
"condition": lambda options: options.get("force_profile_file_association")
is True,
},
}

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

ini_profiles_path = self.qgis_profiles_path / "profiles.ini"
self.ini_profiles_path = self.qgis_profiles_path / "profiles.ini"

# check if the profiles.ini file exists and create it with default profile set
# if not
if not ini_profiles_path.exists():
logger.warning(
"Configuration file profiles.ini doesn't exist. "
"It will be created but maybe it was not the expected behavior."
if not self.ini_profiles_path.exists():
self._create_ini_profiles()
else:
self._alter_ini_profiles()

if self.options.get("force_profile_file_association"):
qgis_cmd = (
f'"{self.os_config.get_qgis_bin_path}" --profile "{qdt_profile.name}"'
)
ini_profiles_path.touch(exist_ok=True)
if self.options.get("profile_file_association_arguments"):
qgis_cmd += f' {self.options.get("profile_file_association_arguments")}'
qgis_cmd += ' "%1"'
logger.debug(f"Command to set file association: {qgis_cmd}")
if opersys == "win32":
command_setted = set_qgis_command(
qgis_cmd=qgis_cmd,
force_key_creation=self.options.get(
"force_registry_key_creation", False
),
)
if not command_setted:
logger.error(
"Failed to set file association for the default profile."
)
else:
logger.warning(
"File association is only supported on Windows. "
"Skipping file association setting."
)

logger.debug(f"Job {self.ID} ran successfully.")

def _create_ini_profiles(self) -> None:
"""Create the profiles.ini file with the default profile."""
if not self.ini_profiles_path.exists():
self.ini_profiles_path.touch(exist_ok=True)
data = f"[core]\ndefaultProfile={self.options.get('profile')}"
if self.options.get("force_profile_selection_policy"):
data += "\nselectionPolicy=1"
ini_profiles_path.write_text(
self.ini_profiles_path.write_text(
data=data,
encoding="UTF8",
)
logger.info(f"Default profile set to {self.options.get('profile')}")
logger.debug(f"Job {self.ID} ran successfully.")
return

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

# set the default profile
if not ini_profiles.has_section("core"):
ini_profiles.add_section("core")

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

with ini_profiles_path.open("w", encoding="UTF8") as wf:
with self.ini_profiles_path.open("w", encoding="UTF8") as wf:
ini_profiles.write(wf, space_around_delimiters=False)
logger.info(f"Default profile set to {self.options.get('profile')}")

logger.debug(f"Job {self.ID} ran successfully.")
98 changes: 90 additions & 8 deletions qgis_deployment_toolbelt/utils/win32utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from os import sep # required since pathlib strips trailing whitespace
from pathlib import Path
from sys import platform as opersys
from typing import Literal

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

if opersys == "win32":
"""windows"""
system_hkey = (
env_system_hkey = (
winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
)
user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
env_user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")
qgis_command_system_hkey = (
winreg.HKEY_LOCAL_MACHINE,
r"QGIS Project\Shell\open\command",
)
qgis_command_user_hkey = (
winreg.HKEY_CURRENT_USER,
r"Software\Classes\QGIS Project\Shell\open\command",
)


# #############################################################################
Expand Down Expand Up @@ -115,9 +124,9 @@ def delete_environment_variable(envvar_name: str, scope: str = "user") -> bool:
"""
# user or system
if scope == "user":
hkey = user_hkey
hkey = env_user_hkey
else:
hkey = system_hkey
hkey = env_system_hkey

# get it to check if variable exits
try:
Expand Down Expand Up @@ -150,9 +159,9 @@ def get_environment_variable(envvar_name: str, scope: str = "user") -> str | Non
"""
# user or system
if scope == "user":
hkey = user_hkey
hkey = env_user_hkey
else:
hkey = system_hkey
hkey = env_system_hkey

# try to get the value
try:
Expand Down Expand Up @@ -281,9 +290,9 @@ def set_environment_variable(
"""
# user or system
if scope == "user":
hkey = user_hkey
hkey = env_user_hkey
else:
hkey = system_hkey
hkey = env_system_hkey

# try to set the value
try:
Expand All @@ -298,6 +307,79 @@ def set_environment_variable(
return False


def read_registry_value(
key: tuple, value_name: str, access_mode: Literal["read", "write"] = "read"
) -> str | None:
r"""Read a value from the Windows registry.
Args:
key (tuple): registry key to read from, e.g. (winreg.HKEY_CURRENT_USER, r"Software\Classes\QGIS Project\Shell\open\command")
value_name (str): name of the value to read
access (str, optional): access mode for the registry key, defaults to read
Returns:
str | None: the value as a string if found, None if not found or an error occurs
"""
if access_mode == "read":
access = winreg.KEY_READ
elif access_mode == "write":
access = winreg.KEY_WRITE

try:
with winreg.OpenKey(*key, access=access) as reg_key:
value, _ = winreg.QueryValueEx(reg_key, value_name)
return value
except FileNotFoundError:
logger.error(f"Registry key {key} or value {value_name} not found.")
return None
except OSError as err:
logger.error(f"Error reading registry key {key}: {err}")
return None


def set_qgis_command(
qgis_cmd: str, scope: str = "user", force_key_creation: bool = False
) -> bool:
"""Set QGIS command in Windows registry.

Args:
qgis_path (str): path to QGIS installation folder
scope (str, optional): environment variable scope. Must be "user" or "system",
defaults to "user". Defaults to "user".

Returns:
bool: True is the variable has been successfully set
"""
# user or system
if scope == "user":
hkey = qgis_command_user_hkey
else:
hkey = qgis_command_system_hkey

if force_key_creation:
# ensure the key exists
try:
with winreg.CreateKeyEx(*hkey, access=winreg.KEY_WRITE) as key:
pass # just create the key if it does not exist
except OSError as err:
logger.error(
f"Create QGIS command registry key for scope '{scope}' failed. Trace: {err}"
)
return False

# try to set the value
try:
with winreg.OpenKey(*hkey, access=winreg.KEY_WRITE) as key:
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, qgis_cmd)
return True
except FileNotFoundError:
logger.error(f"Registry key {hkey} not found. Is QGIS installed?")
return False
except OSError as err:
logger.error(
f"Set QGIS command '{qgis_cmd}' to scope '{scope}' failed. Trace: {err}"
)
return False


# #############################################################################
# ##### Stand alone program ########
# ##################################
Expand Down
Loading