Commit f3fdd807 authored by jan.koester's avatar jan.koester
Browse files

deb

parent fa4ec8d8
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
blogi (20260518+5) unstable; urgency=medium

  * Editor: add table editing to TextBox widget (insert table, add/remove
    rows and columns)
  * Editor: fix H1/H2/H3/P buttons nesting instead of replacing — new
    setBlockFormat() strips existing block tags before applying
  * Editor: fix wrapSelection destroying existing formatting — map plain
    text cursor positions to HTML positions via plainToHtmlPos()
  * Editor: add font size ComboBox for per-paragraph font sizes
  * Editor: fix stripQtArtifacts stripping user-set font sizes — only
    remove Qt-injected pt values, preserve px/em/rem
  * Editor: toolbar wraps to next line when window is narrow (Flow layout)
  * Editor: fix FileBrowserTab/GitTab/AiTab not growing with sidebar size
  * TextBox plugin: fix fromPropertiesJson preventing property value reset

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 18 May 2026 12:00:00 +0200

blogi (20260516+4) unstable; urgency=medium

  * GTM plugin: always load gtm.js container regardless of cookie consent
+336 −18
Original line number Diff line number Diff line
@@ -43,9 +43,9 @@ ColumnLayout {

        /* Remove font-family / font-size from inline styles (Qt injects system font) */
        s = s.replace(/\s*font-family\s*:\s*'[^']*'\s*;?/gi, "")
        s = s.replace(/\s*font-size\s*:\s*[^;'"]+;?/gi, "")
        s = s.replace(/\s*font-weight\s*:\s*[^;'"]+;?/gi, "")
        s = s.replace(/\s*font-style\s*:\s*[^;'"]+;?/gi, "")
        s = s.replace(/\s*font-size\s*:\s*[\d.]+pt\s*;?/gi, "")
        s = s.replace(/\s*font-weight\s*:\s*\d+\s*;?/gi, "")
        s = s.replace(/\s*font-style\s*:\s*normal\s*;?/gi, "")

        /* Clean up remaining empty style attributes after font removal */
        s = s.replace(/\s+style\s*=\s*"[\s;]*"/gi, "")
@@ -62,7 +62,7 @@ ColumnLayout {
        return s.trim()
    }

    RowLayout {
    Flow {
        Layout.fillWidth: true
        spacing: 2

@@ -70,19 +70,102 @@ ColumnLayout {
        ToolButton { text: "<i>I</i>"; onClicked: wrapSelection("<i>", "</i>"); font.italic: true; visible: !rawMode }
        ToolButton { text: "<u>U</u>"; onClicked: wrapSelection("<u>", "</u>"); font.underline: true; visible: !rawMode }

        ToolSeparator { visible: !rawMode }
        Rectangle { width: 1; height: 24; color: Theme.border; visible: !rawMode }

        /* Font size */
        ComboBox {
            id: fontSizeCombo
            visible: !rawMode
            implicitWidth: 80
            model: ["", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "28px", "32px", "36px", "48px", "64px"]
            displayText: currentIndex > 0 ? currentText : qsTr("Size")
            editable: true
            ToolTip.text: qsTr("Font Size")
            ToolTip.visible: hovered
            onActivated: function(index) {
                if (index > 0) {
                    wrapSelection("<span style=\"font-size:" + model[index] + "\">", "</span>")
                }
            }
            onAccepted: {
                var val = editText.trim()
                if (val.length > 0) {
                    if (/^\d+$/.test(val)) val += "px"
                    wrapSelection("<span style=\"font-size:" + val + "\">", "</span>")
                }
            }
        }

        Rectangle { width: 1; height: 24; color: Theme.border; visible: !rawMode }

        ToolButton { text: "H1"; onClicked: wrapSelection("<h1>", "</h1>"); visible: !rawMode }
        ToolButton { text: "H2"; onClicked: wrapSelection("<h2>", "</h2>"); visible: !rawMode }
        ToolButton { text: "H3"; onClicked: wrapSelection("<h3>", "</h3>"); visible: !rawMode }
        ToolButton { text: "H1"; onClicked: setBlockFormat("h1"); visible: !rawMode }
        ToolButton { text: "H2"; onClicked: setBlockFormat("h2"); visible: !rawMode }
        ToolButton { text: "H3"; onClicked: setBlockFormat("h3"); visible: !rawMode }

        ToolSeparator { visible: !rawMode }
        Rectangle { width: 1; height: 24; color: Theme.border; visible: !rawMode }

        ToolButton { text: "p"; onClicked: wrapSelection("<p>", "</p>"); visible: !rawMode }
        ToolButton { text: "p"; onClicked: setBlockFormat("p"); visible: !rawMode }
        ToolButton { text: "br"; onClicked: wrapSelection("<br/>", ""); visible: !rawMode }
        ToolButton { text: "Link"; onClicked: wrapSelection("<a href=\"#\">", "</a>"); visible: !rawMode }

        ToolSeparator { visible: !rawMode }
        Rectangle { width: 1; height: 24; color: Theme.border; visible: !rawMode }

        /* Table editing */
        ToolButton {
            text: "Table"
            visible: !rawMode
            ToolTip.text: qsTr("Insert / Edit Table")
            ToolTip.visible: hovered
            onClicked: tablePopup.open()

            Popup {
                id: tablePopup
                width: tablePopupCol.width + 24
                height: tablePopupCol.height + 24
                padding: 12

                ColumnLayout {
                    id: tablePopupCol
                    spacing: 8

                    Label { text: qsTr("Insert Table"); font.bold: true }

                    RowLayout {
                        spacing: 6
                        Label { text: qsTr("Rows:") }
                        SpinBox { id: tableRowsSpin; from: 1; to: 50; value: 3 }
                    }
                    RowLayout {
                        spacing: 6
                        Label { text: qsTr("Cols:") }
                        SpinBox { id: tableColsSpin; from: 1; to: 20; value: 3 }
                    }
                    CheckBox { id: tableHeaderCheck; text: qsTr("Include header row"); checked: true }

                    Button {
                        text: qsTr("Insert Table")
                        onClicked: {
                            insertTable(tableRowsSpin.value, tableColsSpin.value, tableHeaderCheck.checked)
                            tablePopup.close()
                        }
                    }

                    ToolSeparator { Layout.fillWidth: true }

                    Label { text: qsTr("Modify Existing Table"); font.bold: true }

                    RowLayout {
                        spacing: 4
                        Button { text: qsTr("+ Row"); onClicked: { tableAddRow(); tablePopup.close() } }
                        Button { text: qsTr("- Row"); onClicked: { tableRemoveRow(); tablePopup.close() } }
                        Button { text: qsTr("+ Col"); onClicked: { tableAddColumn(); tablePopup.close() } }
                        Button { text: qsTr("- Col"); onClicked: { tableRemoveColumn(); tablePopup.close() } }
                    }
                }
            }
        }

        Rectangle { width: 1; height: 24; color: Theme.border; visible: !rawMode }

        /* Text color */
        ToolButton {
@@ -237,8 +320,6 @@ ColumnLayout {
            }
        }

        Item { Layout.fillWidth: true }

        Switch {
            id: modeSwitch
            text: qsTr("Raw HTML")
@@ -292,18 +373,255 @@ ColumnLayout {
        }
    }

    function plainToHtmlPos(html, plainPos) {
        var count = 0
        var i = 0
        while (i < html.length && count < plainPos) {
            if (html[i] === '<') {
                // Skip entire HTML tag
                while (i < html.length && html[i] !== '>') i++
                i++ // skip '>'
            } else if (html[i] === '&') {
                // HTML entity counts as one plain text character
                var semiPos = html.indexOf(';', i)
                if (semiPos > i && semiPos - i < 10) {
                    i = semiPos + 1
                } else {
                    i++
                }
                count++
            } else {
                i++
                count++
            }
        }
        return i
    }

    function wrapSelection(prefix, suffix) {
        var start = editorArea.selectionStart
        var end = editorArea.selectionEnd
        var currentText = editorArea.getText(0, editorArea.length)
        var selectedText = currentText.substring(start, end)
        var newText = currentText.substring(0, start) + prefix + selectedText + suffix + currentText.substring(end)

        if (root.rawMode) {
            // In raw mode, positions map directly to text
            var rawText = editorArea.text
            var selectedText = rawText.substring(start, end)
            var newText = rawText.substring(0, start) + prefix + selectedText + suffix + rawText.substring(end)
            _switching = true
            editorArea.text = newText
            editorArea.cursorPosition = start + prefix.length + selectedText.length
            _switching = false
            root.textChangedByUser(newText)
            return
        }

        // In RichText mode, work with cleaned HTML and map positions
        var html = stripQtArtifacts(editorArea.text)
        var htmlStart = plainToHtmlPos(html, start)
        var htmlEnd = plainToHtmlPos(html, end)

        var newHtml = html.substring(0, htmlStart) + prefix + html.substring(htmlStart, htmlEnd) + suffix + html.substring(htmlEnd)

        _switching = true
        editorArea.textFormat = TextEdit.RichText
        editorArea.text = newHtml
        editorArea.cursorPosition = end + suffix.length
        _switching = false

        root.textChangedByUser(stripQtArtifacts(editorArea.text))
    }

    function setBlockFormat(tag) {
        var start = editorArea.selectionStart
        var end = editorArea.selectionEnd

        if (start === end) {
            // No selection: just insert an empty block tag at cursor
            wrapSelection("<" + tag + ">", "</" + tag + ">")
            return
        }

        var html = root.rawMode ? editorArea.text : stripQtArtifacts(editorArea.text)
        var htmlStart = root.rawMode ? start : plainToHtmlPos(html, start)
        var htmlEnd = root.rawMode ? end : plainToHtmlPos(html, end)

        var selectedHtml = html.substring(htmlStart, htmlEnd)

        // Strip any existing block-level heading/paragraph tags from the selected HTML
        var blockTags = ["h1", "h2", "h3", "h4", "h5", "h6", "p"]
        var stripped = selectedHtml
        for (var i = 0; i < blockTags.length; i++) {
            var openRe = new RegExp("<" + blockTags[i] + "\\b[^>]*>", "gi")
            var closeRe = new RegExp("</" + blockTags[i] + ">", "gi")
            stripped = stripped.replace(openRe, "")
            stripped = stripped.replace(closeRe, "")
        }

        // Also strip from surrounding context to handle tags wrapping the selection
        var before = html.substring(0, htmlStart)
        var after = html.substring(htmlEnd)
        for (var j = 0; j < blockTags.length; j++) {
            // Remove an opening tag immediately before the selection
            var openEnd = new RegExp("<" + blockTags[j] + "\\b[^>]*>\\s*$", "i")
            before = before.replace(openEnd, "")
            // Remove a closing tag immediately after the selection
            var closeStart = new RegExp("^\\s*</" + blockTags[j] + ">", "i")
            after = after.replace(closeStart, "")
        }

        var newHtml = before + "<" + tag + ">" + stripped + "</" + tag + ">" + after

        _switching = true
        if (root.rawMode) {
            editorArea.text = newHtml
        } else {
            editorArea.textFormat = TextEdit.RichText
            editorArea.text = newHtml
        }
        _switching = false
        root.textChangedByUser(root.rawMode ? newHtml : stripQtArtifacts(newHtml))
    }

    function insertTable(rows, cols, hasHeader) {
        var html = "<table style=\"border-collapse:collapse; width:100%;\">"
        var startRow = 0
        if (hasHeader) {
            html += "<thead><tr>"
            for (var c = 0; c < cols; c++)
                html += "<th style=\"border:1px solid #ccc; padding:8px;\">Header " + (c + 1) + "</th>"
            html += "</tr></thead>"
            startRow = 0
        }
        html += "<tbody>"
        for (var r = 0; r < rows; r++) {
            html += "<tr>"
            for (var c2 = 0; c2 < cols; c2++)
                html += "<td style=\"border:1px solid #ccc; padding:8px;\"></td>"
            html += "</tr>"
        }
        html += "</tbody></table>"
        wrapSelection(html, "")
    }

    function tableAddRow() {
        var html = root.rawMode ? editorArea.text : stripQtArtifacts(editorArea.text)
        var lastRowClose = html.lastIndexOf("</tr>")
        if (lastRowClose < 0) return

        // Find the last row to determine column count
        var rowStart = html.lastIndexOf("<tr>", lastRowClose)
        if (rowStart < 0) rowStart = html.lastIndexOf("<tr ", lastRowClose)
        if (rowStart < 0) return

        var lastRow = html.substring(rowStart, lastRowClose + 5)
        var colCount = (lastRow.match(/<td[\s>]/g) || []).length
        if (colCount === 0) colCount = (lastRow.match(/<th[\s>]/g) || []).length
        if (colCount === 0) colCount = 1

        var newRow = "<tr>"
        for (var i = 0; i < colCount; i++)
            newRow += "<td style=\"border:1px solid #ccc; padding:8px;\"></td>"
        newRow += "</tr>"

        var insertPos = lastRowClose + 5
        var newHtml = html.substring(0, insertPos) + newRow + html.substring(insertPos)
        _switching = true
        if (root.rawMode) {
            editorArea.text = newHtml
        } else {
            editorArea.textFormat = TextEdit.RichText
            editorArea.text = newHtml
        }
        _switching = false
        root.textChangedByUser(newHtml)
    }

    function tableRemoveRow() {
        var html = root.rawMode ? editorArea.text : stripQtArtifacts(editorArea.text)
        var lastRowClose = html.lastIndexOf("</tr>")
        if (lastRowClose < 0) return

        var rowStart = html.lastIndexOf("<tr>", lastRowClose)
        if (rowStart < 0) rowStart = html.lastIndexOf("<tr ", lastRowClose)
        if (rowStart < 0) return

        // Don't remove if it's the only row
        var firstRowStart = html.indexOf("<tr")
        if (firstRowStart === rowStart) return

        var newHtml = html.substring(0, rowStart) + html.substring(lastRowClose + 5)
        _switching = true
        if (root.rawMode) {
            editorArea.text = newHtml
        } else {
            editorArea.textFormat = TextEdit.RichText
            editorArea.text = newHtml
        }
        _switching = false
        root.textChangedByUser(newHtml)
    }

    function tableAddColumn() {
        var html = root.rawMode ? editorArea.text : stripQtArtifacts(editorArea.text)
        if (html.indexOf("<table") < 0) return

        // Insert a new <td> or <th> before each </tr>
        var newHtml = html.replace(/<\/tr>/g, function(match, offset) {
            // Determine if this row has <th> or <td>
            var rowStart = html.lastIndexOf("<tr", offset)
            var rowContent = html.substring(rowStart, offset)
            if (rowContent.indexOf("<th") >= 0)
                return "<th style=\"border:1px solid #ccc; padding:8px;\"></th></tr>"
            else
                return "<td style=\"border:1px solid #ccc; padding:8px;\"></td></tr>"
        })

        _switching = true
        if (root.rawMode) {
            editorArea.text = newHtml
        } else {
            editorArea.textFormat = TextEdit.RichText
            editorArea.text = newHtml
        }
        _switching = false
        root.textChangedByUser(newHtml)
    }

    function tableRemoveColumn() {
        var html = root.rawMode ? editorArea.text : stripQtArtifacts(editorArea.text)
        if (html.indexOf("<table") < 0) return

        // Remove the last <td>...</td> or <th>...</th> from each row
        var newHtml = html.replace(/<tr[\s>][\s\S]*?<\/tr>/g, function(rowMatch) {
            // Remove last <td...>...</td> or <th...>...</th>
            var lastTd = rowMatch.lastIndexOf("<td")
            var lastTh = rowMatch.lastIndexOf("<th")
            var removeStart = Math.max(lastTd, lastTh)
            if (removeStart < 0) return rowMatch

            // Don't remove if it's the only cell
            var firstCell = Math.min(
                rowMatch.indexOf("<td") >= 0 ? rowMatch.indexOf("<td") : 9999,
                rowMatch.indexOf("<th") >= 0 ? rowMatch.indexOf("<th") : 9999
            )
            if (firstCell === removeStart) return rowMatch

            var closeTag = lastTd > lastTh ? "</td>" : "</th>"
            var removeEnd = rowMatch.indexOf(closeTag, removeStart)
            if (removeEnd < 0) return rowMatch
            removeEnd += closeTag.length

            return rowMatch.substring(0, removeStart) + rowMatch.substring(removeEnd)
        })

        _switching = true
        if (root.rawMode) {
            editorArea.text = newHtml
        } else {
            editorArea.textFormat = TextEdit.RichText
            editorArea.text = newHtml
        }
        _switching = false
        root.textChangedByUser(newHtml)
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -253,12 +253,18 @@ ApplicationWindow {
                    }
                    FileBrowserTab {
                        id: fileBrowserInst
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                    }
                    GitTab {
                        id: gitTabInst
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                    }
                    AiTab {
                        id: aiTabInst
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                    }
                }
            }
+7 −7
Original line number Diff line number Diff line
@@ -389,31 +389,31 @@ extern "C" {

                if (json_object_object_get_ex(obj, "font_family", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) fontFamily = val;
                    if (val) fontFamily = val;
                }
                if (json_object_object_get_ex(obj, "font_size", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) fontSize = val;
                    if (val) fontSize = val;
                }
                if (json_object_object_get_ex(obj, "font_color", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) fontColor = val;
                    if (val) fontColor = val;
                }
                if (json_object_object_get_ex(obj, "text_align", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) textAlign = val;
                    if (val) textAlign = val;
                }
                if (json_object_object_get_ex(obj, "font_weight", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) fontWeight = val;
                    if (val) fontWeight = val;
                }
                if (json_object_object_get_ex(obj, "font_style", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) fontStyle = val;
                    if (val) fontStyle = val;
                }
                if (json_object_object_get_ex(obj, "white_space", &val_obj)) {
                    val = json_object_get_string(val_obj);
                    if (val && val[0]) whiteSpace = val;
                    if (val) whiteSpace = val;
                }
                if (json_object_object_get_ex(obj, "width", &val_obj)) {
                    val = json_object_get_string(val_obj); if (val) width = val;