Merge 3a7826a278 into 36a26308d0
48
COPYING
|
|
@ -153,8 +153,8 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
|||
share/icons/application/scalable/actions/configure.svg
|
||||
share/icons/application/scalable/actions/csv.svg
|
||||
share/icons/application/scalable/actions/database-change-key.svg
|
||||
share/icons/application/scalable/actions/database-lock.svg
|
||||
share/icons/application/scalable/actions/database-lock-all.svg
|
||||
share/icons/application/scalable/actions/database-lock.svg
|
||||
share/icons/application/scalable/actions/database-merge.svg
|
||||
share/icons/application/scalable/actions/database-search.svg
|
||||
share/icons/application/scalable/actions/database-settings.svg
|
||||
|
|
@ -165,34 +165,46 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
|||
share/icons/application/scalable/actions/document-export.svg
|
||||
share/icons/application/scalable/actions/document-import.svg
|
||||
share/icons/application/scalable/actions/document-new.svg
|
||||
share/icons/application/scalable/actions/document-open.svg
|
||||
share/icons/application/scalable/actions/document-open-recent.svg
|
||||
share/icons/application/scalable/actions/document-open.svg
|
||||
share/icons/application/scalable/actions/document-properties.svg
|
||||
share/icons/application/scalable/actions/document-save.svg
|
||||
share/icons/application/scalable/actions/document-save-as.svg
|
||||
share/icons/application/scalable/actions/document-save-copy.svg
|
||||
share/icons/application/scalable/actions/document-save.svg
|
||||
share/icons/application/scalable/actions/donate.svg
|
||||
share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg
|
||||
share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg
|
||||
share/icons/application/scalable/actions/entry-clone.svg
|
||||
share/icons/application/scalable/actions/entry-delete.svg
|
||||
share/icons/application/scalable/actions/entry-restore.svg
|
||||
share/icons/application/scalable/actions/entry-edit.svg
|
||||
share/icons/application/scalable/actions/entry-expire.svg
|
||||
share/icons/application/scalable/actions/entry-new.svg
|
||||
share/icons/application/scalable/actions/entry-restore.svg
|
||||
share/icons/application/scalable/actions/favicon-download.svg
|
||||
share/icons/application/scalable/actions/fingerprint.svg
|
||||
share/icons/application/scalable/actions/format-code-block.svg
|
||||
share/icons/application/scalable/actions/format-code.svg
|
||||
share/icons/application/scalable/actions/format-header-decrease.svg
|
||||
share/icons/application/scalable/actions/format-header-increase.svg
|
||||
share/icons/application/scalable/actions/format-text-bold.svg
|
||||
share/icons/application/scalable/actions/format-text-italic.svg
|
||||
share/icons/application/scalable/actions/format-text-strikethrough.svg
|
||||
share/icons/application/scalable/actions/format-text-underline.svg
|
||||
share/icons/application/scalable/actions/getting-started.svg
|
||||
share/icons/application/scalable/actions/group-clone.svg
|
||||
share/icons/application/scalable/actions/group-delete.svg
|
||||
share/icons/application/scalable/actions/group-edit.svg
|
||||
share/icons/application/scalable/actions/group-clone.svg
|
||||
share/icons/application/scalable/actions/group-empty-trash.svg
|
||||
share/icons/application/scalable/actions/group-new.svg
|
||||
share/icons/application/scalable/actions/hammer-wrench.svg
|
||||
share/icons/application/scalable/actions/health.svg
|
||||
share/icons/application/scalable/actions/help-about.svg
|
||||
share/icons/application/scalable/actions/lock-question.svg
|
||||
share/icons/application/scalable/actions/insert-link.svg
|
||||
share/icons/application/scalable/actions/keyboard-shortcuts.svg
|
||||
share/icons/application/scalable/actions/lock-open-alert.svg
|
||||
share/icons/application/scalable/actions/lock-open.svg
|
||||
share/icons/application/scalable/actions/lock-question.svg
|
||||
share/icons/application/scalable/actions/lock.svg
|
||||
share/icons/application/scalable/actions/message-close.svg
|
||||
share/icons/application/scalable/actions/move-down.svg
|
||||
share/icons/application/scalable/actions/move-up.svg
|
||||
|
|
@ -200,31 +212,33 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
|||
share/icons/application/scalable/actions/object-unlocked.svg
|
||||
share/icons/application/scalable/actions/onepassword.svg
|
||||
share/icons/application/scalable/actions/paperclip.svg
|
||||
share/icons/application/scalable/actions/password-copy.svg
|
||||
share/icons/application/scalable/actions/passkey.svg
|
||||
share/icons/application/scalable/actions/password-copy.svg
|
||||
share/icons/application/scalable/actions/password-generator.svg
|
||||
share/icons/application/scalable/actions/password-show-off.svg
|
||||
share/icons/application/scalable/actions/password-show-on.svg
|
||||
share/icons/application/scalable/actions/qrcode.svg
|
||||
share/icons/application/scalable/actions/redo.svg
|
||||
share/icons/application/scalable/actions/refresh.svg
|
||||
share/icons/application/scalable/actions/remote-sync.svg
|
||||
share/icons/application/scalable/actions/reports.svg
|
||||
share/icons/application/scalable/actions/reports-exclude.svg
|
||||
share/icons/application/scalable/actions/reports.svg
|
||||
share/icons/application/scalable/actions/sort-alphabetical-ascending.svg
|
||||
share/icons/application/scalable/actions/sort-alphabetical-descending.svg
|
||||
share/icons/application/scalable/actions/statistics.svg
|
||||
share/icons/application/scalable/actions/system-help.svg
|
||||
share/icons/application/scalable/actions/system-search.svg
|
||||
share/icons/application/scalable/actions/system-software-update.svg
|
||||
share/icons/application/scalable/actions/tag.svg
|
||||
share/icons/application/scalable/actions/tag-multiple.svg
|
||||
share/icons/application/scalable/actions/tag-search.svg
|
||||
share/icons/application/scalable/actions/totp.svg
|
||||
share/icons/application/scalable/actions/totp-copy.svg
|
||||
share/icons/application/scalable/actions/tag.svg
|
||||
share/icons/application/scalable/actions/totp-copy-password.svg
|
||||
share/icons/application/scalable/actions/totp-copy.svg
|
||||
share/icons/application/scalable/actions/totp-edit.svg
|
||||
share/icons/application/scalable/actions/totp-invalid.svg
|
||||
share/icons/application/scalable/actions/totp.svg
|
||||
share/icons/application/scalable/actions/trash.svg
|
||||
share/icons/application/scalable/actions/undo.svg
|
||||
share/icons/application/scalable/actions/url-copy.svg
|
||||
share/icons/application/scalable/actions/user-guide.svg
|
||||
share/icons/application/scalable/actions/username-copy.svg
|
||||
|
|
@ -232,10 +246,10 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
|||
share/icons/application/scalable/actions/web.svg
|
||||
share/icons/application/scalable/actions/yubikey-refresh.svg
|
||||
share/icons/application/scalable/apps/internet-web-browser.svg
|
||||
share/icons/application/scalable/apps/keepassxc.svg
|
||||
share/icons/application/scalable/apps/keepassxc-dark.svg
|
||||
share/icons/application/scalable/apps/keepassxc-locked.svg
|
||||
share/icons/application/scalable/apps/keepassxc-unlocked.svg
|
||||
share/icons/application/scalable/apps/keepassxc.svg
|
||||
share/icons/application/scalable/apps/preferences-desktop-icons.svg
|
||||
share/icons/application/scalable/apps/preferences-system-network-sharing.svg
|
||||
share/icons/application/scalable/apps/utilities-terminal.svg
|
||||
|
|
@ -245,13 +259,17 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
|||
share/icons/application/scalable/status/dialog-information.svg
|
||||
share/icons/application/scalable/status/dialog-warning.svg
|
||||
share/icons/application/scalable/status/security-high.svg
|
||||
share/icons/application/scalable/actions/lock-open-alert.svg
|
||||
share/icons/application/scalable/actions/lock-open.svg
|
||||
share/icons/application/scalable/actions/lock.svg
|
||||
Copyright: 2023 Pictogrammers <https://pictogrammers.com/docs/general/about/>
|
||||
License: Apache-2.0
|
||||
Comment: Some icons are modified to fit KeePassXC design (https://pictogrammers.com/library/mdi/)
|
||||
|
||||
Files: share/icons/application/scalable/actions/format-list-bulleted.svg
|
||||
share/icons/application/scalable/actions/format-list-numbered.svg
|
||||
share/icons/application/scalable/actions/format-list-check.svg
|
||||
Copyright: 2015–2025 IBM Corp. and the Carbon Design System contributors
|
||||
License: Apache-2.0
|
||||
Comment: Taken from https://github.com/carbon-design-system/carbon
|
||||
|
||||
Files: src/streams/qtiocompressor.*
|
||||
src/streams/QtIOCompressor
|
||||
tests/modeltest.*
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.59 3.41L7 4.82L3.82 8L7 11.18L5.59 12.6L1 8L5.59 3.41M11.41 3.41L16 8L11.41 12.6L10 11.18L13.18 8L10 4.82L11.41 3.41M22 6V18C22 19.11 21.11 20 20 20H4C2.9 20 2 19.11 2 18V14H4V18H20V6H17.03V4H20C21.11 4 22 4.89 22 6Z" /></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
1
share/icons/application/scalable/actions/format-code.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z" /></svg>
|
||||
|
After Width: | Height: | Size: 181 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M20.42,7.41L16.83,11L20.42,14.59L19,16L14,11L19,6L20.42,7.41Z" /></svg>
|
||||
|
After Width: | Height: | Size: 175 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M14.59,7.41L18.17,11L14.59,14.59L16,16L21,11L16,6L14.59,7.41Z" /></svg>
|
||||
|
After Width: | Height: | Size: 175 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="7" cy="9" r="3" fill="currentColor"/><circle cx="7" cy="23" r="3" fill="currentColor"/><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16z"/></svg>
|
||||
|
After Width: | Height: | Size: 249 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 8h14v2H16zm0 14h14v2H16zm-6-8H4a2.002 2.002 0 0 1-2-2V6a2.002 2.002 0 0 1 2-2h6a2.002 2.002 0 0 1 2 2v6a2.002 2.002 0 0 1-2 2M4 6v6h6.001L10 6zm6 22H4a2.002 2.002 0 0 1-2-2v-6a2.002 2.002 0 0 1 2-2h6a2.002 2.002 0 0 1 2 2v6a2.002 2.002 0 0 1-2 2m-6-8v6h6.001L10 20z"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 22h14v2H16zm0-14h14v2H16zm-8 4V4H6v1H4v2h2v5H4v2h6v-2zm2 16H4v-4a2 2 0 0 1 2-2h2v-2H4v-2h4a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6v2h4z"/></svg>
|
||||
|
After Width: | Height: | Size: 252 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,15.5H10V12.5H13.5A1.5,1.5 0 0,1 15,14A1.5,1.5 0 0,1 13.5,15.5M10,6.5H13A1.5,1.5 0 0,1 14.5,8A1.5,1.5 0 0,1 13,9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z" /></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z" /></svg>
|
||||
|
After Width: | Height: | Size: 134 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.2 9.8C6 7.5 7.7 4.8 10.1 4.3C13.2 3.3 17.7 4.7 17.6 8.5H14.6C14.6 8.2 14.5 7.9 14.5 7.7C14.3 7.1 13.9 6.8 13.3 6.6C12.5 6.3 11.2 6.4 10.5 6.9C9 8.2 10.4 9.5 12 10H7.4C7.3 9.9 7.3 9.8 7.2 9.8M21 13V11H3V13H12.6C12.8 13.1 13 13.1 13.2 13.2C13.8 13.5 14.3 13.7 14.5 14.3C14.6 14.7 14.7 15.2 14.5 15.6C14.3 16.1 13.9 16.3 13.4 16.5C11.6 17 9.4 16.3 9.5 14.1H6.5C6.4 16.7 8.6 18.5 11 18.8C14.8 19.6 19.3 17.2 17.3 12.9L21 13Z" /></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z" /></svg>
|
||||
|
After Width: | Height: | Size: 194 B |
1
share/icons/application/scalable/actions/insert-link.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>
|
||||
|
After Width: | Height: | Size: 795 B |
1
share/icons/application/scalable/actions/redo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 13.5C3 9.36 6.36 6 10.5 6H13V2L20 8L13 14V10H10.5C8.57 10 7 11.57 7 13.5S8.57 17 10.5 17H18V21H10.5C6.36 21 3 17.64 3 13.5Z" /></svg>
|
||||
|
After Width: | Height: | Size: 205 B |
1
share/icons/application/scalable/actions/undo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5 21H6V17H13.5C15.43 17 17 15.43 17 13.5S15.43 10 13.5 10H11V14L4 8L11 2V6H13.5C17.64 6 21 9.36 21 13.5S17.64 21 13.5 21Z" /></svg>
|
||||
|
After Width: | Height: | Size: 204 B |
|
|
@ -46,6 +46,17 @@
|
|||
<file>application/scalable/actions/entry-new.svg</file>
|
||||
<file>application/scalable/actions/favicon-download.svg</file>
|
||||
<file>application/scalable/actions/fingerprint.svg</file>
|
||||
<file>application/scalable/actions/format-code-block.svg</file>
|
||||
<file>application/scalable/actions/format-code.svg</file>
|
||||
<file>application/scalable/actions/format-header-decrease.svg</file>
|
||||
<file>application/scalable/actions/format-header-increase.svg</file>
|
||||
<file>application/scalable/actions/format-list-bulleted.svg</file>
|
||||
<file>application/scalable/actions/format-list-check.svg</file>
|
||||
<file>application/scalable/actions/format-list-numbered.svg</file>
|
||||
<file>application/scalable/actions/format-text-bold.svg</file>
|
||||
<file>application/scalable/actions/format-text-italic.svg</file>
|
||||
<file>application/scalable/actions/format-text-strikethrough.svg</file>
|
||||
<file>application/scalable/actions/format-text-underline.svg</file>
|
||||
<file>application/scalable/actions/getting-started.svg</file>
|
||||
<file>application/scalable/actions/group-delete.svg</file>
|
||||
<file>application/scalable/actions/group-edit.svg</file>
|
||||
|
|
@ -56,6 +67,7 @@
|
|||
<file>application/scalable/actions/health.svg</file>
|
||||
<file>application/scalable/actions/help-about.svg</file>
|
||||
<file>application/scalable/actions/hibp.svg</file>
|
||||
<file>application/scalable/actions/insert-link.svg</file>
|
||||
<file>application/scalable/actions/lock-question.svg</file>
|
||||
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
|
||||
<file>application/scalable/actions/lock.svg</file>
|
||||
|
|
@ -75,6 +87,7 @@
|
|||
<file>application/scalable/actions/password-show-on.svg</file>
|
||||
<file>application/scalable/actions/proton.svg</file>
|
||||
<file>application/scalable/actions/qrcode.svg</file>
|
||||
<file>application/scalable/actions/redo.svg</file>
|
||||
<file>application/scalable/actions/refresh.svg</file>
|
||||
<file>application/scalable/actions/remote-sync.svg</file>
|
||||
<file>application/scalable/actions/reports.svg</file>
|
||||
|
|
@ -94,6 +107,7 @@
|
|||
<file>application/scalable/actions/totp-edit.svg</file>
|
||||
<file>application/scalable/actions/totp-invalid.svg</file>
|
||||
<file>application/scalable/actions/trash.svg</file>
|
||||
<file>application/scalable/actions/undo.svg</file>
|
||||
<file>application/scalable/actions/url-copy.svg</file>
|
||||
<file>application/scalable/actions/user-guide.svg</file>
|
||||
<file>application/scalable/actions/username-copy.svg</file>
|
||||
|
|
|
|||
|
|
@ -6475,6 +6475,89 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MarkdownToolbar</name>
|
||||
<message>
|
||||
<source>Undo</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Redo</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Decrease heading level</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Increase heading level</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Bold</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Italic</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Strike</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Underline</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Bullet list</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Numbered list</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Task list</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Inline code</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Code block</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Insert Link</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Full link address (e.g. https://example.com or mailto:user@example.com)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Text shown instead of the URL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Link text:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Text shown on hover</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Tooltip:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MergeDialog</name>
|
||||
<message>
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ set(gui_SOURCES
|
|||
gui/osutils/ScreenLockListenerPrivate.cpp
|
||||
gui/widgets/ElidedLabel.cpp
|
||||
gui/widgets/KPToolBar.cpp
|
||||
gui/widgets/MarkdownToolbar.cpp
|
||||
gui/widgets/PopupHelpWidget.cpp
|
||||
gui/widgets/ShortcutWidget.cpp
|
||||
gui/wizard/ImportWizard.cpp
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
<width>720</width>
|
||||
<height>490</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>720</width>
|
||||
<height>490</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string/>
|
||||
</property>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ TextAttachmentsEditWidget::TextAttachmentsEditWidget(QWidget* parent)
|
|||
, m_ui(new Ui::TextAttachmentsEditWidget())
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
m_mdToolbar = new MarkdownToolbar(this);
|
||||
m_mdToolbar->setTarget(m_ui->attachmentsTextEdit);
|
||||
|
||||
connect(m_ui->attachmentsTextEdit, &QTextEdit::textChanged, this, &TextAttachmentsEditWidget::textChanged);
|
||||
connect(m_ui->previewPushButton, &QPushButton::clicked, this, &TextAttachmentsEditWidget::previewButtonClicked);
|
||||
|
|
@ -43,7 +45,6 @@ void TextAttachmentsEditWidget::openAttachment(attachments::Attachment attachmen
|
|||
{
|
||||
m_attachment = std::move(attachments);
|
||||
m_mode = mode;
|
||||
|
||||
updateUi();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
#include "AttachmentTypes.h"
|
||||
|
||||
#include "gui/widgets/MarkdownToolbar.h"
|
||||
|
||||
#include <QScopedPointer>
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -36,6 +38,10 @@ public:
|
|||
|
||||
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||
attachments::Attachment getAttachment() const;
|
||||
QToolBar* markdownToolbar() const
|
||||
{
|
||||
return m_mdToolbar;
|
||||
}
|
||||
|
||||
signals:
|
||||
void textChanged();
|
||||
|
|
@ -46,6 +52,7 @@ private:
|
|||
void updateUi();
|
||||
|
||||
QScopedPointer<Ui::TextAttachmentsEditWidget> m_ui;
|
||||
MarkdownToolbar* m_mdToolbar = nullptr;
|
||||
|
||||
attachments::Attachment m_attachment;
|
||||
attachments::OpenMode m_mode;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
<width>720</width>
|
||||
<height>490</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@
|
|||
#include "TextAttachmentsEditWidget.h"
|
||||
#include "TextAttachmentsPreviewWidget.h"
|
||||
|
||||
#include <core/Tools.h>
|
||||
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
|
|
@ -39,6 +40,14 @@ void TextAttachmentsWidget::openAttachment(attachments::Attachment attachment, a
|
|||
m_attachment = std::move(attachment);
|
||||
m_mode = mode;
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
const auto mimeType = Tools::getMimeType(QFileInfo(m_attachment.name));
|
||||
const bool isMd = (mimeType == Tools::MimeType::Markdown);
|
||||
if (auto* tb = m_editWidget->markdownToolbar()) {
|
||||
tb->setVisible(isMd && m_mode != attachments::OpenMode::ReadOnly);
|
||||
}
|
||||
#endif
|
||||
|
||||
updateWidget();
|
||||
}
|
||||
|
||||
|
|
@ -120,10 +129,18 @@ void TextAttachmentsWidget::initWidget()
|
|||
// Setup this widget with the splitter
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
if (auto* tb = m_editWidget->markdownToolbar()) {
|
||||
tb->setParent(this);
|
||||
layout->addWidget(tb);
|
||||
}
|
||||
|
||||
layout->addWidget(m_splitter);
|
||||
setLayout(layout);
|
||||
setObjectName("TextAttachmentsWidget");
|
||||
|
||||
setMinimumSize(720, 490);
|
||||
|
||||
updateWidget();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,14 +20,8 @@
|
|||
#include "AttachmentTypes.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QScopedPointer>
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class TextAttachmentsWidget;
|
||||
}
|
||||
|
||||
class QSplitter;
|
||||
class QTimer;
|
||||
class TextAttachmentsPreviewWidget;
|
||||
|
|
|
|||
497
src/gui/widgets/MarkdownToolbar.cpp
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MarkdownToolbar.h"
|
||||
|
||||
#include "core/Config.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/styles/StateColorPalette.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextBlock>
|
||||
#include <QTextEdit>
|
||||
#include <QToolButton>
|
||||
#include <algorithm>
|
||||
|
||||
MarkdownToolbar::MarkdownToolbar(QWidget* parent)
|
||||
: QToolBar(parent)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
void MarkdownToolbar::setTarget(QTextEdit* edit)
|
||||
{
|
||||
m_target = edit;
|
||||
if (!m_target)
|
||||
return;
|
||||
|
||||
m_target->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||||
|
||||
// Undo
|
||||
if (m_undoAction) {
|
||||
m_undoAction->disconnect();
|
||||
connect(m_undoAction, &QAction::triggered, m_target, &QTextEdit::undo);
|
||||
|
||||
m_undoAction->setEnabled(m_target->document()->isUndoAvailable());
|
||||
|
||||
connect(m_target->document(), &QTextDocument::undoAvailable, m_undoAction, &QAction::setEnabled);
|
||||
}
|
||||
|
||||
// Redo
|
||||
if (m_redoAction) {
|
||||
m_redoAction->disconnect();
|
||||
connect(m_redoAction, &QAction::triggered, m_target, &QTextEdit::redo);
|
||||
|
||||
m_redoAction->setEnabled(m_target->document()->isRedoAvailable());
|
||||
|
||||
connect(m_target->document(), &QTextDocument::redoAvailable, m_redoAction, &QAction::setEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
QAction* MarkdownToolbar::createAction(const QString& iconName,
|
||||
const QString& tooltip,
|
||||
const QKeySequence& shortcut,
|
||||
const std::function<void()>& callback)
|
||||
{
|
||||
QAction* act = addAction(icons()->icon(iconName), tooltip);
|
||||
|
||||
if (!shortcut.isEmpty()) {
|
||||
act->setShortcut(shortcut);
|
||||
QString tip = tooltip;
|
||||
tip += " (" + shortcut.toString(QKeySequence::NativeText) + ")";
|
||||
act->setToolTip(tip);
|
||||
} else {
|
||||
act->setToolTip(tooltip);
|
||||
}
|
||||
|
||||
act->setShortcutVisibleInContextMenu(true);
|
||||
|
||||
if (auto tb = qobject_cast<QToolButton*>(widgetForAction(act)))
|
||||
tb->setAutoRaise(true);
|
||||
|
||||
if (callback)
|
||||
connect(act, &QAction::triggered, this, callback);
|
||||
|
||||
return act;
|
||||
}
|
||||
|
||||
void MarkdownToolbar::setupUi()
|
||||
{
|
||||
setMovable(false);
|
||||
setFloatable(false);
|
||||
setToolButtonStyle(Qt::ToolButtonIconOnly);
|
||||
|
||||
if (config()->get(Config::GUI_CompactMode).toBool())
|
||||
setIconSize(QSize(20, 20));
|
||||
|
||||
// Undo
|
||||
QString undoText = tr("Undo");
|
||||
QString undoShortcut = QKeySequence(QKeySequence::Undo).toString(QKeySequence::NativeText);
|
||||
m_undoAction = createAction("undo", undoText, QKeySequence(), [=]() {
|
||||
if (m_target)
|
||||
m_target->undo();
|
||||
});
|
||||
m_undoAction->setToolTip(QString("%1 (%2)").arg(undoText, undoShortcut));
|
||||
|
||||
// Redo
|
||||
QString redoText = tr("Redo");
|
||||
QString redoShortcut = QKeySequence(QKeySequence::Redo).toString(QKeySequence::NativeText);
|
||||
m_redoAction = createAction("redo", redoText, QKeySequence(), [=]() {
|
||||
if (m_target)
|
||||
m_target->redo();
|
||||
});
|
||||
m_redoAction->setToolTip(QString("%1 (%2)").arg(redoText, redoShortcut));
|
||||
|
||||
addSeparator();
|
||||
|
||||
// Other groups of buttons
|
||||
struct ButtonSpec
|
||||
{
|
||||
QString iconName;
|
||||
QString tooltip;
|
||||
QKeySequence shortcut;
|
||||
std::function<void()> callback;
|
||||
};
|
||||
|
||||
const QList<QList<ButtonSpec>> buttonGroups = {
|
||||
{
|
||||
{"format-header-decrease",
|
||||
tr("Decrease heading level"),
|
||||
QKeySequence(Qt::CTRL | Qt::Key_Down),
|
||||
[=]() { changeHeadingLevel(-1); }},
|
||||
{"format-header-increase",
|
||||
tr("Increase heading level"),
|
||||
QKeySequence(Qt::CTRL | Qt::Key_Up),
|
||||
[=]() { changeHeadingLevel(1); }},
|
||||
},
|
||||
{
|
||||
{"format-text-bold", tr("Bold"), QKeySequence::Bold, [=]() { wrapSelection("**", "**"); }},
|
||||
{"format-text-italic", tr("Italic"), QKeySequence::Italic, [=]() { wrapSelection("*", "*"); }},
|
||||
{"format-text-strikethrough",
|
||||
tr("Strike"),
|
||||
QKeySequence(Qt::CTRL | Qt::Key_S),
|
||||
[=]() { wrapSelection("~~", "~~"); }},
|
||||
{"format-text-underline",
|
||||
tr("Underline"),
|
||||
QKeySequence::Underline,
|
||||
[=]() { wrapSelection("<u>", "</u>"); }},
|
||||
},
|
||||
{
|
||||
{"insert-link", tr("Link"), QKeySequence(Qt::CTRL | Qt::Key_L), [=]() { insertLink(); }},
|
||||
},
|
||||
{
|
||||
{"format-list-bulleted",
|
||||
tr("Bullet list"),
|
||||
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_B),
|
||||
[=]() { wrapSelection("- ", "", true); }},
|
||||
{"format-list-numbered",
|
||||
tr("Numbered list"),
|
||||
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_N),
|
||||
[=]() { wrapSelectionWithNumbering(); }},
|
||||
{"format-list-check",
|
||||
tr("Task list"),
|
||||
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T),
|
||||
[=]() { wrapSelection("- [ ] ", "", true); }},
|
||||
},
|
||||
{
|
||||
{"format-code", tr("Inline code"), QKeySequence(Qt::CTRL | Qt::Key_E), [=]() { wrapSelection("`", "`"); }},
|
||||
{"format-code-block",
|
||||
tr("Code block"),
|
||||
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_E),
|
||||
[=]() { wrapSelection("```\n", "\n```"); }},
|
||||
}};
|
||||
|
||||
for (int i = 0; i < buttonGroups.size(); ++i) {
|
||||
for (const auto& btn : buttonGroups[i]) {
|
||||
createAction(btn.iconName, btn.tooltip, btn.shortcut, btn.callback);
|
||||
}
|
||||
if (i < buttonGroups.size() - 1)
|
||||
addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownToolbar::wrapSelection(const QString& prefix, const QString& suffix, bool lineWise)
|
||||
{
|
||||
if (!m_target)
|
||||
return;
|
||||
|
||||
QTextCursor cursor = m_target->textCursor();
|
||||
QString selected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
|
||||
|
||||
QString result;
|
||||
if (lineWise) {
|
||||
QStringList lines = selected.split('\n');
|
||||
for (QString& line : lines) {
|
||||
line = prefix + line + suffix;
|
||||
}
|
||||
result = lines.join("\n");
|
||||
} else {
|
||||
result = prefix + selected + suffix;
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
cursor.insertText(result);
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
|
||||
void MarkdownToolbar::wrapSelectionWithNumbering()
|
||||
{
|
||||
if (!m_target)
|
||||
return;
|
||||
|
||||
QTextCursor cursor = m_target->textCursor();
|
||||
QString selected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
|
||||
|
||||
QStringList lines = selected.split('\n');
|
||||
QStringList numbered;
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
numbered << QString::number(i + 1) + ". " + lines[i];
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
cursor.insertText(numbered.join("\n"));
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
|
||||
void MarkdownToolbar::changeHeadingLevel(int delta, bool clearAll)
|
||||
{
|
||||
if (!m_target)
|
||||
return;
|
||||
|
||||
static const QRegularExpression HeadingRegex(R"rx(^(#{1,6})(?:\s+(.*))?$)rx");
|
||||
|
||||
QTextDocument* doc = m_target->document();
|
||||
if (!doc)
|
||||
return;
|
||||
|
||||
QTextCursor cur = m_target->textCursor();
|
||||
const int origAnchor = cur.anchor();
|
||||
const int origCaret = cur.position();
|
||||
const bool collapsed = (origAnchor == origCaret);
|
||||
|
||||
struct CursorPos
|
||||
{
|
||||
int blockNumber;
|
||||
int offsetInContent;
|
||||
};
|
||||
|
||||
auto parseLine = [&](const QString& line, int& oldLevel, QString& pureContent) {
|
||||
oldLevel = 0;
|
||||
pureContent = line;
|
||||
|
||||
auto hm = HeadingRegex.match(pureContent);
|
||||
if (hm.hasMatch()) {
|
||||
oldLevel = hm.captured(1).size();
|
||||
pureContent = hm.captured(2);
|
||||
}
|
||||
};
|
||||
|
||||
auto toCursorPos = [&](int pos) -> CursorPos {
|
||||
QTextBlock b = doc->findBlock(pos);
|
||||
if (!b.isValid()) {
|
||||
return CursorPos{doc->lastBlock().blockNumber(), 0};
|
||||
}
|
||||
const QString text = b.text();
|
||||
int ol = 0;
|
||||
QString pure;
|
||||
parseLine(text, ol, pure);
|
||||
const int prefixLen = text.length() - pure.length();
|
||||
const int offInBlock = pos - b.position();
|
||||
const int offInContent = qBound(0, offInBlock - prefixLen, pure.length());
|
||||
return CursorPos{b.blockNumber(), offInContent};
|
||||
};
|
||||
|
||||
auto fromCursorPos = [&](const CursorPos& cp) -> int {
|
||||
QTextBlock b = doc->findBlockByNumber(cp.blockNumber);
|
||||
if (!b.isValid()) {
|
||||
return qMax(0, doc->characterCount() - 1);
|
||||
}
|
||||
const QString text = b.text();
|
||||
int ol = 0;
|
||||
QString pure;
|
||||
parseLine(text, ol, pure);
|
||||
const int prefixLen = text.length() - pure.length();
|
||||
const int offInContent = qBound(0, cp.offsetInContent, pure.length());
|
||||
return b.position() + prefixLen + offInContent;
|
||||
};
|
||||
|
||||
const bool anchorWasEnd = (origAnchor > origCaret);
|
||||
CursorPos anchorPos = toCursorPos(origAnchor);
|
||||
CursorPos caretPos = toCursorPos(origCaret);
|
||||
|
||||
const int selStart = qMin(origAnchor, origCaret);
|
||||
const int selEnd = qMax(origAnchor, origCaret);
|
||||
|
||||
QVector<QTextBlock> blocks;
|
||||
if (collapsed) {
|
||||
QTextBlock b = doc->findBlock(selStart);
|
||||
if (b.isValid())
|
||||
blocks.push_back(b);
|
||||
} else {
|
||||
for (QTextBlock b = doc->findBlock(selStart); b.isValid(); b = b.next()) {
|
||||
if (b.position() <= selEnd) {
|
||||
blocks.push_back(b);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blocks.isEmpty())
|
||||
return;
|
||||
|
||||
bool hasHeadings = false;
|
||||
for (const QTextBlock& b : blocks) {
|
||||
int ol = 0;
|
||||
QString pc;
|
||||
parseLine(b.text(), ol, pc);
|
||||
if (ol > 0) {
|
||||
hasHeadings = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int targetLevel = -1;
|
||||
if (clearAll) {
|
||||
targetLevel = 0;
|
||||
} else if (!hasHeadings) {
|
||||
targetLevel = std::clamp(delta, 0, 6);
|
||||
}
|
||||
|
||||
cur.beginEditBlock();
|
||||
|
||||
for (const QTextBlock& b : blocks) {
|
||||
const QString original = b.text();
|
||||
int oldLevel = 0;
|
||||
QString pure;
|
||||
parseLine(original, oldLevel, pure);
|
||||
|
||||
const int newLevel = (targetLevel >= 0) ? targetLevel : std::clamp(oldLevel + delta, 0, 6);
|
||||
|
||||
const QString hashes = (newLevel == 0) ? QString() : QString(newLevel, QChar('#'));
|
||||
const QString space = (newLevel == 0 || pure.isEmpty()) ? QString() : QStringLiteral(" ");
|
||||
const QString newLine = (newLevel == 0) ? pure : hashes + space + pure;
|
||||
|
||||
if (newLine != original) {
|
||||
QTextCursor lc(b);
|
||||
lc.movePosition(QTextCursor::StartOfBlock);
|
||||
lc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
lc.insertText(newLine);
|
||||
}
|
||||
}
|
||||
|
||||
cur.endEditBlock();
|
||||
|
||||
// Restore cursor
|
||||
QTextCursor nc = m_target->textCursor();
|
||||
if (collapsed) {
|
||||
nc.setPosition(fromCursorPos(caretPos));
|
||||
} else {
|
||||
const int start = fromCursorPos(anchorPos);
|
||||
const int end = fromCursorPos(caretPos);
|
||||
if (anchorWasEnd) {
|
||||
nc.setPosition(end);
|
||||
nc.setPosition(start, QTextCursor::KeepAnchor);
|
||||
} else {
|
||||
nc.setPosition(start);
|
||||
nc.setPosition(end, QTextCursor::KeepAnchor);
|
||||
}
|
||||
}
|
||||
m_target->setTextCursor(nc);
|
||||
}
|
||||
|
||||
void MarkdownToolbar::insertLink()
|
||||
{
|
||||
if (!m_target)
|
||||
return;
|
||||
|
||||
QTextCursor cursor = m_target->textCursor();
|
||||
QString originalSelected = cursor.selectedText().replace(QChar::ParagraphSeparator, '\n');
|
||||
QString trimmedSelected = originalSelected.trimmed();
|
||||
|
||||
QString prefillText, prefillUrl, prefillTooltip;
|
||||
bool hasSelection = !trimmedSelected.isEmpty();
|
||||
bool matched = false;
|
||||
|
||||
auto tryMatch = [&](const QRegularExpression& re, auto handler) {
|
||||
auto m = re.match(trimmedSelected);
|
||||
if (m.hasMatch()) {
|
||||
matched = true;
|
||||
handler(m);
|
||||
}
|
||||
};
|
||||
|
||||
static const QRegularExpression PlainUrlRegex(R"rx(^(https?://\S+)$)rx", QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression EmailRegex(R"rx(^([A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})$)rx");
|
||||
|
||||
tryMatch(PlainUrlRegex, [&](const auto& m) {
|
||||
prefillUrl = m.captured(1);
|
||||
prefillText = prefillUrl;
|
||||
prefillText.remove(QRegularExpression("^https?://", QRegularExpression::CaseInsensitiveOption));
|
||||
});
|
||||
|
||||
if (!matched)
|
||||
tryMatch(EmailRegex, [&](const auto& m) {
|
||||
prefillUrl = "mailto:" + m.captured(1);
|
||||
prefillText = m.captured(1);
|
||||
});
|
||||
|
||||
if (hasSelection && !matched) {
|
||||
prefillText = trimmedSelected;
|
||||
cursor.removeSelectedText();
|
||||
}
|
||||
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle(tr("Insert Link"));
|
||||
dlg.resize(420, 160);
|
||||
QFormLayout layout(&dlg);
|
||||
layout.setContentsMargins(12, 12, 12, 12);
|
||||
layout.setVerticalSpacing(10);
|
||||
|
||||
QLineEdit urlEdit;
|
||||
urlEdit.setPlaceholderText("https://example.com");
|
||||
urlEdit.setToolTip(tr("Full link address (e.g. https://example.com or mailto:user@example.com)"));
|
||||
|
||||
if (!prefillUrl.isEmpty())
|
||||
urlEdit.setText(prefillUrl);
|
||||
layout.addRow("URL:", &urlEdit);
|
||||
|
||||
QLineEdit textEdit;
|
||||
textEdit.setToolTip(tr("Text shown instead of the URL"));
|
||||
if (!prefillText.isEmpty())
|
||||
textEdit.setText(prefillText);
|
||||
layout.addRow(tr("Link text:"), &textEdit);
|
||||
|
||||
QLineEdit tooltipEdit;
|
||||
tooltipEdit.setToolTip(tr("Text shown on hover"));
|
||||
if (!prefillTooltip.isEmpty())
|
||||
tooltipEdit.setText(prefillTooltip);
|
||||
layout.addRow(tr("Tooltip:"), &tooltipEdit);
|
||||
|
||||
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
auto okBtn = buttons.button(QDialogButtonBox::Ok);
|
||||
okBtn->setAutoDefault(false);
|
||||
okBtn->setDefault(false);
|
||||
layout.addRow(&buttons);
|
||||
|
||||
StateColorPalette statePalette;
|
||||
auto errorColor = statePalette.color(StateColorPalette::ColorRole::Error);
|
||||
|
||||
connect(&urlEdit, &QLineEdit::textChanged, [&](const QString& text) {
|
||||
if (!text.trimmed().isEmpty()) {
|
||||
urlEdit.setStyleSheet("");
|
||||
}
|
||||
});
|
||||
|
||||
auto tryAccept = [&]() {
|
||||
if (urlEdit.text().trimmed().isEmpty()) {
|
||||
urlEdit.setStyleSheet(QString("QLineEdit { background: %1; }").arg(errorColor.name()));
|
||||
urlEdit.setFocus();
|
||||
return;
|
||||
}
|
||||
dlg.accept();
|
||||
};
|
||||
|
||||
connect(&urlEdit, &QLineEdit::returnPressed, tryAccept);
|
||||
connect(&textEdit, &QLineEdit::returnPressed, tryAccept);
|
||||
connect(&tooltipEdit, &QLineEdit::returnPressed, tryAccept);
|
||||
connect(okBtn, &QPushButton::clicked, tryAccept);
|
||||
connect(buttons.button(QDialogButtonBox::Cancel), &QPushButton::clicked, &dlg, &QDialog::reject);
|
||||
|
||||
urlEdit.setFocus();
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted) {
|
||||
if (hasSelection) {
|
||||
cursor.insertText(originalSelected);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const QString url = urlEdit.text().trimmed();
|
||||
const QString text = textEdit.text().trimmed().isEmpty() ? url : textEdit.text().trimmed();
|
||||
const QString tooltip = tooltipEdit.text().trimmed();
|
||||
|
||||
QString link = tooltip.isEmpty() ? QStringLiteral("[%1](%2)").arg(text, url)
|
||||
: QStringLiteral("[%1](%2 \"%3\")").arg(text, url, tooltip);
|
||||
|
||||
cursor.insertText(link);
|
||||
m_target->setTextCursor(cursor);
|
||||
}
|
||||
50
src/gui/widgets/MarkdownToolbar.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QKeySequence>
|
||||
#include <QToolBar>
|
||||
#include <functional>
|
||||
|
||||
class QTextEdit;
|
||||
|
||||
class MarkdownToolbar : public QToolBar
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MarkdownToolbar(QWidget* parent = nullptr);
|
||||
void setTarget(QTextEdit* edit);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void wrapSelection(const QString& prefix, const QString& suffix, bool lineWise = false);
|
||||
void wrapSelectionWithNumbering();
|
||||
void insertLink();
|
||||
void changeHeadingLevel(int delta, bool clearAll = false);
|
||||
|
||||
QAction* createAction(const QString& iconName,
|
||||
const QString& tooltip,
|
||||
const QKeySequence& shortcut,
|
||||
const std::function<void()>& callback);
|
||||
|
||||
private:
|
||||
QTextEdit* m_target = nullptr;
|
||||
|
||||
QAction* m_undoAction = nullptr;
|
||||
QAction* m_redoAction = nullptr;
|
||||
};
|
||||