forked from Shinonome/dots-hyprland
ai chat: latex rendering
This commit is contained in:
@@ -33,3 +33,11 @@ function splitMarkdownBlocks(markdown) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimFileProtocol(str) {
|
||||||
|
return str.startsWith("file://") ? str.slice(7) : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeBackslashes(str) {
|
||||||
|
return str.replace(/\\/g, '\\\\');
|
||||||
|
}
|
||||||
@@ -83,7 +83,8 @@ Item {
|
|||||||
## ✏️ Markdown test
|
## ✏️ Markdown test
|
||||||
### Formatting
|
### Formatting
|
||||||
|
|
||||||
*Italic*, \`Monospace\`, **Bold**, [Link](https://example.com)
|
- *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com)
|
||||||
|
- Arch lincox icon <img src="/home/end/.config/quickshell/assets/icons/arch-symbolic.svg" height="${Appearance.font.pixelSize.small}"/>
|
||||||
|
|
||||||
### Table
|
### Table
|
||||||
|
|
||||||
@@ -114,7 +115,9 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
### LaTeX
|
### LaTeX
|
||||||
|
|
||||||
Inline: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$
|
- Simple inline: $\\frac{1}{2} = \\frac{2}{4}$
|
||||||
|
- Complex inline: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$
|
||||||
|
- Another complex inline: \\\\[\\int_0^\\infty \\frac{1}{x^2} dx = \\infty\\\\]
|
||||||
|
|
||||||
`,
|
`,
|
||||||
Ai.interfaceRole);
|
Ai.interfaceRole);
|
||||||
|
|||||||
@@ -89,19 +89,25 @@ Rectangle {
|
|||||||
|
|
||||||
RowLayout { // Header
|
RowLayout { // Header
|
||||||
spacing: 15
|
spacing: 15
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
Rectangle { // Name
|
Rectangle { // Name
|
||||||
id: nameWrapper
|
id: nameWrapper
|
||||||
color: Appearance.m3colors.m3secondaryContainer
|
color: Appearance.m3colors.m3secondaryContainer
|
||||||
|
// color: "transparent"
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
implicitWidth: nameRowLayout.implicitWidth + 10 * 2
|
|
||||||
implicitHeight: Math.max(nameRowLayout.implicitHeight + 5 * 2, 30)
|
implicitHeight: Math.max(nameRowLayout.implicitHeight + 5 * 2, 30)
|
||||||
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: nameRowLayout
|
id: nameRowLayout
|
||||||
anchors.centerIn: parent
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 5
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 7
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
@@ -141,6 +147,8 @@ Rectangle {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: providerName
|
id: providerName
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
font.weight: Font.DemiBold
|
font.weight: Font.DemiBold
|
||||||
color: Appearance.m3colors.m3onSecondaryContainer
|
color: Appearance.m3colors.m3onSecondaryContainer
|
||||||
@@ -172,8 +180,6 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
@@ -240,11 +246,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
property var segment: modelData
|
// property var segment: modelData
|
||||||
|
property var segmentContent: modelData.content
|
||||||
|
property var segmentLang: modelData.lang
|
||||||
property var messageData: root.messageData
|
property var messageData: root.messageData
|
||||||
property var editing: root.editing
|
property var editing: root.editing
|
||||||
property var renderMarkdown: root.renderMarkdown
|
property var renderMarkdown: root.renderMarkdown
|
||||||
property var enableMouseSelection: root.enableMouseSelection
|
property var enableMouseSelection: root.enableMouseSelection
|
||||||
|
property bool thinking: root.messageData.thinking
|
||||||
|
property bool done: root.messageData.done
|
||||||
|
|
||||||
source: modelData.type === "code" ? "MessageCodeBlock.qml" : "MessageTextBlock.qml"
|
source: modelData.type === "code" ? "MessageCodeBlock.qml" : "MessageTextBlock.qml"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ ColumnLayout {
|
|||||||
property bool editing: parent?.editing ?? false
|
property bool editing: parent?.editing ?? false
|
||||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||||
property var segment: parent?.segment ?? {}
|
property var segmentContent: parent?.segmentContent ?? ({})
|
||||||
|
property var segmentLang: parent?.segmentLang ?? "plaintext"
|
||||||
property var messageData: parent?.messageData ?? {}
|
property var messageData: parent?.messageData ?? {}
|
||||||
|
|
||||||
spacing: codeBlockComponentSpacing
|
spacing: codeBlockComponentSpacing
|
||||||
@@ -57,7 +58,7 @@ ColumnLayout {
|
|||||||
font.pixelSize: Appearance.font.pixelSize.small
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
font.weight: Font.DemiBold
|
font.weight: Font.DemiBold
|
||||||
color: Appearance.colors.colOnLayer2
|
color: Appearance.colors.colOnLayer2
|
||||||
text: segment.lang ? Repository.definitionForName(segment.lang).name : "plain"
|
text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain"
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
Item { Layout.fillWidth: true }
|
||||||
@@ -66,7 +67,7 @@ ColumnLayout {
|
|||||||
id: copyCodeButton
|
id: copyCodeButton
|
||||||
buttonIcon: "content_copy"
|
buttonIcon: "content_copy"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segment.content)}'`)
|
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segmentContent)}'`)
|
||||||
}
|
}
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
content: qsTr("Copy code")
|
content: qsTr("Copy code")
|
||||||
@@ -160,7 +161,7 @@ ColumnLayout {
|
|||||||
id: codeTextArea
|
id: codeTextArea
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
readOnly: !editing
|
readOnly: !editing
|
||||||
selectByMouse: enableMouseSelection || editing
|
// selectByMouse: enableMouseSelection || editing
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
font.family: Appearance.font.family.monospace
|
font.family: Appearance.font.family.monospace
|
||||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||||
@@ -170,9 +171,9 @@ ColumnLayout {
|
|||||||
// wrapMode: TextEdit.Wrap
|
// wrapMode: TextEdit.Wrap
|
||||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||||
|
|
||||||
text: segment.content
|
text: segmentContent
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
segment.content = text
|
segmentContent = text
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
@@ -183,7 +184,7 @@ ColumnLayout {
|
|||||||
codeTextArea.cursorPosition = cursor + 4;
|
codeTextArea.cursorPosition = cursor + 4;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||||
messageText.copy();
|
codeTextArea.copy();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,23 +193,22 @@ ColumnLayout {
|
|||||||
id: highlighter
|
id: highlighter
|
||||||
textEdit: codeTextArea
|
textEdit: codeTextArea
|
||||||
repository: Repository
|
repository: Repository
|
||||||
definition: Repository.definitionForName(segment.lang || "plaintext")
|
definition: Repository.definitionForName(segmentLang || "plaintext")
|
||||||
// definition: Repository.definitionForName("cpp")
|
|
||||||
theme: Appearance.syntaxHighlightingTheme
|
theme: Appearance.syntaxHighlightingTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MouseArea to block scrolling
|
// MouseArea to block scrolling
|
||||||
MouseArea {
|
// MouseArea {
|
||||||
id: codeBlockMouseArea
|
// id: codeBlockMouseArea
|
||||||
anchors.fill: parent
|
// anchors.fill: parent
|
||||||
acceptedButtons: editing ? Qt.NoButton : Qt.LeftButton
|
// acceptedButtons: editing ? Qt.NoButton : Qt.LeftButton
|
||||||
cursorShape: (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
// cursorShape: (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
||||||
onWheel: (event) => {
|
// onWheel: (event) => {
|
||||||
event.accepted = false
|
// event.accepted = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,50 +14,129 @@ import Quickshell
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
TextArea {
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
// These are needed on the parent loader
|
// These are needed on the parent loader
|
||||||
property bool editing: parent?.editing ?? false
|
property bool editing: parent?.editing ?? false
|
||||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||||
property var segment: parent?.segment ?? {}
|
property string segmentContent: parent?.segmentContent ?? ({})
|
||||||
property var messageData: parent?.messageData ?? {}
|
property var messageData: parent?.messageData ?? {}
|
||||||
|
property bool done: parent?.done ?? true
|
||||||
|
property list<string> renderedLatexHashes: []
|
||||||
|
|
||||||
|
property string renderedSegmentContent: ""
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
readOnly: !editing
|
|
||||||
selectByMouse: enableMouseSelection || editing
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
font.family: Appearance.font.family.reading
|
|
||||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.small
|
|
||||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
|
||||||
selectionColor: Appearance.m3colors.m3secondaryContainer
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
|
||||||
textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
|
|
||||||
text: messageData.thinking ? qsTr("Waiting for response...") : segment.content
|
|
||||||
|
|
||||||
onTextChanged: {
|
function renderLatex() {
|
||||||
segment.content = text
|
// Regex for $...$, $$...$$, \[...\]
|
||||||
}
|
// Note: This is a simple approach and may need refinement for edge cases
|
||||||
|
let regex = /(\$\$([\s\S]+?)\$\$)|(\$([^\$]+?)\$)|(\\\[((?:.|\n)+?)\\\])/g;
|
||||||
Keys.onPressed: (event) => {
|
let match;
|
||||||
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
while ((match = regex.exec(segmentContent)) !== null) {
|
||||||
messageText.copy()
|
let expression = match[1] || match[2] || match[3];
|
||||||
event.accepted = true
|
if (expression) {
|
||||||
|
// Qt.callLater(() => {
|
||||||
|
// });
|
||||||
|
const [renderHash, isNew] = LatexRenderer.requestRender(expression.trim());
|
||||||
|
if (!renderedLatexHashes.includes(renderHash)) {
|
||||||
|
renderedLatexHashes.push(renderHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLinkActivated: (link) => {
|
function handleRenderedLatex(hash, force = false) {
|
||||||
Qt.openUrlExternally(link)
|
if (renderedLatexHashes.includes(hash) || force) {
|
||||||
Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
const imagePath = LatexRenderer.renderedImagePaths[hash];
|
||||||
|
const markdownImage = ``;
|
||||||
|
|
||||||
|
const expression = StringUtils.escapeBackslashes(LatexRenderer.processedExpressions[hash]);
|
||||||
|
renderedSegmentContent = renderedSegmentContent.replace(expression, markdownImage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea { // Pointing hand for links
|
onDoneChanged: {
|
||||||
anchors.fill: parent
|
renderLatex()
|
||||||
acceptedButtons: Qt.NoButton // Only for hover
|
for (const hash of renderedLatexHashes) {
|
||||||
hoverEnabled: true
|
handleRenderedLatex(hash, true);
|
||||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
|
}
|
||||||
(enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
}
|
||||||
|
onEditingChanged: {
|
||||||
|
if (!editing) {
|
||||||
|
renderLatex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSegmentContentChanged: {
|
||||||
|
// console.log("Segment content changed: " + segmentContent);
|
||||||
|
renderedSegmentContent = segmentContent;
|
||||||
|
if (!root.editing && segmentContent) {
|
||||||
|
root.renderLatex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRenderedSegmentContentChanged: {
|
||||||
|
// console.log("Rendered segment content changed: " + renderedSegmentContent);
|
||||||
|
if (renderedSegmentContent) {
|
||||||
|
textArea.text = renderedSegmentContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When something finishes rendering
|
||||||
|
// 1. Check if the hash is in the list
|
||||||
|
// 2. If it is, replace the expression with the image path
|
||||||
|
Connections {
|
||||||
|
target: LatexRenderer
|
||||||
|
function onRenderFinished(hash, imagePath) {
|
||||||
|
const expression = LatexRenderer.processedExpressions[hash];
|
||||||
|
// console.log("Render finished: " + hash + " " + expression);
|
||||||
|
handleRenderedLatex(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: textArea
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
readOnly: !editing
|
||||||
|
selectByMouse: enableMouseSelection || editing
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.family: Appearance.font.family.reading
|
||||||
|
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
selectionColor: Appearance.m3colors.m3secondaryContainer
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||||
|
textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
|
||||||
|
text: qsTr("Waiting for response...")
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
segmentContent = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||||
|
messageText.copy()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkActivated: (link) => {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea { // Pointing hand for links
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton // Only for hover
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
|
||||||
|
(enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||||
|
import "root:/modules/common"
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Qt.labs.platform
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders LaTeX snippets with MicroTeX.
|
||||||
|
* For every request:
|
||||||
|
* 1. Hash it
|
||||||
|
* 2. Check if the hash is already processed
|
||||||
|
* 3. If not, render it with MicroTeX and mark as processed
|
||||||
|
*/
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var renderPadding: 4 // This is to prevent cutoff in the rendered images
|
||||||
|
|
||||||
|
property list<string> processedHashes: []
|
||||||
|
property var processedExpressions: ({})
|
||||||
|
property var renderedImagePaths: ({})
|
||||||
|
property string microtexBinaryPath: Qt.resolvedUrl("/opt/MicroTeX/LaTeX")
|
||||||
|
property string latexOutputPath: StringUtils.trimFileProtocol(`${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/latex`)
|
||||||
|
|
||||||
|
signal renderFinished(string hash, string imagePath)
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Hyprland.dispatch(`exec rm -rf ${latexOutputPath} && mkdir -p ${latexOutputPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests rendering of a LaTeX expression.
|
||||||
|
* Returns the [hash, isNew]
|
||||||
|
*/
|
||||||
|
function requestRender(expression) {
|
||||||
|
// 1. Hash it and initialize necessary variables
|
||||||
|
const hash = Qt.md5(expression)
|
||||||
|
const imagePath = `${latexOutputPath}/${hash}.svg`
|
||||||
|
|
||||||
|
// 2. Check if the hash is already processed
|
||||||
|
if (processedHashes.includes(hash)) {
|
||||||
|
// console.log("Already processed: " + hash)
|
||||||
|
renderFinished(hash, imagePath)
|
||||||
|
return [hash, false]
|
||||||
|
} else {
|
||||||
|
root.processedHashes.push(hash)
|
||||||
|
root.processedExpressions[hash] = expression
|
||||||
|
// console.log("Rendering expression: " + expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. If not, render it with MicroTeX and mark as processed
|
||||||
|
const processQml = `
|
||||||
|
import Quickshell.Io
|
||||||
|
Process {
|
||||||
|
id: microtexProcess${hash}
|
||||||
|
running: true
|
||||||
|
command: [ "${microtexBinaryPath}", "-headless",
|
||||||
|
"-input=${StringUtils.escapeBackslashes(expression)}",
|
||||||
|
"-output=${imagePath}",
|
||||||
|
"-textsize=${Appearance.font.pixelSize.normal}",
|
||||||
|
"-padding=${renderPadding}",
|
||||||
|
"-foreground=${Appearance.colors.colOnLayer1}",
|
||||||
|
"-maxwidth=0.85" ]
|
||||||
|
// stdout: SplitParser {
|
||||||
|
// onRead: data => { console.log("MicroTeX: " + data) }
|
||||||
|
// }
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
renderedImagePaths["${hash}"] = "${imagePath}"
|
||||||
|
root.renderFinished("${hash}", "${imagePath}")
|
||||||
|
microtexProcess${hash}.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// console.log("MicroTeX: " + processQml)
|
||||||
|
Qt.createQmlObject(processQml, root, `MicroTeXProcess_${hash}`)
|
||||||
|
return [hash, true]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user