From 2d9ea4dcd95d31b236b172178b9acd0f36751ecd Mon Sep 17 00:00:00 2001 From: enrique hernandez laredo Date: Wed, 24 Sep 2025 13:42:33 -0600 Subject: [PATCH] This update consisted of adding a caret cursor in text and arrow, automatically calculating the size of the text box in text annotation, as well as adding enter, copy, and paste functions within the text annotation. --- TutorialMaker/Lib/Annotations.py | 104 ++++++++++++++++++------------- TutorialMaker/Lib/TutorialGUI.py | 62 +++++++++++++----- 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/TutorialMaker/Lib/Annotations.py b/TutorialMaker/Lib/Annotations.py index 2f7fbec..68e214a 100644 --- a/TutorialMaker/Lib/Annotations.py +++ b/TutorialMaker/Lib/Annotations.py @@ -47,6 +47,9 @@ def __init__(self, self.boundingBoxBottomRight = [0,0] self.__selectionSlideEffect = 0 + self.caretVisible = True + self.caretPosition = len(self.text) + # Need to change this later, make it loaded through resources self.icon_click = qt.QImage(os.path.dirname(__file__) + '/../Resources/Icons/Painter/click_icon.png') self.icon_click = self.icon_click.scaled(20,30) @@ -70,7 +73,7 @@ def getSelectionBoundingBoxSize(self): return [self.boundingBoxBottomRight[0] - self.boundingBoxTopLeft[0], self.boundingBoxBottomRight[1] - self.boundingBoxTopLeft[1]] def wantsOptHelper(self): - return self.type in AnnotationType.Arrow | AnnotationType.TextBox | AnnotationType.ArrowText + return self.type in AnnotationType.Arrow | AnnotationType.ArrowText def wantsOffsetHelper(self): return self.type in AnnotationType.Click | AnnotationType.TextBox @@ -109,6 +112,26 @@ def penConfig(self, color, fontSize, thickness, brush = None, pen = None): self.fontSize = fontSize pass + def drawCaret(self, painter, fontMetrics, textStart, textLines): + + if not getattr(self, "caretVisible", True): + return + + caretLine = min(self.text[:self.caretPosition].count('\n'), max(len(textLines)-1, 0)) + lineStartIndex = sum(len(l)+1 for l in textLines[:caretLine]) + caretCol = self.caretPosition - lineStartIndex + caretCol = max(0, min(caretCol, len(textLines[caretLine]))) + + caretX = textStart[0] + fontMetrics.width(textLines[caretLine][:caretCol]) + fHeight = fontMetrics.height() + lineSpacing = 2 + caretYTop = textStart[1] - fHeight + lineSpacing + fHeight * caretLine + caretYBottom = caretYTop + fHeight + + painter.drawLine(caretX, caretYTop, caretX, caretYBottom) + + + def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBrush = None): #Maybe we can organize this better targetPos = [self.target["position"][0] - self.annotationOffset[0] + self.offsetX, @@ -265,6 +288,8 @@ def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBr for lineIndex, line in enumerate(textLines): painter.drawText(textStart[0], textStart[1] + lineSpacing + fHeight * lineIndex, line) + self.drawCaret(painter, fontMetrics, textStart, textLines) + self.setSelectionBoundingBox(arrowHead[0], arrowHead[1], arrowTail[0], arrowTail[1]) @@ -282,68 +307,63 @@ def draw(self, painter : qt.QPainter = None, pen : qt.QPen = None, brush :qt.QBr elif self.type == AnnotationType.Circle: pass elif self.type == AnnotationType.TextBox: - # So the box will be filled brush.setStyle(qt.Qt.SolidPattern) painter.setBrush(brush) - - # Padding - yPadding = 6 xPadding = 6 lineSpacing = 2 - optX = self.optX - targetCenter[0] - optY = self.optY - targetCenter[1] - - topLeft = qt.QPoint(targetPos[0], targetPos[1]) - bottomRight = qt.QPoint(targetPos[0] + optX, targetPos[1] + optY) - rectToDraw = qt.QRect(topLeft,bottomRight) - painter.drawRect(rectToDraw) - - # Calculate the text break and position font = qt.QFont("Arial", self.fontSize) painter.setFont(font) pen.setColor(qt.Qt.black) painter.setPen(pen) - fontMetrics = qt.QFontMetrics(font) fHeight = fontMetrics.height() - textBoxBottomRight = [targetPos[0] + optX, targetPos[1] + optY] - textBoxTopLeft = [targetPos[0], targetPos[1]] + textToWrite = self.text if self.text else _("Write something here") + textTokens = textToWrite.splitlines(keepends=True) + textLines = [] + line = "" + for token in textTokens: + if "\n" in token: + if line: + textLines.append(line) + textLines.append(token.rstrip("\n")) + line = "" + continue - if textBoxBottomRight[0] < textBoxTopLeft[0]: - tmp = textBoxTopLeft[0] - textBoxTopLeft[0] = textBoxBottomRight[0] - textBoxBottomRight[0] = tmp + if fontMetrics.width(line + token) > 200: # ancho por defecto inicial + if line: + textLines.append(line) + line = token + else: + line += token + if line: + textLines.append(line) - if textBoxBottomRight[1] < textBoxTopLeft[1]: - tmp = textBoxTopLeft[1] - textBoxTopLeft[1] = textBoxBottomRight[1] - textBoxBottomRight[1] = tmp + textWidth = max((fontMetrics.width(line) for line in textLines), default=0) + 2*xPadding + textHeight = len(textLines) * fHeight + (len(textLines)-1)*lineSpacing + 2*yPadding - textStart = [textBoxTopLeft[0] + xPadding, - textBoxTopLeft[1] + yPadding + fHeight] + if not hasattr(self, "_userSetSize"): + bottomRight = [targetPos[0] + textWidth, targetPos[1] + textHeight] + else: + bottomRight = [targetPos[0] + self.optX, targetPos[1] + self.optY] - textToWrite = self.text - if textToWrite == "": - textToWrite = _("Write something here") + topLeft = [targetPos[0], targetPos[1]] - textTokens = textToWrite.splitlines() - textLines = [] - line = "" - for token in textTokens: - if fontMetrics.width(line + token) > textBoxBottomRight[0] - textBoxTopLeft[0] - xPadding: - textLines.append(copy.deepcopy(line)) - line = f"{token} " - continue - line += f"{token} " - textLines.append(line) + rectToDraw = qt.QRect(qt.QPoint(*topLeft), qt.QPoint(*bottomRight)) + painter.drawRect(rectToDraw) + + textStart = [topLeft[0] + xPadding, topLeft[1] + yPadding + fHeight] for lineIndex, line in enumerate(textLines): - painter.drawText(textStart[0], textStart[1] + lineSpacing + fHeight*lineIndex, line) + painter.drawText(textStart[0], textStart[1] + lineSpacing + fHeight * lineIndex, line) + + self.drawCaret(painter, fontMetrics, textStart, textLines) + + self.setSelectionBoundingBox(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]) + - self.setSelectionBoundingBox(targetPos[0], targetPos[1], targetPos[0] + optX, targetPos[1] + optY) elif self.type == AnnotationType.Click: bottomRight = [targetPos[0] + targetSize[0], diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 3900213..d18fb64 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -757,33 +757,65 @@ def keyboardEvent(self, event): self.setFocus() return False - if self.selectedAnnotationType == AnnotationType.Selected: + if self.selectedAnnotationType == AnnotationType.Selected and self.selectedAnnotation is not None: + ann = self.selectedAnnotation + + if event.key() == qt.Qt.Key_Delete: - self.selectedAnnotation.PERSISTENT = False + ann.PERSISTENT = False self.cancelCurrentAnnotation() - elif self.selectedAnnotation.type in [AnnotationType.TextBox, AnnotationType.ArrowText]: - # Detect command Ctrl+C copy text + + elif ann.type in [AnnotationType.TextBox, AnnotationType.ArrowText]: + if event.key() == qt.Qt.Key_C and event.modifiers() & qt.Qt.ControlModifier: - qt.QApplication.clipboard().setText(self.selectedAnnotation.text) - - # Detect command Ctl+v page text + qt.QApplication.clipboard().setText(ann.text) + elif event.key() == qt.Qt.Key_V and event.modifiers() & qt.Qt.ControlModifier: - self.selectedAnnotation.text += qt.QApplication.clipboard().text() + ann.text = ann.text[:ann.caretPosition] + qt.QApplication.clipboard().text() + ann.text[ann.caretPosition:] + ann.caretPosition += len(qt.QApplication.clipboard().text()) + ann.caretPosition = max(0, min(ann.caretPosition, len(ann.text))) - # Detect Enter to add a line break elif event.key() in [qt.Qt.Key_Return, qt.Qt.Key_Enter]: - self.selectedAnnotation.text += "\n" + try: + ann.text = ann.text[:ann.caretPosition] + "\n" + ann.text[ann.caretPosition:] + ann.caretPosition += 1 + ann.caretPosition = max(0, min(ann.caretPosition, len(ann.text))) + except Exception as e: + import traceback + traceback.print_exc() - # Detect Backspace elif event.key() == qt.Qt.Key_Backspace: - self.selectedAnnotation.text = self.selectedAnnotation.text[:-1] + if ann.caretPosition > 0: + ann.text = ann.text[:ann.caretPosition-1] + ann.text[ann.caretPosition:] + ann.caretPosition -= 1 + ann.caretPosition = max(0, min(ann.caretPosition, len(ann.text))) + + elif event.key() == qt.Qt.Key_Left: + ann.caretPosition -= 1 + ann.caretPosition = max(0, ann.caretPosition) + elif event.key() == qt.Qt.Key_Right: + ann.caretPosition += 1 + ann.caretPosition = min(len(ann.text), ann.caretPosition) + elif event.key() == qt.Qt.Key_Home: + lines = ann.text.splitlines(keepends=True) + lineIndex = ann.text[:ann.caretPosition].count('\n') + lineStart = sum(len(l) for l in lines[:lineIndex]) + ann.caretPosition = lineStart + elif event.key() == qt.Qt.Key_End: + lines = ann.text.splitlines(keepends=True) + lineIndex = ann.text[:ann.caretPosition].count('\n') + lineStart = sum(len(l) for l in lines[:lineIndex]) + ann.caretPosition = lineStart + len(lines[lineIndex]) else: - self.selectedAnnotation.text += event.text() + ann.text = ann.text[:ann.caretPosition] + event.text() + ann.text[ann.caretPosition:] + ann.caretPosition += len(event.text()) + ann.caretPosition = max(0, min(ann.caretPosition, len(ann.text))) - return True + return True + # --- Navegar entre anotaciones con flechas --- elif self.selectedAnnotator is not None and self.selectedAnnotation is not None: if event.key() == qt.Qt.Key_Up: self.selectorParentDelta(-1) @@ -795,6 +827,8 @@ def keyboardEvent(self, event): return False + + def selectorParentDelta(self, delta : int): self.selectorParentCount += delta self.previewAnnotation(self.lastAppPos)