Compare commits

...

8 commits

Author SHA1 Message Date
xboxones1
30d3060417
Merge 3a7826a278 into 36a26308d0 2026-03-10 09:51:12 +05:30
Janek Bevendorff
36a26308d0 Fix indentation error in BuildSrc
Some checks failed
CodeQL / Analyze (push) Has been cancelled
2026-03-10 02:35:42 +01:00
Janek Bevendorff
5b5ab5334a Update changelog 2026-03-10 02:35:42 +01:00
Janek Bevendorff
12ef38c192
Also override OPENSSL_ENGINES to prevent DLL injection (#13124)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
2026-03-10 00:38:06 +01:00
Jonathan White
b7ad8b4761
Security: Prevent loading of openssl.cnf (#13118)
* Security: Prevent loading of openssl.cnf

Prevent loading openssl.cnf from the originating vcpkg folder tree to avoid DLL injections. This patch force sets the OPENSSL_CONF and OPENSSL_MODULES env vars to an invalid directory. This prevents openssl from attempting to load a cnf file which can contain settings to load arbitrary DLL files into KeePassXC memory space.

Thank you to zdi-disclosures for reporting this finding!
2026-03-09 23:42:25 +01:00
Sami Vänttinen
d9be1b0682
Passkeys: Fix default BE and BS flag value (#13122) 2026-03-09 17:44:10 +01:00
xboxones1
3a7826a278 Increase the size of the attachment viewing window 2026-01-18 08:40:40 +00:00
xboxones1
b0df86ce74 Markdown toolbar for the attachments editor 2026-01-18 08:32:02 +00:00
31 changed files with 795 additions and 40 deletions

View file

@ -3,6 +3,27 @@
## 2.8.0 (Pending)
* Placeholder for future release notes
## 2.7.12 (2026-03-10)
### Changes
- Passkeys: Set BE and BS flags to true (NOTE: MAY BREAK EXISTING PASSKEYS) [#13042]
- Support TIMEOTP autotype and entry placeholder [#13117]
- Browser: Show URLs in browser access dialog [#12906]
- Bitwarden Import: Add support for nested folders [#13081]
### Fixes
- Prevent exploits through OpenSSL configurations [#13118, #13124]
- Auto-Type: Revert change that caused race condition on Linux [#12738]
- Auto-Type: Fix help button enablement [#12937]
- Browser: Fix showing correct checkbox value in entry Browser Integration settings [#12980]
- Browser: Fix setting browser related values to customData [#13026]
- Passkeys: Add publicKey to register response [#12757]
- Fix main entry URL validation when using placeholders [#12964]
- Fix minor font and theme issues [#12814]
- Fix 'Remove' button in Plugin Data being enabled when no row is selected [#12916]
- Sanitize attachment file names before saving [#13114]
## 2.7.11 (2025-11-23)
### Changes

48
COPYING
View file

@ -153,8 +153,8 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/configure.svg
share/icons/application/scalable/actions/csv.svg
share/icons/application/scalable/actions/database-change-key.svg
share/icons/application/scalable/actions/database-lock.svg
share/icons/application/scalable/actions/database-lock-all.svg
share/icons/application/scalable/actions/database-lock.svg
share/icons/application/scalable/actions/database-merge.svg
share/icons/application/scalable/actions/database-search.svg
share/icons/application/scalable/actions/database-settings.svg
@ -165,34 +165,46 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/document-export.svg
share/icons/application/scalable/actions/document-import.svg
share/icons/application/scalable/actions/document-new.svg
share/icons/application/scalable/actions/document-open.svg
share/icons/application/scalable/actions/document-open-recent.svg
share/icons/application/scalable/actions/document-open.svg
share/icons/application/scalable/actions/document-properties.svg
share/icons/application/scalable/actions/document-save.svg
share/icons/application/scalable/actions/document-save-as.svg
share/icons/application/scalable/actions/document-save-copy.svg
share/icons/application/scalable/actions/document-save.svg
share/icons/application/scalable/actions/donate.svg
share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg
share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg
share/icons/application/scalable/actions/entry-clone.svg
share/icons/application/scalable/actions/entry-delete.svg
share/icons/application/scalable/actions/entry-restore.svg
share/icons/application/scalable/actions/entry-edit.svg
share/icons/application/scalable/actions/entry-expire.svg
share/icons/application/scalable/actions/entry-new.svg
share/icons/application/scalable/actions/entry-restore.svg
share/icons/application/scalable/actions/favicon-download.svg
share/icons/application/scalable/actions/fingerprint.svg
share/icons/application/scalable/actions/format-code-block.svg
share/icons/application/scalable/actions/format-code.svg
share/icons/application/scalable/actions/format-header-decrease.svg
share/icons/application/scalable/actions/format-header-increase.svg
share/icons/application/scalable/actions/format-text-bold.svg
share/icons/application/scalable/actions/format-text-italic.svg
share/icons/application/scalable/actions/format-text-strikethrough.svg
share/icons/application/scalable/actions/format-text-underline.svg
share/icons/application/scalable/actions/getting-started.svg
share/icons/application/scalable/actions/group-clone.svg
share/icons/application/scalable/actions/group-delete.svg
share/icons/application/scalable/actions/group-edit.svg
share/icons/application/scalable/actions/group-clone.svg
share/icons/application/scalable/actions/group-empty-trash.svg
share/icons/application/scalable/actions/group-new.svg
share/icons/application/scalable/actions/hammer-wrench.svg
share/icons/application/scalable/actions/health.svg
share/icons/application/scalable/actions/help-about.svg
share/icons/application/scalable/actions/lock-question.svg
share/icons/application/scalable/actions/insert-link.svg
share/icons/application/scalable/actions/keyboard-shortcuts.svg
share/icons/application/scalable/actions/lock-open-alert.svg
share/icons/application/scalable/actions/lock-open.svg
share/icons/application/scalable/actions/lock-question.svg
share/icons/application/scalable/actions/lock.svg
share/icons/application/scalable/actions/message-close.svg
share/icons/application/scalable/actions/move-down.svg
share/icons/application/scalable/actions/move-up.svg
@ -200,31 +212,33 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/object-unlocked.svg
share/icons/application/scalable/actions/onepassword.svg
share/icons/application/scalable/actions/paperclip.svg
share/icons/application/scalable/actions/password-copy.svg
share/icons/application/scalable/actions/passkey.svg
share/icons/application/scalable/actions/password-copy.svg
share/icons/application/scalable/actions/password-generator.svg
share/icons/application/scalable/actions/password-show-off.svg
share/icons/application/scalable/actions/password-show-on.svg
share/icons/application/scalable/actions/qrcode.svg
share/icons/application/scalable/actions/redo.svg
share/icons/application/scalable/actions/refresh.svg
share/icons/application/scalable/actions/remote-sync.svg
share/icons/application/scalable/actions/reports.svg
share/icons/application/scalable/actions/reports-exclude.svg
share/icons/application/scalable/actions/reports.svg
share/icons/application/scalable/actions/sort-alphabetical-ascending.svg
share/icons/application/scalable/actions/sort-alphabetical-descending.svg
share/icons/application/scalable/actions/statistics.svg
share/icons/application/scalable/actions/system-help.svg
share/icons/application/scalable/actions/system-search.svg
share/icons/application/scalable/actions/system-software-update.svg
share/icons/application/scalable/actions/tag.svg
share/icons/application/scalable/actions/tag-multiple.svg
share/icons/application/scalable/actions/tag-search.svg
share/icons/application/scalable/actions/totp.svg
share/icons/application/scalable/actions/totp-copy.svg
share/icons/application/scalable/actions/tag.svg
share/icons/application/scalable/actions/totp-copy-password.svg
share/icons/application/scalable/actions/totp-copy.svg
share/icons/application/scalable/actions/totp-edit.svg
share/icons/application/scalable/actions/totp-invalid.svg
share/icons/application/scalable/actions/totp.svg
share/icons/application/scalable/actions/trash.svg
share/icons/application/scalable/actions/undo.svg
share/icons/application/scalable/actions/url-copy.svg
share/icons/application/scalable/actions/user-guide.svg
share/icons/application/scalable/actions/username-copy.svg
@ -232,10 +246,10 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/web.svg
share/icons/application/scalable/actions/yubikey-refresh.svg
share/icons/application/scalable/apps/internet-web-browser.svg
share/icons/application/scalable/apps/keepassxc.svg
share/icons/application/scalable/apps/keepassxc-dark.svg
share/icons/application/scalable/apps/keepassxc-locked.svg
share/icons/application/scalable/apps/keepassxc-unlocked.svg
share/icons/application/scalable/apps/keepassxc.svg
share/icons/application/scalable/apps/preferences-desktop-icons.svg
share/icons/application/scalable/apps/preferences-system-network-sharing.svg
share/icons/application/scalable/apps/utilities-terminal.svg
@ -245,13 +259,17 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/status/dialog-information.svg
share/icons/application/scalable/status/dialog-warning.svg
share/icons/application/scalable/status/security-high.svg
share/icons/application/scalable/actions/lock-open-alert.svg
share/icons/application/scalable/actions/lock-open.svg
share/icons/application/scalable/actions/lock.svg
Copyright: 2023 Pictogrammers <https://pictogrammers.com/docs/general/about/>
License: Apache-2.0
Comment: Some icons are modified to fit KeePassXC design (https://pictogrammers.com/library/mdi/)
Files: share/icons/application/scalable/actions/format-list-bulleted.svg
share/icons/application/scalable/actions/format-list-numbered.svg
share/icons/application/scalable/actions/format-list-check.svg
Copyright: 20152025 IBM Corp. and the Carbon Design System contributors
License: Apache-2.0
Comment: Taken from https://github.com/carbon-design-system/carbon
Files: src/streams/qtiocompressor.*
src/streams/QtIOCompressor
tests/modeltest.*

View file

@ -968,11 +968,11 @@ class BuildSrc(Command):
tf.add(fver, Path(prefix) / fver.name)
tf.add(frev, Path(prefix) / frev.name)
logger.info('Compressing source tarball...')
tmp_comp = tmp_export.with_suffix('.tar.xz')
with lzma.open(tmp_comp, 'wb', preset=6) as f:
f.write(tmp_export.read_bytes())
tmp_comp.rename(output_file)
logger.info('Compressing source tarball...')
tmp_comp = tmp_export.with_suffix('.tar.xz')
with lzma.open(tmp_comp, 'wb', preset=6) as f:
f.write(tmp_export.read_bytes())
tmp_comp.rename(output_file)
class Notarize(Command):

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.59 3.41L7 4.82L3.82 8L7 11.18L5.59 12.6L1 8L5.59 3.41M11.41 3.41L16 8L11.41 12.6L10 11.18L13.18 8L10 4.82L11.41 3.41M22 6V18C22 19.11 21.11 20 20 20H4C2.9 20 2 19.11 2 18V14H4V18H20V6H17.03V4H20C21.11 4 22 4.89 22 6Z" /></svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z" /></svg>

After

Width:  |  Height:  |  Size: 181 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M20.42,7.41L16.83,11L20.42,14.59L19,16L14,11L19,6L20.42,7.41Z" /></svg>

After

Width:  |  Height:  |  Size: 175 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M14.59,7.41L18.17,11L14.59,14.59L16,16L21,11L16,6L14.59,7.41Z" /></svg>

After

Width:  |  Height:  |  Size: 175 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="7" cy="9" r="3" fill="currentColor"/><circle cx="7" cy="23" r="3" fill="currentColor"/><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 8h14v2H16zm0 14h14v2H16zm-6-8H4a2.002 2.002 0 0 1-2-2V6a2.002 2.002 0 0 1 2-2h6a2.002 2.002 0 0 1 2 2v6a2.002 2.002 0 0 1-2 2M4 6v6h6.001L10 6zm6 22H4a2.002 2.002 0 0 1-2-2v-6a2.002 2.002 0 0 1 2-2h6a2.002 2.002 0 0 1 2 2v6a2.002 2.002 0 0 1-2 2m-6-8v6h6.001L10 20z"/></svg>

After

Width:  |  Height:  |  Size: 390 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16zm-8 4V4H6v1H4v2h2v5H4v2h6v-2zm2 16H4v-4a2 2 0 0 1 2-2h2v-2H4v-2h4a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 252 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,15.5H10V12.5H13.5A1.5,1.5 0 0,1 15,14A1.5,1.5 0 0,1 13.5,15.5M10,6.5H13A1.5,1.5 0 0,1 14.5,8A1.5,1.5 0 0,1 13,9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z" /></svg>

After

Width:  |  Height:  |  Size: 345 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z" /></svg>

After

Width:  |  Height:  |  Size: 134 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.2 9.8C6 7.5 7.7 4.8 10.1 4.3C13.2 3.3 17.7 4.7 17.6 8.5H14.6C14.6 8.2 14.5 7.9 14.5 7.7C14.3 7.1 13.9 6.8 13.3 6.6C12.5 6.3 11.2 6.4 10.5 6.9C9 8.2 10.4 9.5 12 10H7.4C7.3 9.9 7.3 9.8 7.2 9.8M21 13V11H3V13H12.6C12.8 13.1 13 13.1 13.2 13.2C13.8 13.5 14.3 13.7 14.5 14.3C14.6 14.7 14.7 15.2 14.5 15.6C14.3 16.1 13.9 16.3 13.4 16.5C11.6 17 9.4 16.3 9.5 14.1H6.5C6.4 16.7 8.6 18.5 11 18.8C14.8 19.6 19.3 17.2 17.3 12.9L21 13Z" /></svg>

After

Width:  |  Height:  |  Size: 502 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z" /></svg>

After

Width:  |  Height:  |  Size: 194 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>

After

Width:  |  Height:  |  Size: 795 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 13.5C3 9.36 6.36 6 10.5 6H13V2L20 8L13 14V10H10.5C8.57 10 7 11.57 7 13.5S8.57 17 10.5 17H18V21H10.5C6.36 21 3 17.64 3 13.5Z" /></svg>

After

Width:  |  Height:  |  Size: 205 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5 21H6V17H13.5C15.43 17 17 15.43 17 13.5S15.43 10 13.5 10H11V14L4 8L11 2V6H13.5C17.64 6 21 9.36 21 13.5S17.64 21 13.5 21Z" /></svg>

After

Width:  |  Height:  |  Size: 204 B

View file

@ -46,6 +46,17 @@
<file>application/scalable/actions/entry-new.svg</file>
<file>application/scalable/actions/favicon-download.svg</file>
<file>application/scalable/actions/fingerprint.svg</file>
<file>application/scalable/actions/format-code-block.svg</file>
<file>application/scalable/actions/format-code.svg</file>
<file>application/scalable/actions/format-header-decrease.svg</file>
<file>application/scalable/actions/format-header-increase.svg</file>
<file>application/scalable/actions/format-list-bulleted.svg</file>
<file>application/scalable/actions/format-list-check.svg</file>
<file>application/scalable/actions/format-list-numbered.svg</file>
<file>application/scalable/actions/format-text-bold.svg</file>
<file>application/scalable/actions/format-text-italic.svg</file>
<file>application/scalable/actions/format-text-strikethrough.svg</file>
<file>application/scalable/actions/format-text-underline.svg</file>
<file>application/scalable/actions/getting-started.svg</file>
<file>application/scalable/actions/group-delete.svg</file>
<file>application/scalable/actions/group-edit.svg</file>
@ -56,6 +67,7 @@
<file>application/scalable/actions/health.svg</file>
<file>application/scalable/actions/help-about.svg</file>
<file>application/scalable/actions/hibp.svg</file>
<file>application/scalable/actions/insert-link.svg</file>
<file>application/scalable/actions/lock-question.svg</file>
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
<file>application/scalable/actions/lock.svg</file>
@ -75,6 +87,7 @@
<file>application/scalable/actions/password-show-on.svg</file>
<file>application/scalable/actions/proton.svg</file>
<file>application/scalable/actions/qrcode.svg</file>
<file>application/scalable/actions/redo.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/remote-sync.svg</file>
<file>application/scalable/actions/reports.svg</file>
@ -94,6 +107,7 @@
<file>application/scalable/actions/totp-edit.svg</file>
<file>application/scalable/actions/totp-invalid.svg</file>
<file>application/scalable/actions/trash.svg</file>
<file>application/scalable/actions/undo.svg</file>
<file>application/scalable/actions/url-copy.svg</file>
<file>application/scalable/actions/user-guide.svg</file>
<file>application/scalable/actions/username-copy.svg</file>

View file

@ -52,6 +52,26 @@
</ul>
</description>
</release>
<release version="2.7.12" date="2026-03-10" type="stable">
<description>
<ul>
<li>Passkeys: Set BE and BS flags to true (NOTE: MAY BREAK EXISTING PASSKEYS) [#13042]</li>
<li>Support TIMEOTP autotype and entry placeholder [#13117]</li>
<li>Browser: Show URLs in browser access dialog [#12906]</li>
<li>Bitwarden Import: Add support for nested folders [#13081]</li>
<li>Prevent exploits through OpenSSL configurations [#13118, #13124]</li>
<li>Auto-Type: Revert change that caused race condition on Linux [#12738]</li>
<li>Auto-Type: Fix help button enablement [#12937]</li>
<li>Browser: Fix showing correct checkbox value in entry Browser Integration settings [#12980]</li>
<li>Browser: Fix setting browser related values to customData [#13026]</li>
<li>Passkeys: Add publicKey to register response [#12757]</li>
<li>Fix main entry URL validation when using placeholders [#12964]</li>
<li>Fix minor font and theme issues [#12814]</li>
<li>Fix 'Remove' button in Plugin Data being enabled when no row is selected [#12916]</li>
<li>Sanitize attachment file names before saving [#13114]</li>
</ul>
</description>
</release>
<release version="2.7.11" date="2025-11-23" type="stable">
<description>
<ul>

View file

@ -6475,6 +6475,89 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MarkdownToolbar</name>
<message>
<source>Undo</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Redo</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Decrease heading level</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Increase heading level</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bold</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Italic</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Strike</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Underline</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Link</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bullet list</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Numbered list</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Task list</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Inline code</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Code block</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Insert Link</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Full link address (e.g. https://example.com or mailto:user@example.com)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Text shown instead of the URL</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Link text:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Text shown on hover</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Tooltip:</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MergeDialog</name>
<message>

View file

@ -206,6 +206,7 @@ set(gui_SOURCES
gui/osutils/ScreenLockListenerPrivate.cpp
gui/widgets/ElidedLabel.cpp
gui/widgets/KPToolBar.cpp
gui/widgets/MarkdownToolbar.cpp
gui/widgets/PopupHelpWidget.cpp
gui/widgets/ShortcutWidget.cpp
gui/wizard/ImportWizard.cpp

View file

@ -776,12 +776,16 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
// Get BE and BS flags if present
const auto beFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BE)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == TRUE_STR
: DEFAULT_BE_FLAG;
const auto bsFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BS)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == TRUE_STR
: DEFAULT_BS_FLAG;
const auto beFlag =
selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BE)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == "1"
|| selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == TRUE_STR
: DEFAULT_BE_FLAG;
const auto bsFlag =
selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BS)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == "1"
|| selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == TRUE_STR
: DEFAULT_BS_FLAG;
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(
assertionOptions, credentialId, userHandle, privateKeyPem, beFlag, bsFlag);
@ -864,8 +868,8 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BE, TRUE_STR);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, TRUE_STR);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BE, "1");
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, "1");
entry->addTag(tr("Passkey"));
entry->endUpdate();

View file

@ -6,10 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
<width>720</width>
<height>490</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>720</width>
<height>490</height>
</size>
</property>
<property name="windowTitle">
<string/>
</property>

View file

@ -27,6 +27,8 @@ TextAttachmentsEditWidget::TextAttachmentsEditWidget(QWidget* parent)
, m_ui(new Ui::TextAttachmentsEditWidget())
{
m_ui->setupUi(this);
m_mdToolbar = new MarkdownToolbar(this);
m_mdToolbar->setTarget(m_ui->attachmentsTextEdit);
connect(m_ui->attachmentsTextEdit, &QTextEdit::textChanged, this, &TextAttachmentsEditWidget::textChanged);
connect(m_ui->previewPushButton, &QPushButton::clicked, this, &TextAttachmentsEditWidget::previewButtonClicked);
@ -43,7 +45,6 @@ void TextAttachmentsEditWidget::openAttachment(attachments::Attachment attachmen
{
m_attachment = std::move(attachments);
m_mode = mode;
updateUi();
}

View file

@ -19,6 +19,8 @@
#include "AttachmentTypes.h"
#include "gui/widgets/MarkdownToolbar.h"
#include <QScopedPointer>
#include <QWidget>
@ -36,6 +38,10 @@ public:
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
attachments::Attachment getAttachment() const;
QToolBar* markdownToolbar() const
{
return m_mdToolbar;
}
signals:
void textChanged();
@ -46,6 +52,7 @@ private:
void updateUi();
QScopedPointer<Ui::TextAttachmentsEditWidget> m_ui;
MarkdownToolbar* m_mdToolbar = nullptr;
attachments::Attachment m_attachment;
attachments::OpenMode m_mode;

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
<width>720</width>
<height>490</height>
</rect>
</property>
<property name="windowTitle">

View file

@ -19,8 +19,9 @@
#include "TextAttachmentsEditWidget.h"
#include "TextAttachmentsPreviewWidget.h"
#include <core/Tools.h>
#include <QSplitter>
#include <QTextEdit>
#include <QTimer>
#include <QVBoxLayout>
@ -39,6 +40,14 @@ void TextAttachmentsWidget::openAttachment(attachments::Attachment attachment, a
m_attachment = std::move(attachment);
m_mode = mode;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const auto mimeType = Tools::getMimeType(QFileInfo(m_attachment.name));
const bool isMd = (mimeType == Tools::MimeType::Markdown);
if (auto* tb = m_editWidget->markdownToolbar()) {
tb->setVisible(isMd && m_mode != attachments::OpenMode::ReadOnly);
}
#endif
updateWidget();
}
@ -120,10 +129,18 @@ void TextAttachmentsWidget::initWidget()
// Setup this widget with the splitter
auto layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
if (auto* tb = m_editWidget->markdownToolbar()) {
tb->setParent(this);
layout->addWidget(tb);
}
layout->addWidget(m_splitter);
setLayout(layout);
setObjectName("TextAttachmentsWidget");
setMinimumSize(720, 490);
updateWidget();
}

View file

@ -20,14 +20,8 @@
#include "AttachmentTypes.h"
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class TextAttachmentsWidget;
}
class QSplitter;
class QTimer;
class TextAttachmentsPreviewWidget;

View file

@ -0,0 +1,497 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MarkdownToolbar.h"
#include "core/Config.h"
#include "gui/Icons.h"
#include "gui/styles/StateColorPalette.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QTextBlock>
#include <QTextEdit>
#include <QToolButton>
#include <algorithm>
MarkdownToolbar::MarkdownToolbar(QWidget* parent)
: QToolBar(parent)
{
setupUi();
}
void MarkdownToolbar::setTarget(QTextEdit* edit)
{
m_target = edit;
if (!m_target)
return;
m_target->setContextMenuPolicy(Qt::DefaultContextMenu);
// Undo
if (m_undoAction) {
m_undoAction->disconnect();
connect(m_undoAction, &QAction::triggered, m_target, &QTextEdit::undo);
m_undoAction->setEnabled(m_target->document()->isUndoAvailable());
connect(m_target->document(), &QTextDocument::undoAvailable, m_undoAction, &QAction::setEnabled);
}
// Redo
if (m_redoAction) {
m_redoAction->disconnect();
connect(m_redoAction, &QAction::triggered, m_target, &QTextEdit::redo);
m_redoAction->setEnabled(m_target->document()->isRedoAvailable());
connect(m_target->document(), &QTextDocument::redoAvailable, m_redoAction, &QAction::setEnabled);
}
}
QAction* MarkdownToolbar::createAction(const QString& iconName,
const QString& tooltip,
const QKeySequence& shortcut,
const std::function<void()>& callback)
{
QAction* act = addAction(icons()->icon(iconName), tooltip);
if (!shortcut.isEmpty()) {
act->setShortcut(shortcut);
QString tip = tooltip;
tip += " (" + shortcut.toString(QKeySequence::NativeText) + ")";
act->setToolTip(tip);
} else {
act->setToolTip(tooltip);
}
act->setShortcutVisibleInContextMenu(true);
if (auto tb = qobject_cast<QToolButton*>(widgetForAction(act)))
tb->setAutoRaise(true);
if (callback)
connect(act, &QAction::triggered, this, callback);
return act;
}
void MarkdownToolbar::setupUi()
{
setMovable(false);
setFloatable(false);
setToolButtonStyle(Qt::ToolButtonIconOnly);
if (config()->get(Config::GUI_CompactMode).toBool())
setIconSize(QSize(20, 20));
// Undo
QString undoText = tr("Undo");
QString undoShortcut = QKeySequence(QKeySequence::Undo).toString(QKeySequence::NativeText);
m_undoAction = createAction("undo", undoText, QKeySequence(), [=]() {
if (m_target)
m_target->undo();
});
m_undoAction->setToolTip(QString("%1 (%2)").arg(undoText, undoShortcut));
// Redo
QString redoText = tr("Redo");
QString redoShortcut = QKeySequence(QKeySequence::Redo).toString(QKeySequence::NativeText);
m_redoAction = createAction("redo", redoText, QKeySequence(), [=]() {
if (m_target)
m_target->redo();
});
m_redoAction->setToolTip(QString("%1 (%2)").arg(redoText, redoShortcut));
addSeparator();
// Other groups of buttons
struct ButtonSpec
{
QString iconName;
QString tooltip;
QKeySequence shortcut;
std::function<void()> callback;
};
const QList<QList<ButtonSpec>> buttonGroups = {
{
{"format-header-decrease",
tr("Decrease heading level"),
QKeySequence(Qt::CTRL | Qt::Key_Down),
[=]() { changeHeadingLevel(-1); }},
{"format-header-increase",
tr("Increase heading level"),
QKeySequence(Qt::CTRL | Qt::Key_Up),
[=]() { changeHeadingLevel(1); }},
},
{
{"format-text-bold", tr("Bold"), QKeySequence::Bold, [=]() { wrapSelection("**", "**"); }},
{"format-text-italic", tr("Italic"), QKeySequence::Italic, [=]() { wrapSelection("*", "*"); }},
{"format-text-strikethrough",
tr("Strike"),
QKeySequence(Qt::CTRL | Qt::Key_S),
[=]() { wrapSelection("~~", "~~"); }},
{"format-text-underline",
tr("Underline"),
QKeySequence::Underline,
[=]() { wrapSelection("<u>", "</u>"); }},
},
{
{"insert-link", tr("Link"), QKeySequence(Qt::CTRL | Qt::Key_L), [=]() { insertLink(); }},
},
{
{"format-list-bulleted",
tr("Bullet list"),
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_B),
[=]() { wrapSelection("- ", "", true); }},
{"format-list-numbered",
tr("Numbered list"),
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_N),
[=]() { wrapSelectionWithNumbering(); }},
{"format-list-check",
tr("Task list"),
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T),
[=]() { wrapSelection("- [ ] ", "", true); }},
},
{
{"format-code", tr("Inline code"), QKeySequence(Qt::CTRL | Qt::Key_E), [=]() { wrapSelection("`", "`"); }},
{"format-code-block",
tr("Code block"),
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_E),
[=]() { wrapSelection("```\n", "\n```"); }},
}};
for (int i = 0; i < buttonGroups.size(); ++i) {
for (const auto& btn : buttonGroups[i]) {
createAction(btn.iconName, btn.tooltip, btn.shortcut, btn.callback);
}
if (i < buttonGroups.size() - 1)
addSeparator();
}
}
void MarkdownToolbar::wrapSelection(const QString& prefix, const QString& suffix, bool lineWise)
{
if (!m_target)
return;
QTextCursor cursor = m_target->textCursor();
QString selected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
QString result;
if (lineWise) {
QStringList lines = selected.split('\n');
for (QString& line : lines) {
line = prefix + line + suffix;
}
result = lines.join("\n");
} else {
result = prefix + selected + suffix;
}
cursor.beginEditBlock();
cursor.insertText(result);
cursor.endEditBlock();
}
void MarkdownToolbar::wrapSelectionWithNumbering()
{
if (!m_target)
return;
QTextCursor cursor = m_target->textCursor();
QString selected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
QStringList lines = selected.split('\n');
QStringList numbered;
for (int i = 0; i < lines.size(); ++i) {
numbered << QString::number(i + 1) + ". " + lines[i];
}
cursor.beginEditBlock();
cursor.insertText(numbered.join("\n"));
cursor.endEditBlock();
}
void MarkdownToolbar::changeHeadingLevel(int delta, bool clearAll)
{
if (!m_target)
return;
static const QRegularExpression HeadingRegex(R"rx(^(#{1,6})(?:\s+(.*))?$)rx");
QTextDocument* doc = m_target->document();
if (!doc)
return;
QTextCursor cur = m_target->textCursor();
const int origAnchor = cur.anchor();
const int origCaret = cur.position();
const bool collapsed = (origAnchor == origCaret);
struct CursorPos
{
int blockNumber;
int offsetInContent;
};
auto parseLine = [&](const QString& line, int& oldLevel, QString& pureContent) {
oldLevel = 0;
pureContent = line;
auto hm = HeadingRegex.match(pureContent);
if (hm.hasMatch()) {
oldLevel = hm.captured(1).size();
pureContent = hm.captured(2);
}
};
auto toCursorPos = [&](int pos) -> CursorPos {
QTextBlock b = doc->findBlock(pos);
if (!b.isValid()) {
return CursorPos{doc->lastBlock().blockNumber(), 0};
}
const QString text = b.text();
int ol = 0;
QString pure;
parseLine(text, ol, pure);
const int prefixLen = text.length() - pure.length();
const int offInBlock = pos - b.position();
const int offInContent = qBound(0, offInBlock - prefixLen, pure.length());
return CursorPos{b.blockNumber(), offInContent};
};
auto fromCursorPos = [&](const CursorPos& cp) -> int {
QTextBlock b = doc->findBlockByNumber(cp.blockNumber);
if (!b.isValid()) {
return qMax(0, doc->characterCount() - 1);
}
const QString text = b.text();
int ol = 0;
QString pure;
parseLine(text, ol, pure);
const int prefixLen = text.length() - pure.length();
const int offInContent = qBound(0, cp.offsetInContent, pure.length());
return b.position() + prefixLen + offInContent;
};
const bool anchorWasEnd = (origAnchor > origCaret);
CursorPos anchorPos = toCursorPos(origAnchor);
CursorPos caretPos = toCursorPos(origCaret);
const int selStart = qMin(origAnchor, origCaret);
const int selEnd = qMax(origAnchor, origCaret);
QVector<QTextBlock> blocks;
if (collapsed) {
QTextBlock b = doc->findBlock(selStart);
if (b.isValid())
blocks.push_back(b);
} else {
for (QTextBlock b = doc->findBlock(selStart); b.isValid(); b = b.next()) {
if (b.position() <= selEnd) {
blocks.push_back(b);
} else {
break;
}
}
}
if (blocks.isEmpty())
return;
bool hasHeadings = false;
for (const QTextBlock& b : blocks) {
int ol = 0;
QString pc;
parseLine(b.text(), ol, pc);
if (ol > 0) {
hasHeadings = true;
break;
}
}
int targetLevel = -1;
if (clearAll) {
targetLevel = 0;
} else if (!hasHeadings) {
targetLevel = std::clamp(delta, 0, 6);
}
cur.beginEditBlock();
for (const QTextBlock& b : blocks) {
const QString original = b.text();
int oldLevel = 0;
QString pure;
parseLine(original, oldLevel, pure);
const int newLevel = (targetLevel >= 0) ? targetLevel : std::clamp(oldLevel + delta, 0, 6);
const QString hashes = (newLevel == 0) ? QString() : QString(newLevel, QChar('#'));
const QString space = (newLevel == 0 || pure.isEmpty()) ? QString() : QStringLiteral(" ");
const QString newLine = (newLevel == 0) ? pure : hashes + space + pure;
if (newLine != original) {
QTextCursor lc(b);
lc.movePosition(QTextCursor::StartOfBlock);
lc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
lc.insertText(newLine);
}
}
cur.endEditBlock();
// Restore cursor
QTextCursor nc = m_target->textCursor();
if (collapsed) {
nc.setPosition(fromCursorPos(caretPos));
} else {
const int start = fromCursorPos(anchorPos);
const int end = fromCursorPos(caretPos);
if (anchorWasEnd) {
nc.setPosition(end);
nc.setPosition(start, QTextCursor::KeepAnchor);
} else {
nc.setPosition(start);
nc.setPosition(end, QTextCursor::KeepAnchor);
}
}
m_target->setTextCursor(nc);
}
void MarkdownToolbar::insertLink()
{
if (!m_target)
return;
QTextCursor cursor = m_target->textCursor();
QString originalSelected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
QString trimmedSelected = originalSelected.trimmed();
QString prefillText, prefillUrl, prefillTooltip;
bool hasSelection = !trimmedSelected.isEmpty();
bool matched = false;
auto tryMatch = [&](const QRegularExpression& re, auto handler) {
auto m = re.match(trimmedSelected);
if (m.hasMatch()) {
matched = true;
handler(m);
}
};
static const QRegularExpression PlainUrlRegex(R"rx(^(https?://\S+)$)rx", QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression EmailRegex(R"rx(^([A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})$)rx");
tryMatch(PlainUrlRegex, [&](const auto& m) {
prefillUrl = m.captured(1);
prefillText = prefillUrl;
prefillText.remove(QRegularExpression("^https?://", QRegularExpression::CaseInsensitiveOption));
});
if (!matched)
tryMatch(EmailRegex, [&](const auto& m) {
prefillUrl = "mailto:" + m.captured(1);
prefillText = m.captured(1);
});
if (hasSelection && !matched) {
prefillText = trimmedSelected;
cursor.removeSelectedText();
}
QDialog dlg(this);
dlg.setWindowTitle(tr("Insert Link"));
dlg.resize(420, 160);
QFormLayout layout(&dlg);
layout.setContentsMargins(12, 12, 12, 12);
layout.setVerticalSpacing(10);
QLineEdit urlEdit;
urlEdit.setPlaceholderText("https://example.com");
urlEdit.setToolTip(tr("Full link address (e.g. https://example.com or mailto:user@example.com)"));
if (!prefillUrl.isEmpty())
urlEdit.setText(prefillUrl);
layout.addRow("URL:", &urlEdit);
QLineEdit textEdit;
textEdit.setToolTip(tr("Text shown instead of the URL"));
if (!prefillText.isEmpty())
textEdit.setText(prefillText);
layout.addRow(tr("Link text:"), &textEdit);
QLineEdit tooltipEdit;
tooltipEdit.setToolTip(tr("Text shown on hover"));
if (!prefillTooltip.isEmpty())
tooltipEdit.setText(prefillTooltip);
layout.addRow(tr("Tooltip:"), &tooltipEdit);
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto okBtn = buttons.button(QDialogButtonBox::Ok);
okBtn->setAutoDefault(false);
okBtn->setDefault(false);
layout.addRow(&buttons);
StateColorPalette statePalette;
auto errorColor = statePalette.color(StateColorPalette::ColorRole::Error);
connect(&urlEdit, &QLineEdit::textChanged, [&](const QString& text) {
if (!text.trimmed().isEmpty()) {
urlEdit.setStyleSheet("");
}
});
auto tryAccept = [&]() {
if (urlEdit.text().trimmed().isEmpty()) {
urlEdit.setStyleSheet(QString("QLineEdit { background: %1; }").arg(errorColor.name()));
urlEdit.setFocus();
return;
}
dlg.accept();
};
connect(&urlEdit, &QLineEdit::returnPressed, tryAccept);
connect(&textEdit, &QLineEdit::returnPressed, tryAccept);
connect(&tooltipEdit, &QLineEdit::returnPressed, tryAccept);
connect(okBtn, &QPushButton::clicked, tryAccept);
connect(buttons.button(QDialogButtonBox::Cancel), &QPushButton::clicked, &dlg, &QDialog::reject);
urlEdit.setFocus();
if (dlg.exec() != QDialog::Accepted) {
if (hasSelection) {
cursor.insertText(originalSelected);
}
return;
}
const QString url = urlEdit.text().trimmed();
const QString text = textEdit.text().trimmed().isEmpty() ? url : textEdit.text().trimmed();
const QString tooltip = tooltipEdit.text().trimmed();
QString link = tooltip.isEmpty() ? QStringLiteral("[%1](%2)").arg(text, url)
: QStringLiteral("[%1](%2 \"%3\")").arg(text, url, tooltip);
cursor.insertText(link);
m_target->setTextCursor(cursor);
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QKeySequence>
#include <QToolBar>
#include <functional>
class QTextEdit;
class MarkdownToolbar : public QToolBar
{
Q_OBJECT
public:
explicit MarkdownToolbar(QWidget* parent = nullptr);
void setTarget(QTextEdit* edit);
private:
void setupUi();
void wrapSelection(const QString& prefix, const QString& suffix, bool lineWise = false);
void wrapSelectionWithNumbering();
void insertLink();
void changeHeadingLevel(int delta, bool clearAll = false);
QAction* createAction(const QString& iconName,
const QString& tooltip,
const QKeySequence& shortcut,
const std::function<void()>& callback);
private:
QTextEdit* m_target = nullptr;
QAction* m_undoAction = nullptr;
QAction* m_redoAction = nullptr;
};

View file

@ -53,6 +53,14 @@ int main(int argc, char** argv)
{
QT_REQUIRE_VERSION(argc, argv, QT_VERSION_STR)
#ifdef Q_OS_WIN
// Set OPENSSL_* variables to an invalid location to prevent DLL injection via openssl.cnf.
// vcpkg by default hard-codes this to its packages location, which may be user-writable.
qputenv("OPENSSL_CONF", "::");
qputenv("OPENSSL_MODULES", "::");
qputenv("OPENSSL_ENGINES", "::");
#endif
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && defined(Q_OS_WIN)