Warning, /office/klevernotes/src/contents/ui/textEditor/TextToolBar.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-License-Identifier: GPL-2.0-or-later 0002 // SPDX-FileCopyrightText: 2022 Louis Schul <schul9louis@gmail.com> 0003 0004 import QtQuick 2.15 0005 import QtQuick.Controls 2.15 0006 import QtQuick.Dialogs 0007 0008 import org.kde.kirigami 2.19 as Kirigami 0009 0010 import org.kde.Klever 1.0 0011 0012 import "qrc:/contents/ui/dialogs" 0013 import "qrc:/contents/ui/dialogs/emojiDialog" 0014 import "qrc:/contents/ui/dialogs/imagePickerDialog" 0015 import "qrc:/contents/ui/dialogs/tableMakerDialog" 0016 0017 Kirigami.ActionToolBar { 0018 id: toolbar 0019 0020 required property TextArea editorTextArea 0021 required property string notePath 0022 0023 readonly property QtObject imagePickerDialog: imagePickerDialog 0024 // This 'replicate' the DefaultCardBackground and just change the background color 0025 //(https://api.kde.org/frameworks/kirigami/html/DefaultCardBackground_8qml_source.html) 0026 background: Kirigami.ShadowedRectangle{ 0027 Kirigami.Theme.colorSet: Kirigami.Theme.Header 0028 Kirigami.Theme.inherit: false 0029 color: Kirigami.Theme.backgroundColor 0030 radius: Kirigami.Units.smallSpacing 0031 } 0032 0033 actions: [ 0034 Kirigami.Action { 0035 text: "𝐇" 0036 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Headers") + " (Ctrl+" + i18nc("@tooltip, short form of 'number'", "num") + ")" 0037 0038 Kirigami.Action { 0039 text: "𝐇𝟏" 0040 shortcut: "Ctrl+1" 0041 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 1") + " (" + shortcut + ")" 0042 0043 onTriggered: { 0044 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0045 editorTextArea.selectionEnd); 0046 0047 handleAction(selectionStart, selectionEnd, ["# "], false, false, false) 0048 } 0049 } 0050 Kirigami.Action { 0051 text: "𝐇𝟐" 0052 shortcut: "Ctrl+2" 0053 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 2") + " (" + shortcut + ")" 0054 0055 onTriggered: { 0056 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0057 editorTextArea.selectionEnd); 0058 0059 handleAction(selectionStart, selectionEnd, ["## "], false, false, false) 0060 } 0061 } 0062 Kirigami.Action { 0063 text: "𝐇𝟑" 0064 shortcut: "Ctrl+3" 0065 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 3") + " (" + shortcut + ")" 0066 0067 onTriggered: { 0068 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0069 editorTextArea.selectionEnd); 0070 0071 handleAction(selectionStart, selectionEnd, ["### "], false, false, false) 0072 } 0073 } 0074 Kirigami.Action { 0075 text: "𝐇𝟒" 0076 shortcut: "Ctrl+4" 0077 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 4") + " (" + shortcut + ")" 0078 0079 onTriggered: { 0080 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0081 editorTextArea.selectionEnd); 0082 0083 handleAction(selectionStart, selectionEnd, ["#### "], false, false, false) 0084 } 0085 } 0086 Kirigami.Action { 0087 text: "𝐇𝟓" 0088 shortcut: "Ctrl+5" 0089 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 5") + " (" + shortcut + ")" 0090 0091 onTriggered: { 0092 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0093 editorTextArea.selectionEnd); 0094 0095 handleAction(selectionStart, selectionEnd, ["###### "], false, false, false) 0096 } 0097 } 0098 Kirigami.Action { 0099 text: "𝐇𝟔" 0100 shortcut: "Ctrl+6" 0101 tooltip: i18nc("@tooltip, text format header level, will be followed by the shortcut", "Header 6") + " (" + shortcut + ")" 0102 0103 onTriggered: { 0104 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0105 editorTextArea.selectionEnd); 0106 0107 handleAction(selectionStart, selectionEnd, ["####### "], false, false, false) 0108 } 0109 } 0110 0111 }, 0112 Kirigami.Action { 0113 id: boldAction 0114 shortcut: "Ctrl+B" 0115 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Bold") + " (" + shortcut + ")" 0116 icon.name: "format-text-bold" 0117 onTriggered: handleAction(editorTextArea.selectionStart, 0118 editorTextArea.selectionEnd, ["**","__"], 0119 true, false, false) 0120 }, 0121 Kirigami.Action { 0122 id: italicAction 0123 shortcut: "Ctrl+I" 0124 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Italic") + " (" + shortcut + ")" 0125 icon.name: "format-text-italic" 0126 onTriggered: handleAction(editorTextArea.selectionStart, 0127 editorTextArea.selectionEnd, ["_","*"], 0128 true, false, false) 0129 }, 0130 Kirigami.Action { 0131 id: strikethroughAction 0132 shortcut: "Alt+Shift+S" 0133 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Strikethrough") + " (" + shortcut + ")" 0134 icon.name: "format-text-strikethrough" 0135 onTriggered: handleAction(editorTextArea.selectionStart, 0136 editorTextArea.selectionEnd, ["~~"], 0137 true, false, false) 0138 }, 0139 Kirigami.Action { 0140 id: codeBlockAction 0141 shortcut: "Ctrl+Shift+K" 0142 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Code") + " (" + shortcut + ")" 0143 icon.name: "format-text-code" 0144 onTriggered: handleAction(editorTextArea.selectionStart, 0145 editorTextArea.selectionEnd, ["\n```\n"], 0146 true, false, true) 0147 }, 0148 Kirigami.Action { 0149 id: quoteAction 0150 shortcut: "Ctrl+Shift+Q" 0151 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Quote") + " (" + shortcut + ")" 0152 icon.name: "format-text-blockquote" 0153 onTriggered: handleAction(editorTextArea.selectionStart, 0154 editorTextArea.selectionEnd, ["> "], 0155 false, false, false) 0156 }, 0157 Kirigami.Action { 0158 id: imageAction 0159 shortcut: "Ctrl+Shift+I" 0160 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Image") + " (" + shortcut + ")" 0161 icon.name: "insert-image" 0162 onTriggered: imagePickerDialog.open() 0163 }, 0164 Kirigami.Action { 0165 id: linkAction 0166 shortcut: "Ctrl+K" 0167 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Link") + " (" + shortcut + ")" 0168 icon.name: "insert-link-symbolic" 0169 onTriggered: linkDialog.open() 0170 }, 0171 Kirigami.Action { 0172 id: tableAction 0173 shortcut: "Ctrl+T" 0174 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Table") + " (" + shortcut + ")" 0175 icon.name: "insert-table" 0176 onTriggered: tableMakerDialog.open() 0177 }, 0178 Kirigami.Action { 0179 id: orderedListAction 0180 shortcut: "Ctrl+Shift+O" 0181 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Ordered list") + " (" + shortcut + ")" 0182 icon.name: "format-list-ordered" 0183 onTriggered: { 0184 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0185 editorTextArea.selectionEnd); 0186 0187 handleAction(selectionStart, selectionEnd, [". "], false, true, false) 0188 } 0189 }, 0190 Kirigami.Action { 0191 id: unorderedListAction 0192 shortcut: "Ctrl+Shift+U" 0193 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Unordered list") + " (" + shortcut + ")" 0194 icon.name: "format-list-unordered" 0195 onTriggered: { 0196 const [selectionStart, selectionEnd] = getLinesBlock(editorTextArea.selectionStart, 0197 editorTextArea.selectionEnd); 0198 0199 handleAction(selectionStart, selectionEnd, ["- "], false, false, false) 0200 } 0201 }, 0202 Kirigami.Action { 0203 id: highlightAction 0204 shortcut: "Ctrl+Alt+H" 0205 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Text highlight") + " (" + shortcut + ")" 0206 icon.name: "draw-highlight" 0207 onTriggered: { 0208 handleAction(editorTextArea.selectionStart, editorTextArea.selectionEnd, ["=="], true, false, false) 0209 } 0210 }, 0211 Kirigami.Action { 0212 id: emojiAction 0213 shortcut: "Ctrl+Shift+E" 0214 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Unordered list") + " (" + shortcut + ")" 0215 icon.name: "smiley" 0216 onTriggered: { 0217 emojiDialog.open() 0218 } 0219 }, 0220 Kirigami.Action { 0221 id: linkNoteAction 0222 visible: Config.noteMapEnabled 0223 shortcut: "Ctrl+Alt+K" 0224 tooltip: i18nc("@tooltip, text format, will be followed by the shortcut", "Link note") + " (" + shortcut + ")" 0225 icon.name: "edit-link" 0226 onTriggered: { 0227 linkNoteDialog.open() 0228 } 0229 } 0230 ] 0231 0232 EmojiDialog { 0233 id: emojiDialog 0234 0235 onChosen: function (emoji) { 0236 editorTextArea.insert(editorTextArea.selectionStart, Config.quickEmojiEnabled && Config.quickEmojiDialogEnabled ? (":" + emoji + ":") : emoji) 0237 } 0238 } 0239 0240 ImagePickerDialog { 0241 id: imagePickerDialog 0242 0243 noteImagesStoringPath: toolbar.notePath.replace("note.md","") + "Images/" 0244 0245 onRejected: { 0246 storedImageChoosen = false 0247 } 0248 onAccepted: if (imageLoaded) { 0249 let modifiedPath = path 0250 0251 let useLocalImage = storedImageChoosen 0252 if (storeImage && !storedImageChoosen) { 0253 let wantedImageName = imageName 0254 0255 if (wantedImageName.length === 0 && paintedImageChoosen) { 0256 wantedImageName = "painting" 0257 } else if (!paintedImageChoosen) { 0258 const fileName = KleverUtility.getName(path) 0259 wantedImageName = fileName.substring(0,fileName.lastIndexOf(".")) 0260 } 0261 0262 // We can't asign the result to modifiedPath and use it to saveToFile or it won't work ! 0263 const validPath = KleverUtility.getImageStoragingPath(noteImagesStoringPath, wantedImageName) 0264 modifiedPath = validPath 0265 0266 imageObject.grabToImage(function(result) { 0267 result.saveToFile(validPath) 0268 },Qt.size(imageObject.idealWidth,imageObject.idealHeight)); 0269 0270 useLocalImage = true 0271 0272 storedImagesExist = true 0273 } 0274 0275 if (modifiedPath.startsWith("file://")) modifiedPath = modifiedPath.replace("file://","") 0276 0277 if (useLocalImage) modifiedPath = "./Images/"+modifiedPath.replace(noteImagesStoringPath,"") 0278 0279 if (modifiedPath.startsWith("/home/")) { 0280 // Get the first "/" after the /home/username 0281 modifiedPath = modifiedPath.replace("/home/","") 0282 const idx = modifiedPath.indexOf("/") 0283 modifiedPath = "~" + modifiedPath.substring(idx) 0284 } 0285 0286 let imageString = '![' + imageName + '](' + modifiedPath + ') ' 0287 0288 toolbar.editorTextArea.insert(toolbar.editorTextArea.cursorPosition, imageString) 0289 0290 storedImageChoosen = false 0291 } 0292 } 0293 0294 TableMakerDialog { 0295 id: tableMakerDialog 0296 0297 onAccepted: { 0298 const alignPattern = { 0299 "left": ":" + "-".repeat(i18n("Header").length - 1), 0300 "center": ":" + "-".repeat(i18n("Header").length - 2) + ":", 0301 "right": "-".repeat(i18n("Header").length - 1) + ":" 0302 } 0303 const cells = "|" + (" ".repeat(i18n("Header").length) + "|").repeat(tableMakerDialog.columnCount) + "\n" 0304 const headers = "|" + (i18n("Header") + "|").repeat(tableMakerDialog.columnCount) + "\n" 0305 0306 let columnsAlignments = "|" 0307 0308 for(var childIdx = 0; childIdx < tableMakerDialog.columnCount; childIdx++) { 0309 columnsAlignments = columnsAlignments.concat(alignPattern[tableMakerDialog.alignment],"|") 0310 } 0311 columnsAlignments += "\n" 0312 0313 const result = "\n" + headers + columnsAlignments + cells.repeat(tableMakerDialog.rowCount-1) 0314 0315 toolbar.editorTextArea.insert(toolbar.editorTextArea.cursorPosition, result) 0316 } 0317 } 0318 0319 LinkDialog { 0320 id: linkDialog 0321 0322 onAccepted: { 0323 let linkString = '[' + linkText + '](' + urlText + ') ' 0324 toolbar.editorTextArea.insert(toolbar.editorTextArea.cursorPosition, linkString) 0325 } 0326 } 0327 0328 LinkNoteDialog { 0329 id: linkNoteDialog 0330 0331 listModel: applicationWindow().globalDrawer.treeModel 0332 0333 onAccepted: { 0334 const text = linkText.trim() 0335 const notePath = path.substring(Config.storagePath.length) 0336 const headerPart = headerString.length > 0 0337 ? " : " + headerString 0338 : "" 0339 const textPart = text.length > 0 0340 ? " | " + linkText 0341 : "" 0342 const linkString = '[[' + notePath + headerPart + textPart + ']]' 0343 toolbar.editorTextArea.insert(toolbar.editorTextArea.cursorPosition, linkString) 0344 } 0345 } 0346 0347 function applyInstructions(selectionStart, selectionEnd, info, givenSpecialChars, 0348 multiPlaceApply, applyIncrement, checkByBlock) { 0349 let applied = false 0350 let specialChars = givenSpecialChars 0351 if (checkByBlock) { 0352 const instruction = info.instructions 0353 0354 if (instruction === "apply") { 0355 toolbar.editorTextArea.insert(selectionEnd, specialChars) 0356 toolbar.editorTextArea.insert(selectionStart, specialChars) 0357 applied = true 0358 0359 } else { 0360 toolbar.editorTextArea.remove(selectionEnd - specialChars.length + 1, selectionEnd + 1) 0361 toolbar.editorTextArea.remove(selectionStart, selectionStart + specialChars.length) 0362 } 0363 } else { 0364 const instructions = info.instructions 0365 const lines = info.lines 0366 0367 let end = selectionEnd 0368 0369 // Currently only used for ordered list 0370 const nonEmptyStrNumber = lines.filter((line) => line.trim().length > 0).length 0371 const hasNonEmptyStrings = nonEmptyStrNumber > 0 0372 let counter = hasNonEmptyStrings ? nonEmptyStrNumber : 1 // Pressing ordered list on an single empty line will return 1. and not 0. 0373 0374 for (var i = lines.length-1 ; i >= 0; i--) { 0375 const line = lines[i] 0376 const instruction = instructions[i] 0377 0378 end = (line.length > 0 || lines.length == 1) ? end : end - 1 0379 const start = end - line.length 0380 0381 // Currently only used for ordered list 0382 if (line.trim().length === 0 && hasNonEmptyStrings) continue 0383 if (applyIncrement) { 0384 specialChars = counter.toString() + givenSpecialChars 0385 counter-- 0386 } 0387 0388 switch(instruction) { 0389 case "apply": 0390 if (multiPlaceApply) toolbar.editorTextArea.insert(end, specialChars) 0391 toolbar.editorTextArea.insert(start, specialChars) 0392 0393 applied = true 0394 break; 0395 case "remove": 0396 if (multiPlaceApply) toolbar.editorTextArea.remove(end - specialChars.length, end) 0397 toolbar.editorTextArea.remove(start, start + specialChars.length) 0398 break; 0399 default: 0400 break 0401 } 0402 end = start - 1 0403 } 0404 } 0405 if (applied) { 0406 let end = toolbar.editorTextArea.selectionEnd 0407 0408 let endingNewLineCounter = 0 0409 while (specialChars.endsWith('\n')) { 0410 endingNewLineCounter++ 0411 specialChars = specialChars.substring(0, specialChars.length - 2) 0412 } 0413 0414 end = (checkByBlock) ? end - endingNewLineCounter : end 0415 toolbar.editorTextArea.select(selectionStart, end) 0416 } 0417 } 0418 0419 function handleAction(selectionStart, selectionEnd, specialChars, 0420 multiPlaceApply, applyIncrement, checkByBlock) { 0421 0422 const selectedText = editorTextArea.getText(selectionStart, selectionEnd) 0423 const info = MDHandler.getInstructions(selectedText, specialChars, 0424 multiPlaceApply, applyIncrement, 0425 checkByBlock) 0426 0427 const appliedSpecialChars = specialChars[0] 0428 applyInstructions(selectionStart, selectionEnd, info, 0429 appliedSpecialChars, multiPlaceApply, 0430 applyIncrement, checkByBlock) 0431 } 0432 0433 function getLinesBlock(selectionStart,selectionEnd) { 0434 const startingText = editorTextArea.getText(0, editorTextArea.selectionStart) 0435 const endingText = editorTextArea.getText(editorTextArea.selectionEnd, 0436 editorTextArea.text.length) 0437 0438 0439 const startBlockIndex = startingText.lastIndexOf('\n')+1 0440 0441 return [startBlockIndex, editorTextArea.selectionEnd] 0442 } 0443 }