From 8f89a16c80dec8e0730223bfde99ff9fdf8836f2 Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Wed, 19 Nov 2025 17:12:14 -0300 Subject: [PATCH 1/6] FIX: annotations now load consistently --- TutorialMaker/Lib/TutorialGUI.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index b87a288..1038152 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -357,6 +357,10 @@ def openAnnotationsAsJSON(self): if not os.path.exists(jsonPath): return + self.selectedAnnotator = None + self.selectedAnnotation = None + self.selectedIndexes = [0, 0] + [tInfo, tSlides, tPaths] = AnnotatedTutorial.LoadAnnotatedTutorial(jsonPath) for step in self.steps: self.gridLayout.removeWidget(step) @@ -394,6 +398,12 @@ def openAnnotationsAsJSON(self): self._regenerateCoverPixmap() self._regenerateAcknowledgmentPixmap() + + if len(self.steps) > 0 and len(self.steps[0].Slides) > 0: + self.changeSelectedSlide(0, 0) + else: + self.slideTitleWidget.setText("") + self.slideBodyWidget.setText("") @@ -551,21 +561,15 @@ def changeSelectedSlide(self, stepId, screenshotId): self.selectedSlide.setPixmap(selectedScreenshot.GetResized(*self.selectedSlideSize, keepAspectRatio=True)) self.selectedAnnotator = selectedScreenshot - # Load text from slideAnnotator - self.slideTitleWidget.setText(self.selectedAnnotator.SlideTitle) - self.slideBodyWidget.setText(self.selectedAnnotator.SlideBody) - - # Bind editors depending on layout layout = getattr(selectedScreenshot, "SlideLayout", "") + self._unbindEditorsFromCover() + self._unbindEditorsFromAcknowledgment() + if layout == "CoverPage": self._bindEditorsToCover() - self._unbindEditorsFromAcknowledgment() elif layout == "Acknowledgment": self._bindEditorsToAcknowledgment() - self._unbindEditorsFromCover() else: - self._unbindEditorsFromCover() - self._unbindEditorsFromAcknowledgment() self.slideTitleWidget.setText(self.selectedAnnotator.SlideTitle) self.slideBodyWidget.setText(self.selectedAnnotator.SlideBody) From cca4331f747e4b5ca8f13148b75c636a721fd890 Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Wed, 19 Nov 2025 17:13:41 -0300 Subject: [PATCH 2/6] FIX: loads first slide --- TutorialMaker/Lib/TutorialGUI.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 1038152..9402e9a 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -530,6 +530,9 @@ def loadImagesAndMetadata(self, tutorialData): if acknowledgments_pm is not None: self.addBlankPage(False, len(self.steps), "", type_="Acknowledgment", pixmap=acknowledgments_pm) self.ackStepIndex = len(self.steps) - 1 + + if len(self.steps) > 0 and len(self.steps[0].Slides) > 0: + self.changeSelectedSlide(0, 0) pass def swapStepPosition(self, index, swapTo): From 8957218aeb5f9ead98b9d8784044f9f762b6364f Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Wed, 19 Nov 2025 17:27:21 -0300 Subject: [PATCH 3/6] FIX: last selected nows saves --- TutorialMaker/Lib/TutorialGUI.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 9402e9a..e4f3ae3 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -409,6 +409,11 @@ def openAnnotationsAsJSON(self): def saveAnnotationsAsJSON(self): import re + + if self.selectedAnnotator is not None: + self.selectedAnnotator.SlideTitle = self.slideTitleWidget.text + self.selectedAnnotator.SlideBody = self.slideBodyWidget.toPlainText() + outputFileAnnotations = {**self.tutorialInfo} outputFileTextDict = {} outputFileOld = [] From a0ebed2cf6e220fd2fc4edcace7d123ed2ab57b0 Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Thu, 20 Nov 2025 01:04:52 -0300 Subject: [PATCH 4/6] FEAT: Added limiting popups --- TutorialMaker/TutorialMaker.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/TutorialMaker/TutorialMaker.py b/TutorialMaker/TutorialMaker.py index 1221e00..e354911 100644 --- a/TutorialMaker/TutorialMaker.py +++ b/TutorialMaker/TutorialMaker.py @@ -270,9 +270,20 @@ def FinishTutorial(): slicer.util.selectModule("TutorialMaker") def Generate(self, tutorialName): + modulePath = Lib.TutorialUtils.get_module_basepath("TutorialMaker") + annotationsPath = modulePath + "/Outputs/Annotations/annotations.json" + + if not os.path.exists(annotationsPath): + slicer.util.warningDisplay( + _("You don't have any annotations to export.\n" + "Please annotate your screenshots first using \"Edit Annotations\"."), + _("No Annotations Found") + ) + return + with slicer.util.tryWithErrorDisplay(_("Failed to generate tutorial")): - AnnotationPainter.TutorialPainter().GenerateHTMLfromAnnotatedTutorial(Lib.TutorialUtils.get_module_basepath("TutorialMaker") + "/Outputs/Annotations/annotations.json") - outputPath = Lib.TutorialUtils.get_module_basepath("TutorialMaker") + "/Outputs/" + AnnotationPainter.TutorialPainter().GenerateHTMLfromAnnotatedTutorial(annotationsPath) + outputPath = modulePath + "/Outputs/" if platform.system() == "Windows": os.startfile(outputPath) else: @@ -289,6 +300,18 @@ def CreateNewTutorial(self): pass def OpenAnnotator(Self): + modulePath = Lib.TutorialUtils.get_module_basepath("TutorialMaker") + rawTutorialPath = modulePath + "/Outputs/Raw/Tutorial.json" + annotationsPath = modulePath + "/Outputs/Annotations/annotations.json" + + if not os.path.exists(rawTutorialPath): + slicer.util.warningDisplay( + _("Before editing annotations you should run the capture of the screenshots.\n" + "Select a tutorial and click on \"Capture Screenshots\"."), + _("No Screenshots Found") + ) + return + Annotator = Lib.TutorialGUI.TutorialGUI() Annotator.open_json_file(Lib.TutorialUtils.get_module_basepath("TutorialMaker") + "/Outputs/Raw/Tutorial.json") Annotator.show() From 6688bf8fcd40d95dcc1caa26c1b99a2aebb9aa0e Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Thu, 20 Nov 2025 01:38:27 -0300 Subject: [PATCH 5/6] FEAT: Loaded existing --- TutorialMaker/Lib/TutorialGUI.py | 51 ++++++++++++++++---------------- TutorialMaker/TutorialMaker.py | 14 ++++++++- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index e4f3ae3..55e49ce 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -342,26 +342,12 @@ def setupGUI(self): self.icon_arrowDown = qt.QIcon(qt.QPixmap.fromImage(self.image_ArrowDown)) pass - def openAnnotationsAsJSON(self): - from Lib.TutorialUtils import get_module_basepath as getModulePath - parent = slicer.util.mainWindow() - basePath = getModulePath("TutorialMaker") - jsonPath = qt.QFileDialog.getOpenFileName( - parent, - _("Select a JSON file"), - basePath + "/Outputs/Annotations/", - _("JSON Files (*.json)") - ) - self.raise_() - self.activateWindow() - if not os.path.exists(jsonPath): - return - + def _loadAnnotationsFromFile(self, filepath): self.selectedAnnotator = None self.selectedAnnotation = None self.selectedIndexes = [0, 0] - [tInfo, tSlides, tPaths] = AnnotatedTutorial.LoadAnnotatedTutorial(jsonPath) + [tInfo, tSlides, tPaths] = AnnotatedTutorial.LoadAnnotatedTutorial(filepath) for step in self.steps: self.gridLayout.removeWidget(step) step.deleteLater() @@ -373,24 +359,20 @@ def openAnnotationsAsJSON(self): stepWidget.AddStepWindows(tSlides[stepIndex]) self.steps.append(stepWidget) - self.gridLayout.addWidget(stepWidget) # noqa: F821 - stepWidget.UNDELETABLE = True # noqa: F821 - stepWidget.CreateMergedWindow() # noqa: F821 - stepWidget.ToggleExtended() # noqa: F821 + self.gridLayout.addWidget(stepWidget) + stepWidget.UNDELETABLE = True + stepWidget.CreateMergedWindow() + stepWidget.ToggleExtended() self.tutorialInfo = tInfo - - self.coverStepIndex = self._findStepIndexByLayout("CoverPage") self.ackStepIndex = self._findStepIndexByLayout("Acknowledgment") - # Ensure Cover exists if self.coverStepIndex is None: pm = self.make_cover_pixmap(self.tutorialInfo, tuple(self.selectedSlideSize)) self.addBlankPage(False, 0, "", type_="CoverPage", pixmap=pm) self.coverStepIndex = 0 - # Ensure Acknowledgment exists ALWAYS (even if empty) if self.ackStepIndex is None: pm = self.make_acknowledgments_pixmap(self.tutorialInfo, tuple(self.selectedSlideSize)) self.addBlankPage(False, 1, "", type_="Acknowledgment", pixmap=pm) @@ -405,6 +387,23 @@ def openAnnotationsAsJSON(self): self.slideTitleWidget.setText("") self.slideBodyWidget.setText("") + def openAnnotationsAsJSON(self): + from Lib.TutorialUtils import get_module_basepath as getModulePath + parent = slicer.util.mainWindow() + basePath = getModulePath("TutorialMaker") + jsonPath = qt.QFileDialog.getOpenFileName( + parent, + _("Select a JSON file"), + basePath + "/Outputs/Annotations/", + _("JSON Files (*.json)") + ) + self.raise_() + self.activateWindow() + if not os.path.exists(jsonPath): + return + + self._loadAnnotationsFromFile(jsonPath) + def saveAnnotationsAsJSON(self): @@ -918,11 +917,13 @@ def scrollEvent(self, event): def open_json_file(self, filepath): directory_path = os.path.dirname(filepath) - # Read the data from the file with open(filepath, encoding='utf-8') as file: rawTutorialData = json.load(file) file.close() + if "slides" in rawTutorialData: + self._loadAnnotationsFromFile(filepath) + return tutorial = Tutorial( rawTutorialData["title"], diff --git a/TutorialMaker/TutorialMaker.py b/TutorialMaker/TutorialMaker.py index e354911..bd8d901 100644 --- a/TutorialMaker/TutorialMaker.py +++ b/TutorialMaker/TutorialMaker.py @@ -312,8 +312,20 @@ def OpenAnnotator(Self): ) return + fileToLoad = rawTutorialPath + if os.path.exists(annotationsPath): + loadAnnotations = slicer.util.confirmYesNoDisplay( + _("An existing annotations file was found.\n\n" + "Would you like to load the existing annotations?\n\n" + "Yes: Load existing annotations\n" + "No: Start fresh from raw tutorial"), + _("Load Existing Annotations?") + ) + if loadAnnotations: + fileToLoad = annotationsPath + Annotator = Lib.TutorialGUI.TutorialGUI() - Annotator.open_json_file(Lib.TutorialUtils.get_module_basepath("TutorialMaker") + "/Outputs/Raw/Tutorial.json") + Annotator.open_json_file(fileToLoad) Annotator.show() pass From 4906091966c9abac1f6e667eaae7809bfb89a78a Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Thu, 20 Nov 2025 11:58:02 -0300 Subject: [PATCH 6/6] FEAT/FIX: copy slides --- TutorialMaker/Lib/TutorialGUI.py | 67 +++++++++++++++++++++++++++- TutorialMaker/Lib/TutorialPainter.py | 2 +- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/TutorialMaker/Lib/TutorialGUI.py b/TutorialMaker/Lib/TutorialGUI.py index 55e49ce..d4f7c97 100644 --- a/TutorialMaker/Lib/TutorialGUI.py +++ b/TutorialMaker/Lib/TutorialGUI.py @@ -678,7 +678,72 @@ def add_selected_image(self): def copy_page(self): - pass + if self.selectedAnnotator is None: + return + + if self.selectedAnnotator is not None: + self.selectedAnnotator.SlideTitle = self.slideTitleWidget.text + self.selectedAnnotator.SlideBody = self.slideBodyWidget.toPlainText() + + stepIndex, slideIndex = self.selectedIndexes + currentStep = self.steps[stepIndex] + currentSlide = currentStep.Slides[slideIndex] + + newPixmap = currentSlide.image.copy() + + newMetadata = copy.deepcopy(currentSlide.metadata) + + newAnnotations = [] + for annotation in currentSlide.annotations: + newAnnotation = Annotation( + TargetWidget=copy.deepcopy(annotation.target), + OffsetX=annotation.offsetX, + OffsetY=annotation.offsetY, + OptX=annotation.optX, + OptY=annotation.optY, + Text=annotation.text, + Type=annotation.type + ) + newAnnotation.penConfig(annotation.color, annotation.fontSize, annotation.thickness, annotation.brush, annotation.pen) + newAnnotation.PERSISTENT = annotation.PERSISTENT + newAnnotations.append(newAnnotation) + + newWindowOffset = copy.deepcopy(currentSlide.windowOffset) + + newSlide = AnnotatorSlide(newPixmap, newMetadata, newAnnotations, newWindowOffset) + if currentSlide.SlideLayout == "Screenshot": + newSlide.SlideLayout = "Copy" + else: + newSlide.SlideLayout = currentSlide.SlideLayout + newSlide.SlideTitle = currentSlide.SlideTitle + _(" (Copy)") + newSlide.SlideBody = currentSlide.SlideBody + newSlide.Active = currentSlide.Active + + newStepIndex = stepIndex + 1 + stepWidget = AnnotatorStepWidget(len(self.steps), self.thumbnailSize, parent=self) + stepWidget.thumbnailClicked.connect(self.changeSelectedSlide) + stepWidget.swapRequest.connect(self.swapStepPosition) + stepWidget.AddStepWindows(newSlide) + stepWidget.CreateMergedWindow() + + self.steps.append(stepWidget) + self.gridLayout.addWidget(stepWidget) + + for i in range(len(self.steps) - 1, newStepIndex, -1): + self.steps[i] = self.steps[i - 1] + self.steps[i].stepIndex = i + self.gridLayout.addWidget(self.steps[i], i, 0) + + self.steps[newStepIndex] = stepWidget + stepWidget.stepIndex = newStepIndex + self.gridLayout.addWidget(stepWidget, newStepIndex, 0) + + if self.coverStepIndex is not None and self.coverStepIndex >= newStepIndex: + self.coverStepIndex += 1 + if self.ackStepIndex is not None and self.ackStepIndex >= newStepIndex: + self.ackStepIndex += 1 + + self.changeSelectedSlide(newStepIndex, 0) def updateSelectedAnnotationSettings(self): if self.selectedAnnotation is not None: diff --git a/TutorialMaker/Lib/TutorialPainter.py b/TutorialMaker/Lib/TutorialPainter.py index ef75933..f8391f5 100644 --- a/TutorialMaker/Lib/TutorialPainter.py +++ b/TutorialMaker/Lib/TutorialPainter.py @@ -830,7 +830,7 @@ def GenerateHTMLfromAnnotatedTutorial(self, path): ) - elif slide.SlideLayout == "Screenshot": + elif slide.SlideLayout == "Screenshot" or slide.SlideLayout == "Copy": title = slide.SlideTitle page = Exporter.SimpleSlide( slide.SlideTitle,