Compare commits

..

25 Commits

Author SHA1 Message Date
Jonathan White
01a1982ee0 CLI quiet mode - write database unlock errors to stderr
* Fixes #12402
2025-08-19 20:19:32 -04:00
Eva Zhang
8d59090243 Documentation Updates (#12373)
Added documentation updates for Browser Integration and Secret Service Integration 

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
2025-08-17 17:46:15 -04:00
Juzu-O
9e8a966c23 Add URL auto-type and copy options to auto-type selection popup and menus (#12341)
* Added "Type {URL}" option to the auto-type selection popup right-click context menu
* Added "Copy {URL}" option to the auto-type selection popup right-click context menu
* Added keyboard shortcuts: CTRL+4 for "Type {URL}" and CTRL+SHIFT+4 for "Copy {URL}"
* Updated "Use Virtual Keyboard" shortcut from CTRL+4 to CTRL+5 to avoid inconsistency with order of shortcuts
* Added URL auto-type options "{URL}" and "{URL}{ENTER}" to main window entry view right-click menu
* Added URL auto-type options "{URL}" and "{URL}{ENTER}" to toolbar auto-type button dropdown menu
* Added translation strings for "Type {URL}" and "Copy {URL}" to support internationalization

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: juzu-o <3142026+juzu-o@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
2025-08-09 23:10:04 -04:00
Juzu-O
448165613d Add more granular Auto-Type confirmation settings (#12370)
This new setting gives users more control and safety:

* When Auto-Type is invoked globally (e.g., via a system-wide hotkey), the confirmation popup will always appear, letting the user confirm which credentials will be auto-typed.
* When Auto-Type is invoked from within KeePassXC's main window, the confirmation step can be skipped, since the user already can visually confirm which entry is being auto-typed.

This balances usability and security, reducing friction for intended actions while providing an extra safeguard for potentially ambiguous global Auto-Type triggers.

---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-08-09 22:36:51 -04:00
varjolintu
93423eda30 Fix inheriting browser group settings 2025-08-09 21:48:51 -04:00
thal
56b63a9e0f Update CONTRIBUTING.md
Transifex URL got changed
2025-07-13 07:09:17 -04:00
Jonathan White
2c2b686593 Update to the latest vcpkg baseline 2025-07-12 10:43:01 -04:00
Jonathan White
7c2fd5e3e9 Fix spelling in README/CONTRIBUTING 2025-07-11 19:44:40 -04:00
Jessy LANGE
7ec0f1f5a8 Add "press enter to search" option (#12263)
* Also increase auto-search timeout to 500 ms to improve user experience, especially with large databases. The previous value of 100ms guaranteed a search was performed after every character entered, even when typing relatively fast. 

---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-07-06 14:16:25 -04:00
Jonathan White
76b2f377df Enforce new-line at end of code files 2025-07-06 11:50:24 -04:00
Jonathan White
20c65fbd1e Correct Argon2 settings when creating new database
* Argon2 default parallelism settings were set to the number of threads on the computer. That is excessive on high cpu count computers.
2025-07-06 10:52:11 -04:00
Jonathan White
74326616c5 Fix two problems with URL wildcard matching
* Fixes #12255
* Periods were not being escaped in the url string before being used in a regex resulting in matching 'any character' between domain parts
* Wildcards entered as `*.` were being replaced with simply `*` resulting in unexpected matches to occur. Fixing this has a side effect of `https://*.github.com` NOT matching `https://github.com` which should be the expected behavior. Users can enter both url's if they desire to match the primary and all sub domains or leave out the wildcard entirely to use normal matching behavior.
2025-07-04 09:06:27 -04:00
Jonathan White
634a5b34f1 Improve inactivity timer
* Fix #11957
* Prevent resetting the timer hundreds of times per second
* Improve code flow for inactivity timer in general
2025-07-04 09:05:39 -04:00
Jonathan White
8c7cc90363 Fix TOTP visibility on unlock and setting change
Also fix invalid key message being visible when adding new TOTP secret to an entry.
2025-06-22 11:12:20 -04:00
Samuel Rac
217ee01572 Fix Proton Pass importer not importing email when there is no username 2025-06-22 11:00:25 -04:00
shotor
7a5cd6105c Maintain selected sort option when toggling filters 2025-06-22 10:58:17 -04:00
Jonathan White
64078933ab Fix icon download dialog not appearing above windows
* Fixes #12070
2025-06-19 16:22:17 -04:00
Jonathan White
e5fbab38d8 Remove start menu shortcuts on uninstall 2025-06-19 16:22:17 -04:00
Jonathan White
e2cf37a91f Replace newlines with HTML line breaks in message dialogs
* Keeps readability of translation strings without losing line breaks due to forced rich text display
2025-06-19 16:22:17 -04:00
Jonathan White
f62ea95499 Don't add space to invalid TOTP strings
* Fixes #11357
* Introduces validity parameter to TOTP generator function for future use elsewhere in the code base
* Fixes this in preview panel and TOTP dialog
* Disable actions to copy/show TOTP if the settings are invalid
* Show an error message on the TOTP setup dialog if the settings are invalid
* Show a TOTP icon with an x if the settings are invalid
2025-06-19 16:22:17 -04:00
Jonathan White
b5f4e98925 Fix handling of small passphrase wordlists
* Fixes #11856
* Set the minimum recommended wordlist size to 1,296 - equal to the EFF Short List
* Issue a clear warning when using a smaller wordlist but do not prevent generation of passphrases
* Improve wording when removing custom wordlist
2025-06-19 16:22:17 -04:00
Jonathan White
20aefd0c7a Show main page when editing entry or database settings
* Fixes #11891
2025-06-19 16:22:17 -04:00
Jonathan White
9baf77cbc4 Disable save button when viewing non-database screens
* Fixes #11662 - disable the save button when viewing Password Generator and Application Settings to restore previous behavior of toolbar
2025-06-19 16:22:17 -04:00
Jonathan White
5dfcc72f98 Fix minor issues with tags context menu
* Fixes #11808
* Don't show tear off menu or option to tear off if there are no tags
* Fix "No Tags" not being shown on first hover
* Fix issues when using a tag named "No Tags"
* Fix #12153 - tags becoming unsorted in the context menu when switching between database tabs
2025-06-19 16:22:17 -04:00
Kuznetsov Oleg
f2a4cc7e66 Refactor attachment handling system with enhanced UI (#12085)
* Renamed NewEntryAttachmentsDialog to EditEntryAttachmentsDialog for clarity.
* Introduced EditEntryAttachmentsDialog class to manage editing of existing attachments.
* Added functionality to preview attachments while editing them.
* Enhanced EntryAttachmentsModel with rowByKey method for better key management.
* Add image attachment support with zoom functionality.
* Add html and markdown detection.
* Improve button layout on the attachment section when editing an entry
2025-06-19 13:27:23 -04:00
134 changed files with 4122 additions and 921 deletions

View File

@@ -54,6 +54,7 @@ IncludeCategories:
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertNewlineAtEOF: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''

View File

@@ -77,7 +77,7 @@ Both issue lists are sorted by total number of comments. While not perfect, look
### Using AI
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If you use Generative AI to write the vast majority of your submission (e.g., agent-based or vibe coding) then you **must document your use of AI** in your pull request. Please include the service you used and/or model that generated the code. All code submissions go through a rigourous review process regardless of the development workflow used.
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If you use Generative AI to write the vast majority of your submission (e.g., agent-based or vibe coding) then you **must document your use of AI** in your pull request. Please include the service you used and/or model that generated the code. All code submissions go through a rigorous review process regardless of the development workflow used.
### Pull requests
@@ -87,7 +87,7 @@ All pull requests must comply with the above requirements and with the [stylegui
### Translations
Translations are managed on [Transifex](https://www.transifex.com/keepassxc/keepassxc/) which offers a web interface.
Translations are managed on [Transifex](https://explore.transifex.com/keepassxc/keepassxc/) which offers a web interface.
Please join an existing language team or request a new one if there is none.
If you open a Pull Request with new strings that require translations, you will need to run the following:

View File

@@ -223,6 +223,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/totp-copy.svg
share/icons/application/scalable/actions/totp-copy-password.svg
share/icons/application/scalable/actions/totp-edit.svg
share/icons/application/scalable/actions/totp-invalid.svg
share/icons/application/scalable/actions/trash.svg
share/icons/application/scalable/actions/url-copy.svg
share/icons/application/scalable/actions/user-guide.svg

View File

@@ -58,7 +58,7 @@ Contributors are required to adhere to the project's [Code of Conduct](CODE-OF-C
## Generative AI
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If the majority of a code submission is made using Generative AI (e.g., agent-based or vibe coding) then **we will document that in the pull request.** All code submissions go through a rigourous review process regardless of the development workflow or submitter.
Generative AI is fast becoming a first-party feature in most development environments, including GitHub itself. If the majority of a code submission is made using Generative AI (e.g., agent-based or vibe coding) then **we will document that in the pull request.** All code submissions go through a rigorous review process regardless of the development workflow or submitter.
## License

View File

@@ -37,6 +37,8 @@ include::topics/Passkeys.adoc[tags=*]
include::topics/AutoType.adoc[tags=*]
include::topics/SecretService.adoc[tags=*]
include::topics/SSHAgent.adoc[tags=*]
include::topics/Reference.adoc[tags=*]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -107,12 +107,34 @@ You can then choose to update/add the credentials to your KeePassXC database dir
4. When you have successfully submitted the password on the website, a popup will appear asking you to either update an existing entry or add a new one.
// tag::advanced[]
=== Browser statistics
You can see a cross-section of all browser-related settings applied to entries within a database through the Browser Statistics report. To access these, use the _Database_ -> _Database reports..._ menu option then click on _Browser Statistics_ on the left-hand menu. From here you can see all entries with URLs applied to them, explicitly allowed and denied URLs, and any entries with custom browser settings.
=== Browser Integration Report
You can see a cross-section of all browser-related settings applied to entries within a database through the Browser Statistics report. To access, use the _Database_ -> _Database reports..._ menu option then click on _Browser Statistics_ on the left-hand menu. From here you can see all entries with URLs applied to them, explicitly allowed and denied URLs, and any entries with custom browser settings.
.Browser statistics
TIP: You can delete remembered site settings from the report by right clicking the entry you want to reset and selecting "Delete plugin data from entry".
.Browser Integration Report
image::browser_statistics.png[]
=== Additional Fill-In Fields
Sometimes login pages have additional fields you would like to fill (e.g., account number). Use the following instructions to add them:
1. Edit the entry you want to add fields to. Go to the advanced tab and add the attributes you need. Each attribute *must start with* `KPH:`, but otherwise the name does not matter. If multiple KPH attributes are defined, they are used in alphabetical order (i.e., the order shown in KeePassXC).
2. Within the browser, navigate to the page you want to use the additional fields on. Select the "Choose Custom Login Fields" button from the extension popup window. Choose Username, Password and String Field(s). Confirm the selections.
3. Refresh the web page. The new KPH attribute(s) should be filled to the extra fields.
.String Fields Selection in Browser
image:browser_integration_additional_attribute.png[]
=== Clearing Remembered Sites
Entries that you have chosen to remember allow/deny rules are stored in their respect custom data fields. You can clear all of these remembered settings at once through the database settings. Follow these steps:
1. Go to *Database* → *Database Settings* or click the database settings icon in the toolbar.
2. Go to the *Browser Integration* tab, then click on the *Forget all site-specific settings on entries* button.
3. Confirm this action in the popup dialog. This cannot be undone once the database is saved.
+
.Clear Remembered Sites
image::browser_integration_clear_sites.png[,100%]
=== Advanced Usage
You can configure unique browser integration behavior for each entry. This allows you to add multiple URLs to an entry, hide an entry from the browser integration, and more. To access these settings, open an entry for editing then click on _Browser Integration_ option in the left-hand menu *(1)*.

View File

@@ -0,0 +1,48 @@
= KeePassXC Secret Service Integration
include::.sharedheader[]
:imagesdir: ../images
// tag::content[]
== Secret Service Integration
This feature allows KeePassXC to act as a Secret Service provider over DBus. It enables applications to store and retrieve secrets securely via the https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/[Secret Storage specification]. While running, KeePassXC acts as a Secret Service server registered on DBus so clients like seahorse, python-secretstorage, secret-tool, or other implementations can connect and access the exposed database in KeePassXC.
=== Enabling the Integration
Only one secret service provider can be enabled at a time. You may have to disable other providers, such as GNOME Keyring or KWallet, to use KeePassXC as a secret service provider. You will see a notice when attempting to enable KeePassXC as the secret service provider if another is already running.
To replace most third party secret service providers with KeePassXC, run the following shell snippet:
```bash
mkdir -p "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services"
cat > "${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services/org.freedesktop.secrets.service" <<EOF
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/bin/keepassxc
EOF
```
NOTE: You may need to restart your session or log out and back in for the changes to take effect.
1. Open KeePassXC → **Tools → Settings → Secret Service Integration** → check **Enable KeePassXC Freedesktop.org Secret Service Integration**. Then press OK to save this setting and enable the integration. Go back into this settings screen to see currently open databases that you can unlock and edit their exposure to secret service.
+
.Secret Service Settings
image::secretservice_enable_settings.png[]
2. Either click the pencil icon in the previous settings screen, or go to **Database → Database Settings → Secret Service Integration**. Enable **Expose entries under this group**, and select the desired group. All entries within this group and all subgroups will be exposed to the service.
+
.Secret Service Database Settings
image::secretservice_database_settings.png[]
3. Use apps that integrate with secret service to start saving and using credentials within KeePassXC. If you enabled confirmation prior to access, you will see the following dialog:
+
.Secret Service Access Confirmation Dialog
image::secretservice_access_dialog.png[]
TIP: When applications use `secret-tool` and you have access confirmation enabled, then you will be prompted each time credentials are requested. This is due to `secret-tool` obtaining a new process id each time it is run.
=== Implementation Details
* The user can specify the database and group that is exposed to the service.
* Desktop notifications when a secret is retrieved and access confirmation dialogs.
* `FdoSecrets::Service` is the top level DBus service. There is one `FdoSecrets::Collection` per opened database tab and each entry under the exposed database group has a corresponding `FdoSecrets::Item` DBus object.
* The following entry attributes are exposed to the secret service: Title, Username, Password, URL, Notes, TOTP, and non-protected Custom Attributes.
// end::content[]

View File

@@ -18,7 +18,16 @@ image::main_interface.png[]
*(D) Preview* Shows a preview of the selected group or entry. You can interact with most information stored in an entry from here without opening the entry for editing. You can temporarily hide this preview using the down-arrow button on the right hand side or completely disable it from the View menu.
TIP: You can enable double-click copying of entry username and password in the Application Security Settings. This is turned off by default starting with version 2.7.0.
[TIP]
====
Starting with version 2.7.0, double-click copying of entry usernames and passwords is disabled by default.
To enable it:
. Open *KeePassXC*, and navigate to *Tools* → *Settings*.
. In the left sidebar, select *General*.
. Under *Entry Management*, check the box for _"Copy data on double clicking field in entry view"_.
====
=== Toolbar
The toolbar provides a quick way to perform common tasks with your database. Some entries in the toolbar are dynamically disabled based on the information contained in the selected entry. Every common action in KeePassXC can be controlled with a keyboard shortcut as well.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.47 15.08L11 13V7H12.5V12.25L15.58 14.08C15.17 14.36 14.79 14.7 14.47 15.08M13.08 19.92C12.72 19.97 12.37 20 12 20C7.58 20 4 16.42 4 12S7.58 4 12 4 20 7.58 20 12C20 12.37 19.97 12.72 19.92 13.08C20.61 13.18 21.25 13.4 21.84 13.72C21.94 13.16 22 12.59 22 12C22 6.5 17.5 2 12 2S2 6.5 2 12C2 17.5 6.47 22 12 22C12.59 22 13.16 21.94 13.72 21.84C13.4 21.25 13.18 20.61 13.08 19.92M21.12 15.46L19 17.59L16.88 15.47L15.47 16.88L17.59 19L15.47 21.12L16.88 22.54L19 20.41L21.12 22.54L22.54 21.12L20.41 19L22.54 16.88L21.12 15.46Z" /></svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -92,6 +92,7 @@
<file>application/scalable/actions/totp-copy.svg</file>
<file>application/scalable/actions/totp-copy-password.svg</file>
<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/url-copy.svg</file>
<file>application/scalable/actions/user-guide.svg</file>

View File

@@ -573,6 +573,10 @@
<source>Font size selection</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip confirmation for main window Auto-Type actions</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidgetSecurity</name>
@@ -663,6 +667,17 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AttachmentWidget</name>
<message>
<source>Attachment Viewer</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown attachment type</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AutoType</name>
<message>
@@ -713,6 +728,10 @@
<source>Invalid placeholder: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Entry has invalid TOTP settings</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AutoTypeAssociationsModel</name>
@@ -781,15 +800,6 @@
<source>Double click a row to perform Auto-Type or find an entry using the search:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;You can use advanced search queries to find any entry in your open databases. The following shortcuts are useful:&lt;br/&gt;
Ctrl+F - Toggle database search&lt;br/&gt;
Ctrl+1 - Type username&lt;br/&gt;
Ctrl+2 - Type password&lt;br/&gt;
Ctrl+3 - Type TOTP&lt;br/&gt;
Ctrl+4 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search all open databases</source>
<translation type="unfinished"></translation>
@@ -834,6 +844,24 @@ Ctrl+4 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</source>
<source>Use Virtual Keyboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;You can use advanced search queries to find any entry in your open databases. The following shortcuts are useful:&lt;br/&gt;
Ctrl+F - Toggle database search&lt;br/&gt;
Ctrl+1 - Type username&lt;br/&gt;
Ctrl+2 - Type password&lt;br/&gt;
Ctrl+3 - Type TOTP&lt;br/&gt;
Ctrl+4 - Type URL&lt;br/&gt;
Ctrl+5 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Type {URL}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy URL</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BrowserAccessControlDialog</name>
@@ -2661,10 +2689,6 @@ This is definitely a bug, please report it to the developers.</source>
<source>No Results</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter a unique name or overwrite an existing search from the list:</source>
<translation type="unfinished"></translation>
@@ -2833,6 +2857,17 @@ Disable safe saves and try again?</source>
<source>Failed to save backup database: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditEntryAttachmentsDialog</name>
<message>
<source>Edit: %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditEntryWidget</name>
@@ -3904,21 +3939,6 @@ This may cause the affected plugins to malfunction.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EntryAttachmentsDialog</name>
<message>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>File name</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>File contents...</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EntryAttachmentsModel</name>
<message>
@@ -3944,10 +3964,6 @@ This may cause the affected plugins to malfunction.</source>
<source>Add new attachment</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Add</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remove selected attachment</source>
<translation type="unfinished"></translation>
@@ -3968,10 +3984,6 @@ This may cause the affected plugins to malfunction.</source>
<source>Save selected attachment to disk</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select files</source>
<translation type="unfinished"></translation>
@@ -4065,16 +4077,28 @@ Error: %1</source>
Would you like to overwrite the existing attachment?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to preview an attachment: Attachment not found</source>
<source>Edit</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New Text Document</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Add file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Load from Disk</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -4640,6 +4664,13 @@ You can enable the DuckDuckGo website icon service in the security section of th
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImageAttachmentsWidget</name>
<message>
<source>Zoom:</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImportWizard</name>
<message>
@@ -6466,25 +6497,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NewEntryAttachmentsDialog</name>
<message>
<source>Attachment name cannot be empty</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachment with the same name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save attachment</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New entry attachment</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NixUtils</name>
<message>
@@ -7122,14 +7134,6 @@ The following data is missing:
<comment>Password quality</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Confirm Delete Wordlist</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Do you really want to delete the wordlist &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to delete wordlist</source>
<translation type="unfinished"></translation>
@@ -7183,6 +7187,18 @@ Do you want to overwrite it?</source>
<source>Excluded characters: &quot;0&quot;, &quot;1&quot;, &quot;l&quot;, &quot;I&quot;, &quot;O&quot;, &quot;|&quot;, &quot;&quot;, &quot;B&quot;, &quot;8&quot;, &quot;G&quot;, &quot;6&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Warning: the chosen wordlist is smaller than the minimum recommended size!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Confirm Remove Wordlist</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Do you really want to remove the wordlist &quot;%1&quot;?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PasswordWidget</name>
@@ -7253,15 +7269,15 @@ Do you want to overwrite it?</source>
<context>
<name>PreviewEntryAttachmentsDialog</name>
<message>
<source>Preview entry attachment</source>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No preview available</source>
<source>Preview: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Image format not supported</source>
<source>Save</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -9197,10 +9213,6 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Shortcut %1 conflicts with &apos;%2&apos;. Overwrite shortcut?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot generate valid passphrases because the wordlist is too short</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Encrypted files are not supported.</source>
<translation type="unfinished"></translation>
@@ -9248,6 +9260,24 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Tags</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Warning: the chosen wordlist is smaller than the minimum recommended size!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid Step</source>
<comment>TOTP</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid Digits</source>
<comment>TOTP</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Fit</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>
@@ -9938,6 +9968,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Limit search to selected group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press Enter to search</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsClientModel</name>
@@ -10182,6 +10216,24 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TextAttachmentsEditWidget</name>
<message>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TextAttachmentsPreviewWidget</name>
<message>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Type:</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TotpDialog</name>
<message>
@@ -10296,6 +10348,10 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Are you sure you want to delete TOTP settings for this entry?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error: secret key is invalid</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>URLEdit</name>

View File

@@ -73,6 +73,7 @@
Name="KeePassXC - User Guide"
Target="[#CM_FP_share.docs.KeePassXC_UserGuide.html]"
WorkingDirectory="INSTALL_ROOT" />
<RemoveFile Id="RemoveShortcuts" Name="*.*" On="uninstall" />
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\KeePassXC" Name="StartMenuShortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>

View File

@@ -159,8 +159,14 @@ set(gui_SOURCES
gui/entry/EntryAttachmentsModel.cpp
gui/entry/EntryAttachmentsWidget.cpp
gui/entry/EntryAttributesModel.cpp
gui/entry/NewEntryAttachmentsDialog.cpp
gui/entry/EditEntryAttachmentsDialog.cpp
gui/entry/PreviewEntryAttachmentsDialog.cpp
gui/entry/attachments/TextAttachmentsWidget.cpp
gui/entry/attachments/ImageAttachmentsWidget.cpp
gui/entry/attachments/ImageAttachmentsView.cpp
gui/entry/attachments/TextAttachmentsPreviewWidget.cpp
gui/entry/attachments/TextAttachmentsEditWidget.cpp
gui/entry/attachments/AttachmentWidget.cpp
gui/entry/EntryHistoryModel.cpp
gui/entry/EntryModel.cpp
gui/entry/EntryView.cpp

View File

@@ -637,10 +637,16 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
// Platform-specific field clearing
actions << QSharedPointer<AutoTypeClearField>::create();
} else if (placeholder == "totp") {
// Entry totp (requires special handling)
QString totp = entry->totp();
for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
if (entry->hasValidTotp()) {
// Entry totp (requires special handling)
QString totp = entry->totp();
for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}
} else if (entry->hasTotp()) {
// Entry has TOTP configured but invalid settings
error = tr("Entry has invalid TOTP settings");
return {};
}
} else if (placeholder.startsWith("pickchars")) {
// Reset to the original capture to preserve case

View File

@@ -37,6 +37,7 @@ enum MENU_FIELD
USERNAME = 1,
PASSWORD,
TOTP,
URL,
};
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
@@ -264,7 +265,8 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
bool hasUsername = !match.first->username().isEmpty();
bool hasPassword = !match.first->password().isEmpty();
bool hasTotp = match.first->hasTotp();
bool hasTotp = match.first->hasValidTotp();
bool hasUrl = !match.first->url().isEmpty();
for (auto action : m_actionMenu->actions()) {
auto prop = action->property(MENU_FIELD_PROP_NAME);
@@ -279,6 +281,9 @@ void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
case MENU_FIELD::TOTP:
action->setEnabled(hasTotp);
break;
case MENU_FIELD::URL:
action->setEnabled(hasUrl);
break;
}
}
}
@@ -290,15 +295,19 @@ void AutoTypeSelectDialog::buildActionMenu()
auto typeUsernameAction = new QAction(icons()->icon("auto-type"), tr("Type {USERNAME}"), this);
auto typePasswordAction = new QAction(icons()->icon("auto-type"), tr("Type {PASSWORD}"), this);
auto typeTotpAction = new QAction(icons()->icon("auto-type"), tr("Type {TOTP}"), this);
auto typeUrlAction = new QAction(icons()->icon("auto-type"), tr("Type {URL}"), this);
auto copyUsernameAction = new QAction(icons()->icon("username-copy"), tr("Copy Username"), this);
auto copyPasswordAction = new QAction(icons()->icon("password-copy"), tr("Copy Password"), this);
auto copyTotpAction = new QAction(icons()->icon("totp"), tr("Copy TOTP"), this);
auto copyUrlAction = new QAction(icons()->icon("url-copy"), tr("Copy URL"), this);
m_actionMenu->addAction(typeUsernameAction);
m_actionMenu->addAction(typePasswordAction);
m_actionMenu->addAction(typeTotpAction);
m_actionMenu->addAction(typeUrlAction);
m_actionMenu->addAction(copyUsernameAction);
m_actionMenu->addAction(copyPasswordAction);
m_actionMenu->addAction(copyTotpAction);
m_actionMenu->addAction(copyUrlAction);
typeUsernameAction->setShortcut(Qt::CTRL + Qt::Key_1);
typeUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
@@ -324,10 +333,18 @@ void AutoTypeSelectDialog::buildActionMenu()
submitAutoTypeMatch(match);
});
typeUrlAction->setShortcut(Qt::CTRL + Qt::Key_4);
typeUrlAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::URL);
connect(typeUrlAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{URL}";
submitAutoTypeMatch(match);
});
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
auto typeVirtualAction = new QAction(icons()->icon("auto-type"), tr("Use Virtual Keyboard"), nullptr);
m_actionMenu->insertAction(copyUsernameAction, typeVirtualAction);
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_4);
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_5);
connect(typeVirtualAction, &QAction::triggered, this, [&] {
m_virtualMode = true;
activateCurrentMatch();
@@ -364,17 +381,29 @@ void AutoTypeSelectDialog::buildActionMenu()
}
});
copyUrlAction->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_4);
copyUrlAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::URL);
connect(copyUrlAction, &QAction::triggered, this, [&] {
auto entry = m_ui->view->currentMatch().first;
if (entry) {
clipboard()->setText(entry->resolvePlaceholder(entry->url()));
reject();
}
});
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
// Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
typeUsernameAction->setShortcutVisibleInContextMenu(true);
typePasswordAction->setShortcutVisibleInContextMenu(true);
typeTotpAction->setShortcutVisibleInContextMenu(true);
typeUrlAction->setShortcutVisibleInContextMenu(true);
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
typeVirtualAction->setShortcutVisibleInContextMenu(true);
#endif
copyUsernameAction->setShortcutVisibleInContextMenu(true);
copyPasswordAction->setShortcutVisibleInContextMenu(true);
copyTotpAction->setShortcutVisibleInContextMenu(true);
copyUrlAction->setShortcutVisibleInContextMenu(true);
}
void AutoTypeSelectDialog::showEvent(QShowEvent* event)

View File

@@ -56,7 +56,8 @@ Ctrl+F - Toggle database search&lt;br/&gt;
Ctrl+1 - Type username&lt;br/&gt;
Ctrl+2 - Type password&lt;br/&gt;
Ctrl+3 - Type TOTP&lt;br/&gt;
Ctrl+4 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</string>
Ctrl+4 - Type URL&lt;br/&gt;
Ctrl+5 - Use Virtual Keyboard (Windows Only)&lt;/p&gt;</string>
</property>
<property name="styleSheet">
<string notr="true">QToolButton {

View File

@@ -1192,7 +1192,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
if (entry->hasTotp()) {
if (entry->hasValidTotp()) {
res["totp"] = entry->totp();
}
@@ -1572,11 +1572,11 @@ bool BrowserService::handleURLWithWildcards(const QUrl& entryQUrl, const QString
}
// Escape illegal characters
auto re = firstPart.replace(QRegularExpression(R"(([!\^\$\+\-\(\)@<>]))"), "\\\\1");
auto re = Tools::escapeRegex(firstPart);
if (hostnameUsed) {
// Replace all host parts with wildcards
re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)");
re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)\\.");
}
// Append a + to the end of regex to match all paths after the last asterisk

View File

@@ -68,11 +68,9 @@ int Diceware::execute(const QStringList& arguments)
dicewareGenerator.setWordList(wordListFile);
}
if (!dicewareGenerator.isValid()) {
// We already validated the word count input so if the generator is invalid, it
// must be because the word list is too small.
err << QObject::tr("Cannot generate valid passphrases because the wordlist is too short") << Qt::endl;
return EXIT_FAILURE;
// Show a warning if the wordlist is smaller than the recommended size
if (!dicewareGenerator.isWordListValid()) {
err << QObject::tr("Warning: the chosen wordlist is smaller than the minimum recommended size!") << Qt::endl;
}
QString password = dicewareGenerator.generatePassphrase();

View File

@@ -123,7 +123,7 @@ namespace Utils
const QString& yubiKeySlot,
bool quiet)
{
auto& err = quiet ? DEVNULL : STDERR;
auto& err = STDERR;
auto compositeKey = QSharedPointer<CompositeKey>::create();
QFileInfo dbFileInfo(databaseFilename);

View File

@@ -147,6 +147,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_HidePasswordPreviewPanel, {QS("Security/HidePasswordPreviewPanel"), Roaming, true}},
{Config::Security_HideTotpPreviewPanel, {QS("Security/HideTotpPreviewPanel"), Roaming, false}},
{Config::Security_AutoTypeAsk, {QS("Security/AutotypeAsk"), Roaming, true}},
{Config::Security_AutoTypeSkipMainWindowConfirmation, {QS("Security/AutoTypeSkipMainWindowConfirmation"), Roaming, false}},
{Config::Security_IconDownloadFallback, {QS("Security/IconDownloadFallback"), Roaming, false}},
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},

View File

@@ -98,6 +98,7 @@ public:
GUI_CompactMode,
GUI_CheckForUpdates,
GUI_CheckForUpdatesIncludeBetas,
SearchWaitForEnter,
GUI_ShowExpiredEntriesOnDatabaseUnlock,
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
GUI_FontSizeOffset,
@@ -128,6 +129,7 @@ public:
Security_HidePasswordPreviewPanel,
Security_HideTotpPreviewPanel,
Security_AutoTypeAsk,
Security_AutoTypeSkipMainWindowConfirmation,
Security_IconDownloadFallback,
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,

View File

@@ -826,7 +826,12 @@ void Database::updateTagList()
}
m_tagList = tagSet.values();
m_tagList.sort();
QCollator collator;
collator.setNumericMode(true);
collator.setCaseSensitivity(Qt::CaseInsensitive);
std::sort(m_tagList.begin(), m_tagList.end(), collator);
emit tagListUpdated();
}

View File

@@ -570,6 +570,12 @@ bool Entry::hasTotp() const
return !m_data.totpSettings.isNull();
}
bool Entry::hasValidTotp() const
{
auto error = Totp::checkValidSettings(m_data.totpSettings);
return error.isEmpty();
}
bool Entry::hasPasskey() const
{
return m_attributes->hasPasskey();
@@ -581,10 +587,13 @@ void Entry::removePasskey()
removeTag(tr("Passkey"));
}
QString Entry::totp() const
QString Entry::totp(bool* isValid) const
{
if (hasTotp()) {
return Totp::generateTotp(m_data.totpSettings);
return Totp::generateTotp(m_data.totpSettings, isValid);
}
if (isValid) {
*isValid = false;
}
return {};
}
@@ -1369,9 +1378,6 @@ void Entry::setGroup(Group* group, bool trackPrevious)
setPreviousParentGroup(nullptr);
m_group->database()->addDeletedObject(m_uuid);
// Resolve references before moving to a different database
resolveReferencesBeforeDatabaseMove();
// copy custom icon to the new database
if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid())
&& !group->database()->metadata()->hasCustomIcon(iconUuid())) {
@@ -1414,44 +1420,6 @@ Database* Entry::database()
return nullptr;
}
void Entry::resolveReferencesBeforeDatabaseMove()
{
if (!m_group || !m_group->database()) {
return;
}
// Resolve references in all default attributes
for (const QString& key : EntryAttributes::DefaultAttributes) {
if (m_attributes->contains(key) && m_attributes->isReference(key)) {
QString originalValue = m_attributes->value(key);
QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10);
// Only replace if the resolution produced a different value and it's not empty
// Empty resolution means the reference couldn't be resolved, so keep original
if (!resolvedValue.isEmpty() && resolvedValue != originalValue) {
bool isProtected = m_attributes->isProtected(key);
m_attributes->set(key, resolvedValue, isProtected);
}
}
}
// Resolve references in custom attributes
const QList<QString> customKeys = m_attributes->customKeys();
for (const QString& key : customKeys) {
if (m_attributes->isReference(key)) {
QString originalValue = m_attributes->value(key);
QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10);
// Only replace if the resolution produced a different value and it's not empty
// Empty resolution means the reference couldn't be resolved, so keep original
if (!resolvedValue.isEmpty() && resolvedValue != originalValue) {
bool isProtected = m_attributes->isProtected(key);
m_attributes->set(key, resolvedValue, isProtected);
}
}
}
}
QString Entry::maskPasswordPlaceholders(const QString& str) const
{
return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive);

View File

@@ -109,7 +109,7 @@ public:
QString password() const;
QString notes() const;
QString attribute(const QString& key) const;
QString totp() const;
QString totp(bool* isValid = nullptr) const;
QString totpSettingsString() const;
QSharedPointer<Totp::Settings> totpSettings() const;
Group* previousParentGroup();
@@ -126,6 +126,7 @@ public:
void removePasskey();
bool hasTotp() const;
bool hasValidTotp() const;
bool isExpired() const;
bool willExpireInDays(int days) const;
void expireNow();
@@ -273,8 +274,6 @@ public:
bool canUpdateTimeinfo() const;
void setUpdateTimeinfo(bool value);
void resolveReferencesBeforeDatabaseMove();
signals:
/**
* Emitted when a default attribute has been changed.

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 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
@@ -1142,6 +1142,24 @@ bool Group::resolveAutoTypeEnabled() const
}
}
bool Group::resolveBrowserOptionEnabled(const QString& option) const
{
switch (resolveCustomDataTriState(option, true)) {
case Inherit:
if (!m_parent) {
return false;
}
return m_parent->resolveBrowserOptionEnabled(option);
case Enable:
return true;
case Disable:
return false;
default:
Q_ASSERT(false);
return false;
}
}
Entry* Group::addEntryWithPath(const QString& entryPath)
{
if (entryPath.isEmpty() || findEntryByPath(entryPath)) {

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 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
@@ -94,6 +94,7 @@ public:
Group::MergeMode mergeMode() const;
bool resolveSearchingEnabled() const;
bool resolveAutoTypeEnabled() const;
bool resolveBrowserOptionEnabled(const QString& option) const;
Entry* lastTopVisibleEntry() const;
bool isExpired() const;
bool isRecycled() const;

View File

@@ -20,28 +20,28 @@
#include <QCoreApplication>
#include <QTimer>
namespace
{
// Minimum timeout is 10 seconds
constexpr int MIN_TIMEOUT = 10000;
} // namespace
InactivityTimer::InactivityTimer(QObject* parent)
: QObject(parent)
, m_timer(new QTimer(this))
, m_active(false)
{
m_timer->setSingleShot(true);
m_timer->setSingleShot(false);
connect(m_timer, SIGNAL(timeout()), SLOT(timeout()));
}
void InactivityTimer::setInactivityTimeout(int inactivityTimeout)
{
Q_ASSERT(inactivityTimeout > 0);
m_timer->setInterval(inactivityTimeout);
}
void InactivityTimer::activate()
void InactivityTimer::activate(int inactivityTimeout)
{
if (!m_active) {
qApp->installEventFilter(this);
}
m_active = true;
m_resetBlocked = false;
m_timer->setInterval(qMax(MIN_TIMEOUT, inactivityTimeout));
m_timer->start();
}
@@ -54,12 +54,15 @@ void InactivityTimer::deactivate()
bool InactivityTimer::eventFilter(QObject* watched, QEvent* event)
{
const QEvent::Type type = event->type();
const auto type = event->type();
// clang-format off
if ((type >= QEvent::MouseButtonPress && type <= QEvent::KeyRelease)
|| (type >= QEvent::HoverEnter && type <= QEvent::HoverMove)
|| (type == QEvent::Wheel)) {
if (!m_resetBlocked &&
((type >= QEvent::MouseButtonPress && type <= QEvent::KeyRelease) ||
(type >= QEvent::HoverEnter && type <= QEvent::HoverMove) ||
type == QEvent::Wheel)) {
m_timer->start();
m_resetBlocked = true;
QTimer::singleShot(500, this, [this]() { m_resetBlocked = false; });
}
// clang-format on
@@ -73,7 +76,7 @@ void InactivityTimer::timeout()
return;
}
if (m_active && !m_timer->isActive()) {
if (m_active) {
emit inactivityDetected();
}

View File

@@ -29,8 +29,7 @@ class InactivityTimer : public QObject
public:
explicit InactivityTimer(QObject* parent = nullptr);
void setInactivityTimeout(int inactivityTimeout);
void activate();
void activate(int inactivityTimeout);
void deactivate();
signals:
@@ -44,7 +43,8 @@ private slots:
private:
QTimer* m_timer;
bool m_active;
bool m_active = false;
bool m_resetBlocked = false;
QMutex m_emitMutx;
};

View File

@@ -99,7 +99,7 @@ void PassphraseGenerator::setWordList(const QString& path)
m_wordlist = wordset.toList();
if (m_wordlist.size() < m_minimum_wordlist_length) {
if (!isWordListValid()) {
qWarning("Wordlist is less than minimum acceptable size: %s", qPrintable(path));
}
}
@@ -117,8 +117,7 @@ void PassphraseGenerator::setWordSeparator(const QString& separator)
QString PassphraseGenerator::generatePassphrase() const
{
// In case there was an error loading the wordlist
if (!isValid() || m_wordlist.empty()) {
if (m_wordlist.isEmpty()) {
return {};
}
@@ -149,7 +148,7 @@ QString PassphraseGenerator::generatePassphrase() const
return words.join(m_separator);
}
bool PassphraseGenerator::isValid() const
bool PassphraseGenerator::isWordListValid() const
{
return m_wordCount > 0 && m_wordlist.size() >= m_minimum_wordlist_length;
return m_wordlist.size() >= m_minWordListSize;
}

View File

@@ -40,7 +40,7 @@ public:
void setWordCase(PassphraseWordCase wordCase);
void setDefaultWordList();
void setWordSeparator(const QString& separator);
bool isValid() const;
bool isWordListValid() const;
QString generatePassphrase() const;
@@ -50,7 +50,7 @@ public:
private:
int m_wordCount;
int m_minimum_wordlist_length = 4000;
int m_minWordListSize = 1296;
PassphraseWordCase m_wordCase;
QString m_separator;
QList<QString> m_wordlist;

View File

@@ -35,6 +35,7 @@
#include <QIODevice>
#include <QLocale>
#include <QMetaProperty>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QStringList>
#include <QUrl>
@@ -478,29 +479,57 @@ namespace Tools
MimeType toMimeType(const QString& mimeName)
{
static QStringList textFormats = {
"text/",
"application/json",
"application/xml",
"application/soap+xml",
"application/x-yaml",
"application/protobuf",
};
static QStringList imageFormats = {"image/"};
const static QStringList TextFormats = {"text/",
"application/json",
"application/xml",
"application/soap+xml",
"application/x-yaml",
"application/protobuf",
"application/x-zerosize"};
const static QStringList HtmlFormats = {"text/html"};
const static QStringList MarkdownFormats = {"text/markdown"};
const static QStringList ImageFormats = {"image/"};
static auto isCompatible = [](const QString& format, const QStringList& list) {
return std::any_of(
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
};
if (isCompatible(mimeName, imageFormats)) {
if (isCompatible(mimeName, ImageFormats)) {
return MimeType::Image;
}
if (isCompatible(mimeName, textFormats)) {
if (isCompatible(mimeName, TextFormats)) {
if (isCompatible(mimeName, HtmlFormats)) {
return MimeType::Html;
} else if (isCompatible(mimeName, MarkdownFormats)) {
return MimeType::Markdown;
}
return MimeType::PlainText;
}
return MimeType::Unknown;
}
MimeType getMimeType(const QByteArray& data)
{
QMimeDatabase mimeDb;
const auto mime = mimeDb.mimeTypeForData(data);
return toMimeType(mime.name());
}
MimeType getMimeType(const QFileInfo& fileInfo)
{
QMimeDatabase mimeDb;
const auto mime = mimeDb.mimeTypeForFile(fileInfo);
return toMimeType(mime.name());
}
bool isTextMimeType(MimeType mimeType)
{
return mimeType == Tools::MimeType::PlainText || mimeType == Tools::MimeType::Html
|| mimeType == Tools::MimeType::Markdown;
}
} // namespace Tools

View File

@@ -22,6 +22,7 @@
#include "core/Global.h"
#include <QDateTime>
#include <QFileInfo>
#include <QList>
#include <QProcessEnvironment>
#include <QSet>
@@ -119,10 +120,16 @@ namespace Tools
{
Image,
PlainText,
Html,
Markdown,
Unknown
};
MimeType toMimeType(const QString& mimeName);
MimeType getMimeType(const QByteArray& data);
MimeType getMimeType(const QFileInfo& fileInfo);
bool isTextMimeType(MimeType mimeType);
} // namespace Tools
#endif // KEEPASSX_TOOLS_H

View File

@@ -210,12 +210,33 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
}
}
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time)
QString Totp::checkValidSettings(const QSharedPointer<Totp::Settings>& settings)
{
Q_ASSERT(!settings.isNull());
if (settings.isNull()) {
return QObject::tr("Invalid Settings", "TOTP");
}
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
if (secret.isNull()) {
return QObject::tr("Invalid Key", "TOTP");
}
if (settings->step == 0) {
return QObject::tr("Invalid Step", "TOTP");
}
if (settings->digits == 0) {
return QObject::tr("Invalid Digits", "TOTP");
}
return {};
}
QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, bool* isValid, const quint64 time)
{
auto error = checkValidSettings(settings);
if (!error.isEmpty()) {
if (isValid) {
*isValid = false;
}
return error;
}
const Encoder& encoder = settings->encoder;
uint step = settings->step;
@@ -229,9 +250,6 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
}
QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1()));
if (secret.isNull()) {
return QObject::tr("Invalid Key", "TOTP");
}
QCryptographicHash::Algorithm cryptoHash;
switch (settings->algorithm) {
@@ -274,6 +292,9 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
password /= encoder.alphabet.size();
}
if (isValid) {
*isValid = true;
}
return retval;
}

View File

@@ -91,8 +91,10 @@ namespace Totp
const QString& title = {},
const QString& username = {},
bool forceOtp = false);
QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull);
// Returns an empty string if settings are valid, otherwise an error message is supplied
QString checkValidSettings(const QSharedPointer<Totp::Settings>& settings);
QString
generateTotp(const QSharedPointer<Totp::Settings>& settings, bool* isValid = nullptr, const quint64 time = 0ull);
bool hasCustomSettings(const QSharedPointer<Totp::Settings>& settings);

View File

@@ -33,11 +33,11 @@
*/
Argon2Kdf::Argon2Kdf(Type type)
: Kdf::Kdf(type == Type::Argon2d ? KeePass2::KDF_ARGON2D : KeePass2::KDF_ARGON2ID)
, m_version(0x13)
, m_memory(1 << 16)
, m_parallelism(static_cast<quint32>(QThread::idealThreadCount()))
, m_version(ARGON2_DEFAULT_VERSION)
, m_memory(ARGON2_DEFAULT_MEMORY)
, m_parallelism(qMin<quint32>(QThread::idealThreadCount(), ARGON2_DEFAULT_PARALLELISM))
{
m_rounds = 10;
m_rounds = ARGON2_DEFAULT_ROUNDS;
}
quint32 Argon2Kdf::version() const
@@ -52,7 +52,7 @@ bool Argon2Kdf::setVersion(quint32 version)
m_version = version;
return true;
}
m_version = 0x13;
m_version = ARGON2_DEFAULT_VERSION;
return false;
}
@@ -73,7 +73,7 @@ bool Argon2Kdf::setMemory(quint64 kibibytes)
m_memory = kibibytes;
return true;
}
m_memory = 16;
m_memory = ARGON2_DEFAULT_MEMORY;
return false;
}
@@ -89,7 +89,7 @@ bool Argon2Kdf::setParallelism(quint32 threads)
m_parallelism = threads;
return true;
}
m_parallelism = 1;
m_parallelism = ARGON2_DEFAULT_PARALLELISM;
return false;
}

View File

@@ -20,6 +20,11 @@
#include "Kdf.h"
constexpr auto ARGON2_DEFAULT_VERSION = 0x13;
constexpr auto ARGON2_DEFAULT_ROUNDS = 10;
constexpr auto ARGON2_DEFAULT_MEMORY = 1 << 16;
constexpr auto ARGON2_DEFAULT_PARALLELISM = 4;
class Argon2Kdf : public Kdf
{
public:
@@ -47,6 +52,15 @@ public:
int benchmark(int msec) const override;
static quint64 toMebibytes(quint64 kibibytes)
{
return kibibytes >> 10;
}
static quint64 toKibibytes(quint64 mebibits)
{
return mebibits << 10;
}
quint32 m_version;
quint64 m_memory;
quint32 m_parallelism;

View File

@@ -125,7 +125,7 @@ namespace FdoSecrets
// add some informative and readonly attributes
attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex();
attrs[ItemAttributes::PathKey] = path();
if (m_backend->hasTotp()) {
if (m_backend->hasValidTotp()) {
attrs[ItemAttributes::TotpKey] = m_backend->totp();
}
return {};

View File

@@ -73,7 +73,13 @@ namespace
}
if (loginMap.contains("itemEmail")) {
entry->attributes()->set("login_email", loginMap.value("itemEmail").toString());
// Place the email value as the username if empty, otherwise set it as an attribute
const auto email = loginMap.value("itemEmail").toString();
if (entry->username().isEmpty()) {
entry->setUsername(email);
} else if (!email.isEmpty()) {
entry->attributes()->set("login_email", email);
}
}
// Set the entry url(s)

View File

@@ -129,6 +129,8 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
connect(m_generalUi->backupFilePathPicker, SIGNAL(pressed()), SLOT(selectBackupDirectory()));
connect(m_generalUi->showExpiredEntriesOnDatabaseUnlockCheckBox, SIGNAL(toggled(bool)),
SLOT(showExpiredEntriesOnDatabaseUnlockToggled(bool)));
connect(m_generalUi->autoTypeAskCheckBox, SIGNAL(toggled(bool)),
SLOT(autoTypeAskToggled(bool)));
connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)),
m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool)));
@@ -302,6 +304,9 @@ void ApplicationSettingsWidget::loadSettings()
showExpiredEntriesOnDatabaseUnlockToggled(m_generalUi->showExpiredEntriesOnDatabaseUnlockCheckBox->isChecked());
m_generalUi->autoTypeAskCheckBox->setChecked(config()->get(Config::Security_AutoTypeAsk).toBool());
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->setChecked(
config()->get(Config::Security_AutoTypeSkipMainWindowConfirmation).toBool());
autoTypeAskToggled(m_generalUi->autoTypeAskCheckBox->isChecked());
m_generalUi->autoTypeRelockDatabaseCheckBox->setChecked(config()->get(Config::Security_RelockAutoType).toBool());
if (autoType()->isAvailable()) {
@@ -445,6 +450,8 @@ void ApplicationSettingsWidget::saveSettings()
m_generalUi->showExpiredEntriesOnDatabaseUnlockOffsetSpinBox->value());
config()->set(Config::Security_AutoTypeAsk, m_generalUi->autoTypeAskCheckBox->isChecked());
config()->set(Config::Security_AutoTypeSkipMainWindowConfirmation,
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->isChecked());
config()->set(Config::Security_RelockAutoType, m_generalUi->autoTypeRelockDatabaseCheckBox->isChecked());
if (autoType()->isAvailable()) {
@@ -618,6 +625,11 @@ void ApplicationSettingsWidget::showExpiredEntriesOnDatabaseUnlockToggled(bool c
m_generalUi->showExpiredEntriesOnDatabaseUnlockOffsetSpinBox->setEnabled(checked);
}
void ApplicationSettingsWidget::autoTypeAskToggled(bool checked)
{
m_generalUi->autoTypeSkipMainWindowConfirmationCheckBox->setEnabled(checked);
}
void ApplicationSettingsWidget::selectBackupDirectory()
{
auto backupDirectory =

View File

@@ -63,6 +63,7 @@ private slots:
void rememberDatabasesToggled(bool checked);
void checkUpdatesToggled(bool checked);
void showExpiredEntriesOnDatabaseUnlockToggled(bool checked);
void autoTypeAskToggled(bool checked);
void selectBackupDirectory();
private:

View File

@@ -1157,6 +1157,42 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="autoTypeSkipMainWindowLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="autoTypeSkipMainWindowSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="autoTypeSkipMainWindowConfirmationCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Skip confirmation for main window Auto-Type actions</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="autoTypeHideExpiredEntryCheckBox">
<property name="text">

View File

@@ -877,12 +877,18 @@ void DatabaseWidget::performAutoType(const QString& sequence)
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
// TODO: Include name of previously active window in confirmation question
if (config()->get(Config::Security_AutoTypeAsk).toBool()
&& MessageBox::question(
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
!= MessageBox::Yes) {
return;
// Check if we need to ask for confirmation
bool shouldAsk = config()->get(Config::Security_AutoTypeAsk).toBool();
bool skipMainWindowConfirmation = config()->get(Config::Security_AutoTypeSkipMainWindowConfirmation).toBool();
// Show confirmation if Security_AutoTypeAsk is true AND Security_AutoTypeSkipMainWindowConfirmation is false
if (shouldAsk && !skipMainWindowConfirmation) {
// TODO: Include name of previously active window in confirmation question
if (MessageBox::question(
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
!= MessageBox::Yes) {
return;
}
}
if (sequence.isEmpty()) {
@@ -918,6 +924,16 @@ void DatabaseWidget::performAutoTypeTOTP()
performAutoType(QStringLiteral("{TOTP}"));
}
void DatabaseWidget::performAutoTypeURL()
{
performAutoType(QStringLiteral("{URL}"));
}
void DatabaseWidget::performAutoTypeURLEnter()
{
performAutoType(QStringLiteral("{URL}{ENTER}"));
}
void DatabaseWidget::openUrl()
{
auto currentEntry = currentSelectedEntry();
@@ -1527,7 +1543,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
}
break;
case EntryModel::Totp:
if (entry->hasTotp()) {
if (entry->hasValidTotp()) {
setClipboardTextAndMinimize(entry->totp());
} else {
setupTotp();
@@ -2386,7 +2402,7 @@ bool DatabaseWidget::currentEntryHasTotp()
if (!currentEntry) {
return false;
}
return currentEntry->hasTotp();
return currentEntry->hasValidTotp();
}
#ifdef WITH_XC_SSHAGENT

View File

@@ -214,6 +214,8 @@ public slots:
void performAutoTypePassword();
void performAutoTypePasswordEnter();
void performAutoTypeTOTP();
void performAutoTypeURL();
void performAutoTypeURLEnter();
void setClipboardTextAndMinimize(const QString& text);
void openUrl();
void downloadSelectedFavicons();

View File

@@ -84,7 +84,7 @@ int EditWidget::pageIndex(const QWidget* widget) const
for (int i = 0; i < m_ui->stackedWidget->count(); i++) {
auto* scrollArea = qobject_cast<QScrollArea*>(m_ui->stackedWidget->widget(i));
if (scrollArea && scrollArea->widget() == widget) {
if (scrollArea && (scrollArea == widget || scrollArea->widget() == widget)) {
return i;
}
}

View File

@@ -70,8 +70,7 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
m_ui->entryTotpLabel->installEventFilter(this);
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpLabel, SLOT(setVisible(bool)));
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpProgress, SLOT(setVisible(bool)));
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotp, SLOT(setVisible(bool)));
connect(m_ui->entryCloseButton, SIGNAL(clicked()), SLOT(hide()));
connect(m_ui->toggleUsernameButton, SIGNAL(clicked(bool)), SLOT(setUsernameVisible(bool)));
connect(m_ui->togglePasswordButton, SIGNAL(clicked(bool)), SLOT(setPasswordVisible(bool)));
@@ -95,6 +94,8 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
connect(config(), &Config::changed, this, [this](Config::ConfigKey key) {
if (key == Config::GUI_HidePreviewPanel) {
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
} else if (key == Config::Security_HideTotpPreviewPanel) {
m_ui->entryTotpButton->setChecked(!config()->get(Config::Security_HideTotpPreviewPanel).toBool());
}
refresh();
});
@@ -259,9 +260,9 @@ void EntryPreviewWidget::updateEntryTotp()
m_totpTimer.start(1000);
m_ui->entryTotpProgress->setMaximum(m_currentEntry->totpSettings()->step);
updateTotpLabel();
m_ui->entryTotp->setVisible(m_ui->entryTotpButton->isChecked());
} else {
m_ui->entryTotpLabel->hide();
m_ui->entryTotpProgress->hide();
m_ui->entryTotp->hide();
m_ui->entryTotpButton->setChecked(false);
m_ui->entryTotpLabel->clear();
m_totpTimer.stop();
@@ -546,16 +547,23 @@ void EntryPreviewWidget::updateGroupSharingTab()
void EntryPreviewWidget::updateTotpLabel()
{
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
auto totpCode = m_currentEntry->totp();
totpCode.insert(totpCode.size() / 2, " ");
m_ui->entryTotpLabel->setText(totpCode);
bool isValid = false;
auto totpCode = m_currentEntry->totp(&isValid);
if (isValid) {
totpCode.insert(totpCode.size() / 2, " ");
auto step = m_currentEntry->totpSettings()->step;
auto timeleft = step - (Clock::currentSecondsSinceEpoch() % step);
m_ui->entryTotpProgress->setValue(timeleft);
m_ui->entryTotpProgress->update();
auto step = m_currentEntry->totpSettings()->step;
auto timeleft = step - (Clock::currentSecondsSinceEpoch() % step);
m_ui->entryTotpProgress->setValue(timeleft);
m_ui->entryTotpProgress->update();
} else {
m_totpTimer.stop();
}
m_ui->entryTotpProgress->setVisible(isValid);
m_ui->entryTotpLabel->setText(totpCode);
} else {
m_ui->entryTotpLabel->clear();
m_ui->entryTotp->setVisible(false);
m_totpTimer.stop();
}
}

View File

@@ -110,47 +110,66 @@
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
<widget class="QWidget" name="entryTotp" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<item>
<widget class="QLabel" name="entryTotpLabel">
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Double click to copy to clipboard</string>
</property>
<property name="text">
<string notr="true">1234567</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="entryTotpProgress">
<property name="maximumSize">
<size>
<width>57</width>
<height>4</height>
</size>
</property>
<property name="value">
<number>50</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="entryTotpLabel">
<property name="font">
<font>
<pointsize>10</pointsize>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Double click to copy to clipboard</string>
</property>
<property name="text">
<string notr="true">1234567</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::LinksAccessibleByMouse|Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="entryTotpProgress">
<property name="maximumSize">
<size>
<width>57</width>
<height>4</height>
</size>
</property>
<property name="value">
<number>50</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QToolButton" name="entryTotpButton">

View File

@@ -38,7 +38,6 @@ IconDownloaderDialog::IconDownloaderDialog(QWidget* parent)
, m_ui(new Ui::IconDownloaderDialog())
, m_dataModel(new QStandardItemModel(this))
{
setWindowFlags(Qt::Window);
setAttribute(Qt::WA_DeleteOnClose);
m_ui->setupUi(this);

View File

@@ -172,6 +172,8 @@ MainWindow::MainWindow()
autotypeMenu->addAction(m_ui->actionEntryAutoTypePassword);
autotypeMenu->addAction(m_ui->actionEntryAutoTypePasswordEnter);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeTOTP);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeURL);
autotypeMenu->addAction(m_ui->actionEntryAutoTypeURLEnter);
m_ui->actionEntryAutoType->setMenu(autotypeMenu);
auto autoTypeButton = qobject_cast<QToolButton*>(m_ui->toolBar->widgetForAction(m_ui->actionEntryAutoType));
if (autoTypeButton) {
@@ -273,7 +275,7 @@ MainWindow::MainWindow()
m_ui->actionAllowScreenCapture->setVisible(osUtils->canPreventScreenCapture());
m_inactivityTimer = new InactivityTimer(this);
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockAllDatabases()));
applySettingsChanges();
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
@@ -387,6 +389,8 @@ MainWindow::MainWindow()
m_ui->actionEntryAutoTypePassword->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypePasswordEnter->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeTOTP->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeURL->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryAutoTypeURLEnter->setIcon(icons()->icon("auto-type"));
m_ui->actionEntryMoveUp->setIcon(icons()->icon("move-up"));
m_ui->actionEntryMoveDown->setIcon(icons()->icon("move-down"));
m_ui->actionEntryCopyUsername->setIcon(icons()->icon("username-copy"));
@@ -526,6 +530,9 @@ MainWindow::MainWindow()
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypePasswordEnter, SIGNAL(triggered()), SLOT(performAutoTypePasswordEnter()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoTypeTOTP, SIGNAL(triggered()), SLOT(performAutoTypeTOTP()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoTypeURL, SIGNAL(triggered()), SLOT(performAutoTypeURL()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypeURLEnter, SIGNAL(triggered()), SLOT(performAutoTypeURLEnter()));
m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl()));
m_actionMultiplexer.connect(m_ui->actionEntryDownloadIcon, SIGNAL(triggered()), SLOT(downloadSelectedFavicons()));
#ifdef WITH_XC_SSHAGENT
@@ -821,6 +828,8 @@ void MainWindow::updateSetTagsMenu()
return nullptr;
};
m_ui->menuTags->setTearOffEnabled(true);
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
if (dbWidget) {
// Enumerate tags applied to the selected entries
@@ -831,31 +840,30 @@ void MainWindow::updateSetTagsMenu()
}
}
// Add known database tags as actions and set checked if
// a selected entry has that tag
// Remove missing tags
const auto tagList = dbWidget->database()->tagList();
for (const auto& tag : tagList) {
auto action = actionForTag(m_ui->menuTags, tag);
if (action) {
action->setChecked(selectedTags.contains(tag));
} else {
action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
action->setCheckable(true);
action->setChecked(selectedTags.contains(tag));
m_setTagsMenuActions->addAction(action);
for (const auto action : m_ui->menuTags->actions()) {
if (!tagList.contains(action->text()) || !action->isEnabled()) {
delete action;
}
}
// Remove missing tags
for (const auto action : m_ui->menuTags->actions()) {
if (!tagList.contains(action->text())) {
action->deleteLater();
// Add known database tags as actions and set checked if
// a selected entry has that tag
for (const auto& tag : tagList) {
auto action = actionForTag(m_ui->menuTags, tag);
if (!action) {
action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
action->setCheckable(true);
m_setTagsMenuActions->addAction(action);
}
action->setChecked(selectedTags.contains(tag));
}
}
// If no tags exist in the database then show a tip to the user
if (m_ui->menuTags->isEmpty()) {
m_ui->menuTags->setTearOffEnabled(false);
auto action = m_ui->menuTags->addAction(tr("No Tags"));
action->setEnabled(false);
}
@@ -975,6 +983,8 @@ void MainWindow::updateMenuActionState()
m_ui->actionEntryAutoTypePassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword());
m_ui->actionEntryAutoTypePasswordEnter->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword());
m_ui->actionEntryAutoTypeTOTP->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryAutoTypeURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryAutoTypeURLEnter->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryAutoTypeTOTP->setVisible(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryOpenUrl->setEnabled(singleEntryOrEditing && dbWidget->currentEntryHasUrl());
m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
@@ -1016,7 +1026,7 @@ void MainWindow::updateMenuActionState()
m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && groupHasEntries && !inRecycleBin);
// Database Menu
m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave());
m_ui->actionDatabaseSave->setEnabled(databaseUnlocked && m_ui->tabWidget->canSave());
m_ui->actionDatabaseSaveAs->setEnabled(databaseUnlocked);
m_ui->actionDatabaseSaveBackup->setEnabled(databaseUnlocked);
m_ui->actionDatabaseClose->setEnabled(dbWidget);
@@ -1314,6 +1324,11 @@ void MainWindow::databaseTabChanged(int tabIndex)
m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget());
updateEntryCountLabel();
// Clear the tags menu to prevent re-use between databases
for (const auto action : m_ui->menuTags->actions()) {
delete action;
}
}
bool MainWindow::event(QEvent* event)
@@ -1650,14 +1665,9 @@ void MainWindow::showGroupContextMenu(const QPoint& globalPos)
void MainWindow::applySettingsChanges()
{
int timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000;
if (timeout <= 0) {
timeout = 60;
}
m_inactivityTimer->setInactivityTimeout(timeout);
if (config()->get(Config::Security_LockDatabaseIdle).toBool()) {
m_inactivityTimer->activate();
auto timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000;
m_inactivityTimer->activate(timeout);
} else {
m_inactivityTimer->deactivate();
}
@@ -1827,13 +1837,6 @@ void MainWindow::closeModalWindow()
}
}
void MainWindow::lockDatabasesAfterInactivity()
{
if (!m_ui->tabWidget->lockDatabases()) {
m_inactivityTimer->activate();
}
}
bool MainWindow::isTrayIconEnabled() const
{
return m_trayIcon && m_trayIcon->isVisible();
@@ -1888,7 +1891,7 @@ void MainWindow::bringToFront()
void MainWindow::handleScreenLock()
{
if (config()->get(Config::Security_LockDatabaseScreenLock).toBool()) {
lockDatabasesAfterInactivity();
lockAllDatabases();
}
}
@@ -1938,7 +1941,7 @@ void MainWindow::closeAllDatabases()
void MainWindow::lockAllDatabases()
{
lockDatabasesAfterInactivity();
m_ui->tabWidget->lockDatabases();
}
void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint)

View File

@@ -140,7 +140,6 @@ private slots:
void applySettingsChanges();
void trayIconTriggered(QSystemTrayIcon::ActivationReason reason);
void processTrayIconTrigger();
void lockDatabasesAfterInactivity();
void handleScreenLock();
void showErrorMessage(const QString& message);
void selectNextDatabaseTab();

View File

@@ -859,6 +859,34 @@
<string>Perform Auto-Type: {TOTP}</string>
</property>
</action>
<action name="actionEntryAutoTypeURL">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">{URL}</string>
</property>
<property name="iconText">
<string notr="true">{URL}</string>
</property>
<property name="toolTip">
<string notr="true">{URL}</string>
</property>
</action>
<action name="actionEntryAutoTypeURLEnter">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">{URL}{ENTER}</string>
</property>
<property name="iconText">
<string notr="true">{URL}{ENTER}</string>
</property>
<property name="toolTip">
<string notr="true">{URL}{ENTER}</string>
</property>
</action>
<action name="actionEntryDownloadIcon">
<property name="text">
<string>Download &amp;Favicon</string>

View File

@@ -91,7 +91,9 @@ MessageBox::Button MessageBox::messageBox(QWidget* parent,
msgBox.setTextFormat(Qt::RichText);
msgBox.setIcon(icon);
msgBox.setWindowTitle(title);
msgBox.setText(text);
// Replace newlines with HTML line breaks
auto fixedText = text;
msgBox.setText(fixedText.replace("\n", "<br>"));
if (m_overrideParent) {
// Force the creation of the QWindow, without this windowHandle() will return nullptr

View File

@@ -65,7 +65,7 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
connect(m_ui->buttonApply, SIGNAL(clicked()), SLOT(applyPassword()));
connect(m_ui->buttonCopy, SIGNAL(clicked()), SLOT(copyPassword()));
connect(m_ui->buttonGenerate, SIGNAL(clicked()), SLOT(regeneratePassword()));
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(deleteWordList()));
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(removeCustomWordList()));
connect(m_ui->buttonAddWordList, SIGNAL(clicked()), SLOT(addWordList()));
connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed()));
@@ -115,6 +115,11 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
m_ui->comboBoxWordList->addItem(fileName, path.absolutePath() + QDir::separator() + fileName);
}
// Set color of wordlist warning
StateColorPalette statePalette;
auto color = statePalette.color(StateColorPalette::ColorRole::False);
m_ui->labelWordListWarning->setStyleSheet(QString("QLabel { color: %1; }").arg(color.name()));
loadSettings();
}
@@ -257,9 +262,7 @@ void PasswordGeneratorWidget::regeneratePassword()
m_ui->editNewPassword->setText(m_passwordGenerator->generatePassword());
}
} else {
if (m_dicewareGenerator->isValid()) {
m_ui->editNewPassword->setText(m_dicewareGenerator->generatePassphrase());
}
m_ui->editNewPassword->setText(m_dicewareGenerator->generatePassphrase());
}
}
@@ -379,33 +382,28 @@ bool PasswordGeneratorWidget::isPasswordGenerated() const
return m_passwordGenerated;
}
void PasswordGeneratorWidget::deleteWordList()
void PasswordGeneratorWidget::removeCustomWordList()
{
if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) {
return;
}
QFile file(m_ui->comboBoxWordList->currentData().toString());
if (!file.exists()) {
return;
}
auto wordlist = m_ui->comboBoxWordList->currentText();
auto result = MessageBox::question(this,
tr("Confirm Delete Wordlist"),
tr("Do you really want to delete the wordlist \"%1\"?").arg(file.fileName()),
MessageBox::Delete | MessageBox::Cancel,
tr("Confirm Remove Wordlist"),
tr("Do you really want to remove the wordlist \"%1\"?").arg(wordlist),
MessageBox::Remove | MessageBox::Cancel,
MessageBox::Cancel);
if (result != MessageBox::Delete) {
return;
}
if (!file.remove()) {
MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString());
return;
}
if (result == MessageBox::Remove) {
QFile file(m_ui->comboBoxWordList->currentData().toString());
if (file.exists() && !file.remove()) {
MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString());
}
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
updateGenerator();
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
updateGenerator();
}
}
void PasswordGeneratorWidget::addWordList()
@@ -589,11 +587,7 @@ void PasswordGeneratorWidget::updateGenerator()
}
m_passwordGenerator->setFlags(flags);
if (m_passwordGenerator->isValid()) {
m_ui->buttonGenerate->setEnabled(true);
} else {
m_ui->buttonGenerate->setEnabled(false);
}
m_ui->buttonGenerate->setEnabled(m_passwordGenerator->isValid());
} else {
m_dicewareGenerator->setWordCase(
static_cast<PassphraseGenerator::PassphraseWordCase>(m_ui->wordCaseComboBox->currentData().toInt()));
@@ -610,11 +604,8 @@ void PasswordGeneratorWidget::updateGenerator()
m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text());
if (m_dicewareGenerator->isValid()) {
m_ui->buttonGenerate->setEnabled(true);
} else {
m_ui->buttonGenerate->setEnabled(false);
}
m_ui->labelWordListWarning->setVisible(!m_dicewareGenerator->isWordListValid());
m_ui->buttonGenerate->setEnabled(true);
}
regeneratePassword();

View File

@@ -67,7 +67,7 @@ public slots:
void applyPassword();
void copyPassword();
void setPasswordVisible(bool visible);
void deleteWordList();
void removeCustomWordList();
void addWordList();
protected:

View File

@@ -769,7 +769,113 @@ QProgressBar::chunk {
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Wordlist:</string>
</property>
</widget>
</item>
<item row="3" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLineEdit" name="editWordSeparator">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordCount">
<property name="text">
<string>Word Count:</string>
</property>
<property name="buddy">
<cstring>spinBoxLength</cstring>
</property>
</widget>
</item>
<item row="4" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="wordCaseLabel">
<property name="text">
<string>Word Case:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDeleteWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Delete selected wordlist</string>
</property>
<property name="accessibleDescription">
<string>Delete selected wordlist</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Add custom wordlist</string>
</property>
<property name="accessibleDescription">
<string>Add custom wordlist</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
@@ -814,61 +920,7 @@ QProgressBar::chunk {
</item>
</layout>
</item>
<item row="3" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="wordCaseLabel">
<property name="text">
<string>Word Case:</string>
</property>
</widget>
</item>
<item row="2" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordSeparator">
<property name="text">
<string>Word Separator:</string>
</property>
</widget>
</item>
<item row="0" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDeleteWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Delete selected wordlist</string>
</property>
<property name="accessibleDescription">
<string>Delete selected wordlist</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Add custom wordlist</string>
</property>
<property name="accessibleDescription">
<string>Add custom wordlist</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="2">
<item row="4" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QComboBox" name="wordCaseComboBox"/>
@@ -888,62 +940,23 @@ QProgressBar::chunk {
</item>
</layout>
</item>
<item row="1" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordCount">
<item row="3" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordSeparator">
<property name="text">
<string>Word Count:</string>
</property>
<property name="buddy">
<cstring>spinBoxLength</cstring>
<string>Word Separator:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLineEdit" name="editWordSeparator">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1" alignment="Qt::AlignRight">
<widget class="QLabel" name="labelWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<item row="0" column="2">
<widget class="QLabel" name="labelWordListWarning">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Wordlist:</string>
<string>Warning: the chosen wordlist is smaller than the minimum recommended size!</string>
</property>
</widget>
</item>

View File

@@ -54,6 +54,7 @@ SearchWidget::SearchWidget(QWidget* parent)
connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
connect(m_ui->searchEdit, &QLineEdit::returnPressed, this, &SearchWidget::onReturnPressed);
m_ui->searchEdit->setPlaceholderText(tr("Search (%1)…", "Search placeholder text, %1 is the keyboard shortcut")
.arg(QKeySequence(QKeySequence::Find).toString(QKeySequence::NativeText)));
@@ -69,6 +70,12 @@ SearchWidget::SearchWidget(QWidget* parent)
m_actionLimitGroup->setCheckable(true);
m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
m_actionWaitForEnter = m_searchMenu->addAction(
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::SearchWaitForEnter, state); });
m_actionWaitForEnter->setObjectName("actionSearchWaitForEnter");
m_actionWaitForEnter->setCheckable(true);
m_actionWaitForEnter->setChecked(config()->get(Config::SearchWaitForEnter).toBool());
m_ui->searchIcon->setIcon(icons()->icon("system-search"));
m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);
@@ -153,12 +160,12 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(focusSearch()));
mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
mx.connect(this, SIGNAL(enterPressed()), SLOT(switchToEntryEdit()));
}
void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
{
if (dbWidget != nullptr) {
if (dbWidget) {
// Set current search text from this database
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
// Enforce search policy
@@ -171,18 +178,15 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
void SearchWidget::startSearchTimer()
{
if (!m_searchTimer->isActive()) {
if (m_actionWaitForEnter->isChecked()) {
m_searchTimer->stop();
} else {
m_searchTimer->start(500);
}
m_searchTimer->start(100);
}
void SearchWidget::startSearch()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
m_ui->saveIcon->setVisible(true);
search(m_ui->searchEdit->text());
}
@@ -200,18 +204,18 @@ void SearchWidget::updateCaseSensitive()
emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
}
void SearchWidget::setCaseSensitive(bool state)
{
m_actionCaseSensitive->setChecked(state);
updateCaseSensitive();
}
void SearchWidget::updateLimitGroup()
{
config()->set(Config::SearchLimitGroup, m_actionLimitGroup->isChecked());
emit limitGroupChanged(m_actionLimitGroup->isChecked());
}
void SearchWidget::setCaseSensitive(bool state)
{
m_actionCaseSensitive->setChecked(state);
updateCaseSensitive();
}
void SearchWidget::setLimitGroup(bool state)
{
m_actionLimitGroup->setChecked(state);
@@ -244,3 +248,12 @@ void SearchWidget::showSearchMenu()
{
m_searchMenu->exec(m_ui->searchEdit->mapToGlobal(m_ui->searchEdit->rect().bottomLeft()));
}
void SearchWidget::onReturnPressed()
{
if (m_actionWaitForEnter->isChecked()) {
emit search(m_ui->searchEdit->text());
} else {
emit enterPressed();
}
}

View File

@@ -68,6 +68,7 @@ public slots:
void clearSearch();
private slots:
void onReturnPressed();
void startSearchTimer();
void startSearch();
void updateCaseSensitive();
@@ -83,6 +84,7 @@ private:
QTimer* m_clearSearchTimer;
QAction* m_actionCaseSensitive;
QAction* m_actionLimitGroup;
QAction* m_actionWaitForEnter;
QMenu* m_searchMenu;
};

View File

@@ -39,6 +39,7 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
m_step = m_entry->totpSettings()->step;
resetCounter();
updateProgressBar();
updateSeconds();
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
@@ -88,10 +89,15 @@ void TotpDialog::updateSeconds()
void TotpDialog::updateTotp()
{
QString totpCode = m_entry->totp();
QString firstHalf = totpCode.left(totpCode.size() / 2);
QString secondHalf = totpCode.mid(totpCode.size() / 2);
m_ui->totpLabel->setText(firstHalf + " " + secondHalf);
bool isValid = false;
QString totpCode = m_entry->totp(&isValid);
if (isValid) {
totpCode.insert(totpCode.size() / 2, " ");
}
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid);
m_ui->progressBar->setVisible(isValid);
m_ui->timerLabel->setVisible(isValid);
m_ui->totpLabel->setText(totpCode);
}
void TotpDialog::resetCounter()

View File

@@ -108,6 +108,7 @@ void TotpSetupDialog::init()
m_ui->algorithmComboBox->addItem(item.first, item.second);
}
m_ui->algorithmComboBox->setCurrentIndex(0);
m_ui->invalidKeyLabel->setVisible(false);
// Read entry totp settings
auto settings = m_entry->totpSettings();
@@ -127,5 +128,8 @@ void TotpSetupDialog::init()
m_ui->algorithmComboBox->setCurrentIndex(index);
}
}
auto error = Totp::checkValidSettings(settings);
m_ui->invalidKeyLabel->setVisible(!error.isEmpty());
}
}

View File

@@ -14,6 +14,22 @@
<string>Setup TOTP</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="invalidKeyLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Error: secret key is invalid</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
@@ -210,6 +226,7 @@
<zorder>customSettingsGroup</zorder>
<zorder>buttonBox</zorder>
<zorder>groupBox</zorder>
<zorder>invalidKeyLabel</zorder>
</widget>
<tabstops>
<tabstop>seedEdit</tabstop>

View File

@@ -99,6 +99,8 @@ DatabaseSettingsDialog::~DatabaseSettingsDialog() = default;
void DatabaseSettingsDialog::load(const QSharedPointer<Database>& db)
{
// Default to the main page on load
setCurrentPage(0);
setHeadline(tr("Database Settings: %1").arg(db->canonicalFilePath()));
m_generalWidget->loadSettings(db);

View File

@@ -159,12 +159,7 @@ void DatabaseSettingsWidgetEncryption::initialize()
// Set up KDF algorithms
loadKdfAlgorithms();
// Perform Benchmark if requested
if (isNewDatabase) {
if (IS_ARGON2(m_ui->kdfComboBox->currentData())) {
m_ui->memorySpinBox->setValue(16);
m_ui->parallelismSpinBox->setValue(2);
}
benchmarkTransformRounds();
}
@@ -225,7 +220,7 @@ void DatabaseSettingsWidgetEncryption::loadKdfParameters()
// Set Argon2 parameters
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
m_ui->transformRoundsSpinBox->setValue(argon2Kdf->rounds());
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory()) / (1 << 10));
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(argon2Kdf->memory()));
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
} else if (!dbIsArgon2 && !kdfIsArgon2) {
// Set AES KDF parameters
@@ -233,8 +228,8 @@ void DatabaseSettingsWidgetEncryption::loadKdfParameters()
} else {
// Set reasonable defaults and then benchmark
if (kdfIsArgon2) {
m_ui->memorySpinBox->setValue(16);
m_ui->parallelismSpinBox->setValue(2);
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(ARGON2_DEFAULT_MEMORY));
m_ui->parallelismSpinBox->setValue(ARGON2_DEFAULT_PARALLELISM);
}
benchmarkTransformRounds();
}
@@ -343,7 +338,7 @@ bool DatabaseSettingsWidgetEncryption::saveSettings()
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
if (IS_ARGON2(kdf->uuid())) {
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10));
argon2Kdf->setMemory(Argon2Kdf::toKibibytes(m_ui->memorySpinBox->value()));
argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()));
}
@@ -377,8 +372,8 @@ void DatabaseSettingsWidgetEncryption::benchmarkTransformRounds(int millisecs)
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
// Set a small static number of rounds for the benchmark
argon2Kdf->setRounds(4);
if (!argon2Kdf->setMemory(static_cast<quint64>(m_ui->memorySpinBox->value()) * (1 << 10))) {
m_ui->memorySpinBox->setValue(static_cast<int>(argon2Kdf->memory() / (1 << 10)));
if (!argon2Kdf->setMemory(Argon2Kdf::toKibibytes(m_ui->memorySpinBox->value()))) {
m_ui->memorySpinBox->setValue(Argon2Kdf::toMebibytes(argon2Kdf->memory()));
}
if (!argon2Kdf->setParallelism(static_cast<quint32>(m_ui->parallelismSpinBox->value()))) {
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());

View File

@@ -0,0 +1,52 @@
/*
* 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 "EditEntryAttachmentsDialog.h"
#include "ui_EditEntryAttachmentsDialog.h"
#include <core/EntryAttachments.h>
#include <QDebug>
#include <QMessageBox>
#include <QMimeDatabase>
#include <QPushButton>
EditEntryAttachmentsDialog::EditEntryAttachmentsDialog(QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::EditEntryAttachmentsDialog)
{
m_ui->setupUi(this);
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(m_ui->dialogButtons, &QDialogButtonBox::accepted, this, &EditEntryAttachmentsDialog::accept);
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &EditEntryAttachmentsDialog::reject);
}
EditEntryAttachmentsDialog::~EditEntryAttachmentsDialog() = default;
void EditEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
{
setWindowTitle(tr("Edit: %1").arg(attachment.name));
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadWrite);
}
attachments::Attachment EditEntryAttachmentsDialog::getAttachment() const
{
return m_ui->attachmentWidget->getAttachment();
}

View File

@@ -17,32 +17,29 @@
#pragma once
#include "attachments/AttachmentTypes.h"
#include <QDialog>
#include <QPointer>
namespace Ui
{
class EntryAttachmentsDialog;
class EditEntryAttachmentsDialog;
}
class QByteArray;
class EntryAttachments;
class NewEntryAttachmentsDialog : public QDialog
class EditEntryAttachmentsDialog : public QDialog
{
Q_OBJECT
public:
explicit NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent = nullptr);
~NewEntryAttachmentsDialog() override;
explicit EditEntryAttachmentsDialog(QWidget* parent = nullptr);
~EditEntryAttachmentsDialog() override;
private slots:
void saveAttachment();
void fileNameTextChanged(const QString& fileName);
void setAttachment(attachments::Attachment attachment);
attachments::Attachment getAttachment() const;
private:
bool validateFileName(const QString& fileName, QString& error) const;
QPointer<EntryAttachments> m_attachments;
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
QScopedPointer<Ui::EditEntryAttachmentsDialog> m_ui;
};

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditEntryAttachmentsDialog</class>
<widget class="QDialog" name="EditEntryAttachmentsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>447</width>
<height>424</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true"/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="dialogButtons">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>AttachmentWidget</class>
<extends>QWidget</extends>
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EntryAttachmentsDialog</class>
<widget class="QDialog" name="EntryAttachmentsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>402</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="titleEdit">
<property name="placeholderText">
<string>File name</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="styleSheet">
<string notr="true">color: #FF9696</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="attachmentTextEdit">
<property name="placeholderText">
<string>File contents...</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="dialogButtons">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -130,6 +130,15 @@ QString EntryAttachmentsModel::keyByIndex(const QModelIndex& index) const
return m_entryAttachments->keys().at(index.row());
}
int EntryAttachmentsModel::rowByKey(const QString& key) const
{
if (!m_entryAttachments) {
return -1;
}
return m_entryAttachments->keys().indexOf(key);
}
void EntryAttachmentsModel::attachmentChange(const QString& key)
{
int row = m_entryAttachments->keys().indexOf(key);

View File

@@ -44,6 +44,7 @@ public:
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
QString keyByIndex(const QModelIndex& index) const;
int rowByKey(const QString& key) const;
private slots:
void attachmentChange(const QString& key);

View File

@@ -17,23 +17,41 @@
#include "EntryAttachmentsWidget.h"
#include "EditEntryAttachmentsDialog.h"
#include "EntryAttachmentsModel.h"
#include "NewEntryAttachmentsDialog.h"
#include "PreviewEntryAttachmentsDialog.h"
#include "ui_EntryAttachmentsWidget.h"
#include <QDebug>
#include <QDropEvent>
#include <QLineEdit>
#include <QMenu>
#include <QMimeData>
#include <QStandardPaths>
#include <QTemporaryFile>
#include "EntryAttachmentsModel.h"
#include "core/EntryAttachments.h"
#include "core/Tools.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
namespace
{
constexpr const char* DefaultName = "New Attachment";
constexpr const char* Suffix = ".txt";
QString generateUniqueName(const QString& name, const QStringList& existingNames)
{
uint64_t i = 0;
QString newName = QStringLiteral("%1%2").arg(name).arg(Suffix);
while (existingNames.contains(newName)) {
newName = QStringLiteral("%1_%2%3").arg(name).arg(++i).arg(Suffix);
}
return newName;
}
} // namespace
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EntryAttachmentsWidget)
@@ -67,14 +85,30 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
// clang-format on
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(previewSelectedAttachment()));
connect(m_ui->attachmentsView, &QAbstractItemView::doubleClicked, [this](const QModelIndex&) {
m_readOnly ? previewSelectedAttachment() : editSelectedAttachment();
});
connect(m_ui->attachmentsView->itemDelegate(), &QAbstractItemDelegate::commitData, [this](QWidget* editor) {
if (auto lineEdit = qobject_cast<QLineEdit*>(editor)) {
auto index = m_attachmentsModel->rowByKey(lineEdit->text());
m_ui->attachmentsView->setCurrentIndex(m_attachmentsModel->index(index, 0));
}
});
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
connect(m_ui->newAttachmentButton, SIGNAL(clicked()), SLOT(newAttachments()));
connect(m_ui->editAttachmentButton, SIGNAL(clicked()), SLOT(editSelectedAttachment()));
connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment()));
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
auto addButtonMenu = new QMenu(this);
addButtonMenu->addAction(tr("New Text Document"), this, &EntryAttachmentsWidget::newAttachments);
addButtonMenu->addAction(tr("Load from Disk…"), this, QOverload<>::of(&EntryAttachmentsWidget::insertAttachments));
m_ui->addAttachmentButton->setMenu(addButtonMenu);
updateButtonsVisible();
updateButtonsEnabled();
}
@@ -175,19 +209,34 @@ void EntryAttachmentsWidget::newAttachments()
return;
}
NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this);
if (newEntryDialog.exec() == QDialog::Accepted) {
emit widgetUpdated();
}
// Create a temporary file to allow the user to edit the attachment
auto newFileName = generateUniqueName(DefaultName, m_entryAttachments->keys());
m_entryAttachments->set(newFileName, QByteArray());
auto currentIndex = m_attachmentsModel->index(m_attachmentsModel->rowByKey(newFileName), 0);
m_ui->attachmentsView->setCurrentIndex(currentIndex);
m_ui->attachmentsView->edit(currentIndex);
}
void EntryAttachmentsWidget::previewSelectedAttachment()
{
Q_ASSERT(m_entryAttachments);
const auto index = m_ui->attachmentsView->selectionModel()->selectedIndexes().first();
const auto selectionModel = m_ui->attachmentsView->selectionModel();
if (!selectionModel) {
qWarning() << "Failed to preview an attachment: No selection model";
return;
}
auto indexes = selectionModel->selectedIndexes();
if (indexes.empty()) {
qWarning() << "Failed to edit an attachment: No attachment selected";
return;
}
const auto index = indexes.first();
if (!index.isValid()) {
qWarning() << tr("Failed to preview an attachment: Attachment not found");
qWarning() << "Failed to preview an attachment: Attachment not found";
return;
}
@@ -198,7 +247,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
auto data = m_entryAttachments->value(name);
PreviewEntryAttachmentsDialog previewDialog(this);
previewDialog.setAttachment(name, data);
previewDialog.setAttachment({name, data});
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
@@ -208,7 +257,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
&previewDialog,
[&previewDialog, &name, this](const QString& key) {
if (key == name) {
previewDialog.setAttachment(name, m_entryAttachments->value(name));
previewDialog.setAttachment({name, m_entryAttachments->value(name)});
}
});
@@ -218,6 +267,51 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
setFocus();
}
void EntryAttachmentsWidget::editSelectedAttachment()
{
Q_ASSERT(m_entryAttachments);
const auto selectionModel = m_ui->attachmentsView->selectionModel();
if (!selectionModel) {
qWarning() << "Failed to edit an attachment: No selection model";
return;
}
const auto selectedIndexes = selectionModel->selectedIndexes();
if (selectedIndexes.isEmpty()) {
qWarning() << "Failed to edit an attachment: No attachment selected";
return;
}
const auto index = selectedIndexes.first();
if (!index.isValid()) {
qWarning() << "Failed to edit an attachment: Attachment not found";
return;
}
// Set selection to the first
m_ui->attachmentsView->setCurrentIndex(index);
auto name = m_attachmentsModel->keyByIndex(index);
auto data = m_entryAttachments->value(name);
EditEntryAttachmentsDialog editDialog(this);
editDialog.setAttachment({name, data});
if (editDialog.exec() == QDialog::Accepted) {
auto attachment = editDialog.getAttachment();
// Edit dialog cannot change the name of the attachment
if (attachment.name == name) {
m_entryAttachments->set(attachment.name, attachment.data);
}
}
// Set focus back to the widget to allow keyboard navigation
setFocus();
}
void EntryAttachmentsWidget::removeSelectedAttachments()
{
Q_ASSERT(m_entryAttachments);
@@ -346,35 +440,38 @@ void EntryAttachmentsWidget::openSelectedAttachments()
void EntryAttachmentsWidget::updateButtonsEnabled()
{
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
const auto selectionModel = m_ui->attachmentsView->selectionModel();
const bool hasSelection = selectionModel && selectionModel->hasSelection();
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
m_ui->newAttachmentButton->setEnabled(!m_readOnly);
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly);
if (const auto indexes = selectionModel ? selectionModel->selectedIndexes() : QModelIndexList{}; !indexes.empty()) {
auto mimeType = Tools::getMimeType(m_entryAttachments->value(m_attachmentsModel->keyByIndex(indexes.first())));
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly && Tools::isTextMimeType(mimeType));
}
m_ui->saveAttachmentButton->setEnabled(hasSelection);
m_ui->previewAttachmentButton->setEnabled(hasSelection);
m_ui->openAttachmentButton->setEnabled(hasSelection);
updateSpacers();
updateLinesVisibility();
}
void EntryAttachmentsWidget::updateSpacers()
void EntryAttachmentsWidget::updateLinesVisibility()
{
if (m_buttonsVisible && !m_readOnly) {
m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding);
} else {
m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
}
m_ui->editPreviewLine->setVisible(m_buttonsVisible && !m_readOnly);
m_ui->previewRemoveLine->setVisible(m_buttonsVisible && !m_readOnly);
}
void EntryAttachmentsWidget::updateButtonsVisible()
{
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
m_ui->newAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
m_ui->editAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
updateSpacers();
updateLinesVisibility();
}
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)

View File

@@ -58,6 +58,7 @@ signals:
private slots:
void insertAttachments();
void newAttachments();
void editSelectedAttachment();
void previewSelectedAttachment();
void removeSelectedAttachments();
void saveSelectedAttachments();
@@ -68,7 +69,7 @@ private slots:
void attachmentModifiedExternally(const QString& key, const QString& filePath);
private:
void updateSpacers();
void updateLinesVisibility();
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>337</width>
<height>258</height>
<width>332</width>
<height>312</height>
</rect>
</property>
<property name="windowTitle">
@@ -47,7 +47,7 @@
</item>
<item>
<widget class="QWidget" name="actionsWidget" native="true">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0,0,0,1,0,0">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0,0,0,0,0">
<property name="leftMargin">
<number>0</number>
</property>
@@ -60,16 +60,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="newAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>New</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addAttachmentButton">
<property name="enabled">
@@ -79,22 +69,26 @@
<string>Add new attachment</string>
</property>
<property name="text">
<string>Add</string>
<string>Add file…</string>
</property>
</widget>
</item>
<item>
<spacer name="previewVSpacer">
<widget class="QPushButton" name="editAttachmentButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="editPreviewLine">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</widget>
</item>
<item>
<widget class="QPushButton" name="previewAttachmentButton">
@@ -128,22 +122,19 @@
<string>Save selected attachment to disk</string>
</property>
<property name="text">
<string>Save</string>
<string>Save</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<widget class="Line" name="previewRemoveLine">
<property name="enabled">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>173</height>
</size>
</property>
</spacer>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeAttachmentButton">
@@ -164,7 +155,7 @@
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>

View File

@@ -297,7 +297,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
break;
case Totp:
if (entry->hasTotp()) {
return icons()->icon("totp");
return entry->hasValidTotp() ? icons()->icon("totp") : icons()->icon("totp-invalid");
}
break;
case PasswordStrength:

View File

@@ -1,90 +0,0 @@
/*
* 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 "NewEntryAttachmentsDialog.h"
#include "core/EntryAttachments.h"
#include "ui_EntryAttachmentsDialog.h"
#include <QMessageBox>
#include <QPushButton>
NewEntryAttachmentsDialog::NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent)
: QDialog(parent)
, m_attachments(std::move(attachments))
, m_ui(new Ui::EntryAttachmentsDialog)
{
Q_ASSERT(m_attachments);
m_ui->setupUi(this);
setWindowTitle(tr("New entry attachment"));
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(m_ui->dialogButtons, SIGNAL(accepted()), this, SLOT(saveAttachment()));
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
connect(m_ui->titleEdit, SIGNAL(textChanged(const QString&)), this, SLOT(fileNameTextChanged(const QString&)));
fileNameTextChanged(m_ui->titleEdit->text());
}
NewEntryAttachmentsDialog::~NewEntryAttachmentsDialog() = default;
bool NewEntryAttachmentsDialog::validateFileName(const QString& fileName, QString& error) const
{
if (fileName.isEmpty()) {
error = tr("Attachment name cannot be empty");
return false;
}
if (m_attachments->hasKey(fileName)) {
error = tr("Attachment with the same name already exists");
return false;
}
return true;
}
void NewEntryAttachmentsDialog::saveAttachment()
{
auto fileName = m_ui->titleEdit->text();
auto text = m_ui->attachmentTextEdit->toPlainText().toUtf8();
QString error;
if (!validateFileName(fileName, error)) {
QMessageBox::warning(this, tr("Save attachment"), error);
return;
}
m_attachments->set(fileName, text);
accept();
}
void NewEntryAttachmentsDialog::fileNameTextChanged(const QString& fileName)
{
QString error;
bool valid = validateFileName(fileName, error);
m_ui->errorLabel->setText(error);
m_ui->errorLabel->setVisible(!valid);
auto okButton = m_ui->dialogButtons->button(QDialogButtonBox::Ok);
if (okButton) {
okButton->setDisabled(!valid);
}
}

View File

@@ -16,117 +16,48 @@
*/
#include "PreviewEntryAttachmentsDialog.h"
#include "ui_EntryAttachmentsDialog.h"
#include "ui_PreviewEntryAttachmentsDialog.h"
#include <QDebug>
#include <QDialogButtonBox>
#include <QMimeDatabase>
#include <QPushButton>
#include <QTextCursor>
#include <QtDebug>
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::EntryAttachmentsDialog)
, m_ui(new Ui::PreviewEntryAttachmentsDialog)
{
m_ui->setupUi(this);
setWindowTitle(tr("Preview entry attachment"));
// Disable the help button in the title bar
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// Set to read-only
m_ui->titleEdit->setReadOnly(true);
m_ui->attachmentTextEdit->setReadOnly(true);
m_ui->errorLabel->setVisible(false);
// Initialize dialog buttons
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
closeButton->setDefault(true);
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
auto saveButton = m_ui->dialogButtons->button(QDialogButtonBox::Save);
saveButton->setText(tr("Save…"));
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &PreviewEntryAttachmentsDialog::reject);
connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
auto pressedButton = m_ui->dialogButtons->standardButton(button);
const auto attachment = m_ui->attachmentWidget->getAttachment();
if (pressedButton == QDialogButtonBox::Open) {
emit openAttachment(m_name);
emit openAttachment(attachment.name);
} else if (pressedButton == QDialogButtonBox::Save) {
emit saveAttachment(m_name);
emit saveAttachment(attachment.name);
}
});
}
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data)
void PreviewEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
{
m_name = name;
m_ui->titleEdit->setText(m_name);
setWindowTitle(tr("Preview: %1").arg(attachment.name));
m_type = attachmentType(data);
m_data = data;
m_imageCache = QImage();
update();
}
void PreviewEntryAttachmentsDialog::update()
{
if (m_type == Tools::MimeType::Unknown) {
updateTextAttachment(tr("No preview available").toUtf8());
} else if (m_type == Tools::MimeType::Image) {
updateImageAttachment(m_data);
} else if (m_type == Tools::MimeType::PlainText) {
updateTextAttachment(m_data);
}
}
void PreviewEntryAttachmentsDialog::updateTextAttachment(const QByteArray& data)
{
m_ui->attachmentTextEdit->setPlainText(QString::fromUtf8(data));
}
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QByteArray& data)
{
if (m_imageCache.isNull() && !m_imageCache.loadFromData(data)) {
updateTextAttachment(tr("Image format not supported").toUtf8());
return;
}
updateImageAttachment(m_imageCache);
}
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QImage& image)
{
m_ui->attachmentTextEdit->clear();
auto cursor = m_ui->attachmentTextEdit->textCursor();
cursor.insertImage(image.scaled(calculateImageSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
QSize PreviewEntryAttachmentsDialog::calculateImageSize()
{
// Scale the image to the contents rect minus another set of margins to avoid scrollbars
auto margins = m_ui->attachmentTextEdit->contentsMargins();
auto size = m_ui->attachmentTextEdit->contentsRect().size();
size.setWidth(size.width() - margins.left() - margins.right());
size.setHeight(size.height() - margins.top() - margins.bottom());
return size;
}
Tools::MimeType PreviewEntryAttachmentsDialog::attachmentType(const QByteArray& data) const
{
QMimeDatabase mimeDb{};
const auto mime = mimeDb.mimeTypeForData(data);
return Tools::toMimeType(mime.name());
}
void PreviewEntryAttachmentsDialog::resizeEvent(QResizeEvent* event)
{
QDialog::resizeEvent(event);
if (m_type == Tools::MimeType::Image) {
update();
}
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadOnly);
}

View File

@@ -17,14 +17,16 @@
#pragma once
#include "core/Tools.h"
#include "attachments/AttachmentTypes.h"
#include <core/Tools.h>
#include <QDialog>
#include <QPointer>
namespace Ui
{
class EntryAttachmentsDialog;
class PreviewEntryAttachmentsDialog;
}
class PreviewEntryAttachmentsDialog : public QDialog
@@ -35,29 +37,12 @@ public:
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
~PreviewEntryAttachmentsDialog() override;
void setAttachment(const QString& name, const QByteArray& data);
void setAttachment(attachments::Attachment attachment);
signals:
void openAttachment(const QString& name);
void saveAttachment(const QString& name);
protected:
void resizeEvent(QResizeEvent* event) override;
private:
Tools::MimeType attachmentType(const QByteArray& data) const;
void update();
void updateTextAttachment(const QByteArray& data);
void updateImageAttachment(const QByteArray& data);
void updateImageAttachment(const QImage& data);
QSize calculateImageSize();
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
QString m_name;
QByteArray m_data;
QImage m_imageCache;
Tools::MimeType m_type{Tools::MimeType::Unknown};
QScopedPointer<Ui::PreviewEntryAttachmentsDialog> m_ui;
};

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PreviewEntryAttachmentsDialog</class>
<widget class="QDialog" name="PreviewEntryAttachmentsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>557</width>
<height>454</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item>
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="dialogButtons">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>AttachmentWidget</class>
<extends>QWidget</extends>
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,37 @@
/*
* 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 <QByteArray>
#include <QString>
namespace attachments
{
struct Attachment
{
QString name;
QByteArray data;
};
enum class OpenMode
{
ReadOnly,
ReadWrite
};
} // namespace attachments

View File

@@ -0,0 +1,89 @@
/*
* 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 "AttachmentWidget.h"
#include "ImageAttachmentsWidget.h"
#include "TextAttachmentsWidget.h"
#include <core/Tools.h>
#include <QLabel>
#include <QVBoxLayout>
AttachmentWidget::AttachmentWidget(QWidget* parent)
: QWidget(parent)
{
setWindowTitle(tr("Attachment Viewer"));
auto verticalLayout = new QVBoxLayout(this);
verticalLayout->setSpacing(0);
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
verticalLayout->setContentsMargins(0, 0, 0, 0);
verticalLayout->setAlignment(Qt::AlignCenter);
}
AttachmentWidget::~AttachmentWidget() = default;
void AttachmentWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
{
m_attachment = std::move(attachment);
m_mode = mode;
updateUi();
}
void AttachmentWidget::updateUi()
{
auto type = Tools::getMimeType(m_attachment.data);
if (m_attachmentWidget) {
layout()->removeWidget(m_attachmentWidget);
m_attachmentWidget->deleteLater();
}
if (Tools::isTextMimeType(type)) {
auto widget = new TextAttachmentsWidget(this);
widget->openAttachment(m_attachment, m_mode);
m_attachmentWidget = widget;
} else if (type == Tools::MimeType::Image) {
auto widget = new ImageAttachmentsWidget(this);
widget->openAttachment(m_attachment, m_mode);
m_attachmentWidget = widget;
} else {
auto label = new QLabel(tr("Unknown attachment type"), this);
label->setAlignment(Qt::AlignCenter);
m_attachmentWidget = label;
}
Q_ASSERT(m_attachmentWidget);
m_attachmentWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout()->addWidget(m_attachmentWidget);
}
attachments::Attachment AttachmentWidget::getAttachment() const
{
// Text attachments can be edited at this time so pass this call forward
if (auto textWidget = qobject_cast<TextAttachmentsWidget*>(m_attachmentWidget)) {
return textWidget->getAttachment();
}
return m_attachment;
}

View File

@@ -0,0 +1,67 @@
/*
* 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 "AttachmentTypes.h"
#include <core/Tools.h>
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class AttachmentWidget;
}
/**
* @brief The AttachmentWidget class provides a way to manage attachments in a GUI application.
*
*/
class AttachmentWidget : public QWidget
{
Q_OBJECT
public:
explicit AttachmentWidget(QWidget* parent = nullptr);
~AttachmentWidget() override;
/**
* @brief Opens an attachment in the specified mode.
*
* @param attachment - The attachment to be opened.
* @param mode - The mode in which to open the attachment (read-only or read-write).
*/
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
/**
* @brief Get the current attachment.
*
* @return Attachment - The current attachment.
*/
attachments::Attachment getAttachment() const;
private:
void updateUi();
QPointer<QWidget> m_attachmentWidget;
attachments::Attachment m_attachment;
attachments::OpenMode m_mode;
};

View File

@@ -0,0 +1,98 @@
/*
* 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 "ImageAttachmentsView.h"
#include <QDebug>
#include <QWheelEvent>
#include <limits>
ImageAttachmentsView::ImageAttachmentsView(QWidget* parent)
: QGraphicsView(parent)
{
}
void ImageAttachmentsView::wheelEvent(QWheelEvent* event)
{
if (event->modifiers() == Qt::ControlModifier) {
emit ctrlWheelEvent(event);
return;
}
QGraphicsView::wheelEvent(event);
}
void ImageAttachmentsView::resizeEvent(QResizeEvent* event)
{
QGraphicsView::resizeEvent(event);
if (m_autoFitInView) {
fitSceneInView();
}
}
void ImageAttachmentsView::showEvent(QShowEvent* event)
{
if (m_autoFitInView) {
fitSceneInView();
}
QGraphicsView::showEvent(event);
}
void ImageAttachmentsView::fitSceneInView()
{
if (auto scene = ImageAttachmentsView::scene()) {
ImageAttachmentsView::fitInView(scene->itemsBoundingRect(), Qt::KeepAspectRatio);
}
}
void ImageAttachmentsView::enableAutoFitInView()
{
m_autoFitInView = true;
fitSceneInView();
}
void ImageAttachmentsView::disableAutoFitInView()
{
m_autoFitInView = false;
}
bool ImageAttachmentsView::isAutoFitInViewActivated() const
{
return m_autoFitInView;
}
double ImageAttachmentsView::calculateFitInViewFactor() const
{
auto viewPort = viewport();
if (auto currentScene = scene(); currentScene && viewPort) {
const auto itemsRect = currentScene->itemsBoundingRect().size();
// If the image rect is empty
if (itemsRect.isEmpty()) {
return std::numeric_limits<double>::quiet_NaN();
}
const auto viewPortSize = viewPort->size();
// Calculate the zoom factor based on the current size and the image rect
return std::min(viewPortSize.width() / itemsRect.width(), viewPortSize.height() / itemsRect.height());
}
return std::numeric_limits<double>::quiet_NaN();
}

View File

@@ -0,0 +1,46 @@
/*
* 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 <QGraphicsView>
class ImageAttachmentsView : public QGraphicsView
{
Q_OBJECT
public:
explicit ImageAttachmentsView(QWidget* parent = nullptr);
void enableAutoFitInView();
void disableAutoFitInView();
bool isAutoFitInViewActivated() const;
double calculateFitInViewFactor() const;
signals:
void ctrlWheelEvent(QWheelEvent* event);
protected:
void wheelEvent(QWheelEvent* event) override;
void showEvent(QShowEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
void fitSceneInView();
bool m_autoFitInView = false;
};

View File

@@ -0,0 +1,258 @@
/*
* 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 "ImageAttachmentsWidget.h"
#include "ui_ImageAttachmentsWidget.h"
#include <array>
#include <cmath>
#include <utility>
#include <QDebug>
#include <QEvent>
#include <QGraphicsScene>
#include <QLineEdit>
#include <QPixmap>
#include <QSizeF>
#include <QWheelEvent>
namespace
{
// Predefined zoom levels must be in ascending order
constexpr std::array ZoomList = {0.25, 0.5, 0.75, 1.0, 2.0};
constexpr double WheelZoomStep = 1.1;
const QString FitText = QObject::tr("Fit");
QString formatZoomText(double zoomFactor)
{
return QString("%1%").arg(QString::number(zoomFactor * 100, 'f', 0));
}
double parseZoomText(const QString& zoomText)
{
auto zoomTextTrimmed = zoomText.trimmed();
if (auto percentIndex = zoomTextTrimmed.indexOf('%'); percentIndex != -1) {
// Remove the '%' character and parse the number
zoomTextTrimmed = zoomTextTrimmed.left(percentIndex).trimmed();
}
bool ok;
double zoomFactor = zoomTextTrimmed.toDouble(&ok);
if (!ok) {
qWarning() << "Failed to parse zoom text:" << zoomText;
return std::numeric_limits<double>::quiet_NaN();
}
return zoomFactor / 100.0;
}
} // namespace
ImageAttachmentsWidget::ImageAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ImageAttachmentsWidget)
{
m_ui->setupUi(this);
m_scene = new QGraphicsScene(this);
m_ui->imagesView->setScene(m_scene);
m_ui->imagesView->setDragMode(QGraphicsView::ScrollHandDrag);
m_ui->imagesView->setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
m_ui->imagesView->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
connect(m_ui->imagesView, &ImageAttachmentsView::ctrlWheelEvent, this, &ImageAttachmentsWidget::onWheelZoomEvent);
static_assert(ZoomList.size() > 0, "ZoomList must not be empty");
static_assert(ZoomList.front() < ZoomList.back(), "ZoomList must be in ascending order");
m_zoomHelper = new ZoomHelper(1.0, WheelZoomStep, ZoomList.front(), ZoomList.back(), this);
connect(m_zoomHelper, &ZoomHelper::zoomChanged, this, &ImageAttachmentsWidget::onZoomFactorChanged);
initZoomComboBox();
}
ImageAttachmentsWidget::~ImageAttachmentsWidget() = default;
void ImageAttachmentsWidget::initZoomComboBox()
{
m_ui->zoomComboBox->clear();
auto textWidth = m_ui->zoomComboBox->fontMetrics().horizontalAdvance(FitText);
m_ui->zoomComboBox->addItem(FitText, 0.0);
for (const auto& zoom : ZoomList) {
auto zoomText = formatZoomText(zoom);
textWidth = std::max(textWidth, m_ui->zoomComboBox->fontMetrics().horizontalAdvance(zoomText));
m_ui->zoomComboBox->addItem(zoomText, zoom);
}
constexpr int minWidth = 50;
m_ui->zoomComboBox->setMinimumWidth(textWidth + minWidth);
connect(m_ui->zoomComboBox, &QComboBox::currentTextChanged, this, &ImageAttachmentsWidget::onZoomChanged);
connect(m_ui->zoomComboBox->lineEdit(), &QLineEdit::editingFinished, [this]() {
onZoomChanged(m_ui->zoomComboBox->lineEdit()->text());
});
// Fit by default
m_ui->zoomComboBox->setCurrentIndex(m_ui->zoomComboBox->findData(0.0));
onZoomChanged(m_ui->zoomComboBox->currentText());
}
void ImageAttachmentsWidget::onWheelZoomEvent(QWheelEvent* event)
{
m_ui->imagesView->disableAutoFitInView();
auto finInViewFactor = m_ui->imagesView->calculateFitInViewFactor();
// Limit the fit-in-view factor to a maximum of 100%
m_zoomHelper->setMinZoomOutFactor(std::isnan(finInViewFactor) ? 1.0 : std::min(finInViewFactor, 1.0));
event->angleDelta().y() > 0 ? m_zoomHelper->zoomIn() : m_zoomHelper->zoomOut();
}
void ImageAttachmentsWidget::onZoomFactorChanged(double zoomFactor)
{
if (m_ui->imagesView->isAutoFitInViewActivated()) {
return;
}
m_ui->imagesView->setTransform(QTransform::fromScale(zoomFactor, zoomFactor));
// Update the zoom combo box to reflect the current zoom factor
if (!m_ui->zoomComboBox->lineEdit()->hasFocus()) {
m_ui->zoomComboBox->setCurrentText(formatZoomText(zoomFactor));
}
}
void ImageAttachmentsWidget::onZoomChanged(const QString& zoomText)
{
auto zoomFactor = 1.0;
if (zoomText == FitText) {
m_ui->imagesView->enableAutoFitInView();
zoomFactor = std::min(m_ui->imagesView->calculateFitInViewFactor(), zoomFactor);
} else {
zoomFactor = parseZoomText(zoomText);
if (!std::isnan(zoomFactor)) {
m_ui->imagesView->disableAutoFitInView();
}
}
if (std::isnan(zoomFactor)) {
return;
}
m_zoomHelper->setZoomFactor(zoomFactor);
}
void ImageAttachmentsWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
{
m_attachment = std::move(attachment);
if (mode == attachments::OpenMode::ReadWrite) {
qWarning() << "Read-write mode is not supported for image attachments";
}
loadImage();
}
void ImageAttachmentsWidget::loadImage()
{
QPixmap pixmap{};
pixmap.loadFromData(m_attachment.data);
if (pixmap.isNull()) {
qWarning() << "Failed to load image from data";
return;
}
m_scene->clear();
m_scene->addPixmap(std::move(pixmap));
}
attachments::Attachment ImageAttachmentsWidget::getAttachment() const
{
return m_attachment;
}
// Zoom helper
ZoomHelper::ZoomHelper(double zoomFactor, double step, double min, double max, QObject* parent)
: QObject(parent)
, m_step(step)
, m_minZoomOut(min)
, m_maxZoomIn(max)
{
Q_ASSERT(!std::isnan(step) && step > 0);
Q_ASSERT(!std::isnan(zoomFactor));
Q_ASSERT(!std::isnan(min));
Q_ASSERT(!std::isnan(max));
Q_ASSERT(min < max);
setZoomFactor(zoomFactor);
}
void ZoomHelper::zoomIn()
{
const auto newZoomFactor = m_zoomFactor * m_step;
setZoomFactor(std::isgreater(newZoomFactor, m_maxZoomIn) ? m_zoomFactor : newZoomFactor);
}
void ZoomHelper::zoomOut()
{
const auto newZoomFactor = m_zoomFactor / m_step;
setZoomFactor(std::isless(newZoomFactor, m_minZoomOut) ? m_zoomFactor : newZoomFactor);
}
void ZoomHelper::setZoomFactor(double zoomFactor)
{
if (std::isnan(zoomFactor)) {
qWarning() << "Failed to set NaN zoom factor";
return;
}
auto oldValue = std::exchange(m_zoomFactor, zoomFactor);
if (std::isless(oldValue, m_zoomFactor) || std::isgreater(oldValue, m_zoomFactor)) {
Q_EMIT zoomChanged(m_zoomFactor);
}
}
double ZoomHelper::getZoomFactor() const
{
return m_zoomFactor;
}
void ZoomHelper::setMinZoomOutFactor(double zoomFactor)
{
if (std::isgreater(zoomFactor, m_maxZoomIn)) {
std::swap(m_maxZoomIn, zoomFactor);
}
m_minZoomOut = zoomFactor;
}
void ZoomHelper::setMaxZoomInFactor(double zoomFactor)
{
if (std::isless(zoomFactor, m_minZoomOut)) {
std::swap(m_minZoomOut, zoomFactor);
}
m_maxZoomIn = zoomFactor;
}

View File

@@ -0,0 +1,85 @@
/*
* 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 "AttachmentTypes.h"
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class ImageAttachmentsWidget;
}
class QGraphicsView;
class QGraphicsScene;
class ZoomHelper : public QObject
{
Q_OBJECT
public:
explicit ZoomHelper(double zoomFactor, double step, double min, double max, QObject* parent = nullptr);
void zoomIn();
void zoomOut();
void setZoomFactor(double zoomFactor);
double getZoomFactor() const;
void setMinZoomOutFactor(double zoomFactor);
void setMaxZoomInFactor(double zoomFactor);
signals:
void zoomChanged(double zoomFactor);
private:
double m_zoomFactor;
double m_step;
double m_minZoomOut;
double m_maxZoomIn;
};
class ImageAttachmentsWidget : public QWidget
{
Q_OBJECT
public:
explicit ImageAttachmentsWidget(QWidget* parent = nullptr);
~ImageAttachmentsWidget() override;
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
attachments::Attachment getAttachment() const;
private slots:
void onZoomChanged(const QString& zoomText);
void onWheelZoomEvent(QWheelEvent* event);
void onZoomFactorChanged(double zoomFactor);
private:
void loadImage();
void initZoomComboBox();
QScopedPointer<Ui::ImageAttachmentsWidget> m_ui;
attachments::Attachment m_attachment;
QPointer<QGraphicsScene> m_scene;
QPointer<ZoomHelper> m_zoomHelper;
};

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImageAttachmentsWidget</class>
<widget class="QWidget" name="ImageAttachmentsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0">
<item>
<widget class="QLabel" name="zoomLabel">
<property name="text">
<string>Zoom:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="zoomComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="ImageAttachmentsView" name="imagesView"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ImageAttachmentsView</class>
<extends>QGraphicsView</extends>
<header location="global">gui/entry/attachments/ImageAttachmentsView.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,54 @@
/*
* 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 "TextAttachmentsEditWidget.h"
#include "ui_TextAttachmentsEditWidget.h"
#include <QPushButton>
#include <QTextEdit>
#include <qwidget.h>
TextAttachmentsEditWidget::TextAttachmentsEditWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::TextAttachmentsEditWidget())
{
m_ui->setupUi(this);
connect(m_ui->attachmentsTextEdit, &QTextEdit::textChanged, this, &TextAttachmentsEditWidget::textChanged);
connect(m_ui->previewPushButton, &QPushButton::clicked, this, &TextAttachmentsEditWidget::previewButtonClicked);
}
TextAttachmentsEditWidget::~TextAttachmentsEditWidget() = default;
void TextAttachmentsEditWidget::openAttachment(attachments::Attachment attachments, attachments::OpenMode mode)
{
m_attachment = std::move(attachments);
m_mode = mode;
updateUi();
}
attachments::Attachment TextAttachmentsEditWidget::getAttachment() const
{
return {m_attachment.name, m_ui->attachmentsTextEdit->toPlainText().toUtf8()};
}
void TextAttachmentsEditWidget::updateUi()
{
m_ui->attachmentsTextEdit->setPlainText(m_attachment.data);
m_ui->attachmentsTextEdit->setReadOnly(m_mode == attachments::OpenMode::ReadOnly);
}

View File

@@ -0,0 +1,51 @@
/*
* 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 "AttachmentTypes.h"
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class TextAttachmentsEditWidget;
}
class TextAttachmentsEditWidget : public QWidget
{
Q_OBJECT
public:
explicit TextAttachmentsEditWidget(QWidget* parent = nullptr);
~TextAttachmentsEditWidget() override;
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
attachments::Attachment getAttachment() const;
signals:
void textChanged();
void previewButtonClicked(bool isChecked);
private:
void updateUi();
QScopedPointer<Ui::TextAttachmentsEditWidget> m_ui;
attachments::Attachment m_attachment;
attachments::OpenMode m_mode;
};

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextAttachmentsEditWidget</class>
<widget class="QWidget" name="TextAttachmentsEditWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="previewPushButton">
<property name="text">
<string>Preview</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextEdit" name="attachmentsTextEdit"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,133 @@
/*
* 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 "TextAttachmentsPreviewWidget.h"
#include "ui_TextAttachmentsPreviewWidget.h"
#include <core/Tools.h>
#include <QComboBox>
#include <QDebug>
#include <QMetaEnum>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
namespace
{
constexpr TextAttachmentsPreviewWidget::PreviewTextType ConvertToPreviewTextType(Tools::MimeType mimeType) noexcept
{
if (mimeType == Tools::MimeType::Html) {
return TextAttachmentsPreviewWidget::Html;
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
if (mimeType == Tools::MimeType::Markdown) {
return TextAttachmentsPreviewWidget::Markdown;
}
#endif
return TextAttachmentsPreviewWidget::PlainText;
}
} // namespace
TextAttachmentsPreviewWidget::TextAttachmentsPreviewWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::TextAttachmentsPreviewWidget())
{
m_ui->setupUi(this);
initTypeCombobox();
}
TextAttachmentsPreviewWidget::~TextAttachmentsPreviewWidget() = default;
void TextAttachmentsPreviewWidget::openAttachment(attachments::Attachment attachments, attachments::OpenMode mode)
{
if (mode == attachments::OpenMode::ReadWrite) {
qWarning() << "Read-write mode is not supported for text preview attachments";
}
m_attachment = std::move(attachments);
updateUi();
}
attachments::Attachment TextAttachmentsPreviewWidget::getAttachment() const
{
return m_attachment;
}
void TextAttachmentsPreviewWidget::initTypeCombobox()
{
QStandardItemModel* model = new QStandardItemModel(this);
const auto metaEnum = QMetaEnum::fromType<TextAttachmentsPreviewWidget::PreviewTextType>();
for (int i = 0; i < metaEnum.keyCount(); ++i) {
QStandardItem* item = new QStandardItem(metaEnum.key(i));
item->setData(metaEnum.value(i), Qt::UserRole);
model->appendRow(item);
}
QSortFilterProxyModel* filterProxyMode = new QSortFilterProxyModel(this);
filterProxyMode->setSourceModel(model);
filterProxyMode->sort(0, Qt::SortOrder::DescendingOrder);
m_ui->typeComboBox->setModel(filterProxyMode);
connect(m_ui->typeComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
&TextAttachmentsPreviewWidget::onTypeChanged);
m_ui->typeComboBox->setCurrentIndex(m_ui->typeComboBox->findData(PlainText));
onTypeChanged(m_ui->typeComboBox->currentIndex());
}
void TextAttachmentsPreviewWidget::updateUi()
{
if (!m_attachment.name.isEmpty()) {
const auto mimeType = Tools::getMimeType(QFileInfo(m_attachment.name));
auto index = m_ui->typeComboBox->findData(ConvertToPreviewTextType(mimeType));
m_ui->typeComboBox->setCurrentIndex(index);
}
onTypeChanged(m_ui->typeComboBox->currentIndex());
}
void TextAttachmentsPreviewWidget::onTypeChanged(int index)
{
if (index < 0) {
qWarning() << "TextAttachmentsPreviewWidget: Unknown text format";
}
const auto fileType = m_ui->typeComboBox->itemData(index).toInt();
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::PlainText) {
m_ui->previewTextBrowser->setPlainText(m_attachment.data);
}
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::Html) {
m_ui->previewTextBrowser->setHtml(m_attachment.data);
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::Markdown) {
m_ui->previewTextBrowser->setMarkdown(m_attachment.data);
}
#endif
}

View File

@@ -0,0 +1,61 @@
/*
* 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 "AttachmentTypes.h"
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class TextAttachmentsPreviewWidget;
}
class TextAttachmentsPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit TextAttachmentsPreviewWidget(QWidget* parent = nullptr);
~TextAttachmentsPreviewWidget() override;
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
attachments::Attachment getAttachment() const;
enum PreviewTextType : int
{
Html,
PlainText,
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
Markdown
#endif
};
Q_ENUM(PreviewTextType)
private slots:
void onTypeChanged(int index);
private:
void initTypeCombobox();
void updateUi();
QScopedPointer<Ui::TextAttachmentsPreviewWidget> m_ui;
attachments::Attachment m_attachment;
};

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextAttachmentsPreviewWidget</class>
<widget class="QWidget" name="TextAttachmentsPreviewWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="typeComboBox">
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextBrowser" name="previewTextBrowser"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,105 @@
/*
* 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 "TextAttachmentsWidget.h"
#include "TextAttachmentsEditWidget.h"
#include "TextAttachmentsPreviewWidget.h"
#include "ui_TextAttachmentsWidget.h"
#include <QSplitter>
#include <QTextEdit>
#include <QTimer>
TextAttachmentsWidget::TextAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::TextAttachmentsWidget())
, m_previewUpdateTimer(new QTimer(this))
, m_mode(attachments::OpenMode::ReadOnly)
{
m_ui->setupUi(this);
initWidget();
}
TextAttachmentsWidget::~TextAttachmentsWidget() = default;
void TextAttachmentsWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
{
m_attachment = std::move(attachment);
m_mode = mode;
updateWidget();
}
attachments::Attachment TextAttachmentsWidget::getAttachment() const
{
if (m_mode == attachments::OpenMode::ReadWrite) {
return m_editWidget->getAttachment();
}
return m_attachment;
}
void TextAttachmentsWidget::updateWidget()
{
if (m_mode == attachments::OpenMode::ReadOnly) {
m_splitter->setSizes({0, 1});
m_editWidget->hide();
} else {
m_splitter->setSizes({1, 0});
m_editWidget->show();
}
m_editWidget->openAttachment(m_attachment, m_mode);
m_previewWidget->openAttachment(m_attachment, attachments::OpenMode::ReadOnly);
}
void TextAttachmentsWidget::initWidget()
{
m_splitter = new QSplitter(this);
m_editWidget = new TextAttachmentsEditWidget(this);
m_previewWidget = new TextAttachmentsPreviewWidget(this);
m_previewUpdateTimer->setSingleShot(true);
m_previewUpdateTimer->setInterval(500);
// Only update the preview after a set timeout and if it is visible
connect(m_previewUpdateTimer, &QTimer::timeout, this, [this] {
if (m_previewWidget->width() > 0) {
m_attachment = m_editWidget->getAttachment();
m_previewWidget->openAttachment(m_attachment, attachments::OpenMode::ReadOnly);
}
});
connect(
m_editWidget, &TextAttachmentsEditWidget::textChanged, m_previewUpdateTimer, QOverload<>::of(&QTimer::start));
connect(m_editWidget, &TextAttachmentsEditWidget::previewButtonClicked, [this] {
const auto sizes = m_splitter->sizes();
const auto previewSize = sizes.value(1, 0) > 0 ? 0 : 1;
m_splitter->setSizes({1, previewSize});
});
m_splitter->addWidget(m_editWidget);
m_splitter->addWidget(m_previewWidget);
// Prevent collapsing of the edit widget
m_splitter->setCollapsible(0, false);
m_ui->verticalLayout->addWidget(m_splitter);
updateWidget();
}

View File

@@ -0,0 +1,58 @@
/*
* 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 "AttachmentTypes.h"
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
namespace Ui
{
class TextAttachmentsWidget;
}
class QSplitter;
class QTimer;
class TextAttachmentsPreviewWidget;
class TextAttachmentsEditWidget;
class TextAttachmentsWidget : public QWidget
{
Q_OBJECT
public:
explicit TextAttachmentsWidget(QWidget* parent = nullptr);
~TextAttachmentsWidget() override;
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
attachments::Attachment getAttachment() const;
private:
void updateWidget();
void initWidget();
QScopedPointer<Ui::TextAttachmentsWidget> m_ui;
QPointer<QSplitter> m_splitter;
QPointer<TextAttachmentsEditWidget> m_editWidget;
QPointer<TextAttachmentsPreviewWidget> m_previewWidget;
QPointer<QTimer> m_previewUpdateTimer;
attachments::Attachment m_attachment;
attachments::OpenMode m_mode;
};

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextAttachmentsWidget</class>
<widget class="QWidget" name="TextAttachmentsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>732</width>
<height>432</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2022 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
@@ -200,11 +200,11 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
auto parent = group->parentGroup();
if (parent) {
inheritHideEntries = parent->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY);
inheritSkipSubmit = parent->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
inheritOnlyHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH);
inheritNoHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH);
inheritOmitWww = parent->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW);
inheritHideEntries = parent->resolveBrowserOptionEnabled(BrowserService::OPTION_HIDE_ENTRY);
inheritSkipSubmit = parent->resolveBrowserOptionEnabled(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
inheritOnlyHttp = parent->resolveBrowserOptionEnabled(BrowserService::OPTION_ONLY_HTTP_AUTH);
inheritNoHttp = parent->resolveBrowserOptionEnabled(BrowserService::OPTION_NOT_HTTP_AUTH);
inheritOmitWww = parent->resolveBrowserOptionEnabled(BrowserService::OPTION_OMIT_WWW);
inheritRestrictKey = parent->resolveCustomDataString(BrowserService::OPTION_RESTRICT_KEY);
}

Some files were not shown because too many files have changed in this diff Show More