From 917ea747debb1f4be43b151acc2b42fd9de03508 Mon Sep 17 00:00:00 2001 From: Ken Perry Date: Thu, 28 May 2026 14:57:23 -0400 Subject: [PATCH] Fix internal copy/paste losing block styles across documents I had to Move `clips` and `lastCopiedString` from instance to companion object so clipboard state is shared across the manager which made it possible to copy styles across internal documents. When pasting a STYLE block, copy `the attributes bb:type utd:overrideStyle from the clipboard element to the destination block so the source style is preserved instead of defaulting to Body Text Caused a crash and learned I had to Skip PAGE_NUM blocks and UTD-namespace elements because navigating to elements that have no TextMapElement in the text view Found another crash when I was getting ready to commit and the context menu rebuild is run on a blank document after paste. it was caused by an empty map list but I only got this because of my debug messages but I fixed it anyway. --- .../math/numberLine/NumberLine.kt | 4 +- .../mvc/modules/misc/ClipboardModule.kt | 93 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/brailleblaster-core/src/main/java/org/brailleblaster/math/numberLine/NumberLine.kt b/brailleblaster-core/src/main/java/org/brailleblaster/math/numberLine/NumberLine.kt index d857a69d..851e4314 100644 --- a/brailleblaster-core/src/main/java/org/brailleblaster/math/numberLine/NumberLine.kt +++ b/brailleblaster-core/src/main/java/org/brailleblaster/math/numberLine/NumberLine.kt @@ -567,11 +567,13 @@ class NumberLine : ISpatialMathContainer { @JvmStatic fun currentIsNumberLine(): Boolean { + val mapList = WPManager.getInstance().controller.mapList + if (mapList.isEmpty()) return false val current: Node? = XMLHandler.ancestorVisitorElement( WPManager.getInstance().controller .simpleManager.currentCaret.node ) { node: Element? -> BBX.CONTAINER.NUMBER_LINE.isA(node) } - val isWhitespace = WPManager.getInstance().controller.mapList.current is WhiteSpaceElement + val isWhitespace = mapList.current is WhiteSpaceElement return !isWhitespace && current != null } diff --git a/brailleblaster-core/src/main/java/org/brailleblaster/perspectives/mvc/modules/misc/ClipboardModule.kt b/brailleblaster-core/src/main/java/org/brailleblaster/perspectives/mvc/modules/misc/ClipboardModule.kt index 3b0e1cf3..b7f7d94f 100644 --- a/brailleblaster-core/src/main/java/org/brailleblaster/perspectives/mvc/modules/misc/ClipboardModule.kt +++ b/brailleblaster-core/src/main/java/org/brailleblaster/perspectives/mvc/modules/misc/ClipboardModule.kt @@ -49,6 +49,7 @@ import org.brailleblaster.perspectives.mvc.menu.MenuManager.add import org.brailleblaster.tools.* import org.brailleblaster.utd.internal.xml.FastXPath import org.brailleblaster.utd.internal.xml.XMLHandler +import org.brailleblaster.utils.xml.UTD_NS import org.brailleblaster.utd.properties.UTDElements import org.brailleblaster.utd.utils.TableUtils import org.brailleblaster.utd.utils.stripUTDRecursive @@ -68,8 +69,6 @@ import java.util.function.Consumer import kotlin.math.min class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { - private val clips: MutableList = mutableListOf() - private var lastCopiedString: String? = null val paste: Paste = Paste() override fun onEvent(event: SimpleEvent) { @@ -218,6 +217,7 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { addNodeToClipboard(endElement, startNode, endNode, sel) } } + logCopyClipsAndContext(sel, manager) // Tests will fail when attempting to use the system clipboard if (!debugging) { lastCopiedString = convertBBXClipsToSystemClipboard(clips) @@ -228,11 +228,19 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { val cb = Clipboard(Display.getCurrent()) val tt = TextTransfer.getInstance() val cbString = cb.getContents(tt) as String? + val isInternal = cbString != null && lastCopiedString != null && lastCopiedString == cbString + log.debug( + "=== BB PASTE clipboard check: isInternal={} lastCopiedString=[{}] osCbString=[{}] ===", + isInternal, + lastCopiedString?.take(120), + cbString?.take(120) + ) if (cbString != null) { // If it was copied from outside of BrailleBlaster, or BB's // clipboard is empty, // paste that instead of what's inside BrailleBlaster's clipboard if (lastCopiedString == null || lastCopiedString != cbString) { + log.debug(" --> EXTERNAL path: clips will be rebuilt as plain Body Text (no styles preserved)") // Convert it to BBX clips.clear() val blocks = @@ -248,6 +256,8 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { clips.add(Clip(newBlock)) } + } else { + log.debug(" --> INTERNAL path: keeping {} existing BBX clip(s) with original styles", clips.size) } } } @@ -263,6 +273,35 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { deleteTextViewSelection(manager, false) } + private fun logCopyClipsAndContext(sel: XMLSelection, manager: Manager) { + log.debug("=== BB COPY: {} clip(s) placed in BB clipboard ===", clips.size) + clips.forEachIndexed { i, clip -> + log.debug(" copy-clip[{}]: {}", i, clip.node.toXML().take(400)) + } + // Log the blocks surrounding the selection for context + try { + val startBlock = findBlock(sel.start.node) + val parent = startBlock.parent + val idx = parent.indexOf(startBlock) + log.debug(" context before-selection: {}", + if (idx > 0) parent.getChild(idx - 1).toXML().take(200) else "") + log.debug(" context start-block: {}", startBlock.toXML().take(200)) + if (!sel.isSingleNode) { + val endBlock = findBlock(sel.end.node) + log.debug(" context end-block: {}", endBlock.toXML().take(200)) + val endParent = endBlock.parent + val endIdx = endParent.indexOf(endBlock) + log.debug(" context after-selection: {}", + if (endIdx < endParent.childCount - 1) endParent.getChild(endIdx + 1).toXML().take(200) else "") + } else { + log.debug(" context after-selection: {}", + if (idx < parent.childCount - 1) parent.getChild(idx + 1).toXML().take(200) else "") + } + } catch (e: Exception) { + log.debug(" context: error={}", e.message) + } + } + private fun logPasteVariables( isStartText: Boolean, isStartMath: Boolean, inBlock: Boolean, inSection: Boolean, inList: Boolean, multipleClips: Boolean @@ -319,6 +358,10 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { if (clips.isEmpty()) { return } + log.debug("=== BB PASTE: {} clip(s) about to be inserted ===", clips.size) + clips.forEachIndexed { i, clip -> + log.debug(" paste-clip[{}]: {}", i, clip.node.toXML().take(400)) + } if (isMath) { //I still don't believe it's this simple...there has to be a catch @@ -363,6 +406,13 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { var parent = startNode.parent var index = parent.indexOf(startNode) + 1 if (BBX.BLOCK.isA(clips.first().node)) { + val firstClipBlock = clips.first().node as Element + // Apply the block type/style from the clip to the destination block + // so that pasting into a new/blank document preserves the source style + if (BBX.BLOCK.STYLE.isA(firstClipBlock)) { + BBX._ATTRIB_TYPE[parent as Element] = "STYLE" + BBX._ATTRIB_OVERRIDE_STYLE[parent as Element] = BBX._ATTRIB_OVERRIDE_STYLE[firstClipBlock] + } for (i in 0.. + log.debug(" changed[{}]: {}", i, node.toXML().take(600)) + } val changedNodesArray = changedNodes.toTypedArray() manager.stopFormatting() manager.simpleManager.dispatchEvent(ModifyEvent(Sender.NO_SENDER, true, *changedNodesArray)) // Move cursor to last pasted text val pasteNode = paste.node - if (pasteNode != null && pasteNode.document != null) { + // Guard: PAGE_NUM blocks have no TME in the text view and cannot be navigated to + if (pasteNode != null && pasteNode.document != null + && !BBX.BLOCK.PAGE_NUM.isA(pasteNode) && !BBX.SPAN.PAGE_NUM.isA(pasteNode) + ) { if (pasteNode is Text) { manager.simpleManager.dispatchEvent( XMLCaretEvent(Sender.BRAILLE, XMLTextCaret(pasteNode, paste.offset)) @@ -997,6 +1076,8 @@ class ClipboardModule(private val manager: BBSimpleManager) : SimpleListener { companion object { private val log: Logger = LoggerFactory.getLogger(ClipboardModule::class.java) + private val clips: MutableList = mutableListOf() + private var lastCopiedString: String? = null fun convertBBXClipsToSystemClipboard(clips: MutableList): String { val systemCB = StringBuilder() clips.forEach(Consumer { c: Clip? ->