Loading debian/changelog +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 Loading editor/qml/HtmlEditorField.qml +336 −18 Original line number Diff line number Diff line Loading @@ -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, "") Loading @@ -62,7 +62,7 @@ ColumnLayout { return s.trim() } RowLayout { Flow { Layout.fillWidth: true spacing: 2 Loading @@ -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 { Loading Loading @@ -237,8 +320,6 @@ ColumnLayout { } } Item { Layout.fillWidth: true } Switch { id: modeSwitch text: qsTr("Raw HTML") Loading Loading @@ -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) } } editor/qml/Main.qml +6 −0 Original line number Diff line number Diff line Loading @@ -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 } } } Loading editor/widgets/textbox/textbox.cpp +7 −7 Original line number Diff line number Diff line Loading @@ -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; Loading Loading
debian/changelog +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 Loading
editor/qml/HtmlEditorField.qml +336 −18 Original line number Diff line number Diff line Loading @@ -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, "") Loading @@ -62,7 +62,7 @@ ColumnLayout { return s.trim() } RowLayout { Flow { Layout.fillWidth: true spacing: 2 Loading @@ -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 { Loading Loading @@ -237,8 +320,6 @@ ColumnLayout { } } Item { Layout.fillWidth: true } Switch { id: modeSwitch text: qsTr("Raw HTML") Loading Loading @@ -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) } }
editor/qml/Main.qml +6 −0 Original line number Diff line number Diff line Loading @@ -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 } } } Loading
editor/widgets/textbox/textbox.cpp +7 −7 Original line number Diff line number Diff line Loading @@ -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; Loading