1+ import copy
2+ from PyQt6 .QtCore import QModelIndex
3+ from PyQt6 .QtWidgets import QMessageBox
4+ from models .project_tree import ProjectTreeModel
5+
6+ class DescAnnotationManager :
7+ """
8+ Manages data loading and saving for Description Mode (Right Panel).
9+ Handles the formatting of Q&A from JSON and flattening it upon save.
10+ """
11+ def __init__ (self , main_window ):
12+ self .main = main_window
13+ self .model = main_window .model
14+ self .ui = main_window .ui .description_ui .right_panel
15+ self .current_action_path = None
16+
17+ def setup_connections (self ):
18+ """Connect UI signals to controller methods."""
19+ # Listen to Tree Selection from the Description Panel
20+ tree = self .main .ui .description_ui .left_panel .tree
21+ tree .selectionModel ().currentChanged .connect (self .on_item_selected )
22+
23+ # Connect Editor Buttons
24+ self .ui .confirm_clicked .connect (self .save_current_annotation )
25+ self .ui .clear_clicked .connect (self .clear_current_text )
26+
27+ def on_item_selected (self , current : QModelIndex , previous : QModelIndex ):
28+ """
29+ Triggered when a tree item is selected.
30+ Loads the corresponding data into the text editor.
31+ """
32+ if not current .isValid ():
33+ self .ui .caption_edit .clear ()
34+ self .current_action_path = None
35+ self .ui .caption_edit .setEnabled (False )
36+ return
37+
38+ # 1. Identify the Action Path
39+ path = current .data (ProjectTreeModel .FilePathRole )
40+ model = self .main .tree_model
41+
42+ # If user clicked a child (video), find the parent (action) to show shared annotations
43+ if not model .hasChildren (current ) and current .parent ().isValid ():
44+ parent_idx = current .parent ()
45+ path = parent_idx .data (ProjectTreeModel .FilePathRole )
46+
47+ self .current_action_path = path
48+ self .ui .caption_edit .setEnabled (True )
49+
50+ # 2. Find Data Object in Model
51+ # We search by path first, fallback to ID if needed
52+ action_data = next ((item for item in self .model .action_item_data if item .get ("metadata" , {}).get ("path" ) == path ), None )
53+ if not action_data :
54+ action_data = next ((item for item in self .model .action_item_data if item .get ("id" ) == current .text ()), None )
55+
56+ if not action_data :
57+ self .ui .caption_edit .setPlaceholderText ("No metadata found for this item." )
58+ return
59+
60+ # 3. Format and Display Text
61+ self ._load_and_format_text (action_data )
62+
63+ def _load_and_format_text (self , data ):
64+ """
65+ Formats the display text.
66+ - If 'captions' contains 'question' fields, formats as Q&A blocks.
67+ - If 'captions' is plain text (already saved), displays it directly.
68+ """
69+ captions = data .get ("captions" , [])
70+ formatted_blocks = []
71+
72+ if captions :
73+ # Iterate through existing captions (which might be raw Q&A or edited text)
74+ for cap in captions :
75+ text = cap .get ("text" , "" )
76+ question = cap .get ("question" , "" ) # Check for the 'question' key
77+
78+ if question :
79+ # Format as Q & A if the question key exists
80+ formatted_blocks .append (f'Q: "{ question } "\n A: "{ text } "' )
81+ else :
82+ # Otherwise, just append the text (e.g. for already edited/flattened descriptions)
83+ formatted_blocks .append (text )
84+
85+ full_text = "\n \n " .join (formatted_blocks )
86+
87+ else :
88+ # Fallback: If no captions exist yet, try to generate template from metadata questions
89+ metadata = data .get ("metadata" , {})
90+ questions = metadata .get ("questions" , [])
91+ for i , q in enumerate (questions ):
92+ formatted_blocks .append (f'Q: "{ q } "\n A: ""' )
93+
94+ full_text = "\n \n " .join (formatted_blocks )
95+
96+ self .ui .caption_edit .setPlainText (full_text )
97+
98+ def save_current_annotation (self ):
99+ """
100+ Saves the current text content back to the JSON model.
101+ Flattens the structure: removes 'question' keys and saves everything as one text block.
102+ """
103+ if not self .current_action_path :
104+ return
105+
106+ text_content = self .ui .caption_edit .toPlainText ()
107+
108+ # Find the target data item in the model
109+ target_item = None
110+ for item in self .model .action_item_data :
111+ if item .get ("metadata" , {}).get ("path" ) == self .current_action_path :
112+ target_item = item
113+ break
114+
115+ if target_item :
116+ # [CRITICAL UPDATE]
117+ # Replace the entire 'captions' list with a single entry containing the full text.
118+ # This effectively removes the old 'question' fields from the JSON structure
119+ # and stores the user's edited content purely as 'text'.
120+ target_item ["captions" ] = [
121+ {
122+ "lang" : "en" ,
123+ "text" : text_content
124+ }
125+ ]
126+
127+ # Mark state as dirty so Save button becomes active
128+ self .model .is_data_dirty = True
129+ self .main .update_save_export_button_state ()
130+
131+ # Update Tree Icon (Done/Empty)
132+ is_done = bool (text_content .strip ())
133+ self ._update_tree_icon (self .current_action_path , is_done )
134+
135+ self .main .show_temp_msg ("Saved" , "Description updated." )
136+
137+ # Auto-advance to next item
138+ self ._auto_advance ()
139+
140+ def clear_current_text (self ):
141+ """Clears the editor text."""
142+ self .ui .caption_edit .clear ()
143+
144+ def _update_tree_icon (self , path , is_done ):
145+ """Updates the checkmark icon in the tree view."""
146+ item = self .model .action_item_map .get (path )
147+ if item :
148+ item .setIcon (self .main .done_icon if is_done else self .main .empty_icon )
149+
150+ def _auto_advance (self ):
151+ """Moves selection to the next Action in the tree automatically."""
152+ self .main .desc_nav_manager .nav_next_action ()
0 commit comments