Skip to content

Qt6 GUI Visual Template editor with correct default values#249

Open
matiasman1 wants to merge 7 commits intoUdayraj123:masterfrom
matiasman1:master
Open

Qt6 GUI Visual Template editor with correct default values#249
matiasman1 wants to merge 7 commits intoUdayraj123:masterfrom
matiasman1:master

Conversation

@matiasman1
Copy link
Copy Markdown

@matiasman1 matiasman1 commented Sep 30, 2025

User description

A simple Qt6 GUI editor for templates. Calls the same path on image processing as main.py, and opens cropped re-scaled image on a canvas based on the provided template. Allows for creation of the standard 4 included Qtypes, orientation (vertical vs horizontal), Bubble Values and fieldLabels.


PR Type

Enhancement


Description

  • Add Qt6 GUI template editor with visual editing capabilities

  • Add OpenCV-based simple template editor for basic operations

  • Include sample templates for testing and demonstration

  • Update documentation with editor usage instructions


Diagram Walkthrough

flowchart LR
  A["Template JSON"] --> B["Qt6 Editor"]
  A --> C["OpenCV Editor"]
  D["Image Input"] --> B
  D --> C
  B --> E["Visual Editing"]
  C --> F["Basic Editing"]
  E --> G["Edited Template"]
  F --> G
Loading

File Walkthrough

Relevant files
Enhancement
editor.py
OpenCV template editor implementation                                       

src/ui/editor.py

  • Create OpenCV-based template editor with mouse interactions
  • Support drag-to-create, move, resize operations with handles
  • Include keyboard shortcuts for bubble manipulation
  • Implement save functionality with trackbar controls
+397/-0 
qt_editor.py
Qt6 GUI template editor with full features                             

src/ui/qt_editor.py

  • Implement comprehensive Qt6 GUI template editor
  • Add visual block editing with drag/resize handles
  • Include collapsible sidebar panels for field configuration
  • Integrate preprocessing pipeline for accurate image display
+1039/-0
Tests
template.json
TestMio sample template configuration                                       

samples/TestMio/template.json

  • Add sample template with MCQ block configuration
  • Include CropPage preprocessor settings
  • Define page and bubble dimensions
+23/-0   
template.json
Adrian test template configuration                                             

samples/adrian_test/template.json

  • Add test template with QTYPE_MCQ5 configuration
  • Include CropPage preprocessor with morphKernel settings
  • Define smaller page dimensions for testing
+22/-0   
template.edited.json
Sample1 edited template example                                                   

samples/sample1/template.edited.json

  • Add comprehensive edited template example
  • Include multiple field blocks with different types
  • Define custom labels and preprocessor configurations
+247/-0 
Documentation
README.md
Qt6 editor documentation and usage guide                                 

src/ui/README.md

  • Document Qt6 editor features and requirements
  • Provide installation and usage instructions
  • Include command-line examples and notes
+31/-0   

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Import Duplication

Redundant double import of json and Path aliases at the top may mask errors and reduce clarity; consider removing duplicates and aligning with project import style.

import json
import argparse
from pathlib import Path
import cv2
import numpy as np

# Ensure project root on sys.path then try normal import, else provide fallback.
import sys, json
from pathlib import Path as _P

PROJECT_ROOT = _P(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

try:
    from src.utils.parsing import open_template_with_defaults  # type: ignore
except Exception:
    def open_template_with_defaults(p: _P):
        try:
            with open(p, "r", encoding="utf-8") as f:
                data = json.load(f)
Dimension Semantics

Using 'bubblesGap' and 'labelsGap' as actual width/height of blocks can diverge from template semantics; verify that saving these values back will be interpreted correctly by the main pipeline.

def _rect_from_block(self, fb):
    origin = fb.get("origin", [0, 0])
    x, y = int(origin[0]), int(origin[1])
    # Using stored gaps as width/height fallback
    w = int(fb.get("bubblesGap", fb.get("width", 160)))
    h = int(fb.get("labelsGap", fb.get("height", 80)))
    # Guarantee min size
    w = max(w, 30)
    h = max(h, 30)
    return x, y, w, h

def _update_block_from_rect(self, fb, x, y, w, h):
    fb["origin"] = [int(x), int(y)]
    fb["bubblesGap"] = int(w)
    fb["labelsGap"] = int(h)
Undo Granularity

Model push_state is called after every resize/move step, potentially bloating history and hurting UX; consider capturing state on mouse release or coalescing operations.

    def set_rect(self, rect: QtCore.QRectF):
        self.prepareGeometryChange()
        self._rect = rect.normalized()
        self.update_model_from_item()
        self.update()
        # snapshot after resize
        self.model.push_state("resize_block")

    def mouseReleaseEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
        super().mouseReleaseEvent(event)
        # snapshot after move
        self.model.push_state("move_block")

# New: subclass handle item to forward drag to parent
class _ResizeHandle(QtWidgets.QGraphicsRectItem):
    def __init__(self, parent: BlockGraphicsItem, role: str, size: float):
        super().__init__(0, 0, size, size, parent)
        self._parent = parent
        self.role = role  # type: ignore[attr-defined]
        self.setBrush(QtGui.QBrush(QtGui.QColor(255, 200, 0, 220)))
        self.setPen(QtGui.QPen(QtGui.QColor(30, 30, 30), 1))
        self.setZValue(1000)
        # Let parent control actual geometry change; prevent scene panning of the block while resizing
        self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsScenePositionChanges, True)
        # Suppress recursion when parent repositions this handle
        self._suppress_item_change = False
        self._dragging = False

    def setPosSilently(self, pos: QtCore.QPointF):
        self._suppress_item_change = True
        try:
            super().setPos(pos)
        finally:
            self._suppress_item_change = False

    def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Remove the redundant OpenCV-based editor

The PR adds both a Qt6 and an OpenCV template editor. The suggestion is to
remove the simpler, redundant OpenCV editor (src/ui/editor.py) to reduce
maintenance and focus on the more advanced Qt6 version.

Examples:

src/ui/editor.py [1-397]
import json
import argparse
from pathlib import Path
import cv2
import numpy as np

# Ensure project root on sys.path then try normal import, else provide fallback.
import sys, json
from pathlib import Path as _P


 ... (clipped 387 lines)

Solution Walkthrough:

Before:

# src/ui/editor.py
import cv2

class SimpleTemplateEditor:
    """
    Enhanced OpenCV-based template editor.
    Interactions:
      - Left drag empty area: create new block
      - Click block: select
    """
    def __init__(self, template_path, image_path): ...
    def run(self): ...

# src/ui/qt_editor.py
from PyQt6 import QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    """
    Feature-rich Qt6 GUI template editor.
    """
    def __init__(self, template_path, image_path): ...

After:

# src/ui/editor.py
# (File removed)

# src/ui/qt_editor.py
from PyQt6 import QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    """
    Feature-rich Qt6 GUI template editor.
    """
    def __init__(self, template_path, image_path): ...
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant redundancy by having two template editors (OpenCV and Qt6) and wisely recommends removing the simpler OpenCV one to reduce maintenance overhead and focus on the more robust Qt6 implementation.

Medium
Possible issue
Prevent overwriting existing blocks on rename

Add a check to prevent renaming a block to a name that already exists, which
would otherwise cause data loss by overwriting the existing block.

src/ui/qt_editor.py [732-761]

 def _apply(self):
     new_name = self.field_name.text().strip()
     base = self.model.get_block_base(self.name)
     self.model.push_state("panel_apply")
     # If renamed
     if new_name and new_name != self.name:
-        # Move the dict key
-        self.model.template["fieldBlocks"][new_name] = base
-        del self.model.template["fieldBlocks"][self.name]
-        self.name = new_name
-        self.setTitle(new_name)
+        # Prevent overwriting an existing block
+        if new_name in self.model.template["fieldBlocks"]:
+            LOG.warning(f"Block name '{new_name}' already exists. Cannot rename.")
+            self.field_name.setText(self.name) # Revert UI
+        else:
+            # Move the dict key
+            self.model.template["fieldBlocks"][new_name] = base
+            del self.model.template["fieldBlocks"][self.name]
+            self.name = new_name
+            self.setTitle(new_name)
 
     # Persist bubbleValues only if user provided explicit text
     bv_text = self.bubble_values.text().strip()
     if bv_text != "":
         base["bubbleValues"] = parse_csv_or_range(bv_text)
     else:
         base.pop("bubbleValues", None)
     # Direction: omit if equals type-default
     sel_dir = self.direction.currentText()
     ft = base.get("fieldType")
     if sel_dir == self.model.default_dir_for_type(ft):
         base.pop("direction", None)
     else:
         base["direction"] = sel_dir
     base["fieldLabels"] = parse_csv_or_range(self.field_labels.text())
     base["labelsGap"] = int(self.labels_gap.value())
     base["bubblesGap"] = int(self.bubbles_gap.value())
     base["origin"] = [int(self.origin_x.value()), int(self.origin_y.value())]
     self.changed.emit(self.name)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a critical issue where renaming a block to an existing name causes data loss by overwriting the other block, and it provides a robust fix.

Medium
Clear properties when field type changes

When changing a block's fieldType, clear the bubbleValues and direction
properties to ensure the new type's defaults are applied correctly.

src/ui/qt_editor.py [722-730]

 def _apply_fieldtype(self, text: str):
     base = self.model.get_block_base(self.name)
     self.model.push_state("change_fieldtype")
     if text:
         base["fieldType"] = text
     else:
         base.pop("fieldType", None)
-    # Do not auto-write bubbleValues or direction; renderer derives them.
+    # When fieldType changes, clear bubbleValues and direction so renderer can re-derive them.
+    base.pop("bubbleValues", None)
+    base.pop("direction", None)
     self.changed.emit(self.name)
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that changing fieldType should reset dependent properties like direction and bubbleValues to avoid carrying over stale, potentially conflicting data from the previous type.

Low
  • More

@Udayraj123
Copy link
Copy Markdown
Owner

@matiasman1 thanks for the pr, can you follow the contributing guidelines and share a recording of this feature?

@Udayraj123
Copy link
Copy Markdown
Owner

@matiasman1 bump^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants