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 }