Dekko
Loading...
Searching...
No Matches
HtmlEditorContainer.qml
1/* Copyright (C) 2025 CIT-Services <info@cit-services.eu>
2
3 This file is part of Dekko email client for Ubuntu devices
4
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License as
7 published by the Free Software Foundation; either version 2 of
8 the License or (at your option) version 3
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17*/
18import QtQuick 2.4
19import QtQuick.Controls.Suru 2.2
20import Lomiri.Components 1.3
21import Lomiri.Components.Popups 1.3
22import Dekko.Mail.Stores.Composer 1.0
23import Dekko.Lomiri.Constants 1.0
24import Dekko.Editor 1.0
26Column {
27 id: htmlEditorContainer
28
29 property alias editor: htmlEditor
30 property string currentFontSize: "11pt"
31 readonly property color defaultTextColor: Suru.foregroundColor
32 property color currentTextColor: defaultTextColor
33 property color currentHighlightColor: "transparent"
34
35 property string pendingFontSize: ""
36 property string pendingHighlightColor: ""
37 property string pendingTextColor: ""
38
39 // Editor loading state
40 property bool isEditorLoaded: false
41 property string pendingDocument: ""
42 property string currentDocument: ""
43 property bool isInternallyUpdating: false
44
45 function setHtmlDocument(html) {
46 currentDocument = html || ""
47 if (htmlEditorContainer.visible && isEditorLoaded && htmlEditor) {
48 htmlEditor.setDocument(html)
49 } else {
50 pendingDocument = html || ""
51 }
52 }
53
54 Timer {
55 id: applyFormatTimer
56 interval: 100
57 onTriggered: {
58 htmlEditor.focusEditor(true)
59 if (pendingFontSize !== "") {
60 htmlEditor.setFontSize(pendingFontSize)
61 pendingFontSize = ""
62 }
63 if (pendingHighlightColor !== "") {
64 htmlEditor.setHighlightColor(pendingHighlightColor)
65 pendingHighlightColor = ""
66 }
67 if (pendingTextColor !== "") {
68 htmlEditor.setTextColor(pendingTextColor)
69 pendingTextColor = ""
70 }
71 }
72 }
73
74
75 function rgbToHex(rgbStr) {
76 if (!rgbStr) return defaultTextColor
77
78 // Handle format: rgb(255, 102, 0) or rgb(255,102,0) - gotta be flexible
79 const match = rgbStr.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
80 if (match) {
81 const r = parseInt(match[1])
82 const g = parseInt(match[2])
83 const b = parseInt(match[3])
84 // Ensure 2 digits for each component - padding with zeros if needed
85 const hex = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
86 return hex
87 }
88 return rgbStr
89 }
90
91 function parsePathForFormatting(path) {
92 const parts = path.split('>')
93 let size = "11pt"
94 let color = defaultTextColor
95 let highlight = "transparent"
96
97 // Iterate through all parts to find the innermost formatting - last one wins
98 for (let i = 0; i < parts.length; i++) {
99 const part = parts[i]
100
101 // Extract font size - look for fontSize= something
102 const fontSizeMatch = part.match(/fontSize=([^\]]+)/)
103 if (fontSizeMatch) {
104 size = fontSizeMatch[1]
105 }
106
107 // Extract text color - [color= rgb stuff]
108 const colorMatch = part.match(/\[color=([^\]]+)\]/)
109 if (colorMatch) {
110 color = rgbToHex(colorMatch[1])
111 }
112
113 // Extract highlight color - backgroundColor=
114 const highlightMatch = part.match(/backgroundColor=([^\]]+)/)
115 if (highlightMatch) {
116 highlight = rgbToHex(highlightMatch[1])
117 }
118 }
119
120 // Update the current values
121 currentFontSize = size
122 currentTextColor = color
123 currentHighlightColor = highlight
124
125 // Maybe log this for debugging? console.log("Parsed formatting: size=" + size + ", color=" + color + ", highlight=" + highlight)
126 }
127
128 spacing: 0
129
131 id: htmlToolbar
132 width: parent.width
133 editor: htmlEditor
134 currentFontSize: htmlEditorContainer.currentFontSize
135 currentTextColor: htmlEditorContainer.currentTextColor
136 currentHighlightColor: htmlEditorContainer.currentHighlightColor
137
138 onFontSizeClicked: {
139 PopupUtils.open(fontSizeDialogComponent)
140 }
141
142 onTextColorClicked: {
143 PopupUtils.open(colorPickerDialogComponent, undefined, {isHighlight: false})
144 }
145
146 onHighlightColorClicked: {
147 PopupUtils.open(colorPickerDialogComponent, undefined, {isHighlight: true})
148 }
149
150 onLinkClicked: {
151 PopupUtils.open(linkDialogComponent)
152 }
153 }
154
155 RichTextEditor {
156 id: htmlEditor
157 width: parent.width
158 height: parent.height - htmlToolbar.height
159
160 function requestContentUpdate() {
161 requestDocument()
162 }
163
164 onVisibleChanged: {
165 if (htmlEditorContainer.visible && htmlEditorContainer.isEditorLoaded && htmlEditorContainer.pendingDocument !== "") {
166 htmlEditor.setDocument(htmlEditorContainer.pendingDocument)
167 htmlEditorContainer.pendingDocument = ""
168 }
169 }
170
171 onEditorLoaded: {
172 htmlEditorContainer.isEditorLoaded = true
173 if (htmlEditorContainer.visible && htmlEditorContainer.pendingDocument !== "") {
174 htmlEditor.setDocument(htmlEditorContainer.pendingDocument)
175 htmlEditorContainer.pendingDocument = ""
176 }
177 }
178
179 onDocumentReady: {
180 htmlEditorContainer.currentDocument = document || ""
181 htmlEditorContainer.isInternallyUpdating = true
182 if (ComposerStore.htmlContent !== htmlEditorContainer.currentDocument) {
183 ComposerStore.htmlContent = htmlEditorContainer.currentDocument
184 }
185 htmlEditorContainer.isInternallyUpdating = false
186 }
187
188 onContentChanged: {
189 // Could debounce this or something, but for now just update
190 }
191
192 onPathChanged: {
193 htmlEditorContainer.parsePathForFormatting(path)
194 }
195
196 Timer {
197 interval: 2000
198 running: htmlEditorContainer.visible && htmlEditorContainer.isEditorLoaded
199 repeat: true
200 onTriggered: htmlEditor.requestDocument()
201 }
202 }
203
204 Component {
205 id: linkDialogComponent
206 LinkDialog {
207 onLinkInserted: function(url, text) {
208 // If link text is provided and no text is selected, insert the text first - better UX
209 if (text && text.length > 0) {
210 htmlEditor.insertLinkWithText(url, text, { target: '_blank' })
211 } else {
212 htmlEditor.insertLink(url, { target: '_blank' })
213 }
214 }
215 }
216 }
217
218 Component {
219 id: fontSizeDialogComponent
221 onSizeSelected: {
222 htmlEditorContainer.currentFontSize = size
223 htmlEditorContainer.pendingFontSize = size
224 applyFormatTimer.restart()
225 }
226 }
227 }
228
229 Component {
230 id: colorPickerDialogComponent
232 onColorSelected: {
233 if (isHighlight) {
234 htmlEditorContainer.currentHighlightColor = color
235 htmlEditorContainer.pendingHighlightColor = color
236 } else {
237 htmlEditorContainer.currentTextColor = color
238 htmlEditorContainer.pendingTextColor = color
239 }
240 applyFormatTimer.restart()
241 }
242 }
243 }
244}